diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index b41be1ef03..e6c38fa4fa 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -6,7 +6,7 @@ permissions: contents: write env: - PYTHON_VERSION: "3.11" + PYTHON_VERSION: "3.13" jobs: build: @@ -31,7 +31,7 @@ jobs: - name: Install dependencies shell: bash - run: uv sync + run: uv sync --all-packages - name: mkdocs build shell: bash diff --git a/.github/workflows/python-checks.yml b/.github/workflows/python-checks.yml new file mode 100644 index 0000000000..ae11854e33 --- /dev/null +++ b/.github/workflows/python-checks.yml @@ -0,0 +1,78 @@ +name: Python Build and Type Check +on: + push: + branches: + - "**/main" # match branches like feature/main + - "main" # match the main branch + pull_request: + types: + - opened + - reopened + - synchronize + - ready_for_review + branches: + - "**/main" + - "main" + paths-ignore: + - "**/*.md" + - ".semversioner/**" + +permissions: + contents: read + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # Only run the for the latest commit + cancel-in-progress: true + +jobs: + python-ci: + # skip draft PRs + if: github.event.pull_request.draft == false + strategy: + matrix: + python-version: ["3.11", "3.13"] + os: [ubuntu-latest, windows-latest] + fail-fast: false # Continue running all jobs even if one fails + env: + DEBUG: 1 + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + python: + - 'graphrag/**/*' + - 'uv.lock' + - 'pyproject.toml' + - '**/*.py' + - '**/*.toml' + - '**/*.ipynb' + - '.github/workflows/python*.yml' + - 'tests/**/*' + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Install dependencies + shell: bash + run: | + uv sync --all-packages + + - name: Check + run: | + uv run poe check + + - name: Build + run: | + uv build --all-packages \ No newline at end of file diff --git a/.github/workflows/python-integration-tests.yml b/.github/workflows/python-integration-tests.yml index 48634f7b06..c1d112ee86 100644 --- a/.github/workflows/python-integration-tests.yml +++ b/.github/workflows/python-integration-tests.yml @@ -32,7 +32,7 @@ jobs: if: github.event.pull_request.draft == false strategy: matrix: - python-version: ["3.10"] + python-version: ["3.13"] os: [ubuntu-latest, windows-latest] fail-fast: false # continue running all jobs even if one fails env: @@ -67,12 +67,11 @@ jobs: - name: Install dependencies shell: bash run: | - uv sync - uv pip install gensim + uv sync --all-packages - name: Build run: | - uv build + uv build --all-packages - name: Install and start Azurite shell: bash diff --git a/.github/workflows/python-notebook-tests.yml b/.github/workflows/python-notebook-tests.yml index aae1f5c3a2..9808d690f2 100644 --- a/.github/workflows/python-notebook-tests.yml +++ b/.github/workflows/python-notebook-tests.yml @@ -32,12 +32,13 @@ jobs: if: github.event.pull_request.draft == false strategy: matrix: - python-version: ["3.10"] + python-version: ["3.13"] os: [ubuntu-latest, windows-latest] fail-fast: false # Continue running all jobs even if one fails env: DEBUG: 1 - GRAPHRAG_API_KEY: ${{ secrets.OPENAI_NOTEBOOK_KEY }} + GRAPHRAG_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GRAPHRAG_API_BASE: ${{ secrets.GRAPHRAG_API_BASE }} runs-on: ${{ matrix.os }} steps: @@ -67,8 +68,7 @@ jobs: - name: Install dependencies shell: bash run: | - uv sync - uv pip install gensim + uv sync --all-packages - name: Notebook Test run: | diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 7973c1281f..9cd729eb2f 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -7,7 +7,7 @@ on: branches: [main] env: - PYTHON_VERSION: "3.10" + PYTHON_VERSION: "3.13" jobs: publish: @@ -17,8 +17,6 @@ jobs: environment: name: pypi - url: https://pypi.org/p/graphrag - permissions: id-token: write @@ -38,14 +36,14 @@ jobs: - name: Install dependencies shell: bash - run: uv sync + run: uv sync --all-packages - name: Export Publication Version run: echo "version=$(uv version --short)" >> $GITHUB_OUTPUT - name: Build Distributable shell: bash - run: uv build + run: uv run poe build - name: Inspect all distribution members and metadata shell: bash @@ -99,8 +97,4 @@ jobs: PY - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: dist - skip-existing: true - verbose: true + run: uv publish diff --git a/.github/workflows/python-smoke-tests.yml b/.github/workflows/python-smoke-tests.yml index ec420d310b..e055c66b67 100644 --- a/.github/workflows/python-smoke-tests.yml +++ b/.github/workflows/python-smoke-tests.yml @@ -32,7 +32,7 @@ jobs: if: github.event.pull_request.draft == false strategy: matrix: - python-version: ["3.10"] + python-version: ["3.13"] os: [ubuntu-latest, windows-latest] fail-fast: false # Continue running all jobs even if one fails env: @@ -72,12 +72,11 @@ jobs: - name: Install dependencies shell: bash run: | - uv sync - uv pip install gensim + uv sync --all-packages - name: Build run: | - uv build + uv build --all-packages - name: Install and start Azurite shell: bash diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-unit-tests.yml similarity index 83% rename from .github/workflows/python-ci.yml rename to .github/workflows/python-unit-tests.yml index 37651589e8..b2577bae0c 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-unit-tests.yml @@ -1,4 +1,4 @@ -name: Python CI +name: Python Unit Tests on: push: branches: @@ -32,7 +32,7 @@ jobs: if: github.event.pull_request.draft == false strategy: matrix: - python-version: ["3.10", "3.11"] # add 3.12 once gensim supports it. TODO: watch this issue - https://github.com/piskvorky/gensim/issues/3510 + python-version: ["3.13"] os: [ubuntu-latest, windows-latest] fail-fast: false # Continue running all jobs even if one fails env: @@ -67,16 +67,7 @@ jobs: - name: Install dependencies shell: bash run: | - uv sync - uv pip install gensim - - - name: Check - run: | - uv run poe check - - - name: Build - run: | - uv build + uv sync --all-packages - name: Unit Test run: | diff --git a/.gitignore b/.gitignore index f050557b3a..77ee0a6eb8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ output/lancedb venv/ .conda .tmp +packages/graphrag-llm/notebooks/metrics +packages/graphrag-llm/notebooks/cache .env build.zip @@ -58,3 +60,6 @@ docsite/ # Jupyter notebook .ipynb_checkpoints/ + +# Root build assets +packages/*/LICENSE diff --git a/.semversioner/next-release/major-20260123143225940955.json b/.semversioner/next-release/major-20260123143225940955.json new file mode 100644 index 0000000000..76089e1e50 --- /dev/null +++ b/.semversioner/next-release/major-20260123143225940955.json @@ -0,0 +1,4 @@ +{ + "type": "major", + "description": "Monorepo restructure\n\n New Packages:\n - graphrag-cache\n - graphrag-chunking\n - graphrag-common\n - graphrag-input\n - graphrag-llm\n - graphrag-storage\n - graphrag-vectors\n\n Changes:\n - New config: run graphrag init --force to reinitialize config with new layout and options." +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 9434ab0ef5..a2e2254c84 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,19 @@ "args": [ "index", "--root", - "" + "${input:root_folder}" + ], + "console": "integratedTerminal" + }, + { + "name": "Update", + "type": "debugpy", + "request": "launch", + "module": "graphrag", + "args": [ + "update", + "--root", + "${input:root_folder}" ], "console": "integratedTerminal" }, @@ -21,10 +33,10 @@ "module": "graphrag", "args": [ "query", + "${input:query}", "--root", - "", - "--method", "basic", - "--query", "What are the top themes in this story", + "${input:root_folder}", + "--method", "${input:query_method}" ] }, { @@ -35,7 +47,7 @@ "args": [ "poe", "prompt-tune", "--config", - "/settings.yaml", + "${input:root_folder}/settings.yaml", ] }, { @@ -74,5 +86,22 @@ "console": "integratedTerminal", "justMyCode": false }, - ] + ], + "inputs": [ + { + "id": "root_folder", + "type": "promptString", + "description": "Enter the root folder path" + }, + { + "id": "query_method", + "type": "promptString", + "description": "Enter the query method (e.g., 'global', 'local')" + }, + { + "id": "query", + "type": "promptString", + "description": "Enter the query text" + } + ] } \ No newline at end of file diff --git a/.vsts-ci.yml b/.vsts-ci.yml index 2a1e0964ce..5299ff91e3 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -10,7 +10,7 @@ trigger: variables: isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')] - pythonVersion: "3.10" + pythonVersion: "3.13" poetryVersion: "1.6.1" nodeVersion: "18.x" artifactsFullFeedName: "Resilience/resilience_python" diff --git a/breaking-changes.md b/breaking-changes.md index 4c0505d6dd..93430cbfe4 100644 --- a/breaking-changes.md +++ b/breaking-changes.md @@ -12,6 +12,35 @@ There are five surface areas that may be impacted on any given release. They are > TL;DR: Always run `graphrag init --path [path] --force` between minor version bumps to ensure you have the latest config format. Run the provided migration notebook between major version bumps if you want to avoid re-indexing prior datasets. Note that this will overwrite your configuration and prompts, so backup if necessary. +# v3 +Run the [migration notebook](./docs/examples_notebooks/index_migration_to_v3.ipynb) to convert older tables to the v3 format. Our main goals with v3 were to slim down the core library to minimize long-term maintenance of features that are either largely unused or should have been out of scope for a long time anyway. + +## Data Model +We made minimal data model changes that will affect your index for v3. The primary breaking change is that we removed a rarely-used document-grouping capability that resulted in the `text_units` table having a `document_ids` column with a list instead of a single entry in a column called `document_id`. v3 fixes that, and the migration notebook applies the change so you don't need to re-index. + +Most of the other changes we made are removal of fields that are no longer used or are out of scope. For example, we removed the UMAP step that generates x/y coordinates for the entities - new indexes will not produce these columns, but they won't hurt anything if they are in your existing tables. + +## API +We have removed the multi-search variant from each search method in the API. + +## Config + +We did make several changes to the configuration model. The best way forward is to re-run `init`, which we always recommend for minor and major version bumps. + +This is a summary of changes: +- Removed fnllm as underlying model manager, so the model types "openai_chat", "azure_openai_chat", "openai_embedding", and "azure_openai_embedding" are all invalid. Use "chat" or "embedding". +- fnllm also had an experimental rate limiting "auto" setting, which is no longer allowed. Use `null` in your config as a default, or set explicit limits to tpm/rpm. +- LiteLLM does require a model_provider, so add yours as appropriate. For example, if you previously used "openai_chat" for your model type, this would be "openai", and for "azure_openai_chat" this would be "azure". +- Collapsed the `vector_store` dict into a single root-level object. This is because we no longer support multi-search, and this dict required a lot of downstream complexity for that single use case. +- Removed the `outputs` block that was also only used for multi-search. +- Most workflows had an undocumented `strategy` config dict that allowed fine tuning of internal settings. These fine tunings are never used and had associated complexity, so we removed it. +- Vector store configuration now allows custom schema per embedded field. This overrides the need for the `container_name` prefix, which caused confusion anyway. Now, the default container name will simply be the embedded field name - if you need something custom, add the `index_schema` block and populate as needed. +- We previously supported the ability to embed any text field in the data model. However, we only ever use text_unit_text, entity_description, and community_full_content, so all others have been removed. +- Removed the `umap` and `embed_graph` blocks which were only used to add x/y fields to the entities. This fixed a long-standing dependency issue with graspologic. If you need x/y positions, see the [visualization guide](https://microsoft.github.io/graphrag/visualization_guide/) for using gephi. +- Removed file filtering from input document loading. This was essentially unused. +- Removed the groupby ability for text chunking. This was intended to allow short documents to be grouped before chunking, but is never used and added a bunch of complexity to the chunking process. + + # v2 Run the [migration notebook](./docs/examples_notebooks/index_migration_to_v2.ipynb) to convert older tables to the v2 format. diff --git a/dictionary.txt b/dictionary.txt index 32c6f1ebe4..5d30e2e850 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -26,6 +26,7 @@ noqa dtypes ints genid +isinstance # Azure abfs diff --git a/docs/config/env_vars.md b/docs/config/env_vars.md deleted file mode 100644 index 004b3757e1..0000000000 --- a/docs/config/env_vars.md +++ /dev/null @@ -1,219 +0,0 @@ -# Default Configuration Mode (using Env Vars) - -As of version 1.3, GraphRAG no longer supports a full complement of pre-built environment variables. Instead, we support variable replacement within the [settings.yml file](yaml.md) so you can specify any environment variables you like. - -The only standard environment variable we expect, and include in the default settings.yml, is `GRAPHRAG_API_KEY`. If you are already using a number of the previous GRAPHRAG_* environment variables, you can insert them with template syntax into settings.yml and they will be adopted. - -> **The environment variables below are documented as an aid for migration, but they WILL NOT be read unless you use template syntax in your settings.yml. We also WILL NOT be updating this page as the main config object changes.** - ---- - -### Text-Embeddings Customization - -By default, the GraphRAG indexer will only export embeddings required for our query methods. However, the model has embeddings defined for all plaintext fields, and these can be generated by setting the `GRAPHRAG_EMBEDDING_TARGET` environment variable to `all`. - -#### Embedded Fields - -- `text_unit.text` -- `document.text` -- `entity.title` -- `entity.description` -- `relationship.description` -- `community.title` -- `community.summary` -- `community.full_content` - -### Input Data - -Our pipeline can ingest .csv or .txt data from an input folder. These files can be nested within subfolders. To configure how input data is handled, what fields are mapped over, and how timestamps are parsed, look for configuration values starting with `GRAPHRAG_INPUT_` below. In general, CSV-based data provides the most customizability. Each CSV should at least contain a `text` field (which can be mapped with environment variables), but it's helpful if they also have `title`, `timestamp`, and `source` fields. Additional fields can be included as well, which will land as extra fields on the `Document` table. - -### Base LLM Settings - -These are the primary settings for configuring LLM connectivity. - -| Parameter | Required? | Description | Type | Default Value | -| --------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----- | ------------- | -| `GRAPHRAG_API_KEY` | **Yes for OpenAI. Optional for AOAI** | The API key. (Note: `OPENAI_API_KEY` is also used as a fallback). If not defined when using AOAI, managed identity will be used. | `str` | `None` | -| `GRAPHRAG_API_BASE` | **For AOAI** | The API Base URL | `str` | `None` | -| `GRAPHRAG_API_VERSION` | **For AOAI** | The AOAI API version. | `str` | `None` | -| `GRAPHRAG_API_ORGANIZATION` | | The AOAI organization. | `str` | `None` | -| `GRAPHRAG_API_PROXY` | | The AOAI proxy. | `str` | `None` | - -### Text Generation Settings - -These settings control the text generation model used by the pipeline. Any settings with a fallback will use the base LLM settings, if available. - -| Parameter | Required? | Description | Type | Default Value | -| ------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------ | ------- | --------------------- | -| `GRAPHRAG_LLM_TYPE` | **For AOAI** | The LLM operation type. Either `openai_chat` or `azure_openai_chat` | `str` | `openai_chat` | -| `GRAPHRAG_LLM_DEPLOYMENT_NAME` | **For AOAI** | The AOAI model deployment name. | `str` | `None` | -| `GRAPHRAG_LLM_API_KEY` | Yes (uses fallback) | The API key. If not defined when using AOAI, managed identity will be used. | `str` | `None` | -| `GRAPHRAG_LLM_API_BASE` | For AOAI (uses fallback) | The API Base URL | `str` | `None` | -| `GRAPHRAG_LLM_API_VERSION` | For AOAI (uses fallback) | The AOAI API version. | `str` | `None` | -| `GRAPHRAG_LLM_API_ORGANIZATION` | For AOAI (uses fallback) | The AOAI organization. | `str` | `None` | -| `GRAPHRAG_LLM_API_PROXY` | | The AOAI proxy. | `str` | `None` | -| `GRAPHRAG_LLM_MODEL` | | The LLM model. | `str` | `gpt-4-turbo-preview` | -| `GRAPHRAG_LLM_MAX_TOKENS` | | The maximum number of tokens. | `int` | `4000` | -| `GRAPHRAG_LLM_REQUEST_TIMEOUT` | | The maximum number of seconds to wait for a response from the chat client. | `int` | `180` | -| `GRAPHRAG_LLM_MODEL_SUPPORTS_JSON` | | Indicates whether the given model supports JSON output mode. `True` to enable. | `str` | `None` | -| `GRAPHRAG_LLM_THREAD_COUNT` | | The number of threads to use for LLM parallelization. | `int` | 50 | -| `GRAPHRAG_LLM_THREAD_STAGGER` | | The time to wait (in seconds) between starting each thread. | `float` | 0.3 | -| `GRAPHRAG_LLM_CONCURRENT_REQUESTS` | | The number of concurrent requests to allow for the embedding client. | `int` | 25 | -| `GRAPHRAG_LLM_TOKENS_PER_MINUTE` | | The number of tokens per minute to allow for the LLM client. 0 = Bypass | `int` | 0 | -| `GRAPHRAG_LLM_REQUESTS_PER_MINUTE` | | The number of requests per minute to allow for the LLM client. 0 = Bypass | `int` | 0 | -| `GRAPHRAG_LLM_MAX_RETRIES` | | The maximum number of retries to attempt when a request fails. | `int` | 10 | -| `GRAPHRAG_LLM_MAX_RETRY_WAIT` | | The maximum number of seconds to wait between retries. | `int` | 10 | -| `GRAPHRAG_LLM_SLEEP_ON_RATE_LIMIT_RECOMMENDATION` | | Whether to sleep on rate limit recommendation. (Azure Only) | `bool` | `True` | -| `GRAPHRAG_LLM_TEMPERATURE` | | The temperature to use generation. | `float` | 0 | -| `GRAPHRAG_LLM_TOP_P` | | The top_p to use for sampling. | `float` | 1 | -| `GRAPHRAG_LLM_N` | | The number of responses to generate. | `int` | 1 | - -### Text Embedding Settings - -These settings control the text embedding model used by the pipeline. Any settings with a fallback will use the base LLM settings, if available. - -| Parameter | Required ? | Description | Type | Default | -| ------------------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------ | -| `GRAPHRAG_EMBEDDING_TYPE` | **For AOAI** | The embedding client to use. Either `openai_embedding` or `azure_openai_embedding` | `str` | `openai_embedding` | -| `GRAPHRAG_EMBEDDING_DEPLOYMENT_NAME` | **For AOAI** | The AOAI deployment name. | `str` | `None` | -| `GRAPHRAG_EMBEDDING_API_KEY` | Yes (uses fallback) | The API key to use for the embedding client. If not defined when using AOAI, managed identity will be used. | `str` | `None` | -| `GRAPHRAG_EMBEDDING_API_BASE` | For AOAI (uses fallback) | The API base URL. | `str` | `None` | -| `GRAPHRAG_EMBEDDING_API_VERSION` | For AOAI (uses fallback) | The AOAI API version to use for the embedding client. | `str` | `None` | -| `GRAPHRAG_EMBEDDING_API_ORGANIZATION` | For AOAI (uses fallback) | The AOAI organization to use for the embedding client. | `str` | `None` | -| `GRAPHRAG_EMBEDDING_API_PROXY` | | The AOAI proxy to use for the embedding client. | `str` | `None` | -| `GRAPHRAG_EMBEDDING_MODEL` | | The model to use for the embedding client. | `str` | `text-embedding-3-small` | -| `GRAPHRAG_EMBEDDING_BATCH_SIZE` | | The number of texts to embed at once. [(Azure limit is 16)](https://learn.microsoft.com/en-us/azure/ai-ce) | `int` | 16 | -| `GRAPHRAG_EMBEDDING_BATCH_MAX_TOKENS` | | The maximum tokens per batch [(Azure limit is 8191)](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) | `int` | 8191 | -| `GRAPHRAG_EMBEDDING_TARGET` | | The target fields to embed. Either `required` or `all`. | `str` | `required` | | -| `GRAPHRAG_EMBEDDING_THREAD_COUNT` | | The number of threads to use for parallelization for embeddings. | `int` | | -| `GRAPHRAG_EMBEDDING_THREAD_STAGGER` | | The time to wait (in seconds) between starting each thread for embeddings. | `float` | 50 | -| `GRAPHRAG_EMBEDDING_CONCURRENT_REQUESTS` | | The number of concurrent requests to allow for the embedding client. | `int` | 25 | -| `GRAPHRAG_EMBEDDING_TOKENS_PER_MINUTE` | | The number of tokens per minute to allow for the embedding client. 0 = Bypass | `int` | 0 | -| `GRAPHRAG_EMBEDDING_REQUESTS_PER_MINUTE` | | The number of requests per minute to allow for the embedding client. 0 = Bypass | `int` | 0 | -| `GRAPHRAG_EMBEDDING_MAX_RETRIES` | | The maximum number of retries to attempt when a request fails. | `int` | 10 | -| `GRAPHRAG_EMBEDDING_MAX_RETRY_WAIT` | | The maximum number of seconds to wait between retries. | `int` | 10 | -| `GRAPHRAG_EMBEDDING_SLEEP_ON_RATE_LIMIT_RECOMMENDATION` | | Whether to sleep on rate limit recommendation. (Azure Only) | `bool` | `True` | - -### Input Settings - -These settings control the data input used by the pipeline. Any settings with a fallback will use the base LLM settings, if available. - -#### Plaintext Input Data (`GRAPHRAG_INPUT_FILE_TYPE`=text) - -| Parameter | Description | Type | Required or Optional | Default | -| ----------------------------- | --------------------------------------------------------------------------------- | ----- | -------------------- | ---------- | -| `GRAPHRAG_INPUT_FILE_PATTERN` | The file pattern regexp to use when reading input files from the input directory. | `str` | optional | `.*\.txt$` | - -#### CSV Input Data (`GRAPHRAG_INPUT_FILE_TYPE`=csv) - -| Parameter | Description | Type | Required or Optional | Default | -| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | -------------------- | ---------- | -| `GRAPHRAG_INPUT_TYPE` | The input storage type to use when reading files. (`file` or `blob`) | `str` | optional | `file` | -| `GRAPHRAG_INPUT_FILE_PATTERN` | The file pattern regexp to use when reading input files from the input directory. | `str` | optional | `.*\.txt$` | -| `GRAPHRAG_INPUT_TEXT_COLUMN` | The 'text' column to use when reading CSV input files. | `str` | optional | `text` | -| `GRAPHRAG_INPUT_METADATA` | A list of CSV columns, comma-separated, to incorporate as JSON in a metadata column. | `str` | optional | `None` | -| `GRAPHRAG_INPUT_TITLE_COLUMN` | The 'title' column to use when reading CSV input files. | `str` | optional | `title` | -| `GRAPHRAG_INPUT_STORAGE_ACCOUNT_BLOB_URL` | The Azure Storage blob endpoint to use when in `blob` mode and using managed identity. Will have the format `https://.blob.core.windows.net` | `str` | optional | `None` | -| `GRAPHRAG_INPUT_CONNECTION_STRING` | The connection string to use when reading CSV input files from Azure Blob Storage. | `str` | optional | `None` | -| `GRAPHRAG_INPUT_CONTAINER_NAME` | The container name to use when reading CSV input files from Azure Blob Storage. | `str` | optional | `None` | -| `GRAPHRAG_INPUT_BASE_DIR` | The base directory to read input files from. | `str` | optional | `None` | - -### Data Mapping Settings - -| Parameter | Description | Type | Required or Optional | Default | -| -------------------------- | -------------------------------------------------------- | ----- | -------------------- | ------- | -| `GRAPHRAG_INPUT_FILE_TYPE` | The type of input data, `csv` or `text` | `str` | optional | `text` | -| `GRAPHRAG_INPUT_ENCODING` | The encoding to apply when reading CSV/text input files. | `str` | optional | `utf-8` | - -### Data Chunking - -| Parameter | Description | Type | Required or Optional | Default | -| ------------------------------- | ------------------------------------------------------------------------------------------- | ----- | -------------------- | ----------------------------- | -| `GRAPHRAG_CHUNK_SIZE` | The chunk size in tokens for text-chunk analysis windows. | `str` | optional | 1200 | -| `GRAPHRAG_CHUNK_OVERLAP` | The chunk overlap in tokens for text-chunk analysis windows. | `str` | optional | 100 | -| `GRAPHRAG_CHUNK_BY_COLUMNS` | A comma-separated list of document attributes to groupby when performing TextUnit chunking. | `str` | optional | `id` | -| `GRAPHRAG_CHUNK_ENCODING_MODEL` | The encoding model to use for chunking. | `str` | optional | The top-level encoding model. | - -### Prompting Overrides - -| Parameter | Description | Type | Required or Optional | Default | -| --------------------------------------------- | ------------------------------------------------------------------------------------------ | -------- | -------------------- | ---------------------------------------------------------------- | -| `GRAPHRAG_ENTITY_EXTRACTION_PROMPT_FILE` | The path (relative to the root) of an entity extraction prompt template text file. | `str` | optional | `None` | -| `GRAPHRAG_ENTITY_EXTRACTION_MAX_GLEANINGS` | The maximum number of redrives (gleanings) to invoke when extracting entities in a loop. | `int` | optional | 1 | -| `GRAPHRAG_ENTITY_EXTRACTION_ENTITY_TYPES` | A comma-separated list of entity types to extract. | `str` | optional | `organization,person,event,geo` | -| `GRAPHRAG_ENTITY_EXTRACTION_ENCODING_MODEL` | The encoding model to use for entity extraction. | `str` | optional | The top-level encoding model. | -| `GRAPHRAG_SUMMARIZE_DESCRIPTIONS_PROMPT_FILE` | The path (relative to the root) of an description summarization prompt template text file. | `str` | optional | `None` | -| `GRAPHRAG_SUMMARIZE_DESCRIPTIONS_MAX_LENGTH` | The maximum number of tokens to generate per description summarization. | `int` | optional | 500 | -| `GRAPHRAG_CLAIM_EXTRACTION_ENABLED` | Whether claim extraction is enabled for this pipeline. | `bool` | optional | `False` | -| `GRAPHRAG_CLAIM_EXTRACTION_DESCRIPTION` | The claim_description prompting argument to utilize. | `string` | optional | "Any claims or facts that could be relevant to threat analysis." | -| `GRAPHRAG_CLAIM_EXTRACTION_PROMPT_FILE` | The claim extraction prompt to utilize. | `string` | optional | `None` | -| `GRAPHRAG_CLAIM_EXTRACTION_MAX_GLEANINGS` | The maximum number of redrives (gleanings) to invoke when extracting claims in a loop. | `int` | optional | 1 | -| `GRAPHRAG_CLAIM_EXTRACTION_ENCODING_MODEL` | The encoding model to use for claim extraction. | `str` | optional | The top-level encoding model | -| `GRAPHRAG_COMMUNITY_REPORTS_PROMPT_FILE` | The community reports extraction prompt to utilize. | `string` | optional | `None` | -| `GRAPHRAG_COMMUNITY_REPORTS_MAX_LENGTH` | The maximum number of tokens to generate per community reports. | `int` | optional | 1500 | - -### Storage - -This section controls the storage mechanism used by the pipeline used for exporting output tables. - -| Parameter | Description | Type | Required or Optional | Default | -| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | -------------------- | ------- | -| `GRAPHRAG_STORAGE_TYPE` | The type of storage to use. Options are `file`, `memory`, or `blob` | `str` | optional | `file` | -| `GRAPHRAG_STORAGE_STORAGE_ACCOUNT_BLOB_URL` | The Azure Storage blob endpoint to use when in `blob` mode and using managed identity. Will have the format `https://.blob.core.windows.net` | `str` | optional | None | -| `GRAPHRAG_STORAGE_CONNECTION_STRING` | The Azure Storage connection string to use when in `blob` mode. | `str` | optional | None | -| `GRAPHRAG_STORAGE_CONTAINER_NAME` | The Azure Storage container name to use when in `blob` mode. | `str` | optional | None | -| `GRAPHRAG_STORAGE_BASE_DIR` | The base path to data outputs outputs. | `str` | optional | None | - -### Cache - -This section controls the cache mechanism used by the pipeline. This is used to cache LLM invocation results. - -| Parameter | Description | Type | Required or Optional | Default | -| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | -------------------- | ------- | -| `GRAPHRAG_CACHE_TYPE` | The type of cache to use. Options are `file`, `memory`, `none` or `blob` | `str` | optional | `file` | -| `GRAPHRAG_CACHE_STORAGE_ACCOUNT_BLOB_URL` | The Azure Storage blob endpoint to use when in `blob` mode and using managed identity. Will have the format `https://.blob.core.windows.net` | `str` | optional | None | -| `GRAPHRAG_CACHE_CONNECTION_STRING` | The Azure Storage connection string to use when in `blob` mode. | `str` | optional | None | -| `GRAPHRAG_CACHE_CONTAINER_NAME` | The Azure Storage container name to use when in `blob` mode. | `str` | optional | None | -| `GRAPHRAG_CACHE_BASE_DIR` | The base path to the cache files. | `str` | optional | None | - -### Reporting - -This section controls the reporting mechanism used by the pipeline, for common events and error messages. The default is to write reports to a file in the output directory. However, you can also choose to write reports to an Azure Blob Storage container. - -| Parameter | Description | Type | Required or Optional | Default | -| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | -------------------- | ------- | -| `GRAPHRAG_REPORTING_TYPE` | The type of reporter to use. Options are `file` or `blob` | `str` | optional | `file` | -| `GRAPHRAG_REPORTING_STORAGE_ACCOUNT_BLOB_URL` | The Azure Storage blob endpoint to use when in `blob` mode and using managed identity. Will have the format `https://.blob.core.windows.net` | `str` | optional | None | -| `GRAPHRAG_REPORTING_CONNECTION_STRING` | The Azure Storage connection string to use when in `blob` mode. | `str` | optional | None | -| `GRAPHRAG_REPORTING_CONTAINER_NAME` | The Azure Storage container name to use when in `blob` mode. | `str` | optional | None | -| `GRAPHRAG_REPORTING_BASE_DIR` | The base path to the reporting outputs. | `str` | optional | None | - -### Node2Vec Parameters - -| Parameter | Description | Type | Required or Optional | Default | -| ------------------------------- | ---------------------------------------- | ------ | -------------------- | ------- | -| `GRAPHRAG_NODE2VEC_ENABLED` | Whether to enable Node2Vec | `bool` | optional | False | -| `GRAPHRAG_NODE2VEC_NUM_WALKS` | The Node2Vec number of walks to perform | `int` | optional | 10 | -| `GRAPHRAG_NODE2VEC_WALK_LENGTH` | The Node2Vec walk length | `int` | optional | 40 | -| `GRAPHRAG_NODE2VEC_WINDOW_SIZE` | The Node2Vec window size | `int` | optional | 2 | -| `GRAPHRAG_NODE2VEC_ITERATIONS` | The number of iterations to run node2vec | `int` | optional | 3 | -| `GRAPHRAG_NODE2VEC_RANDOM_SEED` | The random seed to use for node2vec | `int` | optional | 597832 | - -### Data Snapshotting - -| Parameter | Description | Type | Required or Optional | Default | -| -------------------------------------- | ----------------------------------------------- | ------ | -------------------- | ------- | -| `GRAPHRAG_SNAPSHOT_EMBEDDINGS` | Whether to enable embeddings snapshots. | `bool` | optional | False | -| `GRAPHRAG_SNAPSHOT_GRAPHML` | Whether to enable GraphML snapshots. | `bool` | optional | False | -| `GRAPHRAG_SNAPSHOT_RAW_ENTITIES` | Whether to enable raw entity snapshots. | `bool` | optional | False | -| `GRAPHRAG_SNAPSHOT_TOP_LEVEL_NODES` | Whether to enable top-level node snapshots. | `bool` | optional | False | -| `GRAPHRAG_SNAPSHOT_TRANSIENT` | Whether to enable transient table snapshots. | `bool` | optional | False | - -# Miscellaneous Settings - -| Parameter | Description | Type | Required or Optional | Default | -| --------------------------- | --------------------------------------------------------------------- | ------ | -------------------- | ------------- | -| `GRAPHRAG_ASYNC_MODE` | Which async mode to use. Either `asyncio` or `threaded`. | `str` | optional | `asyncio` | -| `GRAPHRAG_ENCODING_MODEL` | The text encoding model, used in tiktoken, to encode text. | `str` | optional | `cl100k_base` | -| `GRAPHRAG_MAX_CLUSTER_SIZE` | The maximum number of entities to include in a single Leiden cluster. | `int` | optional | 10 | -| `GRAPHRAG_UMAP_ENABLED` | Whether to enable UMAP layouts | `bool` | optional | False | diff --git a/docs/config/models.md b/docs/config/models.md index eaa4eea0e7..0339b7f235 100644 --- a/docs/config/models.md +++ b/docs/config/models.md @@ -6,78 +6,62 @@ This page contains information on selecting a model to use and options to supply GraphRAG was built and tested using OpenAI models, so this is the default model set we support. This is not intended to be a limiter or statement of quality or fitness for your use case, only that it's the set we are most familiar with for prompting, tuning, and debugging. -GraphRAG also utilizes a language model wrapper library used by several projects within our team, called fnllm. fnllm provides two important functions for GraphRAG: rate limiting configuration to help us maximize throughput for large indexing jobs, and robust caching of API calls to minimize consumption on repeated indexes for testing, experimentation, or incremental ingest. fnllm uses the OpenAI Python SDK under the covers, so OpenAI-compliant endpoints are a base requirement out-of-the-box. +GraphRAG uses [LiteLLM](https://docs.litellm.ai/) for calling language models. LiteLLM provides support for 100+ models though it is important to note that when choosing a model it must support returning [structured outputs](https://openai.com/index/introducing-structured-outputs-in-the-api/) adhering to a [JSON schema](https://docs.litellm.ai/docs/completion/json_mode). -Starting with version 2.6.0, GraphRAG supports using [LiteLLM](https://docs.litellm.ai/) instead of fnllm for calling language models. LiteLLM provides support for 100+ models though it is important to note that when choosing a model it must support returning [structured outputs](https://openai.com/index/introducing-structured-outputs-in-the-api/) adhering to a [JSON schema](https://docs.litellm.ai/docs/completion/json_mode). - -Example using LiteLLm as the language model tool for GraphRAG: +Example using LiteLLM as the language model manager for GraphRAG: ```yaml -models: - default_chat_model: - type: chat - auth_type: api_key - api_key: ${GEMINI_API_KEY} +completion_models: + default_completion_model: model_provider: gemini model: gemini-2.5-flash-lite - default_embedding_model: - type: embedding - auth_type: api_key + auth_method: api_key api_key: ${GEMINI_API_KEY} + +embedding_models: + default_embedding_model: model_provider: gemini model: gemini-embedding-001 + auth_method: api_key + api_key: ${GEMINI_API_KEY} ``` -To use LiteLLM one must - -- Set `type` to either `chat` or `embedding`. -- Provide a `model_provider`, e.g., `openai`, `azure`, `gemini`, etc. -- Set the `model` to a one supported by the `model_provider`'s API. -- Provide a `deployment_name` if using `azure` as the `model_provider`. - -See [Detailed Configuration](yaml.md) for more details on configuration. [View LiteLLm basic usage](https://docs.litellm.ai/docs/#basic-usage) for details on how models are called (The `model_provider` is the portion prior to `/` while the `model` is the portion following the `/`). +See [Detailed Configuration](yaml.md) for more details on configuration. [View LiteLLM basic usage](https://docs.litellm.ai/docs/#basic-usage) for details on how models are called (The `model_provider` is the portion prior to `/` while the `model` is the portion following the `/`). ## Model Selection Considerations -GraphRAG has been most thoroughly tested with the gpt-4 series of models from OpenAI, including gpt-4 gpt-4-turbo, gpt-4o, and gpt-4o-mini. Our [arXiv paper](https://arxiv.org/abs/2404.16130), for example, performed quality evaluation using gpt-4-turbo. As stated above, non-OpenAI models are now supported with GraphRAG 2.6.0 and onwards through the use of LiteLLM but the suite of gpt-4 series of models from OpenAI remain the most tested and supported suite of models for GraphRAG. +GraphRAG has been most thoroughly tested with the gpt-4 series of models from OpenAI, including gpt-4 gpt-4-turbo, gpt-4o, and gpt-4o-mini. Our [arXiv paper](https://arxiv.org/abs/2404.16130), for example, performed quality evaluation using gpt-4-turbo. As stated above, non-OpenAI models are supported through the use of LiteLLM but the suite of gpt-4 series of models from OpenAI remain the most tested and supported suite of models for GraphRAG – in other words, these are the models we know best and can help resolve issues with. Versions of GraphRAG before 2.2.0 made extensive use of `max_tokens` and `logit_bias` to control generated response length or content. The introduction of the o-series of models added new, non-compatible parameters because these models include a reasoning component that has different consumption patterns and response generation attributes than non-reasoning models. GraphRAG 2.2.0 now supports these models, but there are important differences that need to be understood before you switch. - Previously, GraphRAG used `max_tokens` to limit responses in a few locations. This is done so that we can have predictable content sizes when building downstream context windows for summarization. We have now switched from using `max_tokens` to use a prompted approach, which is working well in our tests. We suggest using `max_tokens` in your language model config only for budgetary reasons if you want to limit consumption, and not for expected response length control. We now also support the o-series equivalent `max_completion_tokens`, but if you use this keep in mind that there may be some unknown fixed reasoning consumption amount in addition to the response tokens, so it is not a good technique for response control. - Previously, GraphRAG used a combination of `max_tokens` and `logit_bias` to strictly control a binary yes/no question during gleanings. This is not possible with reasoning models, so again we have switched to a prompted approach. Our tests with gpt-4o, gpt-4o-mini, and o1 show that this works consistently, but could have issues if you have an older or smaller model. - The o-series models are much slower and more expensive. It may be useful to use an asymmetric approach to model use in your config: you can define as many models as you like in the `models` block of your settings.yaml and reference them by key for every workflow that requires a language model. You could use gpt-4o for indexing and o1 for query, for example. Experiment to find the right balance of cost, speed, and quality for your use case. -- The o-series models contain a form of native native chain-of-thought reasoning that is absent in the non-o-series models. GraphRAG's prompts sometimes contain CoT because it was an effective technique with the gpt-4* series. It may be counterproductive with the o-series, so you may want to tune or even re-write large portions of the prompt templates (particularly for graph and claim extraction). +- The o-series models contain a form of native native chain-of-thought reasoning that is absent in the non-o-series models. GraphRAG's prompts sometimes contain CoT because it was an effective technique with the gpt-4\* series. It may be counterproductive with the o-series, so you may want to tune or even re-write large portions of the prompt templates (particularly for graph and claim extraction). Example config with asymmetric model use: ```yaml -models: - extraction_chat_model: - api_key: ${GRAPHRAG_API_KEY} - type: openai_chat - auth_type: api_key +completion_models: + extraction_completion_model: + model_provider: openai model: gpt-4o - model_supports_json: true - query_chat_model: + auth_method: api_key api_key: ${GRAPHRAG_API_KEY} - type: openai_chat - auth_type: api_key + query_completion_model: + model_provider: openai model: o1 - model_supports_json: true - + auth_method: api_key + api_key: ${GRAPHRAG_API_KEY} ... - extract_graph: - model_id: extraction_chat_model + completion_model_id: extraction_completion_model prompt: "prompts/extract_graph.txt" - entity_types: [organization,person,geo,event] + entity_types: [organization, person, geo, event] max_gleanings: 1 - ... - - global_search: - chat_model_id: query_chat_model + completion_model_id: query_completion_model map_prompt: "prompts/global_search_map_system_prompt.txt" reduce_prompt: "prompts/global_search_reduce_system_prompt.txt" knowledge_prompt: "prompts/global_search_knowledge_system_prompt.txt" @@ -85,9 +69,9 @@ global_search: Another option would be to avoid using a language model at all for the graph extraction, instead using the `fast` [indexing method](../index/methods.md) that uses NLP for portions of the indexing phase in lieu of LLM APIs. -## Using Non-OpenAI Models +## Using Custom Models -As shown above, non-OpenAI models may be used via LiteLLM starting with GraphRAG version 2.6.0 but cases may still exist in which some users wish to use models not supported by LiteLLM. There are two approaches one can use to connect to unsupported models: +LiteLLM supports hundreds of models, but cases may still exist in which some users wish to use models not supported by LiteLLM. There are two approaches one can use to connect to unsupported models: ### Proxy APIs @@ -95,36 +79,37 @@ Many users have used platforms such as [ollama](https://ollama.com/) and [LiteLL ### Model Protocol -As of GraphRAG 2.0.0, we support model injection through the use of a standard chat and embedding Protocol and an accompanying ModelFactory that you can use to register your model implementation. This is not supported with the CLI, so you'll need to use GraphRAG as a library. +We support model injection through the use of a standard completion and embedding Protocol and accompanying factories that you can use to register your model implementation. This is not supported with the CLI, so you'll need to use GraphRAG as a library. -- Our Protocol is [defined here](https://github.com/microsoft/graphrag/blob/main/graphrag/language_model/protocol/base.py) -- Our base implementation, which wraps fnllm, [is here](https://github.com/microsoft/graphrag/blob/main/graphrag/language_model/providers/fnllm/models.py) -- We have a simple mock implementation in our tests that you can [reference here](https://github.com/microsoft/graphrag/blob/main/tests/mock_provider.py) +- Our Protocol is [defined here](https://github.com/microsoft/graphrag/blob/main/packages/graphrag-llm/graphrag_llm/completion/completion.py) +- We have a simple mock implementation in our tests that you can [reference here](https://github.com/microsoft/graphrag/blob/main/packages/graphrag-llm/graphrag_llm/completion/mock_llm_completion.py) -Once you have a model implementation, you need to register it with our ModelFactory: +Once you have a model implementation, you need to register it with our completion model factory or embedding model factory: ```python -class MyCustomModel: +from graphrag_llm.completion import LLMCompletion, register_completion + +class MyCustomCompletionModel(LLMCompletion): ... # implementation # elsewhere... -ModelFactory.register_chat("my-custom-chat-model", lambda **kwargs: MyCustomModel(**kwargs)) +register_completion("my-custom-completion-model", MyCustomCompletionModel) ``` Then in your config you can reference the type name you used: ```yaml -models: - default_chat_model: - type: my-custom-chat-model - +completion_models: + default_completion_model: + type: my-custom-completion-model + ... extract_graph: - model_id: default_chat_model + completion_model_id: default_completion_model prompt: "prompts/extract_graph.txt" - entity_types: [organization,person,geo,event] + entity_types: [organization, person, geo, event] max_gleanings: 1 ``` -Note that your custom model will be passed the same params for init and method calls that we use throughout GraphRAG. There is not currently any ability to define custom parameters, so you may need to use closure scope or a factory pattern within your implementation to get custom config values. \ No newline at end of file +Note that your custom model will be passed the same params for init and method calls that we use throughout GraphRAG. There is not currently any ability to define custom parameters, so you may need to use closure scope or a factory pattern within your implementation to get custom config values. diff --git a/docs/config/overview.md b/docs/config/overview.md index 025f5d718e..278d939ce2 100644 --- a/docs/config/overview.md +++ b/docs/config/overview.md @@ -8,4 +8,3 @@ The default configuration mode is the simplest way to get started with the Graph - [Init command](init.md) (recommended first step) - [Edit settings.yaml for deeper control](yaml.md) -- [Purely using environment variables](env_vars.md) (not recommended) diff --git a/docs/config/yaml.md b/docs/config/yaml.md index 6440eb0520..38ceac652d 100644 --- a/docs/config/yaml.md +++ b/docs/config/yaml.md @@ -6,12 +6,12 @@ Many of these config values have defaults. Rather than replicate them here, plea For example: -``` +```bash # .env GRAPHRAG_API_KEY=some_api_key # settings.yml -llm: +default_chat_model: api_key: ${GRAPHRAG_API_KEY} ``` @@ -21,54 +21,54 @@ llm: ### models -This is a dict of model configurations. The dict key is used to reference this configuration elsewhere when a model instance is desired. In this way, you can specify as many different models as you need, and reference them differentially in the workflow steps. +This is a set of dicts, one for completion model configuration and one for embedding model configuration. The dict keys are used to reference the model configuration elsewhere when a model instance is desired. In this way, you can specify as many different models as you need, and reference them independently in the workflow steps. For example: + ```yml -models: - default_chat_model: +completion_models: + default_completion_model: + model_provider: openai + model: gpt-4.1 + auth_method: api_key api_key: ${GRAPHRAG_API_KEY} - type: openai_chat - model: gpt-4o - model_supports_json: true + +embedding_models: default_embedding_model: + model_provider: openai + model: text-embedding-3-large + auth_method: api_key api_key: ${GRAPHRAG_API_KEY} - type: openai_embedding - model: text-embedding-ada-002 ``` #### Fields -- `api_key` **str** - The OpenAI API key to use. -- `auth_type` **api_key|azure_managed_identity** - Indicate how you want to authenticate requests. -- `type` **chat**|**embedding**|**openai_chat|azure_openai_chat|openai_embedding|azure_openai_embedding|mock_chat|mock_embeddings** - The type of LLM to use. -- `model_provider` **str|None** - The model provider to use, e.g., openai, azure, anthropic, etc. Required when `type == chat|embedding`. When `type == chat|embedding`, [LiteLLM](https://docs.litellm.ai/) is used under the hood which has support for calling 100+ models. [View LiteLLm basic usage](https://docs.litellm.ai/docs/#basic-usage) for details on how models are called (The `model_provider` is the portion prior to `/` while the `model` is the portion following the `/`). [View Language Model Selection](models.md) for more details and examples on using LiteLLM. +- `type` **litellm|mock** - The type of LLM provider to use. GraphRAG uses [LiteLLM](https://docs.litellm.ai/) for calling language models. +- `model_provider` **str** - The model provider to use, e.g., openai, azure, anthropic, etc. [LiteLLM](https://docs.litellm.ai/) is used under the hood which has support for calling 100+ models. [View LiteLLm basic usage](https://docs.litellm.ai/docs/#basic-usage) for details on how models are called (The `model_provider` is the portion prior to `/` while the `model` is the portion following the `/`). [View Language Model Selection](models.md) for more details and examples on using LiteLLM. - `model` **str** - The model name. -- `encoding_model` **str** - The text encoding model to use. Default is to use the encoding model aligned with the language model (i.e., it is retrieved from tiktoken if unset). -- `api_base` **str** - The API base url to use. -- `api_version` **str** - The API version. -- `deployment_name` **str** - The deployment name to use (Azure). -- `organization` **str** - The client organization. -- `proxy` **str** - The proxy URL to use. -- `audience` **str** - (Azure OpenAI only) The URI of the target Azure resource/service for which a managed identity token is requested. Used if `api_key` is not defined. Default=`https://cognitiveservices.azure.com/.default` -- `model_supports_json` **bool** - Whether the model supports JSON-mode output. -- `request_timeout` **float** - The per-request timeout. -- `tokens_per_minute` **int** - Set a leaky-bucket throttle on tokens-per-minute. -- `requests_per_minute` **int** - Set a leaky-bucket throttle on requests-per-minute. -- `retry_strategy` **str** - Retry strategy to use, "native" is the default and uses the strategy built into the OpenAI SDK. Other allowable values include "exponential_backoff", "random_wait", and "incremental_wait". -- `max_retries` **int** - The maximum number of retries to use. -- `max_retry_wait` **float** - The maximum backoff time. -- `concurrent_requests` **int** The number of open requests to allow at once. -- `async_mode` **asyncio|threaded** The async mode to use. Either `asyncio` or `threaded`. -- `responses` **list[str]** - If this model type is mock, this is a list of response strings to return. -- `n` **int** - The number of completions to generate. -- `max_tokens` **int** - The maximum number of output tokens. Not valid for o-series models. -- `temperature` **float** - The temperature to use. Not valid for o-series models. -- `top_p` **float** - The top-p value to use. Not valid for o-series models. -- `frequency_penalty` **float** - Frequency penalty for token generation. Not valid for o-series models. -- `presence_penalty` **float** - Frequency penalty for token generation. Not valid for o-series models. -- `max_completion_tokens` **int** - Max number of tokens to consume for chat completion. Must be large enough to include an unknown amount for "reasoning" by the model. o-series models only. -- `reasoning_effort` **low|medium|high** - Amount of "thought" for the model to expend reasoning about a response. o-series models only. +- `call_args`: **dict[str, Any]** - Default arguments to send with every model request. Example, `{"n": 5, "max_completion_tokens": 1000, "temperature": 1.5, "organization": "..." }` +- `api_key` **str|None** - The OpenAI API key to use. +- `api_base` **str|None** - The API base url to use. +- `api_version` **str|None** - The API version. +- `auth_method` **api_key|azure_managed_identity** - Indicate how you want to authenticate requests. +- `azure_deployment_name` **str|None** - The deployment name to use if your model is hosted on Azure. Note that if your deployment name on Azure matches the model name, this is unnecessary. +- retry **RetryConfig|None** - Retry settings. default=`None`, no retries. + - type **exponential_backoff|immediate** - Type of retry approach. default=`exponential_backoff` + - max_retries **int|None** - Max retries to take. default=`7`. + - base_delay **float|None** - Base delay when using `exponential_backoff`. default=`2.0`. + - jitter **bool|None** - Add jitter to retry delays when using `exponential_backoff`. default=`True` + - max_delay **float|None** - Maximum retry delay. default=`None`, no max. +- rate_limit **RateLimitConfig|None** - Rate limit settings. default=`None`, no rate limiting. + - type **sliding_window** - Type of rate limit approach. default=`sliding_window` + - period_in_seconds **int|None** - Window size for `sliding_window` rate limiting. default=`60`, limit requests per minute. + - requests_per_period **int|None** - Maximum number of requests per period. default=`None` + - tokens_per_period **int|None** - Maximum number of tokens per period. default=`None` +- metrics **MetricsConfig|None** - Metric settings. default=`MetricsConfig()`. View [metrics notebook](https://github.com/microsoft/graphrag/blob/main/packages/graphrag-llm/notebooks/04_metrics.ipynb) for more details on metrics. + - type **default** - The type of `MetricsProcessor` service to use for processing request metrics. default=`default` + - store **memory** - The type of `MetricsStore` service. default=`memory`. + - writer **log|file** - The type of `MetricsWriter` to use. Will write out metrics at the end of the process. default`log`, log metrics out using python standard logging at the end of the process. + - log_level **int|None** - The log level when using `log` writer. default=`20`, log `INFO` messages for metrics. + - base_dir **str|None** - The directory to write metrics to when using `file` writer. default=`Path.cwd()`. ## Input Files and Chunking @@ -79,33 +79,31 @@ Our pipeline can ingest .csv, .txt, or .json data from an input location. See th #### Fields - `storage` **StorageConfig** - - `type` **file|blob|cosmosdb** - The storage type to use. Default=`file` + - `type` **file|memory|blob|cosmosdb** - The storage type to use. Default=`file` + - `encoding`**str** - The encoding to use for file storage. - `base_dir` **str** - The base directory to write output artifacts to, relative to the root. - `connection_string` **str** - (blob/cosmosdb only) The Azure Storage connection string. - `container_name` **str** - (blob/cosmosdb only) The Azure Storage container name. - - `storage_account_blob_url` **str** - (blob only) The storage account blob URL to use. - - `cosmosdb_account_blob_url` **str** - (cosmosdb only) The CosmosDB account blob URL to use. -- `file_type` **text|csv|json** - The type of input data to load. Default is `text` + - `account_url` **str** - (blob only) The storage account blob URL to use. + - `database_name` **str** - (cosmosdb only) The database name to use. +- `type` **text|csv|json** - The type of input data to load. Default is `text` - `encoding` **str** - The encoding of the input file. Default is `utf-8` -- `file_pattern` **str** - A regex to match input files. Default is `.*\.csv$`, `.*\.txt$`, or `.*\.json$` depending on the specified `file_type`, but you can customize it if needed. -- `file_filter` **dict** - Key/value pairs to filter. Default is None. -- `text_column` **str** - (CSV/JSON only) The text column name. If unset we expect a column named `text`. -- `title_column` **str** - (CSV/JSON only) The title column name, filename will be used if unset. -- `metadata` **list[str]** - (CSV/JSON only) The additional document attributes fields to keep. +- `file_pattern` **str** - A regex to match input files. Default is `.*\.csv$`, `.*\.txt$`, or `.*\.json$` depending on the specified `type`, but you can customize it if needed. +- `id_column` **str** - The input ID column to use. +- `title_column` **str** - The input title column to use. +- `text_column` **str** - The input text column to use. -### chunks +### chunking These settings configure how we parse documents into text chunks. This is necessary because very large documents may not fit into a single context window, and graph extraction accuracy can be modulated. Also note the `metadata` setting in the input document config, which will replicate document metadata into each chunk. #### Fields +- `type` **tokens|sentence** - The chunking type to use. +- `encoding_model` **str** - The text encoding model to use for splitting on token boundaries. - `size` **int** - The max chunk size in tokens. - `overlap` **int** - The chunk overlap in tokens. -- `group_by_columns` **list[str]** - Group documents by these fields before chunking. -- `strategy` **str**[tokens|sentences] - How to chunk the text. -- `encoding_model` **str** - The text encoding model to use for splitting on token boundaries. -- `prepend_metadata` **bool** - Determines if metadata values should be added at the beginning of each chunk. Default=`False`. -- `chunk_size_includes_metadata` **bool** - Specifies whether the chunk size calculation should include metadata tokens. Default=`False`. +- `prepend_metadata` **list[str]** - Metadata fields from the source document to prepend on each chunk. ## Outputs and Storage @@ -116,24 +114,30 @@ This section controls the storage mechanism used by the pipeline used for export #### Fields - `type` **file|memory|blob|cosmosdb** - The storage type to use. Default=`file` +- `encoding`**str** - The encoding to use for file storage. - `base_dir` **str** - The base directory to write output artifacts to, relative to the root. - `connection_string` **str** - (blob/cosmosdb only) The Azure Storage connection string. - `container_name` **str** - (blob/cosmosdb only) The Azure Storage container name. -- `storage_account_blob_url` **str** - (blob only) The storage account blob URL to use. -- `cosmosdb_account_blob_url` **str** - (cosmosdb only) The CosmosDB account blob URL to use. +- `account_url` **str** - (blob only) The storage account blob URL to use. +- `database_name` **str** - (cosmosdb only) The database name to use. +- `type` **text|csv|json** - The type of input data to load. Default is `text` +- `encoding` **str** - The encoding of the input file. Default is `utf-8` -### update_index_output +### update_output_storage The section defines a secondary storage location for running incremental indexing, to preserve your original outputs. #### Fields - `type` **file|memory|blob|cosmosdb** - The storage type to use. Default=`file` +- `encoding`**str** - The encoding to use for file storage. - `base_dir` **str** - The base directory to write output artifacts to, relative to the root. - `connection_string` **str** - (blob/cosmosdb only) The Azure Storage connection string. - `container_name` **str** - (blob/cosmosdb only) The Azure Storage container name. -- `storage_account_blob_url` **str** - (blob only) The storage account blob URL to use. -- `cosmosdb_account_blob_url` **str** - (cosmosdb only) The CosmosDB account blob URL to use. +- `account_url` **str** - (blob only) The storage account blob URL to use. +- `database_name` **str** - (cosmosdb only) The database name to use. +- `type` **text|csv|json** - The type of input data to load. Default is `text` +- `encoding` **str** - The encoding of the input file. Default is `utf-8` ### cache @@ -141,12 +145,15 @@ This section controls the cache mechanism used by the pipeline. This is used to #### Fields -- `type` **file|memory|blob|cosmosdb** - The storage type to use. Default=`file` -- `base_dir` **str** - The base directory to write output artifacts to, relative to the root. -- `connection_string` **str** - (blob/cosmosdb only) The Azure Storage connection string. -- `container_name` **str** - (blob/cosmosdb only) The Azure Storage container name. -- `storage_account_blob_url` **str** - (blob only) The storage account blob URL to use. -- `cosmosdb_account_blob_url` **str** - (cosmosdb only) The CosmosDB account blob URL to use. +- `type` **json|memory|none** - The storage type to use. Default=`json` +- `storage` **StorageConfig** + - `type` **file|memory|blob|cosmosdb** - The storage type to use. Default=`file` + - `encoding`**str** - The encoding to use for file storage. + - `base_dir` **str** - The base directory to write output artifacts to, relative to the root. + - `connection_string` **str** - (blob/cosmosdb only) The Azure Storage connection string. + - `container_name` **str** - (blob/cosmosdb only) The Azure Storage container name. + - `account_url` **str** - (blob only) The storage account blob URL to use. + - `database_name` **str** - (cosmosdb only) The database name to use. ### reporting @@ -158,7 +165,7 @@ This section controls the reporting mechanism used by the pipeline, for common e - `base_dir` **str** - The base directory to write reports to, relative to the root. - `connection_string` **str** - (blob only) The Azure Storage connection string. - `container_name` **str** - (blob only) The Azure Storage container name. -- `storage_account_blob_url` **str** - The storage account blob URL to use. +- `account_url` **str** - The storage account blob URL to use. ### vector_store @@ -167,13 +174,41 @@ Where to put all vectors for the system. Configured for lancedb by default. This #### Fields - `type` **lancedb|azure_ai_search|cosmosdb** - Type of vector store. Default=`lancedb` -- `db_uri` **str** (only for lancedb) - The database uri. Default=`storage.base_dir/lancedb` -- `url` **str** (only for AI Search) - AI Search endpoint -- `api_key` **str** (optional - only for AI Search) - The AI Search api key to use. -- `audience` **str** (only for AI Search) - Audience for managed identity token if managed identity authentication is used. -- `container_name` **str** - The name of a vector container. This stores all indexes (tables) for a given dataset ingest. Default=`default` +- `db_uri` **str** (lancedb only) - The database uri. Default=`storage.base_dir/lancedb` +- `url` **str** (blob/cosmosdb only) - Database / AI Search to be used. +- `api_key` **str** (optional - AI Search only) - The AI Search api key to use. +- `audience` **str** (AI Search only) - Audience for managed identity token if managed identity authentication is used. +- `connection_string` **str** - (cosmosdb only) The Azure Storage connection string. - `database_name` **str** - (cosmosdb only) Name of the database. -- `overwrite` **bool** (only used at index creation time) - Overwrite collection if it exist. Default=`True` + +- `index_schema` **dict[str, dict[str, str]]** (optional) - Enables customization for each of your embeddings. + - ``: + - `index_name` **str**: (optional) - Name for the specific embedding index table. + - `id_field` **str**: (optional) - Field name to be used as id. Default=`id` + - `vector_field` **str**: (optional) - Field name to be used as vector. Default=`vector` + - `vector_size` **int**: (optional) - Vector size for the embeddings. Default=`3072` + +The supported embeddings are: + +- `text_unit_text` +- `entity_description` +- `community_full_content` + +For example: + +```yaml +vector_store: + type: lancedb + db_uri: output/lancedb + index_schema: + text_unit_text: + index_name: "text-unit-embeddings" + id_field: "id_custom" + vector_field: "vector_custom" + vector_size: 3072 + entity_description: + id_field: "id_custom" +``` ## Workflow Configurations @@ -189,19 +224,14 @@ By default, the GraphRAG indexer will only export embeddings required for our qu Supported embeddings names are: -- `text_unit.text` -- `document.text` -- `entity.title` -- `entity.description` -- `relationship.description` -- `community.title` -- `community.summary` -- `community.full_content` +- `text_unit_text` +- `entity_description` +- `community_full_content` #### Fields -- `model_id` **str** - Name of the model definition to use for text embedding. -- `vector_store_id` **str** - Name of vector store definition to write to. +- `embedding_model_id` **str** - Name of the model definition to use for text embedding. +- `model_instance_name` **str** - Name of the model singleton instance. Default is "text_embedding". This primarily affects the cache storage partitioning. - `batch_size` **int** - The maximum batch size to use. - `batch_max_tokens` **int** - The maximum batch # of tokens. - `names` **list[str]** - List of the embeddings names to run (must be in supported list). @@ -212,7 +242,8 @@ Tune the language model-based graph extraction process. #### Fields -- `model_id` **str** - Name of the model definition to use for API calls. +- `completion_model_id` **str** - Name of the model definition to use for API calls. +- `model_instance_name` **str** - Name of the model singleton instance. Default is "extract_graph". This primarily affects the cache storage partitioning. - `prompt` **str** - The prompt file to use. - `entity_types` **list[str]** - The entity types to identify. - `max_gleanings` **int** - The maximum number of gleaning cycles to use. @@ -221,7 +252,8 @@ Tune the language model-based graph extraction process. #### Fields -- `model_id` **str** - Name of the model definition to use for API calls. +- `completion_model_id` **str** - Name of the model definition to use for API calls. +- `model_instance_name` **str** - Name of the model singleton instance. Default is "summarize_descriptions". This primarily affects the cache storage partitioning. - `prompt` **str** - The prompt file to use. - `max_length` **int** - The maximum number of output tokens per summarization. - `max_input_length` **int** - The maximum number of tokens to collect for summarization (this will limit how many descriptions you send to be summarized for a given entity or relationship). @@ -233,17 +265,19 @@ Defines settings for NLP-based graph extraction methods. #### Fields - `normalize_edge_weights` **bool** - Whether to normalize the edge weights during graph construction. Default=`True`. +- `concurrent_requests` **int** - The number of threads to use for the extraction process. +- `async_mode` **asyncio|threaded** - The async mode to use. Either `asyncio` or `threaded`. - `text_analyzer` **dict** - Parameters for the NLP model. - - extractor_type **regex_english|syntactic_parser|cfg** - Default=`regex_english`. - - model_name **str** - Name of NLP model (for SpaCy-based models) - - max_word_length **int** - Longest word to allow. Default=`15`. - - word_delimiter **str** - Delimiter to split words. Default ' '. - - include_named_entities **bool** - Whether to include named entities in noun phrases. Default=`True`. - - exclude_nouns **list[str] | None** - List of nouns to exclude. If `None`, we use an internal stopword list. - - exclude_entity_tags **list[str]** - List of entity tags to ignore. - - exclude_pos_tags **list[str]** - List of part-of-speech tags to ignore. - - noun_phrase_tags **list[str]** - List of noun phrase tags to ignore. - - noun_phrase_grammars **dict[str, str]** - Noun phrase grammars for the model (cfg-only). + - `extractor_type` **regex_english|syntactic_parser|cfg** - Default=`regex_english`. + - `model_name` **str** - Name of NLP model (for SpaCy-based models) + - `max_word_length` **int** - Longest word to allow. Default=`15`. + - `word_delimiter` **str** - Delimiter to split words. Default ' '. + - `include_named_entities` **bool** - Whether to include named entities in noun phrases. Default=`True`. + - `exclude_nouns` **list[str] | None** - List of nouns to exclude. If `None`, we use an internal stopword list. + - `exclude_entity_tags` **list[str]** - List of entity tags to ignore. + - `exclude_pos_tags` **list[str]** - List of part-of-speech tags to ignore. + - `noun_phrase_tags` **list[str]** - List of noun phrase tags to ignore. + - `noun_phrase_grammars` **dict[str, str]** - Noun phrase grammars for the model (cfg-only). ### prune_graph @@ -251,13 +285,13 @@ Parameters for manual graph pruning. This can be used to optimize the modularity #### Fields -- min_node_freq **int** - The minimum node frequency to allow. -- max_node_freq_std **float | None** - The maximum standard deviation of node frequency to allow. -- min_node_degree **int** - The minimum node degree to allow. -- max_node_degree_std **float | None** - The maximum standard deviation of node degree to allow. -- min_edge_weight_pct **float** - The minimum edge weight percentile to allow. -- remove_ego_nodes **bool** - Remove ego nodes. -- lcc_only **bool** - Only use largest connected component. +- `min_node_freq` **int** - The minimum node frequency to allow. +- `max_node_freq_std` **float | None** - The maximum standard deviation of node frequency to allow. +- `min_node_degree` **int** - The minimum node degree to allow. +- `max_node_degree_std` **float | None** - The maximum standard deviation of node degree to allow. +- `min_edge_weight_pct` **float** - The minimum edge weight percentile to allow. +- `remove_ego_nodes` **bool** - Remove ego nodes. +- `lcc_only` **bool** - Only use largest connected component. ### cluster_graph @@ -274,7 +308,8 @@ These are the settings used for Leiden hierarchical clustering of the graph to c #### Fields - `enabled` **bool** - Whether to enable claim extraction. Off by default, because claim prompts really need user tuning. -- `model_id` **str** - Name of the model definition to use for API calls. +- `completion_model_id` **str** - Name of the model definition to use for API calls. +- `model_instance_name` **str** - Name of the model singleton instance. Default is "extract_claims". This primarily affects the cache storage partitioning. - `prompt` **str** - The prompt file to use. - `description` **str** - Describes the types of claims we want to extract. - `max_gleanings` **int** - The maximum number of gleaning cycles to use. @@ -283,40 +318,20 @@ These are the settings used for Leiden hierarchical clustering of the graph to c #### Fields -- `model_id` **str** - Name of the model definition to use for API calls. -- `prompt` **str** - The prompt file to use. +- `completion_model_id` **str** - Name of the model definition to use for API calls. +- `model_instance_name` **str** - Name of the model singleton instance. Default is "community_reporting". This primarily affects the cache storage partitioning. +- `graph_prompt` **str | None** - The community report extraction prompt to use for graph-based summarization. +- `text_prompt` **str | None** - The community report extraction prompt to use for text-based summarization. - `max_length` **int** - The maximum number of output tokens per report. - `max_input_length` **int** - The maximum number of input tokens to use when generating reports. -### embed_graph - -We use node2vec to embed the graph. This is primarily used for visualization, so it is not turned on by default. - -#### Fields - -- `enabled` **bool** - Whether to enable graph embeddings. -- `dimensions` **int** - Number of vector dimensions to produce. -- `num_walks` **int** - The node2vec number of walks. -- `walk_length` **int** - The node2vec walk length. -- `window_size` **int** - The node2vec window size. -- `iterations` **int** - The node2vec number of iterations. -- `random_seed` **int** - The node2vec random seed. -- `strategy` **dict** - Fully override the embed graph strategy. - -### umap - -Indicates whether we should run UMAP dimensionality reduction. This is used to provide an x/y coordinate to each graph node, suitable for visualization. If this is not enabled, nodes will receive a 0/0 x/y coordinate. If this is enabled, you *must* enable graph embedding as well. - -#### Fields - -- `enabled` **bool** - Whether to enable UMAP layouts. - ### snapshots #### Fields - `embeddings` **bool** - Export embeddings snapshots to parquet. -- `graphml` **bool** - Export graph snapshots to GraphML. +- `graphml` **bool** - Export graph snapshot to GraphML. +- `raw_graph` **bool** - Export raw extracted graph before merging. ## Query @@ -324,10 +339,10 @@ Indicates whether we should run UMAP dimensionality reduction. This is used to p #### Fields -- `chat_model_id` **str** - Name of the model definition to use for Chat Completion calls. -- `embedding_model_id` **str** - Name of the model definition to use for Embedding calls. - `prompt` **str** - The prompt file to use. -- `text_unit_prop` **float** - The text unit proportion. +- `completion_model_id` **str** - Name of the model definition to use for Chat Completion calls. +- `embedding_model_id` **str** - Name of the model definition to use for Embedding calls. +- `text_unit_prop` **float** - The text unit proportion. - `community_prop` **float** - The community proportion. - `conversation_history_max_turns` **int** - The conversation history maximum turns. - `top_k_entities` **int** - The top k mapped entities. @@ -338,14 +353,10 @@ Indicates whether we should run UMAP dimensionality reduction. This is used to p #### Fields -- `chat_model_id` **str** - Name of the model definition to use for Chat Completion calls. -- `map_prompt` **str** - The mapper prompt file to use. -- `reduce_prompt` **str** - The reducer prompt file to use. +- `map_prompt` **str** - The global search mapper prompt to use. +- `reduce_prompt` **str** - The global search reducer to use. +- `completion_model_id` **str** - Name of the model definition to use for Chat Completion calls. - `knowledge_prompt` **str** - The knowledge prompt file to use. -- `map_prompt` **str | None** - The global search mapper prompt to use. -- `reduce_prompt` **str | None** - The global search reducer to use. -- `knowledge_prompt` **str | None** - The global search general prompt to use. -- `max_context_tokens` **int** - The maximum context size to create, in tokens. - `data_max_tokens` **int** - The maximum tokens to use constructing the final response from the reduces responses. - `map_max_length` **int** - The maximum length to request for map responses, in words. - `reduce_max_length` **int** - The maximum length to request for reduce responses, in words. @@ -359,12 +370,13 @@ Indicates whether we should run UMAP dimensionality reduction. This is used to p #### Fields -- `chat_model_id` **str** - Name of the model definition to use for Chat Completion calls. -- `embedding_model_id` **str** - Name of the model definition to use for Embedding calls. - `prompt` **str** - The prompt file to use. - `reduce_prompt` **str** - The reducer prompt file to use. +- `completion_model_id` **str** - Name of the model definition to use for Chat Completion calls. +- `embedding_model_id` **str** - Name of the model definition to use for Embedding calls. - `data_max_tokens` **int** - The data llm maximum tokens. - `reduce_max_tokens` **int** - The maximum tokens for the reduce phase. Only use if a non-o-series model. +- `reduce_temperature` **float** - The temperature to use for token generation in reduce. - `reduce_max_completion_tokens` **int** - The maximum tokens for the reduce phase. Only use for o-series models. - `concurrency` **int** - The number of concurrent requests. - `drift_k_followups` **int** - The number of top global results to retrieve. @@ -386,7 +398,8 @@ Indicates whether we should run UMAP dimensionality reduction. This is used to p #### Fields -- `chat_model_id` **str** - Name of the model definition to use for Chat Completion calls. -- `embedding_model_id` **str** - Name of the model definition to use for Embedding calls. - `prompt` **str** - The prompt file to use. -- `k` **int | None** - Number of text units to retrieve from the vector store for context building. +- `completion_model_id` **str** - Name of the model definition to use for Chat Completion calls. +- `embedding_model_id` **str** - Name of the model definition to use for Embedding calls. +- `k` **int** - Number of text units to retrieve from the vector store for context building. +- `max_context_tokens` **int** - The maximum context size to create, in tokens. diff --git a/docs/developing.md b/docs/developing.md index 5e7b6671a3..eea14d00b0 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -13,7 +13,7 @@ ```sh # install python dependencies -uv sync +uv sync --all-packages ``` ## Execute the Indexing Engine diff --git a/docs/examples_notebooks/api_overview.ipynb b/docs/examples_notebooks/api_overview.ipynb index 7e219e7688..2a0c0f15de 100644 --- a/docs/examples_notebooks/api_overview.ipynb +++ b/docs/examples_notebooks/api_overview.ipynb @@ -16,7 +16,7 @@ "source": [ "## API Overview\n", "\n", - "This notebook provides a demonstration of how to interact with graphrag as a library using the API as opposed to the CLI. Note that graphrag's CLI actually connects to the library through this API for all operations. " + "This notebook provides a demonstration of how to interact with graphrag as a library using the API as opposed to the CLI. Note that graphrag's CLI actually connects to the library through this API for all operations.\n" ] }, { @@ -28,9 +28,8 @@ "from pathlib import Path\n", "from pprint import pprint\n", "\n", - "import pandas as pd\n", - "\n", "import graphrag.api as api\n", + "import pandas as pd\n", "from graphrag.config.load_config import load_config\n", "from graphrag.index.typing.pipeline_run_result import PipelineRunResult" ] @@ -49,16 +48,17 @@ "metadata": {}, "source": [ "## Prerequisite\n", + "\n", "As a prerequisite to all API operations, a `GraphRagConfig` object is required. It is the primary means to control the behavior of graphrag and can be instantiated from a `settings.yaml` configuration file.\n", "\n", - "Please refer to the [CLI docs](https://microsoft.github.io/graphrag/cli/#init) for more detailed information on how to generate the `settings.yaml` file." + "Please refer to the [CLI docs](https://microsoft.github.io/graphrag/cli/#init) for more detailed information on how to generate the `settings.yaml` file.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Generate a `GraphRagConfig` object" + "### Generate a `GraphRagConfig` object\n" ] }, { @@ -78,14 +78,14 @@ "source": [ "## Indexing API\n", "\n", - "*Indexing* is the process of ingesting raw text data and constructing a knowledge graph. GraphRAG currently supports plaintext (`.txt`) and `.csv` file formats." + "_Indexing_ is the process of ingesting raw text data and constructing a knowledge graph. GraphRAG currently supports plaintext (`.txt`) and `.csv` file formats.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Build an index" + "## Build an index\n" ] }, { @@ -108,7 +108,7 @@ "source": [ "## Query an index\n", "\n", - "To query an index, several index files must first be read into memory and passed to the query API. " + "To query an index, several index files must first be read into memory and passed to the query API.\n" ] }, { @@ -139,7 +139,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The response object is the official reponse from graphrag while the context object holds various metadata regarding the querying process used to obtain the final response." + "The response object is the official reponse from graphrag while the context object holds various metadata regarding the querying process used to obtain the final response.\n" ] }, { @@ -155,7 +155,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Digging into the context a bit more provides users with extremely granular information such as what sources of data (down to the level of text chunks) were ultimately retrieved and used as part of the context sent to the LLM model)." + "Digging into the context a bit more provides users with extremely granular information such as what sources of data (down to the level of text chunks) were ultimately retrieved and used as part of the context sent to the LLM model).\n" ] }, { @@ -170,7 +170,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "graphrag-monorepo", "language": "python", "name": "python3" }, @@ -184,7 +184,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/docs/examples_notebooks/custom_vector_store.ipynb b/docs/examples_notebooks/custom_vector_store.ipynb index 4c3d223c69..2e79c66d86 100644 --- a/docs/examples_notebooks/custom_vector_store.ipynb +++ b/docs/examples_notebooks/custom_vector_store.ipynb @@ -28,7 +28,7 @@ "\n", "### What You'll Learn\n", "\n", - "1. Understanding the `BaseVectorStore` interface\n", + "1. Understanding the `VectorStore` interface\n", "2. Implementing a custom vector store class\n", "3. Registering your vector store with the `VectorStoreFactory`\n", "4. Testing and validating your implementation\n", @@ -50,36 +50,13 @@ "```" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Any\n", - "\n", - "import numpy as np\n", - "import yaml\n", - "\n", - "from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig\n", - "from graphrag.data_model.types import TextEmbedder\n", - "\n", - "# GraphRAG vector store components\n", - "from graphrag.vector_stores.base import (\n", - " BaseVectorStore,\n", - " VectorStoreDocument,\n", - " VectorStoreSearchResult,\n", - ")\n", - "from graphrag.vector_stores.factory import VectorStoreFactory" - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 2: Understand the BaseVectorStore Interface\n", + "## Step 2: Understand the VectorStore Interface\n", "\n", - "Before using a custom vector store, let's examine the `BaseVectorStore` interface to understand what methods need to be implemented." + "Before using a custom vector store, let's examine the `VectorStore` interface to understand what methods need to be implemented." ] }, { @@ -88,18 +65,31 @@ "metadata": {}, "outputs": [], "source": [ - "# Let's inspect the BaseVectorStore class to understand the required methods\n", "import inspect\n", "\n", - "print(\"BaseVectorStore Abstract Methods:\")\n", - "print(\"=\" * 40)\n", + "# Let's inspect the VectorStore class to understand the required methods\n", + "from typing import Any\n", + "\n", + "import numpy as np\n", + "import yaml\n", + "from graphrag_vectors import (\n", + " IndexSchema,\n", + " TextEmbedder,\n", + " VectorStore,\n", + " VectorStoreDocument,\n", + " VectorStoreFactory,\n", + " VectorStoreSearchResult,\n", + ")\n", + "\n", + "print(\"VectorStore Abstract Methods:\")\n", + "print(\"=\" * 80)\n", "\n", "abstract_methods = []\n", - "for name, method in inspect.getmembers(BaseVectorStore, predicate=inspect.isfunction):\n", + "for name, method in inspect.getmembers(VectorStore, predicate=inspect.isfunction):\n", " if getattr(method, \"__isabstractmethod__\", False):\n", - " signature = inspect.signature(method)\n", - " abstract_methods.append(f\"• {name}{signature}\")\n", - " print(f\"• {name}{signature}\")\n", + " abstract_methods.append(name)\n", + " print(f\"\\n{name}:\")\n", + " print(f\" {inspect.signature(method)}\")\n", "\n", "print(f\"\\nTotal abstract methods to implement: {len(abstract_methods)}\")" ] @@ -113,7 +103,7 @@ "Now let's implement a simple in-memory vector store as an example. This vector store will:\n", "\n", "- Store documents and vectors in memory using Python data structures\n", - "- Support all required BaseVectorStore methods\n", + "- Support all required VectorStore methods\n", "\n", "**Note**: This is a simplified example for demonstration. Production vector stores would typically use optimized libraries like FAISS, more sophisticated indexing, and persistent storage." ] @@ -124,7 +114,7 @@ "metadata": {}, "outputs": [], "source": [ - "class SimpleInMemoryVectorStore(BaseVectorStore):\n", + "class SimpleInMemoryVectorStore(VectorStore):\n", " \"\"\"A simple in-memory vector store implementation for demonstration purposes.\n", "\n", " This vector store stores documents and their embeddings in memory and provides\n", @@ -148,118 +138,90 @@ " self.vectors: dict[str, np.ndarray] = {}\n", " self.connected = False\n", "\n", - " print(f\"🚀 SimpleInMemoryVectorStore initialized for index: {self.index_name}\")\n", - "\n", " def connect(self, **kwargs: Any) -> None:\n", - " \"\"\"Connect to the vector storage (no-op for in-memory store).\"\"\"\n", + " \"\"\"Connect to the vector store (simulated for in-memory store).\"\"\"\n", + " print(\"Connecting to in-memory vector store...\")\n", " self.connected = True\n", - " print(f\"✅ Connected to in-memory vector store: {self.index_name}\")\n", + " print(\"Connected successfully!\")\n", + "\n", + " def create_index(self, **kwargs: Any) -> None:\n", + " \"\"\"Create an index (simulated for in-memory store).\n", + "\n", + " In a real vector database, this would create the necessary data structures\n", + " and indexes for efficient vector search.\n", + " \"\"\"\n", + " print(f\"Creating index: {self.index_name}\")\n", + " # For in-memory store, we just ensure our storage dictionaries are ready\n", + " if not isinstance(self.documents, dict):\n", + " self.documents = {}\n", + " if not isinstance(self.vectors, dict):\n", + " self.vectors = {}\n", + " print(\"Index created successfully!\")\n", "\n", " def load_documents(\n", - " self, documents: list[VectorStoreDocument], overwrite: bool = True\n", + " self, documents: list[VectorStoreDocument], overwrite: bool = False\n", " ) -> None:\n", " \"\"\"Load documents into the vector store.\"\"\"\n", - " if not self.connected:\n", - " msg = \"Vector store not connected. Call connect() first.\"\n", - " raise RuntimeError(msg)\n", - "\n", " if overwrite:\n", + " print(\"Clearing existing documents...\")\n", " self.documents.clear()\n", " self.vectors.clear()\n", "\n", - " loaded_count = 0\n", + " print(f\"Loading {len(documents)} documents...\")\n", " for doc in documents:\n", - " if doc.vector is not None:\n", - " doc_id = str(doc.id)\n", - " self.documents[doc_id] = doc\n", - " self.vectors[doc_id] = np.array(doc.vector, dtype=np.float32)\n", - " loaded_count += 1\n", - "\n", - " print(f\"📚 Loaded {loaded_count} documents into vector store\")\n", - "\n", - " def _cosine_similarity(self, vec1: np.ndarray, vec2: np.ndarray) -> float:\n", - " \"\"\"Calculate cosine similarity between two vectors.\"\"\"\n", - " # Normalize vectors\n", - " norm1 = np.linalg.norm(vec1)\n", - " norm2 = np.linalg.norm(vec2)\n", - "\n", - " if norm1 == 0 or norm2 == 0:\n", - " return 0.0\n", + " self.documents[doc.id] = doc\n", + " if doc.vector:\n", + " self.vectors[doc.id] = np.array(doc.vector)\n", "\n", - " return float(np.dot(vec1, vec2) / (norm1 * norm2))\n", + " print(f\"Successfully loaded {len(documents)} documents!\")\n", "\n", " def similarity_search_by_vector(\n", " self, query_embedding: list[float], k: int = 10, **kwargs: Any\n", " ) -> list[VectorStoreSearchResult]:\n", - " \"\"\"Perform similarity search using a query vector.\"\"\"\n", - " if not self.connected:\n", - " msg = \"Vector store not connected. Call connect() first.\"\n", - " raise RuntimeError(msg)\n", - "\n", + " \"\"\"Search for similar documents using a query vector.\"\"\"\n", " if not self.vectors:\n", " return []\n", "\n", - " query_vec = np.array(query_embedding, dtype=np.float32)\n", - " similarities = []\n", + " query_vector = np.array(query_embedding)\n", "\n", - " # Calculate similarity with all stored vectors\n", - " for doc_id, stored_vec in self.vectors.items():\n", - " similarity = self._cosine_similarity(query_vec, stored_vec)\n", + " # Calculate cosine similarity for all documents\n", + " similarities = []\n", + " for doc_id, doc_vector in self.vectors.items():\n", + " # Cosine similarity\n", + " similarity = np.dot(query_vector, doc_vector) / (\n", + " np.linalg.norm(query_vector) * np.linalg.norm(doc_vector)\n", + " )\n", " similarities.append((doc_id, similarity))\n", "\n", - " # Sort by similarity (descending) and take top k\n", + " # Sort by similarity (highest first) and take top k\n", " similarities.sort(key=lambda x: x[1], reverse=True)\n", - " top_k = similarities[:k]\n", + " top_results = similarities[:k]\n", "\n", - " # Create search results\n", + " # Convert to search results\n", " results = []\n", - " for doc_id, score in top_k:\n", - " document = self.documents[doc_id]\n", - " result = VectorStoreSearchResult(document=document, score=score)\n", - " results.append(result)\n", + " for doc_id, score in top_results:\n", + " doc = self.documents[doc_id]\n", + " results.append(VectorStoreSearchResult(document=doc, score=float(score)))\n", "\n", " return results\n", "\n", " def similarity_search_by_text(\n", - " self, text: str, text_embedder: TextEmbedder, k: int = 10, **kwargs: Any\n", + " self,\n", + " text: str,\n", + " text_embedder: TextEmbedder,\n", + " k: int = 10,\n", + " **kwargs: Any,\n", " ) -> list[VectorStoreSearchResult]:\n", - " \"\"\"Perform similarity search using text (which gets embedded first).\"\"\"\n", - " # Embed the text first\n", + " \"\"\"Search for similar documents using a text query.\"\"\"\n", + " # Embed the query text\n", " query_embedding = text_embedder(text)\n", "\n", - " # Use vector search with the embedding\n", + " # Use vector search\n", " return self.similarity_search_by_vector(query_embedding, k, **kwargs)\n", "\n", - " def filter_by_id(self, include_ids: list[str] | list[int]) -> Any:\n", - " \"\"\"Build a query filter to filter documents by id.\n", - "\n", - " For this simple implementation, we return the list of IDs as the filter.\n", - " \"\"\"\n", - " return [str(id_) for id_ in include_ids]\n", - "\n", - " def search_by_id(self, id: str) -> VectorStoreDocument:\n", - " \"\"\"Search for a document by id.\"\"\"\n", - " doc_id = str(id)\n", - " if doc_id not in self.documents:\n", - " msg = f\"Document with id '{id}' not found\"\n", - " raise KeyError(msg)\n", - "\n", - " return self.documents[doc_id]\n", - "\n", - " def get_stats(self) -> dict[str, Any]:\n", - " \"\"\"Get statistics about the vector store (custom method).\"\"\"\n", - " return {\n", - " \"index_name\": self.index_name,\n", - " \"document_count\": len(self.documents),\n", - " \"vector_count\": len(self.vectors),\n", - " \"connected\": self.connected,\n", - " \"vector_dimension\": len(next(iter(self.vectors.values())))\n", - " if self.vectors\n", - " else 0,\n", - " }\n", - "\n", - "\n", - "print(\"✅ SimpleInMemoryVectorStore class defined!\")" + " def search_by_id(self, id: str) -> VectorStoreDocument | None:\n", + " \"\"\"Retrieve a document by its ID.\"\"\"\n", + " return self.documents.get(id)" ] }, { @@ -281,15 +243,15 @@ "CUSTOM_VECTOR_STORE_TYPE = \"simple_memory\"\n", "\n", "# Register the vector store class\n", - "VectorStoreFactory.register(CUSTOM_VECTOR_STORE_TYPE, SimpleInMemoryVectorStore)\n", + "VectorStoreFactory().register(CUSTOM_VECTOR_STORE_TYPE, SimpleInMemoryVectorStore)\n", "\n", "print(f\"✅ Registered custom vector store with type: '{CUSTOM_VECTOR_STORE_TYPE}'\")\n", "\n", "# Verify registration\n", - "available_types = VectorStoreFactory.get_vector_store_types()\n", + "available_types = VectorStoreFactory().keys()\n", "print(f\"\\n📋 Available vector store types: {available_types}\")\n", "print(\n", - " f\"🔍 Is our custom type supported? {VectorStoreFactory.is_supported_type(CUSTOM_VECTOR_STORE_TYPE)}\"\n", + " f\"🔍 Is our custom type supported? {CUSTOM_VECTOR_STORE_TYPE in VectorStoreFactory()}\"\n", ")" ] }, @@ -318,27 +280,19 @@ "sample_documents = [\n", " VectorStoreDocument(\n", " id=\"doc_1\",\n", - " text=\"GraphRAG is a powerful knowledge graph extraction and reasoning framework.\",\n", " vector=create_mock_embedding(),\n", - " attributes={\"category\": \"technology\", \"source\": \"documentation\"},\n", " ),\n", " VectorStoreDocument(\n", " id=\"doc_2\",\n", - " text=\"Vector stores enable efficient similarity search over high-dimensional data.\",\n", " vector=create_mock_embedding(),\n", - " attributes={\"category\": \"technology\", \"source\": \"research\"},\n", " ),\n", " VectorStoreDocument(\n", " id=\"doc_3\",\n", - " text=\"Machine learning models can process and understand natural language text.\",\n", " vector=create_mock_embedding(),\n", - " attributes={\"category\": \"AI\", \"source\": \"article\"},\n", " ),\n", " VectorStoreDocument(\n", " id=\"doc_4\",\n", - " text=\"Custom implementations allow for specialized behavior and integration.\",\n", " vector=create_mock_embedding(),\n", - " attributes={\"category\": \"development\", \"source\": \"tutorial\"},\n", " ),\n", "]\n", "\n", @@ -352,17 +306,24 @@ "outputs": [], "source": [ "# Test creating vector store using the factory\n", - "schema = VectorStoreSchemaConfig(index_name=\"test_collection\")\n", + "schema = IndexSchema(index_name=\"test_collection\")\n", "\n", "# Create vector store instance using factory\n", - "vector_store = VectorStoreFactory.create_vector_store(\n", - " CUSTOM_VECTOR_STORE_TYPE, vector_store_schema_config=schema\n", + "vector_store = VectorStoreFactory().create(\n", + " CUSTOM_VECTOR_STORE_TYPE, {\"index_schema\": schema}\n", ")\n", "\n", "print(f\"✅ Created vector store instance: {type(vector_store).__name__}\")\n", "print(f\"📊 Initial stats: {vector_store.get_stats()}\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, @@ -371,6 +332,7 @@ "source": [ "# Connect and load documents\n", "vector_store.connect()\n", + "vector_store.create_index()\n", "vector_store.load_documents(sample_documents)\n", "\n", "print(f\"📊 Updated stats: {vector_store.get_stats()}\")" @@ -395,9 +357,7 @@ "for i, result in enumerate(search_results, 1):\n", " doc = result.document\n", " print(f\"{i}. ID: {doc.id}\")\n", - " print(f\" Text: {doc.text[:60]}...\")\n", " print(f\" Similarity Score: {result.score:.4f}\")\n", - " print(f\" Category: {doc.attributes.get('category', 'N/A')}\")\n", " print()" ] }, @@ -412,14 +372,8 @@ " found_doc = vector_store.search_by_id(\"doc_2\")\n", " print(\"✅ Found document by ID:\")\n", " print(f\" ID: {found_doc.id}\")\n", - " print(f\" Text: {found_doc.text}\")\n", - " print(f\" Attributes: {found_doc.attributes}\")\n", "except KeyError as e:\n", - " print(f\"❌ Error: {e}\")\n", - "\n", - "# Test filter by ID\n", - "id_filter = vector_store.filter_by_id([\"doc_1\", \"doc_3\"])\n", - "print(f\"\\n🔧 ID filter result: {id_filter}\")" + " print(f\"❌ Error: {e}\")" ] }, { @@ -450,7 +404,8 @@ " # Other GraphRAG configuration...\n", " \"models\": {\n", " \"default_embedding_model\": {\n", - " \"type\": \"openai_embedding\",\n", + " \"type\": \"embedding\",\n", + " \"model_provider\": \"openai\",\n", " \"model\": \"text-embedding-3-small\",\n", " }\n", " },\n", @@ -485,24 +440,21 @@ " print(\"🚀 Simulating GraphRAG pipeline with custom vector store...\\n\")\n", "\n", " # 1. GraphRAG creates vector store using factory\n", - " schema = VectorStoreSchemaConfig(index_name=\"graphrag_entities\")\n", + " schema = IndexSchema(index_name=\"graphrag_entities\")\n", "\n", - " store = VectorStoreFactory.create_vector_store(\n", + " store = VectorStoreFactory().create(\n", " CUSTOM_VECTOR_STORE_TYPE,\n", - " vector_store_schema_config=schema,\n", - " similarity_threshold=0.3,\n", + " {\"index_schema\": schema, \"similarity_threshold\": 0.3},\n", " )\n", " store.connect()\n", - "\n", + " store.create_index()\n", " print(\"✅ Step 1: Vector store created and connected\")\n", "\n", " # 2. During indexing, GraphRAG loads extracted entities\n", " entity_documents = [\n", " VectorStoreDocument(\n", " id=f\"entity_{i}\",\n", - " text=f\"Entity {i} description: Important concept in the knowledge graph\",\n", " vector=create_mock_embedding(),\n", - " attributes={\"type\": \"entity\", \"importance\": i % 3 + 1},\n", " )\n", " for i in range(10)\n", " ]\n", @@ -551,12 +503,12 @@ "\n", " # Test 1: Basic functionality\n", " print(\"Test 1: Basic functionality\")\n", - " store = VectorStoreFactory.create_vector_store(\n", + " store = VectorStoreFactory().create(\n", " CUSTOM_VECTOR_STORE_TYPE,\n", - " vector_store_schema_config=VectorStoreSchemaConfig(index_name=\"test\"),\n", + " {\"index_schema\": IndexSchema(index_name=\"test\")},\n", " )\n", " store.connect()\n", - "\n", + " store.create_index()\n", " # Load test documents\n", " test_docs = sample_documents[:2]\n", " store.load_documents(test_docs)\n", @@ -592,17 +544,11 @@ "\n", " print(\"✅ Search by ID test passed\")\n", "\n", - " # Test 4: Filter functionality\n", - " print(\"\\nTest 4: Filter functionality\")\n", - " filter_result = store.filter_by_id([\"doc_1\", \"doc_2\"])\n", - " assert filter_result == [\"doc_1\", \"doc_2\"], \"Should return filtered IDs\"\n", - " print(\"✅ Filter functionality test passed\")\n", - "\n", - " # Test 5: Error handling\n", + " # Test 4: Error handling\n", " print(\"\\nTest 5: Error handling\")\n", - " disconnected_store = VectorStoreFactory.create_vector_store(\n", + " disconnected_store = VectorStoreFactory().create(\n", " CUSTOM_VECTOR_STORE_TYPE,\n", - " vector_store_schema_config=VectorStoreSchemaConfig(index_name=\"test2\"),\n", + " {\"index_schema\": IndexSchema(index_name=\"test2\")},\n", " )\n", "\n", " try:\n", @@ -641,16 +587,24 @@ "- ✅ **Configuration Examples**: Learned how to configure GraphRAG to use your vector store\n", "\n", "### Key Takeaways\n", - "1. **Interface Compliance**: Always implement all methods from `BaseVectorStore`\n", + "1. **Interface Compliance**: Always implement all methods from `VectorStore`\n", "2. **Factory Pattern**: Use `VectorStoreFactory.register()` to make your vector store available\n", - "3. **Configuration**: Vector stores are configured in GraphRAG settings files\n", - "4. **Testing**: Thoroughly test all functionality before deploying\n", - "\n", - "### Next Steps\n", - "Check out the API Overview notebook to learn how to index and query data via the graphrag API.\n", + "3. **Testing**: Validate your implementation thoroughly before production use\n", + "4. **Configuration**: Use YAML or environment variables for flexible configuration\n", + "\n", + "### Production Considerations\n", + "For production use, consider:\n", + "- **Persistence**: Add data persistence mechanisms\n", + "- **Scalability**: Use optimized vector search libraries (FAISS, HNSW)\n", + "- **Error Handling**: Implement robust error handling and logging\n", + "- **Performance**: Add caching, batching, and connection pooling\n", + "- **Security**: Implement authentication and authorization\n", + "- **Monitoring**: Add metrics and health checks\n", "\n", "### Resources\n", "- [GraphRAG Documentation](https://microsoft.github.io/graphrag/)\n", + "- [Vector Store Examples](https://github.com/microsoft/graphrag/tree/main/packages/graphrag-vectors)\n", + "- [GraphRAG GitHub Repository](https://github.com/microsoft/graphrag)\n", "\n", "Happy building! 🚀" ] @@ -658,7 +612,7 @@ ], "metadata": { "kernelspec": { - "display_name": "graphrag", + "display_name": "Python 3", "language": "python", "name": "python3" }, diff --git a/docs/examples_notebooks/drift_search.ipynb b/docs/examples_notebooks/drift_search.ipynb index 1283018fb0..8d53c7d9cc 100644 --- a/docs/examples_notebooks/drift_search.ipynb +++ b/docs/examples_notebooks/drift_search.ipynb @@ -17,14 +17,11 @@ "outputs": [], "source": [ "import os\n", - "from pathlib import Path\n", "\n", "import pandas as pd\n", - "\n", "from graphrag.config.enums import ModelType\n", "from graphrag.config.models.drift_search_config import DRIFTSearchConfig\n", "from graphrag.config.models.language_model_config import LanguageModelConfig\n", - "from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig\n", "from graphrag.language_model.manager import ModelManager\n", "from graphrag.query.indexer_adapters import (\n", " read_indexer_entities,\n", @@ -38,7 +35,7 @@ ")\n", "from graphrag.query.structured_search.drift_search.search import DRIFTSearch\n", "from graphrag.tokenizer.get_tokenizer import get_tokenizer\n", - "from graphrag.vector_stores.lancedb import LanceDBVectorStore\n", + "from graphrag_vectors.lancedb import LanceDBVectorStore\n", "\n", "INPUT_DIR = \"./inputs/operation dulce\"\n", "LANCEDB_URI = f\"{INPUT_DIR}/lancedb\"\n", @@ -63,18 +60,16 @@ "# load description embeddings to an in-memory lancedb vectorstore\n", "# to connect to a remote db, specify url and port values.\n", "description_embedding_store = LanceDBVectorStore(\n", - " vector_store_schema_config=VectorStoreSchemaConfig(\n", - " index_name=\"default-entity-description\"\n", - " ),\n", + " db_uri=LANCEDB_URI,\n", + " index_name=\"entity_description\",\n", ")\n", - "description_embedding_store.connect(db_uri=LANCEDB_URI)\n", + "description_embedding_store.connect()\n", "\n", "full_content_embedding_store = LanceDBVectorStore(\n", - " vector_store_schema_config=VectorStoreSchemaConfig(\n", - " index_name=\"default-community-full_content\"\n", - " )\n", + " db_uri=LANCEDB_URI,\n", + " index_name=\"community_full_content\",\n", ")\n", - "full_content_embedding_store.connect(db_uri=LANCEDB_URI)\n", + "full_content_embedding_store.connect()\n", "\n", "print(f\"Entity count: {len(entity_df)}\")\n", "entity_df.head()\n", @@ -89,7 +84,11 @@ "text_units = read_indexer_text_units(text_unit_df)\n", "\n", "print(f\"Text unit records: {len(text_unit_df)}\")\n", - "text_unit_df.head()" + "text_unit_df.head()\n", + "\n", + "report_df = pd.read_parquet(f\"{INPUT_DIR}/{COMMUNITY_REPORT_TABLE}.parquet\")\n", + "reports = read_indexer_reports(report_df, community_df, COMMUNITY_LEVEL)\n", + "read_indexer_report_embeddings(reports, full_content_embedding_store)" ] }, { @@ -119,7 +118,7 @@ " api_key=api_key,\n", " type=ModelType.Embedding,\n", " model_provider=\"openai\",\n", - " model=\"text-embedding-3-small\",\n", + " model=\"text-embedding-3-large\",\n", " max_retries=20,\n", ")\n", "\n", @@ -130,31 +129,6 @@ ")" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def read_community_reports(\n", - " input_dir: str,\n", - " community_report_table: str = COMMUNITY_REPORT_TABLE,\n", - "):\n", - " \"\"\"Embeds the full content of the community reports and saves the DataFrame with embeddings to the output path.\"\"\"\n", - " input_path = Path(input_dir) / f\"{community_report_table}.parquet\"\n", - " return pd.read_parquet(input_path)\n", - "\n", - "\n", - "report_df = read_community_reports(INPUT_DIR)\n", - "reports = read_indexer_reports(\n", - " report_df,\n", - " community_df,\n", - " COMMUNITY_LEVEL,\n", - " content_embedding_col=\"full_content_embeddings\",\n", - ")\n", - "read_indexer_report_embeddings(reports, full_content_embedding_store)" - ] - }, { "cell_type": "code", "execution_count": null, @@ -162,12 +136,9 @@ "outputs": [], "source": [ "drift_params = DRIFTSearchConfig(\n", - " temperature=0,\n", - " max_tokens=12_000,\n", " primer_folds=1,\n", " drift_k_followups=3,\n", " n_depth=3,\n", - " n=1,\n", ")\n", "\n", "context_builder = DRIFTSearchContextBuilder(\n", @@ -217,7 +188,7 @@ ], "metadata": { "kernelspec": { - "display_name": "graphrag", + "display_name": "Python 3", "language": "python", "name": "python3" }, diff --git a/docs/examples_notebooks/global_search.ipynb b/docs/examples_notebooks/global_search.ipynb index c95a037df0..605f704bd2 100644 --- a/docs/examples_notebooks/global_search.ipynb +++ b/docs/examples_notebooks/global_search.ipynb @@ -19,7 +19,6 @@ "import os\n", "\n", "import pandas as pd\n", - "\n", "from graphrag.config.enums import ModelType\n", "from graphrag.config.models.language_model_config import LanguageModelConfig\n", "from graphrag.language_model.manager import ModelManager\n", diff --git a/docs/examples_notebooks/global_search_with_dynamic_community_selection.ipynb b/docs/examples_notebooks/global_search_with_dynamic_community_selection.ipynb index bb0e0f0975..6b3763d73b 100644 --- a/docs/examples_notebooks/global_search_with_dynamic_community_selection.ipynb +++ b/docs/examples_notebooks/global_search_with_dynamic_community_selection.ipynb @@ -19,7 +19,6 @@ "import os\n", "\n", "import pandas as pd\n", - "\n", "from graphrag.config.enums import ModelType\n", "from graphrag.config.models.language_model_config import LanguageModelConfig\n", "from graphrag.language_model.manager import ModelManager\n", diff --git a/docs/examples_notebooks/index_migration_to_v1.ipynb b/docs/examples_notebooks/index_migration_to_v1.ipynb index ecff51929a..c5b582d38d 100644 --- a/docs/examples_notebooks/index_migration_to_v1.ipynb +++ b/docs/examples_notebooks/index_migration_to_v1.ipynb @@ -20,7 +20,7 @@ "\n", "NOTE: we recommend regenerating your settings.yml with the latest version of GraphRAG using `graphrag init`. Copy your LLM settings into it before running this notebook. This ensures your config is aligned with the latest version for the migration. This also ensures that you have default vector store config, which is now required or indexing will fail.\n", "\n", - "WARNING: This will overwrite your parquet files, you may want to make a backup!" + "WARNING: This will overwrite your parquet files, you may want to make a backup!\n" ] }, { @@ -67,7 +67,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -87,7 +87,8 @@ " nodes[\"parent\"] = nodes[\"parent\"].fillna(-1).astype(int)\n", "\n", " join = (\n", - " nodes.groupby([\"community\", \"level\", \"parent\"])\n", + " nodes\n", + " .groupby([\"community\", \"level\", \"parent\"])\n", " .agg({\"title\": list})\n", " .reset_index()\n", " )\n", @@ -96,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -168,7 +169,8 @@ " final_communities = final_communities.merge(parent_df, on=\"community\", how=\"left\")\n", "if \"entity_ids\" not in final_communities.columns:\n", " node_mapping = (\n", - " final_nodes.loc[:, [\"community\", \"id\"]]\n", + " final_nodes\n", + " .loc[:, [\"community\", \"id\"]]\n", " .groupby(\"community\")\n", " .agg(entity_ids=(\"id\", list))\n", " )\n", @@ -202,45 +204,51 @@ "metadata": {}, "outputs": [], "source": [ - "from graphrag.index.flows.generate_text_embeddings import generate_text_embeddings\n", - "\n", "from graphrag.cache.factory import CacheFactory\n", "from graphrag.callbacks.noop_workflow_callbacks import NoopWorkflowCallbacks\n", - "from graphrag.config.embeddings import get_embedded_fields, get_embedding_settings\n", + "from graphrag.index.workflows.generate_text_embeddings import generate_text_embeddings\n", + "from graphrag.language_model.manager import ModelManager\n", + "from graphrag.tokenizer.get_tokenizer import get_tokenizer\n", "\n", "# We only need to re-run the embeddings workflow, to ensure that embeddings for all required search fields are in place\n", "# We'll construct the context and run this function flow directly to avoid everything else\n", "\n", - "\n", - "embedded_fields = get_embedded_fields(config)\n", - "text_embed = get_embedding_settings(config)\n", + "model_config = config.get_language_model_config(config.embed_text.model_id)\n", "callbacks = NoopWorkflowCallbacks()\n", "cache_config = config.cache.model_dump() # type: ignore\n", "cache = CacheFactory().create_cache(\n", " cache_type=cache_config[\"type\"], # type: ignore\n", - " root_dir=PROJECT_DIRECTORY,\n", - " kwargs=cache_config,\n", + " **cache_config,\n", + ")\n", + "model = ModelManager().get_or_create_embedding_model(\n", + " name=\"text_embedding\",\n", + " model_type=model_config.type,\n", + " config=model_config,\n", + " callbacks=callbacks,\n", + " cache=cache,\n", ")\n", "\n", + "tokenizer = get_tokenizer(model_config)\n", + "\n", "await generate_text_embeddings(\n", - " final_documents=None,\n", - " final_relationships=None,\n", - " final_text_units=final_text_units,\n", - " final_entities=final_entities,\n", - " final_community_reports=final_community_reports,\n", + " text_units=final_text_units,\n", + " entities=final_entities,\n", + " community_reports=final_community_reports,\n", " callbacks=callbacks,\n", - " cache=cache,\n", - " storage=storage,\n", - " text_embed_config=text_embed,\n", - " embedded_fields=embedded_fields,\n", - " snapshot_embeddings_enabled=False,\n", + " model=model,\n", + " tokenizer=tokenizer,\n", + " batch_size=config.embed_text.batch_size,\n", + " batch_max_tokens=config.embed_text.batch_max_tokens,\n", + " num_threads=model_config.concurrent_requests,\n", + " vector_store_config=config.vector_store,\n", + " embedded_fields=config.embed_text.names,\n", ")" ] } ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "graphrag", "language": "python", "name": "python3" }, @@ -254,7 +262,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.10" } }, "nbformat": 4, diff --git a/docs/examples_notebooks/index_migration_to_v2.ipynb b/docs/examples_notebooks/index_migration_to_v2.ipynb index dad9011696..0681d1a0b2 100644 --- a/docs/examples_notebooks/index_migration_to_v2.ipynb +++ b/docs/examples_notebooks/index_migration_to_v2.ipynb @@ -70,7 +70,6 @@ "outputs": [], "source": [ "import numpy as np\n", - "\n", "from graphrag.utils.storage import (\n", " delete_table_from_storage,\n", " load_table_from_storage,\n", diff --git a/docs/examples_notebooks/index_migration_to_v3.ipynb b/docs/examples_notebooks/index_migration_to_v3.ipynb new file mode 100644 index 0000000000..a0e50be432 --- /dev/null +++ b/docs/examples_notebooks/index_migration_to_v3.ipynb @@ -0,0 +1,146 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Index Migration (v2 to v3)\n", + "\n", + "This notebook is used to maintain data model parity with older indexes for version 3.0 of GraphRAG. If you have a pre-3.0 index and need to migrate without re-running the entire pipeline, you can use this notebook to only update the pieces necessary for alignment. If you have a pre-2.0 index, please run the v2 migration notebook first!\n", + "\n", + "NOTE: we recommend regenerating your settings.yml with the latest version of GraphRAG using `graphrag init`. Copy your LLM settings into it before running this notebook. This ensures your config is aligned with the latest version for the migration. The config changes from v2 to v3 are significant in places!\n", + "\n", + "WARNING: This will overwrite your parquet files, you may want to make a backup!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This is the directory that has your settings.yaml\n", + "PROJECT_DIRECTORY = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "from graphrag.config.models.graph_rag_config import GraphRagConfig\n", + "from graphrag_common.config import load_config\n", + "from graphrag_storage.storage_factory import create_storage\n", + "\n", + "config = load_config(GraphRagConfig, config_path=Path(PROJECT_DIRECTORY))\n", + "storage = create_storage(config.output_storage)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def remove_columns(df, columns):\n", + " \"\"\"Remove columns from a DataFrame, suppressing errors.\"\"\"\n", + " df.drop(labels=columns, axis=1, errors=\"ignore\", inplace=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from graphrag.utils.storage import (\n", + " load_table_from_storage,\n", + " write_table_to_storage,\n", + ")\n", + "\n", + "text_units = await load_table_from_storage(\"text_units\", storage)\n", + "\n", + "text_units[\"document_id\"] = text_units[\"document_ids\"].apply(lambda ids: ids[0])\n", + "remove_columns(text_units, [\"document_ids\"])\n", + "\n", + "await write_table_to_storage(text_units, \"text_units\", storage)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Update settings.yaml\n", + "If you have left the default settings for your vector store schema, you may need to set explicit values that map each embedding type to a vector schema name. If you have already customized your vector store schema it may not be necessary.\n", + "\n", + "Old default index names:\n", + "- default-text_unit-text\n", + "- default-entity-description\n", + "- default-community-full_content\n", + "\n", + "(if you left all of the defaults, check your output/lancedb folder to confirm the above)\n", + "\n", + "v3 versions are:\n", + "- text_unit_text\n", + "- entity_description\n", + "- community_full_content\n", + "\n", + "Therefore, with a v2 index need to explicitly set the old index names so it connects correctly. We no longer support the \"prefix\" - you can just set an explicit index_name for each embedding.\n", + "\n", + "NOTE: we are also setting the default vector_size for each index below, under the assumption that you are using a prior default with 1536 dimensions. Our new default of text-embedding-3-large has 3072 dimensions, which will be populated as the default if unset. Again, if you have a more complicated situation you may want to manually configure this.\n", + "\n", + "Here is an example of the new vector store config block that you may need in your settings.yaml:\n", + "\n", + "```yaml\n", + "vector_store:\n", + " type: lancedb\n", + " db_uri: output/lancedb\n", + " index_schema:\n", + " text_unit_text:\n", + " index_name: default-text_unit-text\n", + " vector_size: 1536\n", + " entity_description:\n", + " index_name: default-entity-description\n", + " vector_size: 1536\n", + " community_full_content:\n", + " index_name: default-community-full_content\n", + " vector_size: 1536\n", + "```\n" + ] + } + ], + "metadata": { + "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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/examples_notebooks/input_documents.ipynb b/docs/examples_notebooks/input_documents.ipynb index 1135cb9eb0..5657770eaf 100644 --- a/docs/examples_notebooks/input_documents.ipynb +++ b/docs/examples_notebooks/input_documents.ipynb @@ -18,7 +18,7 @@ "\n", "Newer versions of GraphRAG let you submit a dataframe directly instead of running through the input processing step. This notebook demonstrates with regular or update runs.\n", "\n", - "If performing an update, the assumption is that your dataframe contains only the new documents to add to the index." + "If performing an update, the assumption is that your dataframe contains only the new documents to add to the index.\n" ] }, { @@ -30,9 +30,8 @@ "from pathlib import Path\n", "from pprint import pprint\n", "\n", - "import pandas as pd\n", - "\n", "import graphrag.api as api\n", + "import pandas as pd\n", "from graphrag.config.load_config import load_config\n", "from graphrag.index.typing.pipeline_run_result import PipelineRunResult" ] @@ -55,7 +54,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Generate a `GraphRagConfig` object" + "### Generate a `GraphRagConfig` object\n" ] }, { @@ -73,14 +72,14 @@ "source": [ "## Indexing API\n", "\n", - "*Indexing* is the process of ingesting raw text data and constructing a knowledge graph. GraphRAG currently supports plaintext (`.txt`) and `.csv` file formats." + "_Indexing_ is the process of ingesting raw text data and constructing a knowledge graph. GraphRAG currently supports plaintext (`.txt`) and `.csv` file formats.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Build an index" + "## Build an index\n" ] }, { @@ -110,7 +109,7 @@ "source": [ "## Query an index\n", "\n", - "To query an index, several index files must first be read into memory and passed to the query API. " + "To query an index, several index files must first be read into memory and passed to the query API.\n" ] }, { @@ -141,7 +140,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The response object is the official reponse from graphrag while the context object holds various metadata regarding the querying process used to obtain the final response." + "The response object is the official reponse from graphrag while the context object holds various metadata regarding the querying process used to obtain the final response.\n" ] }, { @@ -157,7 +156,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Digging into the context a bit more provides users with extremely granular information such as what sources of data (down to the level of text chunks) were ultimately retrieved and used as part of the context sent to the LLM model)." + "Digging into the context a bit more provides users with extremely granular information such as what sources of data (down to the level of text chunks) were ultimately retrieved and used as part of the context sent to the LLM model).\n" ] }, { @@ -172,7 +171,7 @@ ], "metadata": { "kernelspec": { - "display_name": "graphrag", + "display_name": "graphrag-monorepo", "language": "python", "name": "python3" }, @@ -186,7 +185,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.10" + "version": "3.12.9" } }, "nbformat": 4, diff --git a/docs/examples_notebooks/inputs/operation dulce/communities.parquet b/docs/examples_notebooks/inputs/operation dulce/communities.parquet index 885ced1b15..4f4c1a864f 100644 Binary files a/docs/examples_notebooks/inputs/operation dulce/communities.parquet and b/docs/examples_notebooks/inputs/operation dulce/communities.parquet differ diff --git a/docs/examples_notebooks/inputs/operation dulce/community_reports.parquet b/docs/examples_notebooks/inputs/operation dulce/community_reports.parquet index d633ec0047..e6e45e947b 100644 Binary files a/docs/examples_notebooks/inputs/operation dulce/community_reports.parquet and b/docs/examples_notebooks/inputs/operation dulce/community_reports.parquet differ diff --git a/docs/examples_notebooks/inputs/operation dulce/covariates.parquet b/docs/examples_notebooks/inputs/operation dulce/covariates.parquet index cc7b212fbf..1150cd0b00 100644 Binary files a/docs/examples_notebooks/inputs/operation dulce/covariates.parquet and b/docs/examples_notebooks/inputs/operation dulce/covariates.parquet differ diff --git a/docs/examples_notebooks/inputs/operation dulce/embeddings.community_full_content.parquet b/docs/examples_notebooks/inputs/operation dulce/embeddings.community_full_content.parquet new file mode 100644 index 0000000000..d9dd43ce6f Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/embeddings.community_full_content.parquet differ diff --git a/docs/examples_notebooks/inputs/operation dulce/entities.parquet b/docs/examples_notebooks/inputs/operation dulce/entities.parquet index 4378317c7a..466f5058ae 100644 Binary files a/docs/examples_notebooks/inputs/operation dulce/entities.parquet and b/docs/examples_notebooks/inputs/operation dulce/entities.parquet differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_indices/dd5917d9-d48d-4af3-bc2e-43a53b2fdbe6/auxiliary.idx b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_indices/dd5917d9-d48d-4af3-bc2e-43a53b2fdbe6/auxiliary.idx new file mode 100644 index 0000000000..57eee5fba5 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_indices/dd5917d9-d48d-4af3-bc2e-43a53b2fdbe6/auxiliary.idx differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_indices/dd5917d9-d48d-4af3-bc2e-43a53b2fdbe6/index.idx b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_indices/dd5917d9-d48d-4af3-bc2e-43a53b2fdbe6/index.idx new file mode 100644 index 0000000000..1b8db50ecf Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_indices/dd5917d9-d48d-4af3-bc2e-43a53b2fdbe6/index.idx differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/0-a943ac34-0e87-43c2-80d0-8f83fb80f4f5.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/0-a943ac34-0e87-43c2-80d0-8f83fb80f4f5.txn new file mode 100644 index 0000000000..f987e5fb4a Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/0-a943ac34-0e87-43c2-80d0-8f83fb80f4f5.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/1-ec798d7b-a8bf-4985-a5d0-784434802168.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/1-ec798d7b-a8bf-4985-a5d0-784434802168.txn new file mode 100644 index 0000000000..f4850f70d7 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/1-ec798d7b-a8bf-4985-a5d0-784434802168.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/2-78887911-d792-4dc9-b28d-f2858db1139a.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/2-78887911-d792-4dc9-b28d-f2858db1139a.txn new file mode 100644 index 0000000000..53f6243871 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/2-78887911-d792-4dc9-b28d-f2858db1139a.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/3-c16be721-5d7e-46a0-98c2-34d5d9c29383.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/3-c16be721-5d7e-46a0-98c2-34d5d9c29383.txn new file mode 100644 index 0000000000..add345bded Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_transactions/3-c16be721-5d7e-46a0-98c2-34d5d9c29383.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/1.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/1.manifest new file mode 100644 index 0000000000..f9e39ec20b Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/1.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/2.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/2.manifest new file mode 100644 index 0000000000..c9ed390a4f Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/2.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/3.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/3.manifest new file mode 100644 index 0000000000..408240bf4d Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/3.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/4.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/4.manifest new file mode 100644 index 0000000000..0769fe9c6f Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/_versions/4.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/data/667cccff-01b5-4b70-a2a6-8cf4d6ada077.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/data/667cccff-01b5-4b70-a2a6-8cf4d6ada077.lance new file mode 100644 index 0000000000..04aad5948c Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/data/667cccff-01b5-4b70-a2a6-8cf4d6ada077.lance differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/data/a84d995f-111c-45d1-ba5a-32b3747b8a18.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/data/a84d995f-111c-45d1-ba5a-32b3747b8a18.lance new file mode 100644 index 0000000000..3b9d4fb218 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/community_full_content.lance/data/a84d995f-111c-45d1-ba5a-32b3747b8a18.lance differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/0-2fed1d8b-daac-41b0-a93a-e115cda75be3.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/0-2fed1d8b-daac-41b0-a93a-e115cda75be3.txn deleted file mode 100644 index 4ae06f643c..0000000000 --- a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/0-2fed1d8b-daac-41b0-a93a-e115cda75be3.txn +++ /dev/null @@ -1,2 +0,0 @@ -$2fed1d8b-daac-41b0-a93a-e115cda75be3$id *string08Zdefault(text *string08Zdefault>vector *fixed_size_list:float:153608Zdefault. -attributes *string08Zdefault \ No newline at end of file diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/1-61dbb7c2-aec3-4796-b223-941fc7cc93cc.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/1-61dbb7c2-aec3-4796-b223-941fc7cc93cc.txn deleted file mode 100644 index 7af4506c1f..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/1-61dbb7c2-aec3-4796-b223-941fc7cc93cc.txn and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/2-60012692-a153-48f9-8f4e-c479b44cbf3f.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/2-60012692-a153-48f9-8f4e-c479b44cbf3f.txn deleted file mode 100644 index 8989d3ff3e..0000000000 --- a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/2-60012692-a153-48f9-8f4e-c479b44cbf3f.txn +++ /dev/null @@ -1,2 +0,0 @@ -$60012692-a153-48f9-8f4e-c479b44cbf3f$id *string08Zdefault(text *string08Zdefault>vector *fixed_size_list:float:153608Zdefault. -attributes *string08Zdefault \ No newline at end of file diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/3-0d2dc9a1-094f-4220-83c7-6ad6f26fac2b.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/3-0d2dc9a1-094f-4220-83c7-6ad6f26fac2b.txn deleted file mode 100644 index 63c1b2f07d..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_transactions/3-0d2dc9a1-094f-4220-83c7-6ad6f26fac2b.txn and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/1.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/1.manifest deleted file mode 100644 index 321504ba88..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/1.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/2.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/2.manifest deleted file mode 100644 index 6cf8a61ee3..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/2.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/3.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/3.manifest deleted file mode 100644 index 022bec3439..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/3.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/4.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/4.manifest deleted file mode 100644 index ce22c6edb5..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/_versions/4.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/data/1e7b2d94-ed06-4aa0-b22e-86a71d416bc6.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/data/1e7b2d94-ed06-4aa0-b22e-86a71d416bc6.lance deleted file mode 100644 index 92d80e6d52..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/data/1e7b2d94-ed06-4aa0-b22e-86a71d416bc6.lance and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/data/1ed9f301-ce30-46a8-8c0b-9c2a60a3cf43.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/data/1ed9f301-ce30-46a8-8c0b-9c2a60a3cf43.lance deleted file mode 100644 index afc4df25e9..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-community-full_content.lance/data/1ed9f301-ce30-46a8-8c0b-9c2a60a3cf43.lance and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/0-92c031e5-7558-451e-9d0f-f5514db9616d.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/0-92c031e5-7558-451e-9d0f-f5514db9616d.txn deleted file mode 100644 index 9e3261cb97..0000000000 --- a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/0-92c031e5-7558-451e-9d0f-f5514db9616d.txn +++ /dev/null @@ -1,2 +0,0 @@ -$92c031e5-7558-451e-9d0f-f5514db9616d$id *string08Zdefault(text *string08Zdefault>vector *fixed_size_list:float:153608Zdefault. -attributes *string08Zdefault \ No newline at end of file diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/1-7b3cb8d8-3512-4584-a003-91838fed8911.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/1-7b3cb8d8-3512-4584-a003-91838fed8911.txn deleted file mode 100644 index daf150cc07..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/1-7b3cb8d8-3512-4584-a003-91838fed8911.txn and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/2-7de627d2-4c57-49e9-bf73-c17a9582ead4.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/2-7de627d2-4c57-49e9-bf73-c17a9582ead4.txn deleted file mode 100644 index f4f8f42725..0000000000 --- a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/2-7de627d2-4c57-49e9-bf73-c17a9582ead4.txn +++ /dev/null @@ -1,2 +0,0 @@ -$7de627d2-4c57-49e9-bf73-c17a9582ead4$id *string08Zdefault(text *string08Zdefault>vector *fixed_size_list:float:153608Zdefault. -attributes *string08Zdefault \ No newline at end of file diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/3-9ad29d69-9a69-43a8-8b26-252ea267958d.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/3-9ad29d69-9a69-43a8-8b26-252ea267958d.txn deleted file mode 100644 index d3497cbd54..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_transactions/3-9ad29d69-9a69-43a8-8b26-252ea267958d.txn and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/1.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/1.manifest deleted file mode 100644 index b35c6002ee..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/1.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/2.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/2.manifest deleted file mode 100644 index 6837b2effb..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/2.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/3.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/3.manifest deleted file mode 100644 index 4cafb59f5b..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/3.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/4.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/4.manifest deleted file mode 100644 index 95defe4551..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/_versions/4.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/data/a34575c4-5260-457f-bebe-3f40bc0e2ee3.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/data/a34575c4-5260-457f-bebe-3f40bc0e2ee3.lance deleted file mode 100644 index 6777f9c87a..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/data/a34575c4-5260-457f-bebe-3f40bc0e2ee3.lance and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/data/eabd7580-86f5-4022-8aa7-fe0aff816d98.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/data/eabd7580-86f5-4022-8aa7-fe0aff816d98.lance deleted file mode 100644 index 789725b40d..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-entity-description.lance/data/eabd7580-86f5-4022-8aa7-fe0aff816d98.lance and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/0-fd0434ac-e5cd-4ddd-9dd5-e5048d4edb59.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/0-fd0434ac-e5cd-4ddd-9dd5-e5048d4edb59.txn deleted file mode 100644 index 81d66a0b44..0000000000 --- a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/0-fd0434ac-e5cd-4ddd-9dd5-e5048d4edb59.txn +++ /dev/null @@ -1,2 +0,0 @@ -$fd0434ac-e5cd-4ddd-9dd5-e5048d4edb59$id *string08Zdefault(text *string08Zdefault>vector *fixed_size_list:float:153608Zdefault. -attributes *string08Zdefault \ No newline at end of file diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/1-14bb4b1d-cc00-420b-9b14-3626f0bd8c0b.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/1-14bb4b1d-cc00-420b-9b14-3626f0bd8c0b.txn deleted file mode 100644 index 86b642720a..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/1-14bb4b1d-cc00-420b-9b14-3626f0bd8c0b.txn and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/2-8e74264c-f72d-44f5-a6f4-b3b61ae6a43b.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/2-8e74264c-f72d-44f5-a6f4-b3b61ae6a43b.txn deleted file mode 100644 index b26d571dc9..0000000000 --- a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/2-8e74264c-f72d-44f5-a6f4-b3b61ae6a43b.txn +++ /dev/null @@ -1,2 +0,0 @@ -$8e74264c-f72d-44f5-a6f4-b3b61ae6a43b$id *string08Zdefault(text *string08Zdefault>vector *fixed_size_list:float:153608Zdefault. -attributes *string08Zdefault \ No newline at end of file diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/3-7516fb71-9db3-4666-bdef-ea04c1eb9697.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/3-7516fb71-9db3-4666-bdef-ea04c1eb9697.txn deleted file mode 100644 index 9b0915ab9b..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_transactions/3-7516fb71-9db3-4666-bdef-ea04c1eb9697.txn and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/1.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/1.manifest deleted file mode 100644 index a35b96156d..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/1.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/2.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/2.manifest deleted file mode 100644 index c14970b78c..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/2.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/3.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/3.manifest deleted file mode 100644 index acb1546ca7..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/3.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/4.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/4.manifest deleted file mode 100644 index 61ef1262a3..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/_versions/4.manifest and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/data/2794bf5b-de3d-4202-ab16-e76bc27c8e6a.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/data/2794bf5b-de3d-4202-ab16-e76bc27c8e6a.lance deleted file mode 100644 index 8758d96515..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/data/2794bf5b-de3d-4202-ab16-e76bc27c8e6a.lance and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/data/2f74c8e8-3f35-4209-889c-a13cf0780eb3.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/data/2f74c8e8-3f35-4209-889c-a13cf0780eb3.lance deleted file mode 100644 index 8758d96515..0000000000 Binary files a/docs/examples_notebooks/inputs/operation dulce/lancedb/default-text_unit-text.lance/data/2f74c8e8-3f35-4209-889c-a13cf0780eb3.lance and /dev/null differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_indices/b1bd07f3-ad25-40bc-b91c-14215386e477/auxiliary.idx b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_indices/b1bd07f3-ad25-40bc-b91c-14215386e477/auxiliary.idx new file mode 100644 index 0000000000..57eee5fba5 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_indices/b1bd07f3-ad25-40bc-b91c-14215386e477/auxiliary.idx differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_indices/b1bd07f3-ad25-40bc-b91c-14215386e477/index.idx b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_indices/b1bd07f3-ad25-40bc-b91c-14215386e477/index.idx new file mode 100644 index 0000000000..cfbb01f872 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_indices/b1bd07f3-ad25-40bc-b91c-14215386e477/index.idx differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/0-5b75ba0a-bae9-4244-8a6b-31de09f7e03d.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/0-5b75ba0a-bae9-4244-8a6b-31de09f7e03d.txn new file mode 100644 index 0000000000..9796ad0ee9 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/0-5b75ba0a-bae9-4244-8a6b-31de09f7e03d.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/1-71f2ac8d-a101-467d-b57d-2dea6d14f7a7.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/1-71f2ac8d-a101-467d-b57d-2dea6d14f7a7.txn new file mode 100644 index 0000000000..eba1611930 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/1-71f2ac8d-a101-467d-b57d-2dea6d14f7a7.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/2-d621e621-8e92-419f-99e4-f1c7d163bcc2.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/2-d621e621-8e92-419f-99e4-f1c7d163bcc2.txn new file mode 100644 index 0000000000..2b154eac74 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/2-d621e621-8e92-419f-99e4-f1c7d163bcc2.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/3-4ada922f-cf85-44df-bc8b-b132e35009d0.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/3-4ada922f-cf85-44df-bc8b-b132e35009d0.txn new file mode 100644 index 0000000000..1ae8892c8c Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/3-4ada922f-cf85-44df-bc8b-b132e35009d0.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/4-dcac110d-2a49-4777-a51e-5078fed1b0df.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/4-dcac110d-2a49-4777-a51e-5078fed1b0df.txn new file mode 100644 index 0000000000..655b06d660 --- /dev/null +++ b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/4-dcac110d-2a49-4777-a51e-5078fed1b0df.txn @@ -0,0 +1 @@ +$dcac110d-2a49-4777-a51e-5078fed1b0dfid = '__DUMMY__' \ No newline at end of file diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/5-ec86af9f-b799-4457-b07a-24a3459dd952.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/5-ec86af9f-b799-4457-b07a-24a3459dd952.txn new file mode 100644 index 0000000000..6512848103 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/5-ec86af9f-b799-4457-b07a-24a3459dd952.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/6-c2afb84c-4b3a-4ccd-8843-0deaa25bd971.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/6-c2afb84c-4b3a-4ccd-8843-0deaa25bd971.txn new file mode 100644 index 0000000000..821dd8ca62 --- /dev/null +++ b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/6-c2afb84c-4b3a-4ccd-8843-0deaa25bd971.txn @@ -0,0 +1 @@ +$c2afb84c-4b3a-4ccd-8843-0deaa25bd971id = '__DUMMY__' \ No newline at end of file diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/7-6798041f-3283-4b54-8313-54f80e00d338.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/7-6798041f-3283-4b54-8313-54f80e00d338.txn new file mode 100644 index 0000000000..fef8f6ccbd Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_transactions/7-6798041f-3283-4b54-8313-54f80e00d338.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/1.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/1.manifest new file mode 100644 index 0000000000..a3a4bdc459 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/1.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/2.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/2.manifest new file mode 100644 index 0000000000..6f79fc2d12 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/2.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/3.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/3.manifest new file mode 100644 index 0000000000..9deea08227 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/3.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/4.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/4.manifest new file mode 100644 index 0000000000..c7eee87b90 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/4.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/5.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/5.manifest new file mode 100644 index 0000000000..baf532593d Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/5.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/6.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/6.manifest new file mode 100644 index 0000000000..e8c03b450e Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/6.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/7.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/7.manifest new file mode 100644 index 0000000000..3f8fed8f6b Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/7.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/8.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/8.manifest new file mode 100644 index 0000000000..9c4380be0b Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/_versions/8.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/2677858d-16a4-4c0c-9515-ed5a9ee32fd7.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/2677858d-16a4-4c0c-9515-ed5a9ee32fd7.lance new file mode 100644 index 0000000000..f19cd20010 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/2677858d-16a4-4c0c-9515-ed5a9ee32fd7.lance differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/4ed23c16-d11c-49b5-869d-653cfbd9c271.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/4ed23c16-d11c-49b5-869d-653cfbd9c271.lance new file mode 100644 index 0000000000..04aad5948c Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/4ed23c16-d11c-49b5-869d-653cfbd9c271.lance differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/5370b2ef-efd3-434e-9745-9d046b53bb4a.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/5370b2ef-efd3-434e-9745-9d046b53bb4a.lance new file mode 100644 index 0000000000..45878991ef Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/5370b2ef-efd3-434e-9745-9d046b53bb4a.lance differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/ec59d894-a2a6-4a44-8266-d07bbd684c33.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/ec59d894-a2a6-4a44-8266-d07bbd684c33.lance new file mode 100644 index 0000000000..44e433bdce Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/entity_description.lance/data/ec59d894-a2a6-4a44-8266-d07bbd684c33.lance differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_indices/f5099c4f-df9a-476d-a736-1eac0a498173/auxiliary.idx b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_indices/f5099c4f-df9a-476d-a736-1eac0a498173/auxiliary.idx new file mode 100644 index 0000000000..57eee5fba5 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_indices/f5099c4f-df9a-476d-a736-1eac0a498173/auxiliary.idx differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_indices/f5099c4f-df9a-476d-a736-1eac0a498173/index.idx b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_indices/f5099c4f-df9a-476d-a736-1eac0a498173/index.idx new file mode 100644 index 0000000000..cfbb01f872 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_indices/f5099c4f-df9a-476d-a736-1eac0a498173/index.idx differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/0-9abee4db-9914-4c35-b600-d995235f8e27.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/0-9abee4db-9914-4c35-b600-d995235f8e27.txn new file mode 100644 index 0000000000..d6a0e07b3a Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/0-9abee4db-9914-4c35-b600-d995235f8e27.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/1-302f5740-fbd0-4887-9933-13a2842ec8e4.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/1-302f5740-fbd0-4887-9933-13a2842ec8e4.txn new file mode 100644 index 0000000000..3ae71df2c4 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/1-302f5740-fbd0-4887-9933-13a2842ec8e4.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/2-551a917c-5ab9-46a7-9085-fd82aa879717.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/2-551a917c-5ab9-46a7-9085-fd82aa879717.txn new file mode 100644 index 0000000000..6eadc32172 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/2-551a917c-5ab9-46a7-9085-fd82aa879717.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/3-b0b6e7bb-a152-4148-be3b-d1ab50215bda.txn b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/3-b0b6e7bb-a152-4148-be3b-d1ab50215bda.txn new file mode 100644 index 0000000000..869892f078 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_transactions/3-b0b6e7bb-a152-4148-be3b-d1ab50215bda.txn differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/1.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/1.manifest new file mode 100644 index 0000000000..23b69adf1a Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/1.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/2.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/2.manifest new file mode 100644 index 0000000000..bfdf351394 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/2.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/3.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/3.manifest new file mode 100644 index 0000000000..d1ad0b0a8e Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/3.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/4.manifest b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/4.manifest new file mode 100644 index 0000000000..1b1609101e Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/_versions/4.manifest differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/data/4105076e-7a55-4549-b86a-51c6bfa68ea5.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/data/4105076e-7a55-4549-b86a-51c6bfa68ea5.lance new file mode 100644 index 0000000000..04aad5948c Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/data/4105076e-7a55-4549-b86a-51c6bfa68ea5.lance differ diff --git a/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/data/a28d4629-848b-4e1d-854d-cad6297a0c1a.lance b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/data/a28d4629-848b-4e1d-854d-cad6297a0c1a.lance new file mode 100644 index 0000000000..2f101f12d5 Binary files /dev/null and b/docs/examples_notebooks/inputs/operation dulce/lancedb/text_unit_text.lance/data/a28d4629-848b-4e1d-854d-cad6297a0c1a.lance differ diff --git a/docs/examples_notebooks/inputs/operation dulce/relationships.parquet b/docs/examples_notebooks/inputs/operation dulce/relationships.parquet index 4bdf4c85f2..ee07a21e51 100644 Binary files a/docs/examples_notebooks/inputs/operation dulce/relationships.parquet and b/docs/examples_notebooks/inputs/operation dulce/relationships.parquet differ diff --git a/docs/examples_notebooks/inputs/operation dulce/text_units.parquet b/docs/examples_notebooks/inputs/operation dulce/text_units.parquet index 09349e3f65..c4df9cc5ee 100644 Binary files a/docs/examples_notebooks/inputs/operation dulce/text_units.parquet and b/docs/examples_notebooks/inputs/operation dulce/text_units.parquet differ diff --git a/docs/examples_notebooks/local_search.ipynb b/docs/examples_notebooks/local_search.ipynb index 77a74c544b..f7f0c5a54b 100644 --- a/docs/examples_notebooks/local_search.ipynb +++ b/docs/examples_notebooks/local_search.ipynb @@ -19,8 +19,6 @@ "import os\n", "\n", "import pandas as pd\n", - "\n", - "from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig\n", "from graphrag.query.context_builder.entity_extraction import EntityVectorStoreKey\n", "from graphrag.query.indexer_adapters import (\n", " read_indexer_covariates,\n", @@ -34,7 +32,7 @@ " LocalSearchMixedContext,\n", ")\n", "from graphrag.query.structured_search.local_search.search import LocalSearch\n", - "from graphrag.vector_stores.lancedb import LanceDBVectorStore" + "from graphrag_vectors import IndexSchema, LanceDBVectorStore" ] }, { @@ -102,9 +100,7 @@ "# load description embeddings to an in-memory lancedb vectorstore\n", "# to connect to a remote db, specify url and port values.\n", "description_embedding_store = LanceDBVectorStore(\n", - " vector_store_schema_config=VectorStoreSchemaConfig(\n", - " index_name=\"default-entity-description\"\n", - " )\n", + " index_schema=IndexSchema(index_name=\"default-entity-description\")\n", ")\n", "description_embedding_store.connect(db_uri=LANCEDB_URI)\n", "\n", diff --git a/docs/examples_notebooks/multi_index_search.ipynb b/docs/examples_notebooks/multi_index_search.ipynb deleted file mode 100644 index 2e70ed5086..0000000000 --- a/docs/examples_notebooks/multi_index_search.ipynb +++ /dev/null @@ -1,558 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Copyright (c) 2024 Microsoft Corporation.\n", - "# Licensed under the MIT License." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Multi Index Search\n", - "This notebook demonstrates multi-index search using the GraphRAG API.\n", - "\n", - "Indexes created from Wikipedia state articles for Alaska, California, DC, Maryland, NY and Washington are used." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import asyncio\n", - "\n", - "import pandas as pd\n", - "\n", - "from graphrag.api.query import (\n", - " multi_index_basic_search,\n", - " multi_index_drift_search,\n", - " multi_index_global_search,\n", - " multi_index_local_search,\n", - ")\n", - "from graphrag.config.create_graphrag_config import create_graphrag_config\n", - "\n", - "indexes = [\"alaska\", \"california\", \"dc\", \"maryland\", \"ny\", \"washington\"]\n", - "indexes = sorted(indexes)\n", - "\n", - "print(indexes)\n", - "\n", - "vector_store_configs = {\n", - " index: {\n", - " \"type\": \"lancedb\",\n", - " \"db_uri\": f\"inputs/{index}/lancedb\",\n", - " \"container_name\": \"default\",\n", - " \"overwrite\": True,\n", - " \"index_name\": f\"{index}\",\n", - " }\n", - " for index in indexes\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "config_data = {\n", - " \"models\": {\n", - " \"default_chat_model\": {\n", - " \"model_supports_json\": True,\n", - " \"parallelization_num_threads\": 50,\n", - " \"parallelization_stagger\": 0.3,\n", - " \"async_mode\": \"threaded\",\n", - " \"type\": \"azure_openai_chat\",\n", - " \"model\": \"gpt-4o\",\n", - " \"auth_type\": \"azure_managed_identity\",\n", - " \"api_base\": \"\",\n", - " \"api_version\": \"2024-02-15-preview\",\n", - " \"deployment_name\": \"gpt-4o\",\n", - " },\n", - " \"default_embedding_model\": {\n", - " \"parallelization_num_threads\": 50,\n", - " \"parallelization_stagger\": 0.3,\n", - " \"async_mode\": \"threaded\",\n", - " \"type\": \"azure_openai_embedding\",\n", - " \"model\": \"text-embedding-3-large\",\n", - " \"auth_type\": \"azure_managed_identity\",\n", - " \"api_base\": \"\",\n", - " \"api_version\": \"2024-02-15-preview\",\n", - " \"deployment_name\": \"text-embedding-3-large\",\n", - " },\n", - " },\n", - " \"vector_store\": vector_store_configs,\n", - " \"local_search\": {\n", - " \"prompt\": \"prompts/local_search_system_prompt.txt\",\n", - " \"llm_max_tokens\": 12000,\n", - " },\n", - " \"global_search\": {\n", - " \"map_prompt\": \"prompts/global_search_map_system_prompt.txt\",\n", - " \"reduce_prompt\": \"prompts/global_search_reduce_system_prompt.txt\",\n", - " \"knowledge_prompt\": \"prompts/global_search_knowledge_system_prompt.txt\",\n", - " },\n", - " \"drift_search\": {\n", - " \"prompt\": \"prompts/drift_search_system_prompt.txt\",\n", - " \"reduce_prompt\": \"prompts/drift_search_reduce_prompt.txt\",\n", - " },\n", - " \"basic_search\": {\"prompt\": \"prompts/basic_search_system_prompt.txt\"},\n", - "}\n", - "parameters = create_graphrag_config(config_data, \".\")\n", - "loop = asyncio.get_event_loop()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Multi-index Global Search" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "entities = [pd.read_parquet(f\"inputs/{index}/entities.parquet\") for index in indexes]\n", - "communities = [\n", - " pd.read_parquet(f\"inputs/{index}/communities.parquet\") for index in indexes\n", - "]\n", - "community_reports = [\n", - " pd.read_parquet(f\"inputs/{index}/community_reports.parquet\") for index in indexes\n", - "]\n", - "\n", - "task = loop.create_task(\n", - " multi_index_global_search(\n", - " parameters,\n", - " entities,\n", - " communities,\n", - " community_reports,\n", - " indexes,\n", - " 1,\n", - " False,\n", - " \"Multiple Paragraphs\",\n", - " False,\n", - " \"Describe this dataset.\",\n", - " )\n", - ")\n", - "results = await task" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Print report" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(results[0])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Show context links back to original index" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for report_id in [120, 129, 40, 16, 204, 143, 85, 122, 83]:\n", - " index_name = [i for i in results[1][\"reports\"] if i[\"id\"] == str(report_id)][0][ # noqa: RUF015\n", - " \"index_name\"\n", - " ]\n", - " index_id = [i for i in results[1][\"reports\"] if i[\"id\"] == str(report_id)][0][ # noqa: RUF015\n", - " \"index_id\"\n", - " ]\n", - " print(report_id, index_name, index_id)\n", - " index_reports = pd.read_parquet(\n", - " f\"inputs/{index_name}/create_final_community_reports.parquet\"\n", - " )\n", - " print([i for i in results[1][\"reports\"] if i[\"id\"] == str(report_id)][0][\"title\"]) # noqa: RUF015\n", - " print(\n", - " index_reports[index_reports[\"community\"] == int(index_id)][\"title\"].to_numpy()[\n", - " 0\n", - " ]\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Multi-index Local Search" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "entities = [pd.read_parquet(f\"inputs/{index}/entities.parquet\") for index in indexes]\n", - "communities = [\n", - " pd.read_parquet(f\"inputs/{index}/communities.parquet\") for index in indexes\n", - "]\n", - "community_reports = [\n", - " pd.read_parquet(f\"inputs/{index}/community_reports.parquet\") for index in indexes\n", - "]\n", - "covariates = [\n", - " pd.read_parquet(f\"inputs/{index}/covariates.parquet\") for index in indexes\n", - "]\n", - "text_units = [\n", - " pd.read_parquet(f\"inputs/{index}/text_units.parquet\") for index in indexes\n", - "]\n", - "relationships = [\n", - " pd.read_parquet(f\"inputs/{index}/relationships.parquet\") for index in indexes\n", - "]\n", - "\n", - "task = loop.create_task(\n", - " multi_index_local_search(\n", - " parameters,\n", - " entities,\n", - " communities,\n", - " community_reports,\n", - " text_units,\n", - " relationships,\n", - " covariates,\n", - " indexes,\n", - " 1,\n", - " \"Multiple Paragraphs\",\n", - " False,\n", - " \"weather\",\n", - " )\n", - ")\n", - "results = await task" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Print report" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(results[0])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Show context links back to original index" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for report_id in [47, 213]:\n", - " index_name = [i for i in results[1][\"reports\"] if i[\"id\"] == str(report_id)][0][ # noqa: RUF015\n", - " \"index_name\"\n", - " ]\n", - " index_id = [i for i in results[1][\"reports\"] if i[\"id\"] == str(report_id)][0][ # noqa: RUF015\n", - " \"index_id\"\n", - " ]\n", - " print(report_id, index_name, index_id)\n", - " index_reports = pd.read_parquet(\n", - " f\"inputs/{index_name}/create_final_community_reports.parquet\"\n", - " )\n", - " print([i for i in results[1][\"reports\"] if i[\"id\"] == str(report_id)][0][\"title\"]) # noqa: RUF015\n", - " print(\n", - " index_reports[index_reports[\"community\"] == int(index_id)][\"title\"].to_numpy()[\n", - " 0\n", - " ]\n", - " )\n", - "for entity_id in [500, 502, 506, 1960, 1961, 1962]:\n", - " index_name = [i for i in results[1][\"entities\"] if i[\"id\"] == str(entity_id)][0][ # noqa: RUF015\n", - " \"index_name\"\n", - " ]\n", - " index_id = [i for i in results[1][\"entities\"] if i[\"id\"] == str(entity_id)][0][ # noqa: RUF015\n", - " \"index_id\"\n", - " ]\n", - " print(entity_id, index_name, index_id)\n", - " index_entities = pd.read_parquet(\n", - " f\"inputs/{index_name}/create_final_entities.parquet\"\n", - " )\n", - " print(\n", - " [i for i in results[1][\"entities\"] if i[\"id\"] == str(entity_id)][0][ # noqa: RUF015\n", - " \"description\"\n", - " ][:100]\n", - " )\n", - " print(\n", - " index_entities[index_entities[\"human_readable_id\"] == int(index_id)][\n", - " \"description\"\n", - " ].to_numpy()[0][:100]\n", - " )\n", - "for relationship_id in [1805, 1806]:\n", - " index_name = [ # noqa: RUF015\n", - " i for i in results[1][\"relationships\"] if i[\"id\"] == str(relationship_id)\n", - " ][0][\"index_name\"]\n", - " index_id = [ # noqa: RUF015\n", - " i for i in results[1][\"relationships\"] if i[\"id\"] == str(relationship_id)\n", - " ][0][\"index_id\"]\n", - " print(relationship_id, index_name, index_id)\n", - " index_relationships = pd.read_parquet(\n", - " f\"inputs/{index_name}/create_final_relationships.parquet\"\n", - " )\n", - " print(\n", - " [i for i in results[1][\"relationships\"] if i[\"id\"] == str(relationship_id)][0][ # noqa: RUF015\n", - " \"description\"\n", - " ]\n", - " )\n", - " print(\n", - " index_relationships[index_relationships[\"human_readable_id\"] == int(index_id)][\n", - " \"description\"\n", - " ].to_numpy()[0]\n", - " )\n", - "for claim_id in [100]:\n", - " index_name = [i for i in results[1][\"claims\"] if i[\"id\"] == str(claim_id)][0][ # noqa: RUF015\n", - " \"index_name\"\n", - " ]\n", - " index_id = [i for i in results[1][\"claims\"] if i[\"id\"] == str(claim_id)][0][ # noqa: RUF015\n", - " \"index_id\"\n", - " ]\n", - " print(relationship_id, index_name, index_id)\n", - " index_claims = pd.read_parquet(\n", - " f\"inputs/{index_name}/create_final_covariates.parquet\"\n", - " )\n", - " print(\n", - " [i for i in results[1][\"claims\"] if i[\"id\"] == str(claim_id)][0][\"description\"] # noqa: RUF015\n", - " )\n", - " print(\n", - " index_claims[index_claims[\"human_readable_id\"] == int(index_id)][\n", - " \"description\"\n", - " ].to_numpy()[0]\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Multi-index Drift Search" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "entities = [pd.read_parquet(f\"inputs/{index}/entities.parquet\") for index in indexes]\n", - "communities = [\n", - " pd.read_parquet(f\"inputs/{index}/communities.parquet\") for index in indexes\n", - "]\n", - "community_reports = [\n", - " pd.read_parquet(f\"inputs/{index}/community_reports.parquet\") for index in indexes\n", - "]\n", - "text_units = [\n", - " pd.read_parquet(f\"inputs/{index}/text_units.parquet\") for index in indexes\n", - "]\n", - "relationships = [\n", - " pd.read_parquet(f\"inputs/{index}/relationships.parquet\") for index in indexes\n", - "]\n", - "\n", - "task = loop.create_task(\n", - " multi_index_drift_search(\n", - " parameters,\n", - " entities,\n", - " communities,\n", - " community_reports,\n", - " text_units,\n", - " relationships,\n", - " indexes,\n", - " 1,\n", - " \"Multiple Paragraphs\",\n", - " False,\n", - " \"agriculture\",\n", - " )\n", - ")\n", - "results = await task" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Print report" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(results[0])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Show context links back to original index" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for report_id in [47, 236]:\n", - " for question in results[1]:\n", - " resq = results[1][question]\n", - " if len(resq[\"reports\"]) == 0:\n", - " continue\n", - " if len([i for i in resq[\"reports\"] if i[\"id\"] == str(report_id)]) == 0:\n", - " continue\n", - " index_name = [i for i in resq[\"reports\"] if i[\"id\"] == str(report_id)][0][ # noqa: RUF015\n", - " \"index_name\"\n", - " ]\n", - " index_id = [i for i in resq[\"reports\"] if i[\"id\"] == str(report_id)][0][ # noqa: RUF015\n", - " \"index_id\"\n", - " ]\n", - " print(question, report_id, index_name, index_id)\n", - " index_reports = pd.read_parquet(\n", - " f\"inputs/{index_name}/create_final_community_reports.parquet\"\n", - " )\n", - " print([i for i in resq[\"reports\"] if i[\"id\"] == str(report_id)][0][\"title\"]) # noqa: RUF015\n", - " print(\n", - " index_reports[index_reports[\"community\"] == int(index_id)][\n", - " \"title\"\n", - " ].to_numpy()[0]\n", - " )\n", - " break\n", - "for source_id in [10, 16, 19, 20, 21, 22, 24, 29, 93, 95]:\n", - " for question in results[1]:\n", - " resq = results[1][question]\n", - " if len(resq[\"sources\"]) == 0:\n", - " continue\n", - " if len([i for i in resq[\"sources\"] if i[\"id\"] == str(source_id)]) == 0:\n", - " continue\n", - " index_name = [i for i in resq[\"sources\"] if i[\"id\"] == str(source_id)][0][ # noqa: RUF015\n", - " \"index_name\"\n", - " ]\n", - " index_id = [i for i in resq[\"sources\"] if i[\"id\"] == str(source_id)][0][ # noqa: RUF015\n", - " \"index_id\"\n", - " ]\n", - " print(question, source_id, index_name, index_id)\n", - " index_sources = pd.read_parquet(\n", - " f\"inputs/{index_name}/create_final_text_units.parquet\"\n", - " )\n", - " print(\n", - " [i for i in resq[\"sources\"] if i[\"id\"] == str(source_id)][0][\"text\"][:250] # noqa: RUF015\n", - " )\n", - " print(index_sources.loc[int(index_id)][\"text\"][:250])\n", - " break" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Multi-index Basic Search" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "text_units = [\n", - " pd.read_parquet(f\"inputs/{index}/text_units.parquet\") for index in indexes\n", - "]\n", - "\n", - "task = loop.create_task(\n", - " multi_index_basic_search(\n", - " parameters, text_units, indexes, False, \"industry in maryland\"\n", - " )\n", - ")\n", - "results = await task" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Print report" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(results[0])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Show context links back to original text\n", - "\n", - "Note that original index name is not saved in context data for basic search" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for source_id in [0, 1]:\n", - " print(results[1][\"sources\"][source_id][\"text\"][:250])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/get_started.md b/docs/get_started.md index a9d5ff5436..a173611744 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -6,50 +6,65 @@ [Python 3.10-3.12](https://www.python.org/downloads/) -To get started with the GraphRAG system, you have a few options: +The following is a simple end-to-end example for using GraphRAG on the command line after installing from [pypi](https://pypi.org/project/graphrag/). -👉 [Install from pypi](https://pypi.org/project/graphrag/).
-👉 [Use it from source](developing.md)
+It shows how to use the system to index some text, and then use the indexed data to answer questions about the documents. -The following is a simple end-to-end example for using the GraphRAG system, using the install from pypi option. +## Install GraphRAG -It shows how to use the system to index some text, and then use the indexed data to answer questions about the documents. +To get started, create a project space and python virtual environment to install `graphrag`. -# Install GraphRAG +### Create Project Space ```bash -pip install graphrag +mkdir graphrag_quickstart +cd graphrag_quickstart +python -m venv .venv ``` +### Activate Python Virtual Environment - Unix/MacOS -# Running the Indexer +```bash +source .venv/bin/activate +``` -We need to set up a data project and some initial configuration. First let's get a sample dataset ready: +### Activate Python Virtual Environment - Windows -```sh -mkdir -p ./christmas/input +```bash +.venv\Scripts\activate ``` -Get a copy of A Christmas Carol by Charles Dickens from a trusted source: +### Install GraphRAG -```sh -curl https://www.gutenberg.org/cache/epub/24022/pg24022.txt -o ./christmas/input/book.txt +```bash +python -m pip install graphrag ``` -## Set Up Your Workspace Variables +### Initialize GraphRAG To initialize your workspace, first run the `graphrag init` command. -Since we have already configured a directory named `./christmas` in the previous step, run the following command: ```sh -graphrag init --root ./christmas +graphrag init ``` -This will create two files: `.env` and `settings.yaml` in the `./christmas` directory. +When prompted, specify the default chat and embedding models you would like to use in your config. + +This will create two files, `.env` and `settings.yaml`, and a directory `input`, in the current directory. +- `input` Location of text files to process with `graphrag`. - `.env` contains the environment variables required to run the GraphRAG pipeline. If you inspect the file, you'll see a single environment variable defined, `GRAPHRAG_API_KEY=`. Replace `` with your own OpenAI or Azure API key. - `settings.yaml` contains the settings for the pipeline. You can modify this file to change the settings for the pipeline. -
+ +### Download Sample Text + +Get a copy of A Christmas Carol by Charles Dickens from a trusted source: + +```sh +curl https://www.gutenberg.org/cache/epub/24022/pg24022.txt -o ./input/book.txt +``` + +## Set Up Workspace Variables ### Using OpenAI @@ -60,13 +75,16 @@ If running in OpenAI mode, you only need to update the value of `GRAPHRAG_API_KE In addition to setting your API key, Azure OpenAI users should set the variables below in the settings.yaml file. To find the appropriate sections, just search for the `models:` root configuration; you should see two sections, one for the default chat endpoint and one for the default embeddings endpoint. Here is an example of what to add to the chat model config: ```yaml -type: azure_openai_chat # Or azure_openai_embedding for embeddings +type: chat +model_provider: azure +model: gpt-4.1 +deployment_name: api_base: https://.openai.azure.com api_version: 2024-02-15-preview # You can customize this for other versions -deployment_name: ``` #### Using Managed Auth on Azure + To use managed auth, edit the auth_type in your model config and *remove* the api_key line: ```yaml @@ -75,39 +93,34 @@ auth_type: azure_managed_identity # Default auth_type is is api_key You will also need to login with [az login](https://learn.microsoft.com/en-us/cli/azure/authenticate-azure-cli) and select the subscription with your endpoint. -## Running the Indexing pipeline +## Index -Finally we'll run the pipeline! +Now we're ready to index! ```sh -graphrag index --root ./christmas +graphrag index ``` ![pipeline executing from the CLI](img/pipeline-running.png) -This process will take some time to run. This depends on the size of your input data, what model you're using, and the text chunk size being used (these can be configured in your `settings.yaml` file). -Once the pipeline is complete, you should see a new folder called `./christmas/output` with a series of parquet files. +This process will usually take a few minutes to run. Once the pipeline is complete, you should see a new folder called `./output` with a series of parquet files. -# Using the Query Engine +# Query Now let's ask some questions using this dataset. Here is an example using Global search to ask a high-level question: ```sh -graphrag query \ ---root ./christmas \ ---method global \ ---query "What are the top themes in this story?" +graphrag query "What are the top themes in this story?" ``` Here is an example using Local search to ask a more specific question about a particular character: ```sh graphrag query \ ---root ./christmas \ ---method local \ ---query "Who is Scrooge and what are his main relationships?" +"Who is Scrooge and what are his main relationships?" \ +--method local ``` Please refer to [Query Engine](query/overview.md) docs for detailed information about how to leverage our Local and Global search mechanisms for extracting meaningful insights from data after the Indexer has wrapped up execution. diff --git a/docs/index/architecture.md b/docs/index/architecture.md index 31ec5090f6..7c3e1ed2bc 100644 --- a/docs/index/architecture.md +++ b/docs/index/architecture.md @@ -6,24 +6,25 @@ In order to support the GraphRAG system, the outputs of the indexing engine (in the Default Configuration Mode) are aligned to a knowledge model we call the _GraphRAG Knowledge Model_. This model is designed to be an abstraction over the underlying data storage technology, and to provide a common interface for the GraphRAG system to interact with. -In normal use-cases the outputs of the GraphRAG Indexer would be loaded into a database system, and the GraphRAG's Query Engine would interact with the database using the knowledge model data-store types. ### Workflows -Because of the complexity of our data indexing tasks, we needed to be able to express our data pipeline as series of multiple, interdependent workflows. +Below is the core GraphRAG indexing pipeline. Individual workflows are described in detail in the [dataflow](./default_dataflow.md) page. ```mermaid --- -title: Sample Workflow DAG +title: Basic GraphRAG --- stateDiagram-v2 - [*] --> Prepare - Prepare --> Chunk - Chunk --> ExtractGraph - Chunk --> EmbedDocuments - ExtractGraph --> GenerateReports + [*] --> LoadDocuments + LoadDocuments --> ChunkDocuments + ChunkDocuments --> ExtractGraph + ChunkDocuments --> ExtractClaims + ChunkDocuments --> EmbedChunks + ExtractGraph --> DetectCommunities ExtractGraph --> EmbedEntities - ExtractGraph --> EmbedGraph + DetectCommunities --> GenerateReports + GenerateReports --> EmbedReports ``` ### LLM Caching @@ -35,11 +36,12 @@ This allows our indexer to be more resilient to network issues, to act idempoten ### Providers & Factories -Several subsystems within GraphRAG use a factory pattern to register and retrieve provider implementations. This allows deep customization to support models, storage, and so on that you may use but isn't built directly into GraphRAG. +Several subsystems within GraphRAG use a factory pattern to register and retrieve provider implementations. This allows deep customization to support your own implementations of models, storage, and so on that we haven't built into the core library. The following subsystems use a factory pattern that allows you to register your own implementations: -- [language model](https://github.com/microsoft/graphrag/blob/main/graphrag/language_model/factory.py) - implement your own `chat` and `embed` methods to use a model provider of choice beyond the built-in OpenAI/Azure support +- [language model](https://github.com/microsoft/graphrag/blob/main/graphrag/language_model/factory.py) - implement your own `chat` and `embed` methods to use a model provider of choice beyond the built-in LiteLLM wrapper +- [input reader](https://github.com/microsoft/graphrag/blob/main/graphrag/index/input/factory.py) - implement your own input document reader to support file types other than text, CSV, and JSON - [cache](https://github.com/microsoft/graphrag/blob/main/graphrag/cache/factory.py) - create your own cache storage location in addition to the file, blob, and CosmosDB ones we provide - [logger](https://github.com/microsoft/graphrag/blob/main/graphrag/logger/factory.py) - create your own log writing location in addition to the built-in file and blob storage - [storage](https://github.com/microsoft/graphrag/blob/main/graphrag/storage/factory.py) - create your own storage provider (database, etc.) beyond the file, blob, and CosmosDB ones built in diff --git a/docs/index/byog.md b/docs/index/byog.md index 781b922466..acd7679348 100644 --- a/docs/index/byog.md +++ b/docs/index/byog.md @@ -16,8 +16,6 @@ The approach described here will be to run a custom GraphRAG workflow pipeline t See the full entities [table schema](./outputs.md#entities). For graph summarization purposes, you only need id, title, description, and the list of text_unit_ids. -The additional properties are used for optional graph visualization purposes. - ### Relationships See the full relationships [table schema](./outputs.md#relationships). For graph summarization purposes, you only need id, source, target, description, weight, and the list of text_unit_ids. @@ -67,4 +65,4 @@ Putting it all together: - `output`: Create an output folder and put your entities and relationships (and optionally text_units) parquet files in it. - Update your config as noted above to only run the workflows subset you need. -- Run `graphrag index --root ` \ No newline at end of file +- Run `graphrag index --root ` \ No newline at end of file diff --git a/docs/index/default_dataflow.md b/docs/index/default_dataflow.md index be290ed7a6..9a810accb0 100644 --- a/docs/index/default_dataflow.md +++ b/docs/index/default_dataflow.md @@ -4,8 +4,8 @@ The knowledge model is a specification for data outputs that conform to our data-model definition. You can find these definitions in the python/graphrag/graphrag/model folder within the GraphRAG repository. The following entity types are provided. The fields here represent the fields that are text-embedded by default. -- `Document` - An input document into the system. These either represent individual rows in a CSV or individual .txt file. -- `TextUnit` - A chunk of text to analyze. The size of these chunks, their overlap, and whether they adhere to any data boundaries may be configured below. A common use case is to set `CHUNK_BY_COLUMNS` to `id` so that there is a 1-to-many relationship between documents and TextUnits instead of a many-to-many. +- `Document` - An input document into the system. These either represent individual rows in a CSV or individual .txt files. +- `TextUnit` - A chunk of text to analyze. The size of these chunks, their overlap, and whether they adhere to any data boundaries may be configured below. - `Entity` - An entity extracted from a TextUnit. These represent people, places, events, or some other entity-model that you provide. - `Relationship` - A relationship between two entities. - `Covariate` - Extracted claim information, which contains statements about entities which may be time-bound. @@ -25,31 +25,26 @@ flowchart TB documents[Documents] --> chunk[Chunk] chunk --> textUnits[Text Units] end - subgraph phase2[Phase 2: Graph Extraction] + subgraph phase2[Phase 2: Document Processing] + documents --> link_to_text_units[Link to TextUnits] + textUnits --> link_to_text_units + link_to_text_units --> document_outputs[Documents Table] + end + subgraph phase3[Phase 3 Graph Extraction] textUnits --> graph_extract[Entity & Relationship Extraction] graph_extract --> graph_summarize[Entity & Relationship Summarization] graph_summarize --> claim_extraction[Claim Extraction] claim_extraction --> graph_outputs[Graph Tables] end - subgraph phase3[Phase 3: Graph Augmentation] + subgraph phase4[Phase 4: Graph Augmentation] graph_outputs --> community_detect[Community Detection] community_detect --> community_outputs[Communities Table] end - subgraph phase4[Phase 4: Community Summarization] + subgraph phase5[Phase 5: Community Summarization] community_outputs --> summarized_communities[Community Summarization] summarized_communities --> community_report_outputs[Community Reports Table] end - subgraph phase5[Phase 5: Document Processing] - documents --> link_to_text_units[Link to TextUnits] - textUnits --> link_to_text_units - link_to_text_units --> document_outputs[Documents Table] - end - subgraph phase6[Phase 6: Network Visualization] - graph_outputs --> graph_embed[Graph Embedding] - graph_embed --> umap_entities[Umap Entities] - umap_entities --> combine_nodes[Final Entities] - end - subgraph phase7[Phase 7: Text Embeddings] + subgraph phase6[Phase 6: Text Embeddings] textUnits --> text_embed[Text Embedding] graph_outputs --> description_embed[Description Embedding] community_report_outputs --> content_embed[Content Embedding] @@ -60,9 +55,7 @@ flowchart TB The first phase of the default-configuration workflow is to transform input documents into _TextUnits_. A _TextUnit_ is a chunk of text that is used for our graph extraction techniques. They are also used as source-references by extracted knowledge items in order to empower breadcrumbs and provenance by concepts back to their original source text. -The chunk size (counted in tokens), is user-configurable. By default this is set to 300 tokens, although we've had positive experience with 1200-token chunks using a single "glean" step. (A "glean" step is a follow-on extraction). Larger chunks result in lower-fidelity output and less meaningful reference texts; however, using larger chunks can result in much faster processing time. - -The group-by configuration is also user-configurable. By default, we align our chunks to document boundaries, meaning that there is a strict 1-to-many relationship between Documents and TextUnits. In rare cases, this can be turned into a many-to-many relationship. This is useful when the documents are very short and we need several of them to compose a meaningful analysis unit (e.g. Tweets or a chat log) +The chunk size (counted in tokens), is user-configurable. By default this is set to 1200 tokens. Larger chunks result in lower-fidelity output and less meaningful reference texts; however, using larger chunks can result in much faster processing time. ```mermaid --- @@ -76,10 +69,30 @@ flowchart LR ``` -## Phase 2: Graph Extraction +## Phase 2: Document Processing + +In this phase of the workflow, we create the _Documents_ table for the knowledge model. Final documents are not used directly in GraphRAG, but this step links them to their constituent text units for provenance in your own applications. + +```mermaid +--- +title: Document Processing +--- +flowchart LR + aug[Augment] --> dp[Link to TextUnits] --> dg[Documents Table] +``` + +### Link to TextUnits + +In this step, we link each document to the text-units that were created in the first phase. This allows us to understand which documents are related to which text-units and vice-versa. + +### Documents Table + +At this point, we can export the **Documents** table into the knowledge Model. + +## Phase 3: Graph Extraction In this phase, we analyze each text unit and extract our graph primitives: _Entities_, _Relationships_, and _Claims_. -Entities and Relationships are extracted at once in our _entity_extract_ verb, and claims are extracted in our _claim_extract_ verb. Results are then combined and passed into following phases of the pipeline. +Entities and Relationships are extracted at once in our _extract_graph_ workflow, and claims are extracted in our _extract_claims_ workflow. Results are then combined and passed into following phases of the pipeline. ```mermaid --- @@ -90,9 +103,11 @@ flowchart LR tu --> ce[Claim Extraction] ``` +> Note: if you are using the [FastGraphRAG](https://microsoft.github.io/graphrag/index/methods/#fastgraphrag) option, entity and relationship extraction will be performed using NLP to conserve LLM resources, and claim extraction will always be skipped. + ### Entity & Relationship Extraction -In this first step of graph extraction, we process each text-unit in order to extract entities and relationships out of the raw text using the LLM. The output of this step is a subgraph-per-TextUnit containing a list of **entities** with a _title_, _type_, and _description_, and a list of **relationships** with a _source_, _target_, and _description_. +In this first step of graph extraction, we process each text-unit to extract entities and relationships out of the raw text using the LLM. The output of this step is a subgraph-per-TextUnit containing a list of **entities** with a _title_, _type_, and _description_, and a list of **relationships** with a _source_, _target_, and _description_. These subgraphs are merged together - any entities with the same _title_ and _type_ are merged by creating an array of their descriptions. Similarly, any relationships with the same _source_ and _target_ are merged by creating an array of their descriptions. @@ -106,9 +121,9 @@ Finally, as an independent workflow, we extract claims from the source TextUnits Note: claim extraction is _optional_ and turned off by default. This is because claim extraction generally requires prompt tuning to be useful. -## Phase 3: Graph Augmentation +## Phase 4: Graph Augmentation -Now that we have a usable graph of entities and relationships, we want to understand their community structure. These give us explicit ways of understanding the topological structure of our graph. +Now that we have a usable graph of entities and relationships, we want to understand their community structure. These give us explicit ways of understanding the organization of our graph. ```mermaid --- @@ -126,7 +141,7 @@ In this step, we generate a hierarchy of entity communities using the Hierarchic Once our graph augmentation steps are complete, the final **Entities**, **Relationships**, and **Communities** tables are exported. -## Phase 4: Community Summarization +## Phase 5: Community Summarization ```mermaid --- @@ -152,51 +167,7 @@ In this step, each _community report_ is then summarized via the LLM for shortha At this point, some bookkeeping work is performed and we export the **Community Reports** tables. -## Phase 5: Document Processing - -In this phase of the workflow, we create the _Documents_ table for the knowledge model. - -```mermaid ---- -title: Document Processing ---- -flowchart LR - aug[Augment] --> dp[Link to TextUnits] --> dg[Documents Table] -``` - -### Augment with Columns (CSV Only) - -If the workflow is operating on CSV data, you may configure your workflow to add additional fields to Documents output. These fields should exist on the incoming CSV tables. Details about configuring this can be found in the [configuration documentation](../config/overview.md). - -### Link to TextUnits - -In this step, we link each document to the text-units that were created in the first phase. This allows us to understand which documents are related to which text-units and vice-versa. - -### Documents Table - -At this point, we can export the **Documents** table into the knowledge Model. - -## Phase 6: Network Visualization (optional) - -In this phase of the workflow, we perform some steps to support network visualization of our high-dimensional vector spaces within our existing graphs. At this point there are two logical graphs at play: the _Entity-Relationship_ graph and the _Document_ graph. - -```mermaid ---- -title: Network Visualization Workflows ---- -flowchart LR - ag[Graph Table] --> ge[Node2Vec Graph Embedding] --> ne[Umap Entities] --> ng[Entities Table] -``` - -### Graph Embedding - -In this step, we generate a vector representation of our graph using the Node2Vec algorithm. This will allow us to understand the implicit structure of our graph and provide an additional vector-space in which to search for related concepts during our query phase. - -### Dimensionality Reduction - -For each of the logical graphs, we perform a UMAP dimensionality reduction to generate a 2D representation of the graph. This will allow us to visualize the graph in a 2D space and understand the relationships between the nodes in the graph. The UMAP embeddings are reduced to two dimensions as x/y coordinates. - -## Phase 7: Text Embedding +## Phase 6: Text Embedding For all artifacts that require downstream vector search, we generate text embeddings as a final step. These embeddings are written directly to a configured vector store. By default we embed entity descriptions, text unit text, and community report text. diff --git a/docs/index/inputs.md b/docs/index/inputs.md index af8a310825..458306bf49 100644 --- a/docs/index/inputs.md +++ b/docs/index/inputs.md @@ -18,11 +18,15 @@ Also see the [outputs](outputs.md) documentation for the final documents table s ## Bring-your-own DataFrame -As of version 2.6.0, GraphRAG's [indexing API method](https://github.com/microsoft/graphrag/blob/main/graphrag/api/index.py) allows you to pass in your own pandas DataFrame and bypass all of the input loading/parsing described in the next section. This is convenient if you have content in a format or storage location we don't support out-of-the-box. __You must ensure that your input DataFrame conforms to the schema described above.__ All of the chunking behavior described later will proceed exactly the same. +GraphRAG's [indexing API method](https://github.com/microsoft/graphrag/blob/main/graphrag/api/index.py) allows you to pass in your own pandas DataFrame and bypass all of the input loading/parsing described in the next section. This is convenient if you have content in a format or storage location we don't support out-of-the-box. _You must ensure that your input DataFrame conforms to the schema described above._ All of the chunking behavior described later will proceed exactly the same. + +## Custom File Handling + +We use an injectable InputReader provider class. This means you can implement any input file handling you want in a class that extends InputReader and register it with the InputReaderFactory. See the [architecture page](https://microsoft.github.io/graphrag/index/architecture/) for more info on our standard provider pattern. ## Formats -We support three file formats out-of-the-box. This covers the overwhelming majority of use cases we have encountered. If you have a different format, we recommend writing a script to convert to one of these, which are widely used and supported by many tools and libraries. +We support three file formats out-of-the-box. This covers the overwhelming majority of use cases we have encountered. If you have a different format, we recommend either implementing your own InputReader or writing a script to convert to one of these, which are widely used and supported by many tools and libraries. ### Plain Text @@ -40,7 +44,7 @@ JSON files (typically ending in a .json extension) contain [structured objects]( ## Metadata -With the structured file formats (CSV and JSON) you can configure any number of columns to be added to a persisted `metadata` field in the DataFrame. This is configured by supplying a list of columns name to collect. If this is configured, the output `metadata` column will have a dict containing a key for each column, and the value of the column for that document. This metadata can optionally be used later in the GraphRAG pipeline. +With the structured file formats (CSV and JSON) you can configure any number of columns to be added to a persisted `metadata` field in the DataFrame. This is configured by supplying a list of column names to collect. If this is configured, the output `metadata` column will have a dict containing a key for each column, and the value of the column for that document. This metadata can optionally be used later in the GraphRAG pipeline. ### Example @@ -68,7 +72,7 @@ Documents DataFrame ## Chunking and Metadata -As described on the [default dataflow](default_dataflow.md#phase-1-compose-textunits) page, documents are *chunked* into smaller "text units" for processing. This is done because document content size often exceeds the available context window for a given language model. There are a handful of settings you can adjust for this chunking, the most relevant being the `chunk_size` and `overlap`. We now also support a metadata processing scheme that can improve indexing results for some use cases. We will describe this feature in detail here. +As described on the [default dataflow](default_dataflow.md#phase-1-compose-textunits) page, documents are *chunked* into smaller "text units" for processing. This is done because document content size often exceeds the available context window for a given language model. There are a handful of settings you can adjust for this chunking, the most relevant being the `chunk_size` and `overlap`. We also support a metadata processing scheme that can improve indexing results for some use cases. We will describe this feature in detail here. Imagine the following scenario: you are indexing a collection of news articles. Each article text starts with a headline and author, and then proceeds with the content. When documents are chunked, they are split evenly according to your configured chunk size. In other words, the first *n* tokens are read into a text unit, and then the next *n*, until the end of the content. This means that front matter at the beginning of the document (such as the headline and author in this example) *is not copied to each chunk*. It only exists in the first chunk. When we later retrieve those chunks for summarization, they may therefore be missing shared information about the source document that should always be provided to the model. We have configuration options to copy repeated content into each text unit to address this issue. @@ -78,14 +82,13 @@ As described above, when documents are imported you can specify a list of `metad ### Chunking Config -Next, the `chunks` block needs to instruct the chunker how to handle this metadata when creating text units. By default, it is ignored. We have two settings to include it: +Next, the `chunks` block needs to instruct the chunker how to handle this metadata when creating text units. By default, it is ignored. We have the following setting to include it: - `prepend_metadata`. This instructs the importer to copy the contents of the `metadata` column for each row into the start of every single text chunk. This metadata is copied as key: value pairs on new lines. -- `chunk_size_includes_metadata`: This tells the chunker how to compute the chunk size when metadata is included. By default, we create the text units using your specified `chunk_size` *and then* prepend the metadata. This means that the final text unit lengths may be longer than your configured `chunk_size`, and it will vary based on the length of the metadata for each document. When this setting is `True`, we will compute the raw text using the remainder after measuring the metadata length so that the resulting text units always comply with your configured `chunk_size`. ### Examples -The following are several examples to help illustrate how chunking config and metadate prepending works for each file format. Note that we are using word count here as "tokens" for the illustration, but language model tokens are [not equivalent to words](https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them). +The following are several examples to help illustrate how chunking config and metadata prepending works for each file format. Note that we are using word count here as "tokens" for the illustration, but language model tokens are [not equivalent to words](https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them). #### Text files @@ -113,14 +116,13 @@ settings.yaml ```yaml input: - file_type: text + type: text metadata: [title] chunks: size: 100 overlap: 0 prepend_metadata: true - chunk_size_includes_metadata: false ``` Documents DataFrame @@ -158,54 +160,6 @@ US to lift most federal COVID-19 vaccine mandates,WASHINGTON (AP) The Biden admi NY lawmakers begin debating budget 1 month after due date,ALBANY, N.Y. (AP) New York lawmakers began voting Monday on a $229 billion state budget due a month ago that would raise the minimum wage, crack down on illicit pot shops and ban gas stoves and furnaces in new buildings. Negotiations among Gov. Kathy Hochul and her fellow Democrats in control of the Legislature dragged on past the April 1 budget deadline, largely because of disagreements over changes to the bail law and other policy proposals included in the spending plan. Floor debates on some budget bills began Monday. State Senate Majority Leader Andrea Stewart-Cousins said she expected voting to be wrapped up Tuesday for a budget she said contains "significant wins" for New Yorkers. "I would have liked to have done this sooner. I think we would all agree to that," Cousins told reporters before voting began. "This has been a very policy-laden budget and a lot of the policies had to parsed through." Hochul was able to push through a change to the bail law that will eliminate the standard that requires judges to prescribe the "least restrictive" means to ensure defendants return to court. Hochul said judges needed the extra discretion. Some liberal lawmakers argued that it would undercut the sweeping bail reforms approved in 2019 and result in more people with low incomes and people of color in pretrial detention. Here are some other policy provisions that will be included in the budget, according to state officials. The minimum wage would be raised to $17 in New York City and some of its suburbs and $16 in the rest of the state by 2026. That's up from $15 in the city and $14.20 upstate. --- - -settings.yaml - -```yaml -input: - file_type: csv - title_column: headline - text_column: article - metadata: [headline] - -chunks: - size: 50 - overlap: 5 - prepend_metadata: true - chunk_size_includes_metadata: true -``` - -Documents DataFrame - -| id | title | text | creation_date | metadata | -| --------------------- | --------------------------------------------------------- | ------------------------ | ----------------------------- | --------------------------------------------------------------------------- | -| (generated from text) | US to lift most federal COVID-19 vaccine mandates | (article column content) | (create date of articles.csv) | { "headline": "US to lift most federal COVID-19 vaccine mandates" } | -| (generated from text) | NY lawmakers begin debating budget 1 month after due date | (article column content) | (create date of articles.csv) | { "headline": "NY lawmakers begin debating budget 1 month after due date" } | - -Raw Text Chunks - -| content | length | -| ------- | ------: | -| title: US to lift most federal COVID-19 vaccine mandates
WASHINGTON (AP) The Biden administration will end most of the last remaining federal COVID-19 vaccine requirements next week when the national public health emergency for the coronavirus ends, the White House said Monday. Vaccine requirements for federal workers and federal contractors, | 50 | -| title: US to lift most federal COVID-19 vaccine mandates
federal workers and federal contractors as well as foreign air travelers to the U.S., will end May 11. The government is also beginning the process of lifting shot requirements for Head Start educators, healthcare workers, and noncitizens at U.S. land borders. | 50 | -| title: US to lift most federal COVID-19 vaccine mandates
noncitizens at U.S. land borders. The requirements are among the last vestiges of some of the more coercive measures taken by the federal government to promote vaccination as the deadly virus raged, and their end marks the latest display of how | 50 | -| title: US to lift most federal COVID-19 vaccine mandates
the latest display of how President Joe Biden's administration is moving to treat COVID-19 as a routine, endemic illness. "While I believe that these vaccine mandates had a tremendous beneficial impact, we are now at a point where we think that | 50 | -| title: US to lift most federal COVID-19 vaccine mandates
point where we think that it makes a lot of sense to pull these requirements down," White House COVID-19 coordinator Dr. Ashish Jha told The Associated Press on Monday. | 38 | -| title: NY lawmakers begin debating budget 1 month after due date
ALBANY, N.Y. (AP) New York lawmakers began voting Monday on a $229 billion state budget due a month ago that would raise the minimum wage, crack down on illicit pot shops and ban gas stoves and furnaces in new | 50 | -| title: NY lawmakers begin debating budget 1 month after due date
stoves and furnaces in new buildings. Negotiations among Gov. Kathy Hochul and her fellow Democrats in control of the Legislature dragged on past the April 1 budget deadline, largely because of disagreements over changes to the bail law and | 50 | -| title: NY lawmakers begin debating budget 1 month after due date
to the bail law and other policy proposals included in the spending plan. Floor debates on some budget bills began Monday. State Senate Majority Leader Andrea Stewart-Cousins said she expected voting to be wrapped up Tuesday for a budget | 50 | -|title: NY lawmakers begin debating budget 1 month after due date
up Tuesday for a budget she said contains "significant wins" for New Yorkers. "I would have liked to have done this sooner. I think we would all agree to that," Cousins told reporters before voting began. "This has been | 50 | -| title: NY lawmakers begin debating budget 1 month after due date
voting began. "This has been a very policy-laden budget and a lot of the policies had to parsed through." Hochul was able to push through a change to the bail law that will eliminate the standard that requires judges | 50 | -| title: NY lawmakers begin debating budget 1 month after due date
the standard that requires judges to prescribe the "least restrictive" means to ensure defendants return to court. Hochul said judges needed the extra discretion. Some liberal lawmakers argued that it would undercut the sweeping bail reforms approved in 2019 | 50 | -| title: NY lawmakers begin debating budget 1 month after due date
bail reforms approved in 2019 and result in more people with low incomes and people of color in pretrial detention. Here are some other policy provisions that will be included in the budget, according to state officials. The minimum | 50 | -| title: NY lawmakers begin debating budget 1 month after due date
to state officials. The minimum wage would be raised to $17 in be raised to $17 in New York City and some of its suburbs and $16 in the rest of the state by 2026. That's up from $15 | 50 | -| title: NY lawmakers begin debating budget 1 month after due date
2026. That's up from $15 in the city and $14.20 upstate. | 22 | - - -In this example we can see that the two input documents were parsed into fourteen output text chunks. The title (headline) of each document is prepended and included in the computed chunk size, so each chunk matches the configured chunk size (except the last one for each document). We've also configured some overlap in these text chunks, so the last five tokens are shared. Why would you use overlap in your text chunks? Consider that when you are splitting documents based on tokens, it is highly likely that sentences or even related concepts will be split into separate chunks. Each text chunk is processed separately by the language model, so this may result in incomplete "ideas" at the boundaries of the chunk. Overlap ensures that these split concepts are fully contained in at least one of the chunks. - - #### JSON files This final example uses a JSON file for each of the same two articles. In this example we'll set the object fields to read, but we will not add metadata to the text chunks. @@ -240,7 +194,7 @@ settings.yaml ```yaml input: - file_type: json + type: json title_column: headline text_column: content diff --git a/docs/index/methods.md b/docs/index/methods.md index 9da2c83d73..3c10cfb21d 100644 --- a/docs/index/methods.md +++ b/docs/index/methods.md @@ -13,7 +13,7 @@ This is the method described in the original [blog post](https://www.microsoft.c - claim extraction (optional): LLM is prompted to extract and describe claims from each text unit. - community report generation: entity and relationship descriptions (and optionally claims) for each community are collected and used to prompt the LLM to generate a summary report. -`graphrag index --method standard`. This is the default method, so the method param can actual be omitted. +`graphrag index --method standard`. This is the default method, so the method param can be omitted on the command line. ## FastGraphRAG @@ -23,7 +23,7 @@ FastGraphRAG is a method that substitutes some of the language model reasoning f - relationship extraction: relationships are defined as text unit co-occurrence between entity pairs. There is no description. - entity summarization: not necessary. - relationship summarization: not necessary. -- claim extraction (optional): unused. +- claim extraction: unused. - community report generation: The direct text unit content containing each entity noun phrase is collected and used to prompt the LLM to generate a summary report. `graphrag index --method fast` @@ -41,4 +41,4 @@ You can install it manually by running `python -m spacy download `, ## Choosing a Method -Standard GraphRAG provides a rich description of real-world entities and relationships, but is more expensive that FastGraphRAG. We estimate graph extraction to constitute roughly 75% of indexing cost. FastGraphRAG is therefore much cheaper, but the tradeoff is that the extracted graph is less directly relevant for use outside of GraphRAG, and the graph tends to be quite a bit noisier. If high fidelity entities and graph exploration are important to your use case, we recommend staying with traditional GraphRAG. If your use case is primarily aimed at summary questions using global search, FastGraphRAG provides high quality summarization at much less LLM cost. +Standard GraphRAG provides a rich description of real-world entities and relationships, but is more expensive than FastGraphRAG. We estimate graph extraction to constitute roughly 75% of indexing cost. FastGraphRAG is therefore much cheaper, but the tradeoff is that the extracted graph is less directly relevant for use outside of GraphRAG, and the graph tends to be quite a bit noisier. If high fidelity entities and graph exploration are important to your use case, we recommend staying with traditional GraphRAG. If your use case is primarily aimed at summary questions using global search, FastGraphRAG provides high quality summarization with much lower language model cost. diff --git a/docs/index/outputs.md b/docs/index/outputs.md index add60182b1..89a48e8526 100644 --- a/docs/index/outputs.md +++ b/docs/index/outputs.md @@ -82,8 +82,6 @@ List of all entities found in the data by the LM. | text_unit_ids | str[] | List of the text units containing the entity. | | frequency | int | Count of text units the entity was found within. | | degree | int | Node degree (connectedness) in the graph. | -| x | float | X position of the node for visual layouts. If graph embeddings and UMAP are not turned on, this will be 0. | -| y | float | Y position of the node for visual layouts. If graph embeddings and UMAP are not turned on, this will be 0. | ## relationships List of all entity-to-entity relationships found in the data by the LM. This is also the _edge list_ for the graph. @@ -104,7 +102,7 @@ List of all text chunks parsed from the input documents. | ----------------- | ----- | ----------- | | text | str | Raw full text of the chunk. | | n_tokens | int | Number of tokens in the chunk. This should normally match the `chunk_size` config parameter, except for the last chunk which is often shorter. | -| document_ids | str[] | List of document IDs the chunk came from. This is normally only 1 due to our default groupby, but for very short text documents (e.g., microblogs) it can be configured so text units span multiple documents. | +| document_id | str | ID of the document the chunk came from. | | entity_ids | str[] | List of entities found in the text unit. | | relationships_ids | str[] | List of relationships found in the text unit. | | covariate_ids | str[] | Optional list of covariates found in the text unit. | \ No newline at end of file diff --git a/docs/index/overview.md b/docs/index/overview.md index 550909f6a2..d3974cc645 100644 --- a/docs/index/overview.md +++ b/docs/index/overview.md @@ -7,8 +7,7 @@ Indexing Pipelines are configurable. They are composed of workflows, standard an - extract entities, relationships and claims from raw text - perform community detection in entities - generate community summaries and reports at multiple levels of granularity -- embed entities into a graph vector space -- embed text chunks into a textual vector space +- embed text into a vector space The outputs of the pipeline are stored as Parquet tables by default, and embeddings are written to your configured vector store. diff --git a/docs/prompt_tuning/auto_prompt_tuning.md b/docs/prompt_tuning/auto_prompt_tuning.md index 31375db3da..eb77044cd5 100644 --- a/docs/prompt_tuning/auto_prompt_tuning.md +++ b/docs/prompt_tuning/auto_prompt_tuning.md @@ -1,8 +1,8 @@ # Auto Prompt Tuning ⚙️ -GraphRAG provides the ability to create domain adapted prompts for the generation of the knowledge graph. This step is optional, though it is highly encouraged to run it as it will yield better results when executing an Index Run. +GraphRAG provides the ability to create domain-adapted prompts for the generation of the knowledge graph. This step is optional, though it is highly encouraged to run it as it will yield better results when executing an Index Run. -These are generated by loading the inputs, splitting them into chunks (text units) and then running a series of LLM invocations and template substitutions to generate the final prompts. We suggest using the default values provided by the script, but in this page you'll find the detail of each in case you want to further explore and tweak the prompt tuning algorithm. +These are generated by loading the inputs, splitting them into chunks (text units) and then running a series of LLM invocations and template substitutions to generate the final prompts. We suggest using the default values provided by the script, but in this page you'll find the details of each in case you want to further explore and tweak the prompt tuning algorithm.

Figure 1: Auto Tuning Conceptual Diagram. @@ -20,16 +20,14 @@ Before running auto tuning, ensure you have already initialized your workspace w You can run the main script from the command line with various options: ```bash -graphrag prompt-tune [--root ROOT] [--config CONFIG] [--domain DOMAIN] [--selection-method METHOD] [--limit LIMIT] [--language LANGUAGE] \ +graphrag prompt-tune [--root ROOT] [--domain DOMAIN] [--selection-method METHOD] [--limit LIMIT] [--language LANGUAGE] \ [--max-tokens MAX_TOKENS] [--chunk-size CHUNK_SIZE] [--n-subset-max N_SUBSET_MAX] [--k K] \ [--min-examples-required MIN_EXAMPLES_REQUIRED] [--discover-entity-types] [--output OUTPUT] ``` ## Command-Line Options -- `--config` (required): The path to the configuration file. This is required to load the data and model settings. - -- `--root` (optional): The data project root directory, including the config files (YML, JSON, or .env). Defaults to the current directory. +- `--root` (optional): Path to the project directory that contains the config file (settings.yaml). Defaults to the current directory. - `--domain` (optional): The domain related to your input data, such as 'space science', 'microbiology', or 'environmental news'. If left empty, the domain will be inferred from the input data. @@ -56,7 +54,7 @@ graphrag prompt-tune [--root ROOT] [--config CONFIG] [--domain DOMAIN] [--selec ## Example Usage ```bash -python -m graphrag prompt-tune --root /path/to/project --config /path/to/settings.yaml --domain "environmental news" \ +python -m graphrag prompt-tune --root /path/to/project --domain "environmental news" \ --selection-method random --limit 10 --language English --max-tokens 2048 --chunk-size 256 --min-examples-required 3 \ --no-discover-entity-types --output /path/to/output ``` @@ -64,7 +62,7 @@ python -m graphrag prompt-tune --root /path/to/project --config /path/to/setting or, with minimal configuration (suggested): ```bash -python -m graphrag prompt-tune --root /path/to/project --config /path/to/settings.yaml --no-discover-entity-types +python -m graphrag prompt-tune --root /path/to/project --no-discover-entity-types ``` ## Document Selection Methods @@ -73,17 +71,17 @@ The auto tuning feature ingests the input data and then divides it into text uni After that, it uses one of the following selection methods to pick a sample to work with for prompt generation: - `random`: Select text units randomly. This is the default and recommended option. -- `top`: Select the head n text units. +- `top`: Select the head _n_ text units. - `all`: Use all text units for the generation. Use only with small datasets; this option is not usually recommended. -- `auto`: Embed text units in a lower-dimensional space and select the k nearest neighbors to the centroid. This is useful when you have a large dataset and want to select a representative sample. +- `auto`: Embed text units in a lower-dimensional space and select the _k_ nearest neighbors to the centroid. This is useful when you have a large dataset and want to select a representative sample. -## Modify Env Vars +## Modify Config After running auto tuning, you should modify the following config variables to pick up the new prompts on your index run. Note: Please make sure to update the correct path to the generated prompts, in this example we are using the default "prompts" path. ```yaml -entity_extraction: - prompt: "prompts/entity_extraction.txt" +extract_graph: + prompt: "prompts/extract_graph.txt" summarize_descriptions: prompt: "prompts/summarize_descriptions.txt" diff --git a/docs/prompt_tuning/overview.md b/docs/prompt_tuning/overview.md index a12cebb257..e8518f6b21 100644 --- a/docs/prompt_tuning/overview.md +++ b/docs/prompt_tuning/overview.md @@ -8,8 +8,8 @@ The default prompts are the simplest way to get started with the GraphRAG system ## Auto Tuning -Auto Tuning leverages your input data and LLM interactions to create domain adapted prompts for the generation of the knowledge graph. It is highly encouraged to run it as it will yield better results when executing an Index Run. For more details about how to use it, please refer to the [Auto Tuning](auto_prompt_tuning.md) documentation. +Auto Tuning leverages your input data and LLM interactions to create domain-adapted prompts for the generation of the knowledge graph. It is highly encouraged to run it as it will yield better results when executing an Index Run. For more details about how to use it, please refer to the [Auto Tuning](auto_prompt_tuning.md) page. ## Manual Tuning -Manual tuning is an advanced use-case. Most users will want to use the Auto Tuning feature instead. Details about how to use manual configuration are available in the [manual tuning](manual_prompt_tuning.md) documentation. +Manual tuning is an advanced use-case. Most users will want to use the Auto Tuning feature instead. Details about how to use manual configuration are available in the [manual tuning](manual_prompt_tuning.md) page. diff --git a/docs/query/drift_search.md b/docs/query/drift_search.md index e3d9ddc021..80c0607330 100644 --- a/docs/query/drift_search.md +++ b/docs/query/drift_search.md @@ -21,7 +21,7 @@ DRIFT Search introduces a new approach to local search queries by including comm Below are the key parameters of the [DRIFTSearch class](https://github.com/microsoft/graphrag/blob/main//graphrag/query/structured_search/drift_search/search.py): -- `llm`: OpenAI model object to be used for response generation +* `model`: Language model chat completion object to be used for response generation - `context_builder`: [context builder](https://github.com/microsoft/graphrag/blob/main/graphrag/query/structured_search/drift_search/drift_context.py) object to be used for preparing context data from community reports and query information - `config`: model to define the DRIFT Search hyperparameters. [DRIFT Config model](https://github.com/microsoft/graphrag/blob/main/graphrag/config/models/drift_search_config.py) - `tokenizer`: token encoder for tracking the budget for the algorithm. diff --git a/docs/query/global_search.md b/docs/query/global_search.md index a9685d70be..da453dbe66 100644 --- a/docs/query/global_search.md +++ b/docs/query/global_search.md @@ -54,7 +54,7 @@ The quality of the global search’s response can be heavily influenced by the l Below are the key parameters of the [GlobalSearch class](https://github.com/microsoft/graphrag/blob/main//graphrag/query/structured_search/global_search/search.py): -* `llm`: OpenAI model object to be used for response generation +* `model`: Language model chat completion object to be used for response generation * `context_builder`: [context builder](https://github.com/microsoft/graphrag/blob/main//graphrag/query/structured_search/global_search/community_context.py) object to be used for preparing context data from community reports * `map_system_prompt`: prompt template used in the `map` stage. Default template can be found at [map_system_prompt](https://github.com/microsoft/graphrag/blob/main//graphrag/prompts/query/global_search_map_system_prompt.py) * `reduce_system_prompt`: prompt template used in the `reduce` stage, default template can be found at [reduce_system_prompt](https://github.com/microsoft/graphrag/blob/main//graphrag/prompts/query/global_search_reduce_system_prompt.py) diff --git a/docs/query/local_search.md b/docs/query/local_search.md index bf0f43e3ce..2cd77e640e 100644 --- a/docs/query/local_search.md +++ b/docs/query/local_search.md @@ -48,7 +48,7 @@ Given a user query and, optionally, the conversation history, the local search m Below are the key parameters of the [LocalSearch class](https://github.com/microsoft/graphrag/blob/main//graphrag/query/structured_search/local_search/search.py): -* `llm`: OpenAI model object to be used for response generation +* `model`: Language model chat completion object to be used for response generation * `context_builder`: [context builder](https://github.com/microsoft/graphrag/blob/main//graphrag/query/structured_search/local_search/mixed_context.py) object to be used for preparing context data from collections of knowledge model objects * `system_prompt`: prompt template used to generate the search response. Default template can be found at [system_prompt](https://github.com/microsoft/graphrag/blob/main//graphrag/prompts/query/local_search_system_prompt.py) * `response_type`: free-form text describing the desired response type and format (e.g., `Multiple Paragraphs`, `Multi-Page Report`) diff --git a/docs/query/multi_index_search.md b/docs/query/multi_index_search.md deleted file mode 100644 index 6b6ff2b41a..0000000000 --- a/docs/query/multi_index_search.md +++ /dev/null @@ -1,20 +0,0 @@ -# Multi Index Search 🔎 - -## Multi Dataset Reasoning - -GraphRAG takes in unstructured data contained in text documents and uses large languages models to “read” the documents in a targeted fashion and create a knowledge graph. This knowledge graph, or index, contains information about specific entities in the data, how the entities relate to one another, and high-level reports about communities and topics found in the data. Indexes can be searched by users to get meaningful information about the underlying data, including reports with citations that point back to the original unstructured text. - -Multi-index search is a new capability that has been added to the GraphRAG python library to query multiple knowledge stores at once. Multi-index search allows for many new search scenarios, including: - -- Combining knowledge from different domains – Many documents contain similar types of entities: person, place, thing. But GraphRAG can be tuned for highly specialized domains, such as science and engineering. With the recent updates to search, GraphRAG can now simultaneously query multiple datasets with completely different schemas and entity definitions. - -- Combining knowledge with different access levels – Not all datasets are accessible to all people, even within an organization. Some datasets are publicly available. Some datasets, such as internal financial information or intellectual property, may only be accessible by a small number of employees at a company. Multi-index search allows multiple sources with different access controls to be queried at the same time, creating more nuanced and informative reports. Internal R&D findings can be seamlessly combined with open-source scientific publications. - -- Combining knowledge in different locations – With multi-index search, indexes do not need to be in the same location or type of storage to be queried. Indexes in the cloud in Azure Storage can be queried at the same time as indexes stored on a personal computer. Multi-index search makes these types of data joins easy and accessible. - -To search across multiple datasets, the underlying contexts from each index, based on the user query, are combined in-memory at query time, saving on computation and allowing the joint querying of indexes that can’t be joined inherently, either do access controls or differing schemas. Multi-index search automatically keeps track of provenance information, so that any references can be traced back to the correct indexes and correct original documents. - - -## How to Use - -An example of a global search scenario can be found in the following [notebook](../examples_notebooks/multi_index_search.ipynb). \ No newline at end of file diff --git a/docs/query/overview.md b/docs/query/overview.md index dd375b8b41..bf2b612642 100644 --- a/docs/query/overview.md +++ b/docs/query/overview.md @@ -1,34 +1,35 @@ # Query Engine 🔎 -The Query Engine is the retrieval module of the Graph RAG Library. It is one of the two main components of the Graph RAG library, the other being the Indexing Pipeline (see [Indexing Pipeline](../index/overview.md)). +The Query Engine is the retrieval module of the GraphRAG library, and operates over completed [indexes](../index/overview.md). It is responsible for the following tasks: - [Local Search](#local-search) - [Global Search](#global-search) - [DRIFT Search](#drift-search) +- Basic Search - [Question Generation](#question-generation) ## Local Search -Local search method generates answers by combining relevant data from the AI-extracted knowledge-graph with text chunks of the raw documents. This method is suitable for questions that require an understanding of specific entities mentioned in the documents (e.g. What are the healing properties of chamomile?). +Local search generates answers by combining relevant data from the AI-extracted knowledge-graph with text chunks of the raw documents. This method is suitable for questions that require an understanding of specific entities mentioned in the documents (e.g. What are the healing properties of chamomile?). -For more details about how Local Search works please refer to the [Local Search](local_search.md) documentation. +For more details about how Local Search works please refer to the [Local Search](local_search.md) page. ## Global Search -Global search method generates answers by searching over all AI-generated community reports in a map-reduce fashion. This is a resource-intensive method, but often gives good responses for questions that require an understanding of the dataset as a whole (e.g. What are the most significant values of the herbs mentioned in this notebook?). +Global search generates answers by searching over all AI-generated community reports in a map-reduce fashion. This is a resource-intensive method, but often gives good responses for questions that require an understanding of the dataset as a whole (e.g. What are the most significant values of the herbs mentioned in this notebook?). -More about this can be checked at the [Global Search](global_search.md) documentation. +More about this is provided on the [Global Search](global_search.md) page. ## DRIFT Search -DRIFT Search introduces a new approach to local search queries by including community information in the search process. This greatly expands the breadth of the query’s starting point and leads to retrieval and usage of a far higher variety of facts in the final answer. This addition expands the GraphRAG query engine by providing a more comprehensive option for local search, which uses community insights to refine a query into detailed follow-up questions. +DRIFT Search introduces a new approach to local search queries by including community information in the search process. This greatly expands the breadth of the query’s starting point and leads to retrieval and usage of a far higher variety of facts in the final answer. This expands the GraphRAG query engine by providing a more comprehensive option for local search, which uses community insights to refine a query into detailed follow-up questions. -To learn more about DRIFT Search, please refer to the [DRIFT Search](drift_search.md) documentation. +To learn more about DRIFT Search, please refer to the [DRIFT Search](drift_search.md) page. ## Basic Search -GraphRAG includes a rudimentary implementation of basic vector RAG to make it easy to compare different search results based on the type of question you are asking. You can specify the top `k` txt unit chunks to include in the summarization context. +GraphRAG includes a rudimentary implementation of basic vector RAG to make it easy to compare different search results based on the type of question you are asking. You can specify the top `k` text unit chunks to include in the summarization context. ## Question Generation diff --git a/docs/query/question_generation.md b/docs/query/question_generation.md index 525a465499..6f5da81e5d 100644 --- a/docs/query/question_generation.md +++ b/docs/query/question_generation.md @@ -11,7 +11,7 @@ Given a list of prior user questions, the question generation method uses the sa Below are the key parameters of the [Question Generation class](https://github.com/microsoft/graphrag/blob/main//graphrag/query/question_gen/local_gen.py): -* `llm`: OpenAI model object to be used for response generation +* `model`: Language model chat completion object to be used for response generation * `context_builder`: [context builder](https://github.com/microsoft/graphrag/blob/main//graphrag/query/structured_search/local_search/mixed_context.py) object to be used for preparing context data from collections of knowledge model objects, using the same context builder class as in local search * `system_prompt`: prompt template used to generate candidate questions. Default template can be found at [system_prompt](https://github.com/microsoft/graphrag/blob/main//graphrag/prompts/query/question_gen_system_prompt.py) * `llm_params`: a dictionary of additional parameters (e.g., temperature, max_tokens) to be passed to the LLM call diff --git a/docs/visualization_guide.md b/docs/visualization_guide.md index d09319e74d..14b4dbe55f 100644 --- a/docs/visualization_guide.md +++ b/docs/visualization_guide.md @@ -8,13 +8,6 @@ Before building an index, please review your `settings.yaml` configuration file snapshots: graphml: true ``` -(Optional) To support other visualization tools and exploration, additional parameters can be enabled that provide access to vector embeddings. -```yaml -embed_graph: - enabled: true # will generate node2vec embeddings for nodes -umap: - enabled: true # will generate UMAP embeddings for nodes, giving the entities table an x/y position to plot -``` After running the indexing pipeline over your data, there will be an output folder (defined by the `storage.base_dir` setting). - **Output Folder**: Contains artifacts from the LLM’s indexing pass. diff --git a/examples_notebooks/community_contrib/README.md b/examples_notebooks/community_contrib/README.md deleted file mode 100644 index 0915dc1a60..0000000000 --- a/examples_notebooks/community_contrib/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## Disclaimer - -This folder contains community contributed notebooks that are not officially supported by the GraphRAG team. The notebooks are provided as-is and are not guaranteed to work with the latest version of GraphRAG. If you have any questions or issues, please reach out to the author of the notebook directly. - -For more information on how to contribute to the GraphRAG project, please refer to the [contribution guidelines](https://github.com/microsoft/graphrag/blob/main/CONTRIBUTING.md) diff --git a/examples_notebooks/community_contrib/neo4j/graphrag_import_neo4j_cypher.ipynb b/examples_notebooks/community_contrib/neo4j/graphrag_import_neo4j_cypher.ipynb deleted file mode 100644 index a8b30a819b..0000000000 --- a/examples_notebooks/community_contrib/neo4j/graphrag_import_neo4j_cypher.ipynb +++ /dev/null @@ -1,1215 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "b4fea928", - "metadata": {}, - "outputs": [], - "source": [ - "# Copyright (c) 2024 Microsoft Corporation.\n", - "# Licensed under the MIT License." - ] - }, - { - "cell_type": "markdown", - "id": "0c4bc9ba", - "metadata": {}, - "source": [ - "# Neo4j Import of GraphRAG Result Parquet files\n", - "\n", - "This notebook imports the results of the GraphRAG indexing process into the Neo4j Graph database for further processing, analysis or visualization. \n", - "\n", - "You can also build your own GenAI applications using Neo4j and a number of RAG strategies with LangChain, LlamaIndex, Haystack, and many other frameworks.\n", - "See: https://neo4j.com/labs/genai-ecosystem\n", - "\n", - "Here is what the end result looks like:\n", - "\n", - "![](https://dev.assets.neo4j.com/wp-content/uploads/graphrag-neo4j-visualization.png)" - ] - }, - { - "cell_type": "markdown", - "id": "3924e246", - "metadata": {}, - "source": [ - "## How does it work?\n", - "\n", - "The notebook loads the parquet files from the `output` folder of your indexing process and loads them into Pandas dataframes.\n", - "It then uses a batching approach to send a slice of the data into Neo4j to create nodes and relationships and add relevant properties. The id-arrays on most entities are turned into relationships. \n", - "\n", - "All operations use MERGE, so they are idempotent, and you can run the script multiple times.\n", - "\n", - "If you need to clean out the database, you can run the following statement\n", - "\n", - "```cypher\n", - "MATCH (n)\n", - "CALL { WITH n DETACH DELETE n } IN TRANSACTIONS OF 25000 ROWS;\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 59, - "id": "adca1803", - "metadata": {}, - "outputs": [], - "source": [ - "GRAPHRAG_FOLDER = \"PATH_TO_OUTPUT/artifacts\"" - ] - }, - { - "cell_type": "markdown", - "id": "7fb27b941602401d91542211134fc71a", - "metadata": {}, - "source": [ - "### Depedendencies\n", - "\n", - "We only need Pandas and the neo4j Python driver with the rust extension for faster network transport." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b57beec0", - "metadata": {}, - "outputs": [], - "source": [ - "%pip install --quiet pandas neo4j-rust-ext" - ] - }, - { - "cell_type": "code", - "execution_count": 61, - "id": "3eeee95f-e4f2-4052-94fb-a5dc8ab542ae", - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "\n", - "import pandas as pd\n", - "from neo4j import GraphDatabase" - ] - }, - { - "cell_type": "markdown", - "id": "307dd2f4", - "metadata": {}, - "source": [ - "## Neo4j Installation\n", - "\n", - "You can create a free instance of Neo4j [online](https://console.neo4j.io). You get a credentials file that you can use for the connection credentials. You can also get an instance in any of the cloud marketplaces.\n", - "\n", - "If you want to install Neo4j locally either use [Neo4j Desktop](https://neo4j.com/download) or \n", - "the official Docker image: `docker run -e NEO4J_AUTH=neo4j/password -p 7687:7687 -p 7474:7474 neo4j` " - ] - }, - { - "cell_type": "code", - "execution_count": 62, - "id": "b6c15443-4acb-4f91-88ea-4e08abaa4c29", - "metadata": {}, - "outputs": [], - "source": [ - "NEO4J_URI = \"neo4j://localhost\" # or neo4j+s://xxxx.databases.neo4j.io\n", - "NEO4J_USERNAME = \"neo4j\"\n", - "NEO4J_PASSWORD = \"\" # your password\n", - "NEO4J_DATABASE = \"neo4j\"\n", - "\n", - "# Create a Neo4j driver\n", - "driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))" - ] - }, - { - "cell_type": "markdown", - "id": "70f37ab6", - "metadata": {}, - "source": [ - "## Batched Import\n", - "\n", - "The batched import function takes a Cypher insert statement (needs to use the variable `value` for the row) and a dataframe to import.\n", - "It will send by default 1k rows at a time as query parameter to the database to be inserted." - ] - }, - { - "cell_type": "code", - "execution_count": 63, - "id": "d787bf7b-ac9b-4bfb-b140-a50a3fd205c5", - "metadata": {}, - "outputs": [], - "source": [ - "def batched_import(statement, df, batch_size=1000):\n", - " \"\"\"\n", - " Import a dataframe into Neo4j using a batched approach.\n", - "\n", - " Parameters: statement is the Cypher query to execute, df is the dataframe to import, and batch_size is the number of rows to import in each batch.\n", - " \"\"\"\n", - " total = len(df)\n", - " start_s = time.time()\n", - " for start in range(0, total, batch_size):\n", - " batch = df.iloc[start : min(start + batch_size, total)]\n", - " result = driver.execute_query(\n", - " \"UNWIND $rows AS value \" + statement,\n", - " rows=batch.to_dict(\"records\"),\n", - " database_=NEO4J_DATABASE,\n", - " )\n", - " print(result.summary.counters)\n", - " print(f\"{total} rows in {time.time() - start_s} s.\")\n", - " return total" - ] - }, - { - "cell_type": "markdown", - "id": "0fb45f42", - "metadata": {}, - "source": [ - "## Indexes and Constraints\n", - "\n", - "Indexes in Neo4j are only used to find the starting points for graph queries, e.g. quickly finding two nodes to connect.\n", - "Constraints exist to avoid duplicates, we create them mostly on id's of Entity types.\n", - "\n", - "We use some Types as markers with two underscores before and after to distinguish them from the actual entity types.\n", - "\n", - "The default relationship type here is `RELATED` but we could also infer a real relationship-type from the description or the types of the start and end-nodes.\n", - "\n", - "* `__Entity__`\n", - "* `__Document__`\n", - "* `__Chunk__`\n", - "* `__Community__`\n", - "* `__Covariate__`" - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "id": "ed7f212e-9148-424c-adc6-d81db9f8e5a5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "create constraint chunk_id if not exists for (c:__Chunk__) require c.id is unique\n", - "\n", - "create constraint document_id if not exists for (d:__Document__) require d.id is unique\n", - "\n", - "create constraint entity_id if not exists for (c:__Community__) require c.community is unique\n", - "\n", - "create constraint entity_id if not exists for (e:__Entity__) require e.id is unique\n", - "\n", - "create constraint entity_title if not exists for (e:__Entity__) require e.name is unique\n", - "\n", - "create constraint entity_title if not exists for (e:__Covariate__) require e.title is unique\n", - "\n", - "create constraint related_id if not exists for ()-[rel:RELATED]->() require rel.id is unique\n" - ] - } - ], - "source": [ - "# create constraints, idempotent operation\n", - "\n", - "statements = [\n", - " \"\\ncreate constraint chunk_id if not exists for (c:__Chunk__) require c.id is unique\",\n", - " \"\\ncreate constraint document_id if not exists for (d:__Document__) require d.id is unique\",\n", - " \"\\ncreate constraint entity_id if not exists for (c:__Community__) require c.community is unique\",\n", - " \"\\ncreate constraint entity_id if not exists for (e:__Entity__) require e.id is unique\",\n", - " \"\\ncreate constraint entity_title if not exists for (e:__Entity__) require e.name is unique\",\n", - " \"\\ncreate constraint entity_title if not exists for (e:__Covariate__) require e.title is unique\",\n", - " \"\\ncreate constraint related_id if not exists for ()-[rel:RELATED]->() require rel.id is unique\",\n", - " \"\\n\",\n", - "]\n", - "\n", - "for statement in statements:\n", - " if len((statement or \"\").strip()) > 0:\n", - " print(statement)\n", - " driver.execute_query(statement)" - ] - }, - { - "cell_type": "markdown", - "id": "beea073b", - "metadata": {}, - "source": [ - "## Import Process\n", - "\n", - "### Importing the Documents\n", - "\n", - "We're loading the parquet file for the documents and create nodes with their ids and add the title property.\n", - "We don't need to store text_unit_ids as we can create the relationships and the text content is also contained in the chunks." - ] - }, - { - "cell_type": "code", - "execution_count": 65, - "id": "1ba023e7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idtitle
0c305886e4aa2f6efcf64b57762777055book.txt
\n", - "
" - ], - "text/plain": [ - " id title\n", - "0 c305886e4aa2f6efcf64b57762777055 book.txt" - ] - }, - "execution_count": 65, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "doc_df = pd.read_parquet(\n", - " f\"{GRAPHRAG_FOLDER}/documents.parquet\", columns=[\"id\", \"title\"]\n", - ")\n", - "doc_df.head(2)" - ] - }, - { - "cell_type": "code", - "execution_count": 66, - "id": "96391c15", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'_contains_updates': True, 'labels_added': 1, 'nodes_created': 1, 'properties_set': 2}\n", - "1 rows in 0.05211496353149414 s.\n" - ] - }, - { - "data": { - "text/plain": [ - "1" - ] - }, - "execution_count": 66, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Import documents\n", - "statement = \"\"\"\n", - "MERGE (d:__Document__ {id:value.id})\n", - "SET d += value {.title}\n", - "\"\"\"\n", - "\n", - "batched_import(statement, doc_df)" - ] - }, - { - "cell_type": "markdown", - "id": "f97bbadb", - "metadata": {}, - "source": [ - "### Loading Text Units\n", - "\n", - "We load the text units, create a node per id and set the text and number of tokens.\n", - "Then we connect them to the documents that we created before." - ] - }, - { - "cell_type": "code", - "execution_count": 67, - "id": "0d825626", - "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", - "
idtextn_tokensdocument_ids
0680dd6d2a970a49082fa4f34bf63a34eThe Project Gutenberg eBook of A Christmas Ca...300[c305886e4aa2f6efcf64b57762777055]
195f1f8f5bdbf0bee3a2c6f2f4a4907f6THE PROJECT GUTENBERG EBOOK A CHRISTMAS CAROL...300[c305886e4aa2f6efcf64b57762777055]
\n", - "
" - ], - "text/plain": [ - " id \\\n", - "0 680dd6d2a970a49082fa4f34bf63a34e \n", - "1 95f1f8f5bdbf0bee3a2c6f2f4a4907f6 \n", - "\n", - " text n_tokens \\\n", - "0 The Project Gutenberg eBook of A Christmas Ca... 300 \n", - "1 THE PROJECT GUTENBERG EBOOK A CHRISTMAS CAROL... 300 \n", - "\n", - " document_ids \n", - "0 [c305886e4aa2f6efcf64b57762777055] \n", - "1 [c305886e4aa2f6efcf64b57762777055] " - ] - }, - "execution_count": 67, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "text_df = pd.read_parquet(\n", - " f\"{GRAPHRAG_FOLDER}/text_units.parquet\",\n", - " columns=[\"id\", \"text\", \"n_tokens\", \"document_ids\"],\n", - ")\n", - "text_df.head(2)" - ] - }, - { - "cell_type": "code", - "execution_count": 68, - "id": "ffd3d380-8710-46f5-b90a-04ed8482192c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'_contains_updates': True, 'relationships_created': 231, 'properties_set': 462}\n", - "231 rows in 0.05993008613586426 s.\n" - ] - }, - { - "data": { - "text/plain": [ - "231" - ] - }, - "execution_count": 68, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "statement = \"\"\"\n", - "MERGE (c:__Chunk__ {id:value.id})\n", - "SET c += value {.text, .n_tokens}\n", - "WITH c, value\n", - "UNWIND value.document_ids AS document\n", - "MATCH (d:__Document__ {id:document})\n", - "MERGE (c)-[:PART_OF]->(d)\n", - "\"\"\"\n", - "\n", - "batched_import(statement, text_df)" - ] - }, - { - "cell_type": "markdown", - "id": "f01b2094", - "metadata": {}, - "source": [ - "### Loading Nodes\n", - "\n", - "For the nodes we store id, name, description, embedding (if available), human readable id." - ] - }, - { - "cell_type": "code", - "execution_count": 78, - "id": "2392f9e9", - "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", - "
nametypedescriptionhuman_readable_ididdescription_embeddingtext_unit_ids
0\"PROJECT GUTENBERG\"\"ORGANIZATION\"Project Gutenberg is a pioneering organization...0b45241d70f0e43fca764df95b2b81f77[-0.020793898031115532, 0.02951139025390148, 0...[01e84646075b255eab0a34d872336a89, 10bab8e9773...
1\"UNITED STATES\"\"GEO\"The United States is prominently recognized fo...14119fd06010c494caa07f439b333f4c5[-0.009704762138426304, 0.013335365802049637, ...[01e84646075b255eab0a34d872336a89, 28f242c4515...
\n", - "
" - ], - "text/plain": [ - " name type \\\n", - "0 \"PROJECT GUTENBERG\" \"ORGANIZATION\" \n", - "1 \"UNITED STATES\" \"GEO\" \n", - "\n", - " description human_readable_id \\\n", - "0 Project Gutenberg is a pioneering organization... 0 \n", - "1 The United States is prominently recognized fo... 1 \n", - "\n", - " id \\\n", - "0 b45241d70f0e43fca764df95b2b81f77 \n", - "1 4119fd06010c494caa07f439b333f4c5 \n", - "\n", - " description_embedding \\\n", - "0 [-0.020793898031115532, 0.02951139025390148, 0... \n", - "1 [-0.009704762138426304, 0.013335365802049637, ... \n", - "\n", - " text_unit_ids \n", - "0 [01e84646075b255eab0a34d872336a89, 10bab8e9773... \n", - "1 [01e84646075b255eab0a34d872336a89, 28f242c4515... " - ] - }, - "execution_count": 78, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "entity_df = pd.read_parquet(\n", - " f\"{GRAPHRAG_FOLDER}/entities.parquet\",\n", - " columns=[\n", - " \"name\",\n", - " \"type\",\n", - " \"description\",\n", - " \"human_readable_id\",\n", - " \"id\",\n", - " \"description_embedding\",\n", - " \"text_unit_ids\",\n", - " ],\n", - ")\n", - "entity_df.head(2)" - ] - }, - { - "cell_type": "code", - "execution_count": 81, - "id": "1d038114-0714-48ee-a48a-c421cd539661", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'_contains_updates': True, 'properties_set': 831}\n", - "277 rows in 0.6978070735931396 s.\n" - ] - }, - { - "data": { - "text/plain": [ - "277" - ] - }, - "execution_count": 81, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "entity_statement = \"\"\"\n", - "MERGE (e:__Entity__ {id:value.id})\n", - "SET e += value {.human_readable_id, .description, name:replace(value.name,'\"','')}\n", - "WITH e, value\n", - "CALL db.create.setNodeVectorProperty(e, \"description_embedding\", value.description_embedding)\n", - "CALL apoc.create.addLabels(e, case when coalesce(value.type,\"\") = \"\" then [] else [apoc.text.upperCamelCase(replace(value.type,'\"',''))] end) yield node\n", - "UNWIND value.text_unit_ids AS text_unit\n", - "MATCH (c:__Chunk__ {id:text_unit})\n", - "MERGE (c)-[:HAS_ENTITY]->(e)\n", - "\"\"\"\n", - "\n", - "batched_import(entity_statement, entity_df)" - ] - }, - { - "cell_type": "markdown", - "id": "018d4f87", - "metadata": {}, - "source": [ - "### Import Relationships\n", - "\n", - "For the relationships we find the source and target node by name, using the base `__Entity__` type.\n", - "After creating the `RELATED` relationships, we set the description as attribute." - ] - }, - { - "cell_type": "code", - "execution_count": 71, - "id": "b347a047", - "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", - "
sourcetargetidrankweighthuman_readable_iddescriptiontext_unit_ids
0\"PROJECT GUTENBERG\"\"A CHRISTMAS CAROL\"b84d71ed9c3b45819eb3205fd28e13a0201.00\"Project Gutenberg is responsible for releasin...[680dd6d2a970a49082fa4f34bf63a34e]
1\"PROJECT GUTENBERG\"\"SUZANNE SHELL\"b0b464bc92a541e48547fe9738378dab151.01\"Suzanne Shell produced the eBook version of '...[680dd6d2a970a49082fa4f34bf63a34e]
\n", - "
" - ], - "text/plain": [ - " source target id \\\n", - "0 \"PROJECT GUTENBERG\" \"A CHRISTMAS CAROL\" b84d71ed9c3b45819eb3205fd28e13a0 \n", - "1 \"PROJECT GUTENBERG\" \"SUZANNE SHELL\" b0b464bc92a541e48547fe9738378dab \n", - "\n", - " rank weight human_readable_id \\\n", - "0 20 1.0 0 \n", - "1 15 1.0 1 \n", - "\n", - " description \\\n", - "0 \"Project Gutenberg is responsible for releasin... \n", - "1 \"Suzanne Shell produced the eBook version of '... \n", - "\n", - " text_unit_ids \n", - "0 [680dd6d2a970a49082fa4f34bf63a34e] \n", - "1 [680dd6d2a970a49082fa4f34bf63a34e] " - ] - }, - "execution_count": 71, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "rel_df = pd.read_parquet(\n", - " f\"{GRAPHRAG_FOLDER}/relationships.parquet\",\n", - " columns=[\n", - " \"source\",\n", - " \"target\",\n", - " \"id\",\n", - " \"rank\",\n", - " \"weight\",\n", - " \"human_readable_id\",\n", - " \"description\",\n", - " \"text_unit_ids\",\n", - " ],\n", - ")\n", - "rel_df.head(2)" - ] - }, - { - "cell_type": "code", - "execution_count": 72, - "id": "27900c01-89e1-4dec-9d5c-c07317c68baf", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'_contains_updates': True, 'properties_set': 1710}\n", - "342 rows in 0.14740705490112305 s.\n" - ] - }, - { - "data": { - "text/plain": [ - "342" - ] - }, - "execution_count": 72, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "rel_statement = \"\"\"\n", - " MATCH (source:__Entity__ {name:replace(value.source,'\"','')})\n", - " MATCH (target:__Entity__ {name:replace(value.target,'\"','')})\n", - " // not necessary to merge on id as there is only one relationship per pair\n", - " MERGE (source)-[rel:RELATED {id: value.id}]->(target)\n", - " SET rel += value {.rank, .weight, .human_readable_id, .description, .text_unit_ids}\n", - " RETURN count(*) as createdRels\n", - "\"\"\"\n", - "\n", - "batched_import(rel_statement, rel_df)" - ] - }, - { - "cell_type": "markdown", - "id": "e6365dd7", - "metadata": {}, - "source": [ - "### Importing Communities\n", - "\n", - "For communities we import their id, title, level.\n", - "We connect the `__Community__` nodes to the start and end nodes of the relationships they refer to.\n", - "\n", - "Connecting them to the chunks they orignate from is optional, as the entites are already connected to the chunks." - ] - }, - { - "cell_type": "code", - "execution_count": 73, - "id": "c2fab66c", - "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", - "
idleveltitletext_unit_idsrelationship_ids
020Community 2[0546d296a4d3bb0486bd0c94c01dc9be,0d6bc6e701a0...[ba481175ee1d4329bf07757a30abd3a1, 8d8da35190b...
140Community 4[054bdcba0a3690b43609d9226a47f84d,3a450ed2b7fb...[929f30875e1744b49e7b416eaf5a790c, 4920fda0318...
\n", - "
" - ], - "text/plain": [ - " id level title text_unit_ids \\\n", - "0 2 0 Community 2 [0546d296a4d3bb0486bd0c94c01dc9be,0d6bc6e701a0... \n", - "1 4 0 Community 4 [054bdcba0a3690b43609d9226a47f84d,3a450ed2b7fb... \n", - "\n", - " relationship_ids \n", - "0 [ba481175ee1d4329bf07757a30abd3a1, 8d8da35190b... \n", - "1 [929f30875e1744b49e7b416eaf5a790c, 4920fda0318... " - ] - }, - "execution_count": 73, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "community_df = pd.read_parquet(\n", - " f\"{GRAPHRAG_FOLDER}/communities.parquet\",\n", - " columns=[\"id\", \"level\", \"title\", \"text_unit_ids\", \"relationship_ids\"],\n", - ")\n", - "\n", - "community_df.head(2)" - ] - }, - { - "cell_type": "code", - "execution_count": 74, - "id": "1351f7e3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'_contains_updates': True, 'properties_set': 94}\n", - "47 rows in 0.07877922058105469 s.\n" - ] - }, - { - "data": { - "text/plain": [ - "47" - ] - }, - "execution_count": 74, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "statement = \"\"\"\n", - "MERGE (c:__Community__ {community:value.id})\n", - "SET c += value {.level, .title}\n", - "/*\n", - "UNWIND value.text_unit_ids as text_unit_id\n", - "MATCH (t:__Chunk__ {id:text_unit_id})\n", - "MERGE (c)-[:HAS_CHUNK]->(t)\n", - "WITH distinct c, value\n", - "*/\n", - "WITH *\n", - "UNWIND value.relationship_ids as rel_id\n", - "MATCH (start:__Entity__)-[:RELATED {id:rel_id}]->(end:__Entity__)\n", - "MERGE (start)-[:IN_COMMUNITY]->(c)\n", - "MERGE (end)-[:IN_COMMUNITY]->(c)\n", - "RETURn count(distinct c) as createdCommunities\n", - "\"\"\"\n", - "\n", - "batched_import(statement, community_df)" - ] - }, - { - "cell_type": "markdown", - "id": "dd9adf50", - "metadata": {}, - "source": [ - "### Importing Community Reports\n", - "\n", - "Fo the community reports we create nodes for each communitiy set the id, community, level, title, summary, rank, and rank_explanation and connect them to the entities they are about.\n", - "For the findings we create the findings in context of the communities." - ] - }, - { - "cell_type": "code", - "execution_count": 75, - "id": "1be9e7a9-69ee-406b-bce5-95a9c41ecffe", - "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", - "
idcommunityleveltitlesummaryfindingsrankrank_explanationfull_content
0e7822326-4da8-4954-afa9-be7f4f5791a5422Scrooge's Supernatural Encounters: Marley's Gh...This report delves into the pivotal supernatur...[{'explanation': 'Marley's Ghost plays a cruci...8.0The impact severity rating is high due to the ...# Scrooge's Supernatural Encounters: Marley's ...
18a5afac1-99ef-4f01-a1b1-f044ce392ff9432The Ghost's Influence on Scrooge's TransformationThis report delves into the pivotal role of 'T...[{'explanation': 'The Ghost, identified at tim...8.5The impact severity rating is high due to the ...# The Ghost's Influence on Scrooge's Transform...
\n", - "
" - ], - "text/plain": [ - " id community level \\\n", - "0 e7822326-4da8-4954-afa9-be7f4f5791a5 42 2 \n", - "1 8a5afac1-99ef-4f01-a1b1-f044ce392ff9 43 2 \n", - "\n", - " title \\\n", - "0 Scrooge's Supernatural Encounters: Marley's Gh... \n", - "1 The Ghost's Influence on Scrooge's Transformation \n", - "\n", - " summary \\\n", - "0 This report delves into the pivotal supernatur... \n", - "1 This report delves into the pivotal role of 'T... \n", - "\n", - " findings rank \\\n", - "0 [{'explanation': 'Marley's Ghost plays a cruci... 8.0 \n", - "1 [{'explanation': 'The Ghost, identified at tim... 8.5 \n", - "\n", - " rank_explanation \\\n", - "0 The impact severity rating is high due to the ... \n", - "1 The impact severity rating is high due to the ... \n", - "\n", - " full_content \n", - "0 # Scrooge's Supernatural Encounters: Marley's ... \n", - "1 # The Ghost's Influence on Scrooge's Transform... " - ] - }, - "execution_count": 75, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "community_report_df = pd.read_parquet(\n", - " f\"{GRAPHRAG_FOLDER}/community_reports.parquet\",\n", - " columns=[\n", - " \"id\",\n", - " \"community\",\n", - " \"level\",\n", - " \"title\",\n", - " \"summary\",\n", - " \"findings\",\n", - " \"rank\",\n", - " \"rank_explanation\",\n", - " \"full_content\",\n", - " ],\n", - ")\n", - "community_report_df.head(2)" - ] - }, - { - "cell_type": "code", - "execution_count": 76, - "id": "5c6ed591-f98c-4403-9fde-8d4cb4c01cca", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'_contains_updates': True, 'properties_set': 729}\n", - "47 rows in 0.02472519874572754 s.\n" - ] - }, - { - "data": { - "text/plain": [ - "47" - ] - }, - "execution_count": 76, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Import communities\n", - "community_statement = \"\"\"\n", - "MERGE (c:__Community__ {community:value.community})\n", - "SET c += value {.level, .title, .rank, .rank_explanation, .full_content, .summary}\n", - "WITH c, value\n", - "UNWIND range(0, size(value.findings)-1) AS finding_idx\n", - "WITH c, value, finding_idx, value.findings[finding_idx] as finding\n", - "MERGE (c)-[:HAS_FINDING]->(f:Finding {id:finding_idx})\n", - "SET f += finding\n", - "\"\"\"\n", - "batched_import(community_statement, community_report_df)" - ] - }, - { - "cell_type": "markdown", - "id": "50a1a24a", - "metadata": {}, - "source": [ - "### Importing Covariates\n", - "\n", - "Covariates are for instance claims on entities, we connect them to the chunks where they originate from." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "523bed92-d12c-4fc4-aa44-6c62321b36bc", - "metadata": {}, - "outputs": [], - "source": [ - "cov_df = (pd.read_parquet(f\"{GRAPHRAG_FOLDER}/covariates.parquet\"),)\n", - "# columns=[\"id\",\"text_unit_id\"])\n", - "cov_df.head(2)\n", - "# Subject id do not match entity ids" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3e064234-5fce-448e-8bb4-ab2f35699049", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'_contains_updates': True, 'labels_added': 89, 'relationships_created': 89, 'nodes_created': 89, 'properties_set': 1061}\n", - "89 rows in 0.13370895385742188 s.\n" - ] - }, - { - "data": { - "text/plain": [ - "89" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Import covariates\n", - "cov_statement = \"\"\"\n", - "MERGE (c:__Covariate__ {id:value.id})\n", - "SET c += apoc.map.clean(value, [\"text_unit_id\", \"document_ids\", \"n_tokens\"], [NULL, \"\"])\n", - "WITH c, value\n", - "MATCH (ch:__Chunk__ {id: value.text_unit_id})\n", - "MERGE (ch)-[:HAS_COVARIATE]->(c)\n", - "\"\"\"\n", - "batched_import(cov_statement, cov_df)" - ] - }, - { - "cell_type": "markdown", - "id": "00340bae", - "metadata": {}, - "source": [ - "### Visualize your data\n", - "\n", - "You can now [Open] Neo4j on Aura, you need to log in with either SSO or your credentials.\n", - "\n", - "Or open https://workspace-preview.neo4j.io and connect to your local instance, remember the URI is `neo4j://localhost` and `neo4j` as username and `password` as password.\n", - "\n", - "In \"Explore\" you can explore by using visual graph patterns and then explore and expand further.\n", - "\n", - "In \"Query\", you can open the left sidebar and explore by clicking on the nodes and relationships.\n", - "You can also use the co-pilot to generate Cypher queries for your, here are some examples.\n", - "\n", - "#### Show a few `__Entity__` nodes and their relationships (Entity Graph)\n", - "\n", - "```cypher\n", - "MATCH path = (:__Entity__)-[:RELATED]->(:__Entity__)\n", - "RETURN path LIMIT 200\n", - "```\n", - "\n", - "#### Show the Chunks and the Document (Lexical Graph)\n", - "\n", - "```cypher\n", - "MATCH (d:__Document__) WITH d LIMIT 1\n", - "MATCH path = (d)<-[:PART_OF]-(c:__Chunk__)\n", - "RETURN path LIMIT 100\n", - "```\n", - "\n", - "#### Show a Community and it's Entities\n", - "\n", - "```cypher\n", - "MATCH (c:__Community__) WITH c LIMIT 1\n", - "MATCH path = (c)<-[:IN_COMMUNITY]-()-[:RELATED]-(:__Entity__)\n", - "RETURN path LIMIT 100\n", - "```\n", - "\n", - "#### Show everything\n", - "\n", - "```cypher\n", - "MATCH (d:__Document__) WITH d LIMIT 1\n", - "MATCH path = (d)<-[:PART_OF]-(:__Chunk__)-[:HAS_ENTIY]->()-[:RELATED]-()-[:IN_COMMUNITY]->()\n", - "RETURN path LIMIT 250\n", - "```\n", - "\n", - "We showed the visualization of this last query at the beginning." - ] - }, - { - "cell_type": "markdown", - "id": "a0aa8529", - "metadata": {}, - "source": [ - "If you have questions, feel free to reach out in the GraphRAG discord server: \n", - "https://discord.gg/graphrag" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.8" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples_notebooks/community_contrib/yfiles-jupyter-graphs/graph-visualization.ipynb b/examples_notebooks/community_contrib/yfiles-jupyter-graphs/graph-visualization.ipynb deleted file mode 100644 index fb53287b55..0000000000 --- a/examples_notebooks/community_contrib/yfiles-jupyter-graphs/graph-visualization.ipynb +++ /dev/null @@ -1,523 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Visualizing the knowledge graph with `yfiles-jupyter-graphs`\n", - "\n", - "This notebook is a partial copy of [local_search.ipynb](../../local_search.ipynb) that shows how to use `yfiles-jupyter-graphs` to add interactive graph visualizations of the parquet files and how to visualize the result context of `graphrag` queries (see at the end of this notebook)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Copyright (c) 2024 Microsoft Corporation.\n", - "# Licensed under the MIT License." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "import pandas as pd\n", - "import tiktoken\n", - "from graphrag.query.llm.oai.chat_openai import ChatOpenAI\n", - "from graphrag.query.llm.oai.embedding import OpenAIEmbedding\n", - "from graphrag.query.llm.oai.typing import OpenaiApiType\n", - "\n", - "from graphrag.query.context_builder.entity_extraction import EntityVectorStoreKey\n", - "from graphrag.query.indexer_adapters import (\n", - " read_indexer_covariates,\n", - " read_indexer_entities,\n", - " read_indexer_relationships,\n", - " read_indexer_reports,\n", - " read_indexer_text_units,\n", - ")\n", - "from graphrag.query.structured_search.local_search.mixed_context import (\n", - " LocalSearchMixedContext,\n", - ")\n", - "from graphrag.query.structured_search.local_search.search import LocalSearch\n", - "from graphrag.vector_stores.lancedb import LanceDBVectorStore" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Local Search Example\n", - "\n", - "Local search method generates answers by combining relevant data from the AI-extracted knowledge-graph with text chunks of the raw documents. This method is suitable for questions that require an understanding of specific entities mentioned in the documents (e.g. What are the healing properties of chamomile?)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load text units and graph data tables as context for local search\n", - "\n", - "- In this test we first load indexing outputs from parquet files to dataframes, then convert these dataframes into collections of data objects aligning with the knowledge model." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load tables to dataframes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "INPUT_DIR = \"../../inputs/operation dulce\"\n", - "LANCEDB_URI = f\"{INPUT_DIR}/lancedb\"\n", - "\n", - "COMMUNITY_REPORT_TABLE = \"community_reports\"\n", - "COMMUNITY_TABLE = \"communities\"\n", - "ENTITY_TABLE = \"entities\"\n", - "RELATIONSHIP_TABLE = \"relationships\"\n", - "COVARIATE_TABLE = \"covariates\"\n", - "TEXT_UNIT_TABLE = \"text_units\"\n", - "COMMUNITY_LEVEL = 2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Read entities" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# read nodes table to get community and degree data\n", - "entity_df = pd.read_parquet(f\"{INPUT_DIR}/{ENTITY_TABLE}.parquet\")\n", - "community_df = pd.read_parquet(f\"{INPUT_DIR}/{COMMUNITY_TABLE}.parquet\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Read relationships" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "relationship_df = pd.read_parquet(f\"{INPUT_DIR}/{RELATIONSHIP_TABLE}.parquet\")\n", - "relationships = read_indexer_relationships(relationship_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Visualizing nodes and relationships with `yfiles-jupyter-graphs`\n", - "\n", - "`yfiles-jupyter-graphs` is a graph visualization extension that provides interactive and customizable visualizations for structured node and relationship data.\n", - "\n", - "In this case, we use it to provide an interactive visualization for the knowledge graph of the [local_search.ipynb](../../local_search.ipynb) sample by passing node and relationship lists converted from the given parquet files. The requirements for the input data is an `id` attribute for the nodes and `start`/`end` properties for the relationships that correspond to the node ids. Additional attributes can be added in the `properties` of each node/relationship dict:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install yfiles_jupyter_graphs --quiet\n", - "from yfiles_jupyter_graphs import GraphWidget\n", - "\n", - "\n", - "# converts the entities dataframe to a list of dicts for yfiles-jupyter-graphs\n", - "def convert_entities_to_dicts(df):\n", - " \"\"\"Convert the entities dataframe to a list of dicts for yfiles-jupyter-graphs.\"\"\"\n", - " nodes_dict = {}\n", - " for _, row in df.iterrows():\n", - " # Create a dictionary for each row and collect unique nodes\n", - " node_id = row[\"title\"]\n", - " if node_id not in nodes_dict:\n", - " nodes_dict[node_id] = {\n", - " \"id\": node_id,\n", - " \"properties\": row.to_dict(),\n", - " }\n", - " return list(nodes_dict.values())\n", - "\n", - "\n", - "# converts the relationships dataframe to a list of dicts for yfiles-jupyter-graphs\n", - "def convert_relationships_to_dicts(df):\n", - " \"\"\"Convert the relationships dataframe to a list of dicts for yfiles-jupyter-graphs.\"\"\"\n", - " relationships = []\n", - " for _, row in df.iterrows():\n", - " # Create a dictionary for each row\n", - " relationships.append({\n", - " \"start\": row[\"source\"],\n", - " \"end\": row[\"target\"],\n", - " \"properties\": row.to_dict(),\n", - " })\n", - " return relationships\n", - "\n", - "\n", - "w = GraphWidget()\n", - "w.directed = True\n", - "w.nodes = convert_entities_to_dicts(entity_df)\n", - "w.edges = convert_relationships_to_dicts(relationship_df)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Configure data-driven visualization\n", - "\n", - "The additional properties can be used to configure the visualization for different use cases." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# show title on the node\n", - "w.node_label_mapping = \"title\"\n", - "\n", - "\n", - "# map community to a color\n", - "def community_to_color(community):\n", - " \"\"\"Map a community to a color.\"\"\"\n", - " colors = [\n", - " \"crimson\",\n", - " \"darkorange\",\n", - " \"indigo\",\n", - " \"cornflowerblue\",\n", - " \"cyan\",\n", - " \"teal\",\n", - " \"green\",\n", - " ]\n", - " return (\n", - " colors[int(community) % len(colors)] if community is not None else \"lightgray\"\n", - " )\n", - "\n", - "\n", - "def edge_to_source_community(edge):\n", - " \"\"\"Get the community of the source node of an edge.\"\"\"\n", - " source_node = next(\n", - " (entry for entry in w.nodes if entry[\"properties\"][\"title\"] == edge[\"start\"]),\n", - " None,\n", - " )\n", - " source_node_community = source_node[\"properties\"][\"community\"]\n", - " return source_node_community if source_node_community is not None else None\n", - "\n", - "\n", - "w.node_color_mapping = lambda node: community_to_color(node[\"properties\"][\"community\"])\n", - "w.edge_color_mapping = lambda edge: community_to_color(edge_to_source_community(edge))\n", - "# map size data to a reasonable factor\n", - "w.node_scale_factor_mapping = lambda node: 0.5 + node[\"properties\"][\"size\"] * 1.5 / 20\n", - "# use weight for edge thickness\n", - "w.edge_thickness_factor_mapping = \"weight\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Automatic layouts\n", - "\n", - "The widget provides different automatic layouts that serve different purposes: `Circular`, `Hierarchic`, `Organic (interactiv or static)`, `Orthogonal`, `Radial`, `Tree`, `Geo-spatial`.\n", - "\n", - "For the knowledge graph, this sample uses the `Circular` layout, though `Hierarchic` or `Organic` are also suitable choices." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Use the circular layout for this visualization. For larger graphs, the default organic layout is often preferrable.\n", - "w.circular_layout()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Display the graph" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "display(w)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Visualizing the result context of `graphrag` queries\n", - "\n", - "The result context of `graphrag` queries allow to inspect the context graph of the request. This data can similarly be visualized as graph with `yfiles-jupyter-graphs`.\n", - "\n", - "## Making the request\n", - "\n", - "The following cell recreates the sample queries from [local_search.ipynb](../../local_search.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# setup (see also ../../local_search.ipynb)\n", - "entities = read_indexer_entities(entity_df, community_df, COMMUNITY_LEVEL)\n", - "\n", - "description_embedding_store = LanceDBVectorStore(\n", - " collection_name=\"default-entity-description\",\n", - ")\n", - "description_embedding_store.connect(db_uri=LANCEDB_URI)\n", - "covariate_df = pd.read_parquet(f\"{INPUT_DIR}/{COVARIATE_TABLE}.parquet\")\n", - "claims = read_indexer_covariates(covariate_df)\n", - "covariates = {\"claims\": claims}\n", - "report_df = pd.read_parquet(f\"{INPUT_DIR}/{COMMUNITY_REPORT_TABLE}.parquet\")\n", - "reports = read_indexer_reports(report_df, community_df, COMMUNITY_LEVEL)\n", - "text_unit_df = pd.read_parquet(f\"{INPUT_DIR}/{TEXT_UNIT_TABLE}.parquet\")\n", - "text_units = read_indexer_text_units(text_unit_df)\n", - "\n", - "api_key = os.environ[\"GRAPHRAG_API_KEY\"]\n", - "llm_model = os.environ[\"GRAPHRAG_LLM_MODEL\"]\n", - "embedding_model = os.environ[\"GRAPHRAG_EMBEDDING_MODEL\"]\n", - "\n", - "llm = ChatOpenAI(\n", - " api_key=api_key,\n", - " model=llm_model,\n", - " api_type=OpenaiApiType.OpenAI, # OpenaiApiType.OpenAI or OpenaiApiType.AzureOpenAI\n", - " max_retries=20,\n", - ")\n", - "\n", - "token_encoder = tiktoken.get_encoding(\"cl100k_base\")\n", - "\n", - "text_embedder = OpenAIEmbedding(\n", - " api_key=api_key,\n", - " api_base=None,\n", - " api_type=OpenaiApiType.OpenAI,\n", - " model=embedding_model,\n", - " deployment_name=embedding_model,\n", - " max_retries=20,\n", - ")\n", - "\n", - "context_builder = LocalSearchMixedContext(\n", - " community_reports=reports,\n", - " text_units=text_units,\n", - " entities=entities,\n", - " relationships=relationships,\n", - " covariates=covariates,\n", - " entity_text_embeddings=description_embedding_store,\n", - " embedding_vectorstore_key=EntityVectorStoreKey.ID, # if the vectorstore uses entity title as ids, set this to EntityVectorStoreKey.TITLE\n", - " text_embedder=text_embedder,\n", - " token_encoder=token_encoder,\n", - ")\n", - "\n", - "local_context_params = {\n", - " \"text_unit_prop\": 0.5,\n", - " \"community_prop\": 0.1,\n", - " \"conversation_history_max_turns\": 5,\n", - " \"conversation_history_user_turns_only\": True,\n", - " \"top_k_mapped_entities\": 10,\n", - " \"top_k_relationships\": 10,\n", - " \"include_entity_rank\": True,\n", - " \"include_relationship_weight\": True,\n", - " \"include_community_rank\": False,\n", - " \"return_candidate_context\": False,\n", - " \"embedding_vectorstore_key\": EntityVectorStoreKey.ID, # set this to EntityVectorStoreKey.TITLE if the vectorstore uses entity title as ids\n", - " \"max_tokens\": 12_000, # change this based on the token limit you have on your model (if you are using a model with 8k limit, a good setting could be 5000)\n", - "}\n", - "\n", - "llm_params = {\n", - " \"max_tokens\": 2_000, # change this based on the token limit you have on your model (if you are using a model with 8k limit, a good setting could be 1000=1500)\n", - " \"temperature\": 0.0,\n", - "}\n", - "\n", - "search_engine = LocalSearch(\n", - " llm=llm,\n", - " context_builder=context_builder,\n", - " token_encoder=token_encoder,\n", - " llm_params=llm_params,\n", - " context_builder_params=local_context_params,\n", - " response_type=\"multiple paragraphs\", # free form text describing the response type and format, can be anything, e.g. prioritized list, single paragraph, multiple paragraphs, multiple-page report\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Run local search on sample queries" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "result = await search_engine.search(\"Tell me about Agent Mercer\")\n", - "print(result.response)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "question = \"Tell me about Dr. Jordan Hayes\"\n", - "result = await search_engine.search(question)\n", - "print(result.response)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Inspecting the context data used to generate the response" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "result.context_data[\"entities\"].head()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "result.context_data[\"relationships\"].head()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualizing the result context as graph" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\"\"\"\n", - "Helper function to visualize the result context with `yfiles-jupyter-graphs`.\n", - "\n", - "The dataframes are converted into supported nodes and relationships lists and then passed to yfiles-jupyter-graphs.\n", - "Additionally, some values are mapped to visualization properties.\n", - "\"\"\"\n", - "\n", - "\n", - "def show_graph(result):\n", - " \"\"\"Visualize the result context with yfiles-jupyter-graphs.\"\"\"\n", - " from yfiles_jupyter_graphs import GraphWidget\n", - "\n", - " if (\n", - " \"entities\" not in result.context_data\n", - " or \"relationships\" not in result.context_data\n", - " ):\n", - " msg = \"The passed results do not contain 'entities' or 'relationships'\"\n", - " raise ValueError(msg)\n", - "\n", - " # converts the entities dataframe to a list of dicts for yfiles-jupyter-graphs\n", - " def convert_entities_to_dicts(df):\n", - " \"\"\"Convert the entities dataframe to a list of dicts for yfiles-jupyter-graphs.\"\"\"\n", - " nodes_dict = {}\n", - " for _, row in df.iterrows():\n", - " # Create a dictionary for each row and collect unique nodes\n", - " node_id = row[\"entity\"]\n", - " if node_id not in nodes_dict:\n", - " nodes_dict[node_id] = {\n", - " \"id\": node_id,\n", - " \"properties\": row.to_dict(),\n", - " }\n", - " return list(nodes_dict.values())\n", - "\n", - " # converts the relationships dataframe to a list of dicts for yfiles-jupyter-graphs\n", - " def convert_relationships_to_dicts(df):\n", - " \"\"\"Convert the relationships dataframe to a list of dicts for yfiles-jupyter-graphs.\"\"\"\n", - " relationships = []\n", - " for _, row in df.iterrows():\n", - " # Create a dictionary for each row\n", - " relationships.append({\n", - " \"start\": row[\"source\"],\n", - " \"end\": row[\"target\"],\n", - " \"properties\": row.to_dict(),\n", - " })\n", - " return relationships\n", - "\n", - " w = GraphWidget()\n", - " # use the converted data to visualize the graph\n", - " w.nodes = convert_entities_to_dicts(result.context_data[\"entities\"])\n", - " w.edges = convert_relationships_to_dicts(result.context_data[\"relationships\"])\n", - " w.directed = True\n", - " # show title on the node\n", - " w.node_label_mapping = \"entity\"\n", - " # use weight for edge thickness\n", - " w.edge_thickness_factor_mapping = \"weight\"\n", - " display(w)\n", - "\n", - "\n", - "show_graph(result)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "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.10.0" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_latest.manifest b/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_latest.manifest deleted file mode 100644 index b41640ffb9..0000000000 Binary files a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_latest.manifest and /dev/null differ diff --git a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_transactions/0-498c6e24-dd0a-42b9-8f7e-5e3d2ab258b0.txn b/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_transactions/0-498c6e24-dd0a-42b9-8f7e-5e3d2ab258b0.txn deleted file mode 100644 index deb4cffeae..0000000000 --- a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_transactions/0-498c6e24-dd0a-42b9-8f7e-5e3d2ab258b0.txn +++ /dev/null @@ -1 +0,0 @@ -$498c6e24-dd0a-42b9-8f7e-5e3d2ab258b0id *string08text *string085vector *fixed_size_list:float:153608 title *string08 \ No newline at end of file diff --git a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_transactions/1-bf5aa024-a229-461f-8d78-699841a302fe.txn b/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_transactions/1-bf5aa024-a229-461f-8d78-699841a302fe.txn deleted file mode 100644 index ba0b9ee5a0..0000000000 Binary files a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_transactions/1-bf5aa024-a229-461f-8d78-699841a302fe.txn and /dev/null differ diff --git a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_versions/1.manifest b/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_versions/1.manifest deleted file mode 100644 index 6566b33fd1..0000000000 Binary files a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_versions/1.manifest and /dev/null differ diff --git a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_versions/2.manifest b/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_versions/2.manifest deleted file mode 100644 index b41640ffb9..0000000000 Binary files a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/_versions/2.manifest and /dev/null differ diff --git a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/data/fe64774f-5412-4c9c-8dea-f6ed55c81119.lance b/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/data/fe64774f-5412-4c9c-8dea-f6ed55c81119.lance deleted file mode 100644 index a324ab999f..0000000000 Binary files a/examples_notebooks/inputs/operation dulce/lancedb/entity_description_embeddings.lance/data/fe64774f-5412-4c9c-8dea-f6ed55c81119.lance and /dev/null differ diff --git a/graphrag/api/query.py b/graphrag/api/query.py deleted file mode 100644 index b92cd8145b..0000000000 --- a/graphrag/api/query.py +++ /dev/null @@ -1,1226 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -""" -Query Engine API. - -This API provides access to the query engine of graphrag, allowing external applications -to hook into graphrag and run queries over a knowledge graph generated by graphrag. - -Contains the following functions: - - global_search: Perform a global search. - - global_search_streaming: Perform a global search and stream results back. - - local_search: Perform a local search. - - local_search_streaming: Perform a local search and stream results back. - -WARNING: This API is under development and may undergo changes in future releases. -Backwards compatibility is not guaranteed at this time. -""" - -import logging -from collections.abc import AsyncGenerator -from typing import Any - -import pandas as pd -from pydantic import validate_call - -from graphrag.callbacks.noop_query_callbacks import NoopQueryCallbacks -from graphrag.callbacks.query_callbacks import QueryCallbacks -from graphrag.config.embeddings import ( - community_full_content_embedding, - entity_description_embedding, - text_unit_text_embedding, -) -from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.logger.standard_logging import init_loggers -from graphrag.query.factory import ( - get_basic_search_engine, - get_drift_search_engine, - get_global_search_engine, - get_local_search_engine, -) -from graphrag.query.indexer_adapters import ( - read_indexer_communities, - read_indexer_covariates, - read_indexer_entities, - read_indexer_relationships, - read_indexer_report_embeddings, - read_indexer_reports, - read_indexer_text_units, -) -from graphrag.utils.api import ( - get_embedding_store, - load_search_prompt, - truncate, - update_context_data, -) -from graphrag.utils.cli import redact - -# Initialize standard logger -logger = logging.getLogger(__name__) - - -@validate_call(config={"arbitrary_types_allowed": True}) -async def global_search( - config: GraphRagConfig, - entities: pd.DataFrame, - communities: pd.DataFrame, - community_reports: pd.DataFrame, - community_level: int | None, - dynamic_community_selection: bool, - response_type: str, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> tuple[ - str | dict[str, Any] | list[dict[str, Any]], - str | list[pd.DataFrame] | dict[str, pd.DataFrame], -]: - """Perform a global search and return the context data and response. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) - - communities (pd.DataFrame): A DataFrame containing the final communities (from communities.parquet) - - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) - - community_level (int): The community level to search at. - - dynamic_community_selection (bool): Enable dynamic community selection instead of using all community reports at a fixed level. Note that you can still provide community_level cap the maximum level to search. - - response_type (str): The type of response to return. - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - callbacks = callbacks or [] - full_response = "" - context_data = {} - - def on_context(context: Any) -> None: - nonlocal context_data - context_data = context - - local_callbacks = NoopQueryCallbacks() - local_callbacks.on_context = on_context - callbacks.append(local_callbacks) - - logger.debug("Executing global search query: %s", query) - async for chunk in global_search_streaming( - config=config, - entities=entities, - communities=communities, - community_reports=community_reports, - community_level=community_level, - dynamic_community_selection=dynamic_community_selection, - response_type=response_type, - query=query, - callbacks=callbacks, - ): - full_response += chunk - logger.debug("Query response: %s", truncate(full_response, 400)) - return full_response, context_data - - -@validate_call(config={"arbitrary_types_allowed": True}) -def global_search_streaming( - config: GraphRagConfig, - entities: pd.DataFrame, - communities: pd.DataFrame, - community_reports: pd.DataFrame, - community_level: int | None, - dynamic_community_selection: bool, - response_type: str, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> AsyncGenerator: - """Perform a global search and return the context data and response via a generator. - - Context data is returned as a dictionary of lists, with one list entry for each record. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) - - communities (pd.DataFrame): A DataFrame containing the final communities (from communities.parquet) - - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) - - community_level (int): The community level to search at. - - dynamic_community_selection (bool): Enable dynamic community selection instead of using all community reports at a fixed level. Note that you can still provide community_level cap the maximum level to search. - - response_type (str): The type of response to return. - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - communities_ = read_indexer_communities(communities, community_reports) - reports = read_indexer_reports( - community_reports, - communities, - community_level=community_level, - dynamic_community_selection=dynamic_community_selection, - ) - entities_ = read_indexer_entities( - entities, communities, community_level=community_level - ) - map_prompt = load_search_prompt(config.root_dir, config.global_search.map_prompt) - reduce_prompt = load_search_prompt( - config.root_dir, config.global_search.reduce_prompt - ) - knowledge_prompt = load_search_prompt( - config.root_dir, config.global_search.knowledge_prompt - ) - - logger.debug("Executing streaming global search query: %s", query) - search_engine = get_global_search_engine( - config, - reports=reports, - entities=entities_, - communities=communities_, - response_type=response_type, - dynamic_community_selection=dynamic_community_selection, - map_system_prompt=map_prompt, - reduce_system_prompt=reduce_prompt, - general_knowledge_inclusion_prompt=knowledge_prompt, - callbacks=callbacks, - ) - return search_engine.stream_search(query=query) - - -@validate_call(config={"arbitrary_types_allowed": True}) -async def multi_index_global_search( - config: GraphRagConfig, - entities_list: list[pd.DataFrame], - communities_list: list[pd.DataFrame], - community_reports_list: list[pd.DataFrame], - index_names: list[str], - community_level: int | None, - dynamic_community_selection: bool, - response_type: str, - streaming: bool, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> tuple[ - str | dict[str, Any] | list[dict[str, Any]], - str | list[pd.DataFrame] | dict[str, pd.DataFrame], -]: - """Perform a global search across multiple indexes and return the context data and response. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - entities_list (list[pd.DataFrame]): A list of DataFrames containing the final entities (from entities.parquet) - - communities_list (list[pd.DataFrame]): A list of DataFrames containing the final communities (from communities.parquet) - - community_reports_list (list[pd.DataFrame]): A list of DataFrames containing the final community reports (from community_reports.parquet) - - index_names (list[str]): A list of index names. - - community_level (int): The community level to search at. - - dynamic_community_selection (bool): Enable dynamic community selection instead of using all community reports at a fixed level. Note that you can still provide community_level cap the maximum level to search. - - response_type (str): The type of response to return. - - streaming (bool): Whether to stream the results or not. - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - logger.warning( - "Multi-index search is deprecated and will be removed in GraphRAG v3." - ) - - # Streaming not supported yet - if streaming: - message = "Streaming not yet implemented for multi_global_search" - raise NotImplementedError(message) - - links = { - "communities": {}, - "community_reports": {}, - "entities": {}, - } - max_vals = { - "communities": -1, - "community_reports": -1, - "entities": -1, - } - - communities_dfs = [] - community_reports_dfs = [] - entities_dfs = [] - - for idx, index_name in enumerate(index_names): - # Prepare each index's community reports dataframe for merging - community_reports_df = community_reports_list[idx] - community_reports_df["community"] = community_reports_df["community"].astype( - int - ) - for i in community_reports_df["community"]: - links["community_reports"][i + max_vals["community_reports"] + 1] = { - "index_name": index_name, - "id": str(i), - } - community_reports_df["community"] += max_vals["community_reports"] + 1 - community_reports_df["human_readable_id"] += max_vals["community_reports"] + 1 - max_vals["community_reports"] = int(community_reports_df["community"].max()) - community_reports_dfs.append(community_reports_df) - - # Prepare each index's communities dataframe for merging - communities_df = communities_list[idx] - communities_df["community"] = communities_df["community"].astype(int) - communities_df["parent"] = communities_df["parent"].astype(int) - for i in communities_df["community"]: - links["communities"][i + max_vals["communities"] + 1] = { - "index_name": index_name, - "id": str(i), - } - communities_df["community"] += max_vals["communities"] + 1 - communities_df["parent"] = communities_df["parent"].apply( - lambda x: x if x == -1 else x + max_vals["communities"] + 1 - ) - communities_df["human_readable_id"] += max_vals["communities"] + 1 - # concat the index name to the entity_ids, since this is used for joining later - communities_df["entity_ids"] = communities_df["entity_ids"].apply( - lambda x, index_name=index_name: [i + f"-{index_name}" for i in x] - ) - max_vals["communities"] = int(communities_df["community"].max()) - communities_dfs.append(communities_df) - - # Prepare each index's entities dataframe for merging - entities_df = entities_list[idx] - for i in entities_df["human_readable_id"]: - links["entities"][i + max_vals["entities"] + 1] = { - "index_name": index_name, - "id": i, - } - entities_df["human_readable_id"] += max_vals["entities"] + 1 - entities_df["title"] = entities_df["title"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - entities_df["text_unit_ids"] = entities_df["text_unit_ids"].apply( - lambda x, index_name=index_name: [i + f"-{index_name}" for i in x] - ) - max_vals["entities"] = int(entities_df["human_readable_id"].max()) - entities_dfs.append(entities_df) - - # Merge the dataframes - community_reports_combined = pd.concat( - community_reports_dfs, axis=0, ignore_index=True, sort=False - ) - entities_combined = pd.concat(entities_dfs, axis=0, ignore_index=True, sort=False) - communities_combined = pd.concat( - communities_dfs, axis=0, ignore_index=True, sort=False - ) - - logger.debug("Executing multi-index global search query: %s", query) - result = await global_search( - config, - entities=entities_combined, - communities=communities_combined, - community_reports=community_reports_combined, - community_level=community_level, - dynamic_community_selection=dynamic_community_selection, - response_type=response_type, - query=query, - callbacks=callbacks, - ) - - # Update the context data by linking index names and community ids - context = update_context_data(result[1], links) - - logger.debug("Query response: %s", truncate(result[0], 400)) # type: ignore - return (result[0], context) - - -@validate_call(config={"arbitrary_types_allowed": True}) -async def local_search( - config: GraphRagConfig, - entities: pd.DataFrame, - communities: pd.DataFrame, - community_reports: pd.DataFrame, - text_units: pd.DataFrame, - relationships: pd.DataFrame, - covariates: pd.DataFrame | None, - community_level: int, - response_type: str, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> tuple[ - str | dict[str, Any] | list[dict[str, Any]], - str | list[pd.DataFrame] | dict[str, pd.DataFrame], -]: - """Perform a local search and return the context data and response. - - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) - - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) - - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) - - relationships (pd.DataFrame): A DataFrame containing the final relationships (from relationships.parquet) - - covariates (pd.DataFrame): A DataFrame containing the final covariates (from covariates.parquet) - - community_level (int): The community level to search at. - - response_type (str): The response type to return. - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - callbacks = callbacks or [] - full_response = "" - context_data = {} - - def on_context(context: Any) -> None: - nonlocal context_data - context_data = context - - local_callbacks = NoopQueryCallbacks() - local_callbacks.on_context = on_context - callbacks.append(local_callbacks) - - logger.debug("Executing local search query: %s", query) - async for chunk in local_search_streaming( - config=config, - entities=entities, - communities=communities, - community_reports=community_reports, - text_units=text_units, - relationships=relationships, - covariates=covariates, - community_level=community_level, - response_type=response_type, - query=query, - callbacks=callbacks, - ): - full_response += chunk - logger.debug("Query response: %s", truncate(full_response, 400)) - return full_response, context_data - - -@validate_call(config={"arbitrary_types_allowed": True}) -def local_search_streaming( - config: GraphRagConfig, - entities: pd.DataFrame, - communities: pd.DataFrame, - community_reports: pd.DataFrame, - text_units: pd.DataFrame, - relationships: pd.DataFrame, - covariates: pd.DataFrame | None, - community_level: int, - response_type: str, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> AsyncGenerator: - """Perform a local search and return the context data and response via a generator. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) - - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) - - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) - - relationships (pd.DataFrame): A DataFrame containing the final relationships (from relationships.parquet) - - covariates (pd.DataFrame): A DataFrame containing the final covariates (from covariates.parquet) - - community_level (int): The community level to search at. - - response_type (str): The response type to return. - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - vector_store_args = {} - for index, store in config.vector_store.items(): - vector_store_args[index] = store.model_dump() - msg = f"Vector Store Args: {redact(vector_store_args)}" - logger.debug(msg) - - description_embedding_store = get_embedding_store( - config_args=vector_store_args, - embedding_name=entity_description_embedding, - ) - - entities_ = read_indexer_entities(entities, communities, community_level) - covariates_ = read_indexer_covariates(covariates) if covariates is not None else [] - prompt = load_search_prompt(config.root_dir, config.local_search.prompt) - - logger.debug("Executing streaming local search query: %s", query) - search_engine = get_local_search_engine( - config=config, - reports=read_indexer_reports(community_reports, communities, community_level), - text_units=read_indexer_text_units(text_units), - entities=entities_, - relationships=read_indexer_relationships(relationships), - covariates={"claims": covariates_}, - description_embedding_store=description_embedding_store, - response_type=response_type, - system_prompt=prompt, - callbacks=callbacks, - ) - return search_engine.stream_search(query=query) - - -@validate_call(config={"arbitrary_types_allowed": True}) -async def multi_index_local_search( - config: GraphRagConfig, - entities_list: list[pd.DataFrame], - communities_list: list[pd.DataFrame], - community_reports_list: list[pd.DataFrame], - text_units_list: list[pd.DataFrame], - relationships_list: list[pd.DataFrame], - covariates_list: list[pd.DataFrame] | None, - index_names: list[str], - community_level: int, - response_type: str, - streaming: bool, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> tuple[ - str | dict[str, Any] | list[dict[str, Any]], - str | list[pd.DataFrame] | dict[str, pd.DataFrame], -]: - """Perform a local search across multiple indexes and return the context data and response. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - entities_list (list[pd.DataFrame]): A list of DataFrames containing the final entities (from entities.parquet) - - community_reports_list (list[pd.DataFrame]): A list of DataFrames containing the final community reports (from community_reports.parquet) - - text_units_list (list[pd.DataFrame]): A list of DataFrames containing the final text units (from text_units.parquet) - - relationships_list (list[pd.DataFrame]): A list of DataFrames containing the final relationships (from relationships.parquet) - - covariates_list (list[pd.DataFrame]): [Optional] A list of DataFrames containing the final covariates (from covariates.parquet) - - index_names (list[str]): A list of index names. - - community_level (int): The community level to search at. - - response_type (str): The response type to return. - - streaming (bool): Whether to stream the results or not. - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - logger.warning( - "Multi-index search is deprecated and will be removed in GraphRAG v3." - ) - # Streaming not supported yet - if streaming: - message = "Streaming not yet implemented for multi_index_local_search" - raise NotImplementedError(message) - - links = { - "community_reports": {}, - "communities": {}, - "entities": {}, - "text_units": {}, - "relationships": {}, - "covariates": {}, - } - max_vals = { - "community_reports": -1, - "communities": -1, - "entities": -1, - "text_units": 0, - "relationships": -1, - "covariates": 0, - } - community_reports_dfs = [] - communities_dfs = [] - entities_dfs = [] - relationships_dfs = [] - text_units_dfs = [] - covariates_dfs = [] - - for idx, index_name in enumerate(index_names): - # Prepare each index's communities dataframe for merging - communities_df = communities_list[idx] - communities_df["community"] = communities_df["community"].astype(int) - for i in communities_df["community"]: - links["communities"][i + max_vals["communities"] + 1] = { - "index_name": index_name, - "id": str(i), - } - communities_df["community"] += max_vals["communities"] + 1 - communities_df["human_readable_id"] += max_vals["communities"] + 1 - # concat the index name to the entity_ids, since this is used for joining later - communities_df["entity_ids"] = communities_df["entity_ids"].apply( - lambda x, index_name=index_name: [i + f"-{index_name}" for i in x] - ) - max_vals["communities"] = int(communities_df["community"].max()) - communities_dfs.append(communities_df) - - # Prepare each index's community reports dataframe for merging - community_reports_df = community_reports_list[idx] - community_reports_df["community"] = community_reports_df["community"].astype( - int - ) - for i in community_reports_df["community"]: - links["community_reports"][i + max_vals["community_reports"] + 1] = { - "index_name": index_name, - "id": str(i), - } - community_reports_df["community"] += max_vals["community_reports"] + 1 - community_reports_df["human_readable_id"] += max_vals["community_reports"] + 1 - max_vals["community_reports"] = int(community_reports_df["community"].max()) - community_reports_dfs.append(community_reports_df) - - # Prepare each index's entities dataframe for merging - entities_df = entities_list[idx] - for i in entities_df["human_readable_id"]: - links["entities"][i + max_vals["entities"] + 1] = { - "index_name": index_name, - "id": i, - } - entities_df["human_readable_id"] += max_vals["entities"] + 1 - entities_df["title"] = entities_df["title"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - entities_df["id"] = entities_df["id"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - entities_df["text_unit_ids"] = entities_df["text_unit_ids"].apply( - lambda x, index_name=index_name: [i + f"-{index_name}" for i in x] - ) - max_vals["entities"] = int(entities_df["human_readable_id"].max()) - entities_dfs.append(entities_df) - - # Prepare each index's relationships dataframe for merging - relationships_df = relationships_list[idx] - for i in relationships_df["human_readable_id"].astype(int): - links["relationships"][i + max_vals["relationships"] + 1] = { - "index_name": index_name, - "id": i, - } - if max_vals["relationships"] != -1: - col = ( - relationships_df["human_readable_id"].astype(int) - + max_vals["relationships"] - + 1 - ) - relationships_df["human_readable_id"] = col.astype(str) - relationships_df["source"] = relationships_df["source"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - relationships_df["target"] = relationships_df["target"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - relationships_df["text_unit_ids"] = relationships_df["text_unit_ids"].apply( - lambda x, index_name=index_name: [i + f"-{index_name}" for i in x] - ) - max_vals["relationships"] = int(relationships_df["human_readable_id"].max()) - relationships_dfs.append(relationships_df) - - # Prepare each index's text units dataframe for merging - text_units_df = text_units_list[idx] - for i in range(text_units_df.shape[0]): - links["text_units"][i + max_vals["text_units"]] = { - "index_name": index_name, - "id": i, - } - text_units_df["id"] = text_units_df["id"].apply( - lambda x, index_name=index_name: f"{x}-{index_name}" - ) - text_units_df["human_readable_id"] = ( - text_units_df["human_readable_id"] + max_vals["text_units"] - ) - max_vals["text_units"] += text_units_df.shape[0] - text_units_dfs.append(text_units_df) - - # If presents, prepare each index's covariates dataframe for merging - if covariates_list is not None: - covariates_df = covariates_list[idx] - for i in covariates_df["human_readable_id"].astype(int): - links["covariates"][i + max_vals["covariates"]] = { - "index_name": index_name, - "id": i, - } - covariates_df["id"] = covariates_df["id"].apply( - lambda x, index_name=index_name: f"{x}-{index_name}" - ) - covariates_df["human_readable_id"] = ( - covariates_df["human_readable_id"] + max_vals["covariates"] - ) - covariates_df["text_unit_id"] = covariates_df["text_unit_id"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - covariates_df["subject_id"] = covariates_df["subject_id"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - max_vals["covariates"] += covariates_df.shape[0] - covariates_dfs.append(covariates_df) - - # Merge the dataframes - communities_combined = pd.concat( - communities_dfs, axis=0, ignore_index=True, sort=False - ) - community_reports_combined = pd.concat( - community_reports_dfs, axis=0, ignore_index=True, sort=False - ) - entities_combined = pd.concat(entities_dfs, axis=0, ignore_index=True, sort=False) - relationships_combined = pd.concat( - relationships_dfs, axis=0, ignore_index=True, sort=False - ) - text_units_combined = pd.concat( - text_units_dfs, axis=0, ignore_index=True, sort=False - ) - covariates_combined = None - if len(covariates_dfs) > 0: - covariates_combined = pd.concat( - covariates_dfs, axis=0, ignore_index=True, sort=False - ) - logger.debug("Executing multi-index local search query: %s", query) - result = await local_search( - config, - entities=entities_combined, - communities=communities_combined, - community_reports=community_reports_combined, - text_units=text_units_combined, - relationships=relationships_combined, - covariates=covariates_combined, - community_level=community_level, - response_type=response_type, - query=query, - callbacks=callbacks, - ) - - # Update the context data by linking index names and community ids - context = update_context_data(result[1], links) - - logger.debug("Query response: %s", truncate(result[0], 400)) # type: ignore - return (result[0], context) - - -@validate_call(config={"arbitrary_types_allowed": True}) -async def drift_search( - config: GraphRagConfig, - entities: pd.DataFrame, - communities: pd.DataFrame, - community_reports: pd.DataFrame, - text_units: pd.DataFrame, - relationships: pd.DataFrame, - community_level: int, - response_type: str, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> tuple[ - str | dict[str, Any] | list[dict[str, Any]], - str | list[pd.DataFrame] | dict[str, pd.DataFrame], -]: - """Perform a DRIFT search and return the context data and response. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) - - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) - - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) - - relationships (pd.DataFrame): A DataFrame containing the final relationships (from relationships.parquet) - - community_level (int): The community level to search at. - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - callbacks = callbacks or [] - full_response = "" - context_data = {} - - def on_context(context: Any) -> None: - nonlocal context_data - context_data = context - - local_callbacks = NoopQueryCallbacks() - local_callbacks.on_context = on_context - callbacks.append(local_callbacks) - - logger.debug("Executing drift search query: %s", query) - async for chunk in drift_search_streaming( - config=config, - entities=entities, - communities=communities, - community_reports=community_reports, - text_units=text_units, - relationships=relationships, - community_level=community_level, - response_type=response_type, - query=query, - callbacks=callbacks, - ): - full_response += chunk - logger.debug("Query response: %s", truncate(full_response, 400)) - return full_response, context_data - - -@validate_call(config={"arbitrary_types_allowed": True}) -def drift_search_streaming( - config: GraphRagConfig, - entities: pd.DataFrame, - communities: pd.DataFrame, - community_reports: pd.DataFrame, - text_units: pd.DataFrame, - relationships: pd.DataFrame, - community_level: int, - response_type: str, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> AsyncGenerator: - """Perform a DRIFT search and return the context data and response. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) - - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) - - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) - - relationships (pd.DataFrame): A DataFrame containing the final relationships (from relationships.parquet) - - community_level (int): The community level to search at. - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - vector_store_args = {} - for index, store in config.vector_store.items(): - vector_store_args[index] = store.model_dump() - msg = f"Vector Store Args: {redact(vector_store_args)}" - logger.debug(msg) - - description_embedding_store = get_embedding_store( - config_args=vector_store_args, - embedding_name=entity_description_embedding, - ) - - full_content_embedding_store = get_embedding_store( - config_args=vector_store_args, - embedding_name=community_full_content_embedding, - ) - - entities_ = read_indexer_entities(entities, communities, community_level) - reports = read_indexer_reports(community_reports, communities, community_level) - read_indexer_report_embeddings(reports, full_content_embedding_store) - prompt = load_search_prompt(config.root_dir, config.drift_search.prompt) - reduce_prompt = load_search_prompt( - config.root_dir, config.drift_search.reduce_prompt - ) - - logger.debug("Executing streaming drift search query: %s", query) - search_engine = get_drift_search_engine( - config=config, - reports=reports, - text_units=read_indexer_text_units(text_units), - entities=entities_, - relationships=read_indexer_relationships(relationships), - description_embedding_store=description_embedding_store, - local_system_prompt=prompt, - reduce_system_prompt=reduce_prompt, - response_type=response_type, - callbacks=callbacks, - ) - return search_engine.stream_search(query=query) - - -@validate_call(config={"arbitrary_types_allowed": True}) -async def multi_index_drift_search( - config: GraphRagConfig, - entities_list: list[pd.DataFrame], - communities_list: list[pd.DataFrame], - community_reports_list: list[pd.DataFrame], - text_units_list: list[pd.DataFrame], - relationships_list: list[pd.DataFrame], - index_names: list[str], - community_level: int, - response_type: str, - streaming: bool, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> tuple[ - str | dict[str, Any] | list[dict[str, Any]], - str | list[pd.DataFrame] | dict[str, pd.DataFrame], -]: - """Perform a DRIFT search across multiple indexes and return the context data and response. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - entities_list (list[pd.DataFrame]): A list of DataFrames containing the final entities (from entities.parquet) - - community_reports_list (list[pd.DataFrame]): A list of DataFrames containing the final community reports (from community_reports.parquet) - - text_units_list (list[pd.DataFrame]): A list of DataFrames containing the final text units (from text_units.parquet) - - relationships_list (list[pd.DataFrame]): A list of DataFrames containing the final relationships (from relationships.parquet) - - index_names (list[str]): A list of index names. - - community_level (int): The community level to search at. - - response_type (str): The response type to return. - - streaming (bool): Whether to stream the results or not. - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - logger.warning( - "Multi-index search is deprecated and will be removed in GraphRAG v3." - ) - - # Streaming not supported yet - if streaming: - message = "Streaming not yet implemented for multi_drift_search" - raise NotImplementedError(message) - - links = { - "community_reports": {}, - "communities": {}, - "entities": {}, - "text_units": {}, - "relationships": {}, - } - max_vals = { - "community_reports": -1, - "communities": -1, - "entities": -1, - "text_units": 0, - "relationships": -1, - } - - communities_dfs = [] - community_reports_dfs = [] - entities_dfs = [] - relationships_dfs = [] - text_units_dfs = [] - - for idx, index_name in enumerate(index_names): - # Prepare each index's communities dataframe for merging - communities_df = communities_list[idx] - communities_df["community"] = communities_df["community"].astype(int) - for i in communities_df["community"]: - links["communities"][i + max_vals["communities"] + 1] = { - "index_name": index_name, - "id": str(i), - } - communities_df["community"] += max_vals["communities"] + 1 - communities_df["human_readable_id"] += max_vals["communities"] + 1 - # concat the index name to the entity_ids, since this is used for joining later - communities_df["entity_ids"] = communities_df["entity_ids"].apply( - lambda x, index_name=index_name: [i + f"-{index_name}" for i in x] - ) - max_vals["communities"] = int(communities_df["community"].max()) - communities_dfs.append(communities_df) - - # Prepare each index's community reports dataframe for merging - community_reports_df = community_reports_list[idx] - community_reports_df["community"] = community_reports_df["community"].astype( - int - ) - for i in community_reports_df["community"]: - links["community_reports"][i + max_vals["community_reports"] + 1] = { - "index_name": index_name, - "id": str(i), - } - community_reports_df["community"] += max_vals["community_reports"] + 1 - community_reports_df["human_readable_id"] += max_vals["community_reports"] + 1 - community_reports_df["id"] = community_reports_df["id"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - max_vals["community_reports"] = int(community_reports_df["community"].max()) - community_reports_dfs.append(community_reports_df) - - # Prepare each index's entities dataframe for merging - entities_df = entities_list[idx] - for i in entities_df["human_readable_id"]: - links["entities"][i + max_vals["entities"] + 1] = { - "index_name": index_name, - "id": i, - } - entities_df["human_readable_id"] += max_vals["entities"] + 1 - entities_df["title"] = entities_df["title"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - entities_df["id"] = entities_df["id"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - entities_df["text_unit_ids"] = entities_df["text_unit_ids"].apply( - lambda x, index_name=index_name: [i + f"-{index_name}" for i in x] - ) - max_vals["entities"] = int(entities_df["human_readable_id"].max()) - entities_dfs.append(entities_df) - - # Prepare each index's relationships dataframe for merging - relationships_df = relationships_list[idx] - for i in relationships_df["human_readable_id"].astype(int): - links["relationships"][i + max_vals["relationships"] + 1] = { - "index_name": index_name, - "id": i, - } - if max_vals["relationships"] != -1: - col = ( - relationships_df["human_readable_id"].astype(int) - + max_vals["relationships"] - + 1 - ) - relationships_df["human_readable_id"] = col.astype(str) - relationships_df["source"] = relationships_df["source"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - relationships_df["target"] = relationships_df["target"].apply( - lambda x, index_name=index_name: x + f"-{index_name}" - ) - relationships_df["text_unit_ids"] = relationships_df["text_unit_ids"].apply( - lambda x, index_name=index_name: [i + f"-{index_name}" for i in x] - ) - max_vals["relationships"] = int( - relationships_df["human_readable_id"].astype(int).max() - ) - - relationships_dfs.append(relationships_df) - - # Prepare each index's text units dataframe for merging - text_units_df = text_units_list[idx] - for i in range(text_units_df.shape[0]): - links["text_units"][i + max_vals["text_units"]] = { - "index_name": index_name, - "id": i, - } - text_units_df["id"] = text_units_df["id"].apply( - lambda x, index_name=index_name: f"{x}-{index_name}" - ) - text_units_df["human_readable_id"] = ( - text_units_df["human_readable_id"] + max_vals["text_units"] - ) - max_vals["text_units"] += text_units_df.shape[0] - text_units_dfs.append(text_units_df) - - # Merge the dataframes - communities_combined = pd.concat( - communities_dfs, axis=0, ignore_index=True, sort=False - ) - community_reports_combined = pd.concat( - community_reports_dfs, axis=0, ignore_index=True, sort=False - ) - entities_combined = pd.concat(entities_dfs, axis=0, ignore_index=True, sort=False) - relationships_combined = pd.concat( - relationships_dfs, axis=0, ignore_index=True, sort=False - ) - text_units_combined = pd.concat( - text_units_dfs, axis=0, ignore_index=True, sort=False - ) - - logger.debug("Executing multi-index drift search query: %s", query) - result = await drift_search( - config, - entities=entities_combined, - communities=communities_combined, - community_reports=community_reports_combined, - text_units=text_units_combined, - relationships=relationships_combined, - community_level=community_level, - response_type=response_type, - query=query, - callbacks=callbacks, - ) - - # Update the context data by linking index names and community ids - context = {} - if type(result[1]) is dict: - for key in result[1]: - context[key] = update_context_data(result[1][key], links) - else: - context = result[1] - - logger.debug("Query response: %s", truncate(result[0], 400)) # type: ignore - return (result[0], context) - - -@validate_call(config={"arbitrary_types_allowed": True}) -async def basic_search( - config: GraphRagConfig, - text_units: pd.DataFrame, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> tuple[ - str | dict[str, Any] | list[dict[str, Any]], - str | list[pd.DataFrame] | dict[str, pd.DataFrame], -]: - """Perform a basic search and return the context data and response. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - callbacks = callbacks or [] - full_response = "" - context_data = {} - - def on_context(context: Any) -> None: - nonlocal context_data - context_data = context - - local_callbacks = NoopQueryCallbacks() - local_callbacks.on_context = on_context - callbacks.append(local_callbacks) - - logger.debug("Executing basic search query: %s", query) - async for chunk in basic_search_streaming( - config=config, - text_units=text_units, - query=query, - callbacks=callbacks, - ): - full_response += chunk - logger.debug("Query response: %s", truncate(full_response, 400)) - return full_response, context_data - - -@validate_call(config={"arbitrary_types_allowed": True}) -def basic_search_streaming( - config: GraphRagConfig, - text_units: pd.DataFrame, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> AsyncGenerator: - """Perform a local search and return the context data and response via a generator. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - vector_store_args = {} - for index, store in config.vector_store.items(): - vector_store_args[index] = store.model_dump() - msg = f"Vector Store Args: {redact(vector_store_args)}" - logger.debug(msg) - - embedding_store = get_embedding_store( - config_args=vector_store_args, - embedding_name=text_unit_text_embedding, - ) - - prompt = load_search_prompt(config.root_dir, config.basic_search.prompt) - - logger.debug("Executing streaming basic search query: %s", query) - search_engine = get_basic_search_engine( - config=config, - text_units=read_indexer_text_units(text_units), - text_unit_embeddings=embedding_store, - system_prompt=prompt, - callbacks=callbacks, - ) - return search_engine.stream_search(query=query) - - -@validate_call(config={"arbitrary_types_allowed": True}) -async def multi_index_basic_search( - config: GraphRagConfig, - text_units_list: list[pd.DataFrame], - index_names: list[str], - streaming: bool, - query: str, - callbacks: list[QueryCallbacks] | None = None, - verbose: bool = False, -) -> tuple[ - str | dict[str, Any] | list[dict[str, Any]], - str | list[pd.DataFrame] | dict[str, pd.DataFrame], -]: - """Perform a basic search across multiple indexes and return the context data and response. - - Parameters - ---------- - - config (GraphRagConfig): A graphrag configuration (from settings.yaml) - - text_units_list (list[pd.DataFrame]): A list of DataFrames containing the final text units (from text_units.parquet) - - index_names (list[str]): A list of index names. - - streaming (bool): Whether to stream the results or not. - - query (str): The user query to search for. - - Returns - ------- - TODO: Document the search response type and format. - """ - init_loggers(config=config, verbose=verbose, filename="query.log") - - logger.warning( - "Multi-index search is deprecated and will be removed in GraphRAG v3." - ) - - # Streaming not supported yet - if streaming: - message = "Streaming not yet implemented for multi_basic_search" - raise NotImplementedError(message) - - links = { - "text_units": {}, - } - max_vals = { - "text_units": 0, - } - - text_units_dfs = [] - - for idx, index_name in enumerate(index_names): - # Prepare each index's text units dataframe for merging - text_units_df = text_units_list[idx] - for i in range(text_units_df.shape[0]): - links["text_units"][i + max_vals["text_units"]] = { - "index_name": index_name, - "id": i, - } - text_units_df["id"] = text_units_df["id"].apply( - lambda x, index_name=index_name: f"{x}-{index_name}" - ) - text_units_df["human_readable_id"] = ( - text_units_df["human_readable_id"] + max_vals["text_units"] - ) - max_vals["text_units"] += text_units_df.shape[0] - text_units_dfs.append(text_units_df) - - # Merge the dataframes - text_units_combined = pd.concat( - text_units_dfs, axis=0, ignore_index=True, sort=False - ) - - logger.debug("Executing multi-index basic search query: %s", query) - return await basic_search( - config, - text_units=text_units_combined, - query=query, - callbacks=callbacks, - ) diff --git a/graphrag/cache/__init__.py b/graphrag/cache/__init__.py deleted file mode 100644 index 9c4e8be3fe..0000000000 --- a/graphrag/cache/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A package containing cache implementations.""" diff --git a/graphrag/cache/factory.py b/graphrag/cache/factory.py deleted file mode 100644 index 331540260d..0000000000 --- a/graphrag/cache/factory.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Factory functions for creating a cache.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, ClassVar - -from graphrag.cache.json_pipeline_cache import JsonPipelineCache -from graphrag.cache.memory_pipeline_cache import InMemoryCache -from graphrag.cache.noop_pipeline_cache import NoopPipelineCache -from graphrag.config.enums import CacheType -from graphrag.storage.blob_pipeline_storage import BlobPipelineStorage -from graphrag.storage.cosmosdb_pipeline_storage import CosmosDBPipelineStorage -from graphrag.storage.file_pipeline_storage import FilePipelineStorage - -if TYPE_CHECKING: - from collections.abc import Callable - - from graphrag.cache.pipeline_cache import PipelineCache - - -class CacheFactory: - """A factory class for cache implementations. - - Includes a method for users to register a custom cache implementation. - - Configuration arguments are passed to each cache implementation as kwargs - for individual enforcement of required/optional arguments. - """ - - _registry: ClassVar[dict[str, Callable[..., PipelineCache]]] = {} - - @classmethod - def register(cls, cache_type: str, creator: Callable[..., PipelineCache]) -> None: - """Register a custom cache implementation. - - Args: - cache_type: The type identifier for the cache. - creator: A class or callable that creates an instance of PipelineCache. - """ - cls._registry[cache_type] = creator - - @classmethod - def create_cache(cls, cache_type: str, kwargs: dict) -> PipelineCache: - """Create a cache object from the provided type. - - Args: - cache_type: The type of cache to create. - root_dir: The root directory for file-based caches. - kwargs: Additional keyword arguments for the cache constructor. - - Returns - ------- - A PipelineCache instance. - - Raises - ------ - ValueError: If the cache type is not registered. - """ - if cache_type not in cls._registry: - msg = f"Unknown cache type: {cache_type}" - raise ValueError(msg) - - return cls._registry[cache_type](**kwargs) - - @classmethod - def get_cache_types(cls) -> list[str]: - """Get the registered cache implementations.""" - return list(cls._registry.keys()) - - @classmethod - def is_supported_type(cls, cache_type: str) -> bool: - """Check if the given cache type is supported.""" - return cache_type in cls._registry - - -# --- register built-in cache implementations --- -def create_file_cache(root_dir: str, base_dir: str, **kwargs) -> PipelineCache: - """Create a file-based cache implementation.""" - # Create storage with base_dir in kwargs since FilePipelineStorage expects it there - storage_kwargs = {"base_dir": root_dir, **kwargs} - storage = FilePipelineStorage(**storage_kwargs).child(base_dir) - return JsonPipelineCache(storage) - - -def create_blob_cache(**kwargs) -> PipelineCache: - """Create a blob storage-based cache implementation.""" - storage = BlobPipelineStorage(**kwargs) - return JsonPipelineCache(storage) - - -def create_cosmosdb_cache(**kwargs) -> PipelineCache: - """Create a CosmosDB-based cache implementation.""" - storage = CosmosDBPipelineStorage(**kwargs) - return JsonPipelineCache(storage) - - -def create_noop_cache(**_kwargs) -> PipelineCache: - """Create a no-op cache implementation.""" - return NoopPipelineCache() - - -def create_memory_cache(**kwargs) -> PipelineCache: - """Create a memory cache implementation.""" - return InMemoryCache(**kwargs) - - -# --- register built-in cache implementations --- -CacheFactory.register(CacheType.none.value, create_noop_cache) -CacheFactory.register(CacheType.memory.value, create_memory_cache) -CacheFactory.register(CacheType.file.value, create_file_cache) -CacheFactory.register(CacheType.blob.value, create_blob_cache) -CacheFactory.register(CacheType.cosmosdb.value, create_cosmosdb_cache) diff --git a/graphrag/cli/query.py b/graphrag/cli/query.py deleted file mode 100644 index 0075734859..0000000000 --- a/graphrag/cli/query.py +++ /dev/null @@ -1,534 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""CLI implementation of the query subcommand.""" - -import asyncio -import sys -from pathlib import Path -from typing import TYPE_CHECKING, Any - -import graphrag.api as api -from graphrag.callbacks.noop_query_callbacks import NoopQueryCallbacks -from graphrag.config.load_config import load_config -from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.utils.api import create_storage_from_config -from graphrag.utils.storage import load_table_from_storage, storage_has_table - -if TYPE_CHECKING: - import pandas as pd - -# ruff: noqa: T201 - - -def run_global_search( - config_filepath: Path | None, - data_dir: Path | None, - root_dir: Path, - community_level: int | None, - dynamic_community_selection: bool, - response_type: str, - streaming: bool, - query: str, - verbose: bool, -): - """Perform a global search with a given query. - - Loads index files required for global search and calls the Query API. - """ - root = root_dir.resolve() - cli_overrides = {} - if data_dir: - cli_overrides["output.base_dir"] = str(data_dir) - config = load_config(root, config_filepath, cli_overrides) - - dataframe_dict = _resolve_output_files( - config=config, - output_list=[ - "entities", - "communities", - "community_reports", - ], - optional_list=[], - ) - - # Call the Multi-Index Global Search API - if dataframe_dict["multi-index"]: - final_entities_list = dataframe_dict["entities"] - final_communities_list = dataframe_dict["communities"] - final_community_reports_list = dataframe_dict["community_reports"] - index_names = dataframe_dict["index_names"] - - response, context_data = asyncio.run( - api.multi_index_global_search( - config=config, - entities_list=final_entities_list, - communities_list=final_communities_list, - community_reports_list=final_community_reports_list, - index_names=index_names, - community_level=community_level, - dynamic_community_selection=dynamic_community_selection, - response_type=response_type, - streaming=streaming, - query=query, - verbose=verbose, - ) - ) - print(response) - return response, context_data - - # Otherwise, call the Single-Index Global Search API - final_entities: pd.DataFrame = dataframe_dict["entities"] - final_communities: pd.DataFrame = dataframe_dict["communities"] - final_community_reports: pd.DataFrame = dataframe_dict["community_reports"] - - if streaming: - - async def run_streaming_search(): - full_response = "" - context_data = {} - - def on_context(context: Any) -> None: - nonlocal context_data - context_data = context - - callbacks = NoopQueryCallbacks() - callbacks.on_context = on_context - - async for stream_chunk in api.global_search_streaming( - config=config, - entities=final_entities, - communities=final_communities, - community_reports=final_community_reports, - community_level=community_level, - dynamic_community_selection=dynamic_community_selection, - response_type=response_type, - query=query, - callbacks=[callbacks], - verbose=verbose, - ): - full_response += stream_chunk - print(stream_chunk, end="") - sys.stdout.flush() - print() - return full_response, context_data - - return asyncio.run(run_streaming_search()) - # not streaming - response, context_data = asyncio.run( - api.global_search( - config=config, - entities=final_entities, - communities=final_communities, - community_reports=final_community_reports, - community_level=community_level, - dynamic_community_selection=dynamic_community_selection, - response_type=response_type, - query=query, - verbose=verbose, - ) - ) - print(response) - - return response, context_data - - -def run_local_search( - config_filepath: Path | None, - data_dir: Path | None, - root_dir: Path, - community_level: int, - response_type: str, - streaming: bool, - query: str, - verbose: bool, -): - """Perform a local search with a given query. - - Loads index files required for local search and calls the Query API. - """ - root = root_dir.resolve() - cli_overrides = {} - if data_dir: - cli_overrides["output.base_dir"] = str(data_dir) - config = load_config(root, config_filepath, cli_overrides) - - dataframe_dict = _resolve_output_files( - config=config, - output_list=[ - "communities", - "community_reports", - "text_units", - "relationships", - "entities", - ], - optional_list=[ - "covariates", - ], - ) - # Call the Multi-Index Local Search API - if dataframe_dict["multi-index"]: - final_entities_list = dataframe_dict["entities"] - final_communities_list = dataframe_dict["communities"] - final_community_reports_list = dataframe_dict["community_reports"] - final_text_units_list = dataframe_dict["text_units"] - final_relationships_list = dataframe_dict["relationships"] - index_names = dataframe_dict["index_names"] - - # If any covariates tables are missing from any index, set the covariates list to None - if len(dataframe_dict["covariates"]) != dataframe_dict["num_indexes"]: - final_covariates_list = None - else: - final_covariates_list = dataframe_dict["covariates"] - - response, context_data = asyncio.run( - api.multi_index_local_search( - config=config, - entities_list=final_entities_list, - communities_list=final_communities_list, - community_reports_list=final_community_reports_list, - text_units_list=final_text_units_list, - relationships_list=final_relationships_list, - covariates_list=final_covariates_list, - index_names=index_names, - community_level=community_level, - response_type=response_type, - streaming=streaming, - query=query, - verbose=verbose, - ) - ) - print(response) - - return response, context_data - - # Otherwise, call the Single-Index Local Search API - final_communities: pd.DataFrame = dataframe_dict["communities"] - final_community_reports: pd.DataFrame = dataframe_dict["community_reports"] - final_text_units: pd.DataFrame = dataframe_dict["text_units"] - final_relationships: pd.DataFrame = dataframe_dict["relationships"] - final_entities: pd.DataFrame = dataframe_dict["entities"] - final_covariates: pd.DataFrame | None = dataframe_dict["covariates"] - - if streaming: - - async def run_streaming_search(): - full_response = "" - context_data = {} - - def on_context(context: Any) -> None: - nonlocal context_data - context_data = context - - callbacks = NoopQueryCallbacks() - callbacks.on_context = on_context - - async for stream_chunk in api.local_search_streaming( - config=config, - entities=final_entities, - communities=final_communities, - community_reports=final_community_reports, - text_units=final_text_units, - relationships=final_relationships, - covariates=final_covariates, - community_level=community_level, - response_type=response_type, - query=query, - callbacks=[callbacks], - verbose=verbose, - ): - full_response += stream_chunk - print(stream_chunk, end="") - sys.stdout.flush() - print() - return full_response, context_data - - return asyncio.run(run_streaming_search()) - # not streaming - response, context_data = asyncio.run( - api.local_search( - config=config, - entities=final_entities, - communities=final_communities, - community_reports=final_community_reports, - text_units=final_text_units, - relationships=final_relationships, - covariates=final_covariates, - community_level=community_level, - response_type=response_type, - query=query, - verbose=verbose, - ) - ) - print(response) - - return response, context_data - - -def run_drift_search( - config_filepath: Path | None, - data_dir: Path | None, - root_dir: Path, - community_level: int, - response_type: str, - streaming: bool, - query: str, - verbose: bool, -): - """Perform a local search with a given query. - - Loads index files required for local search and calls the Query API. - """ - root = root_dir.resolve() - cli_overrides = {} - if data_dir: - cli_overrides["output.base_dir"] = str(data_dir) - config = load_config(root, config_filepath, cli_overrides) - - dataframe_dict = _resolve_output_files( - config=config, - output_list=[ - "communities", - "community_reports", - "text_units", - "relationships", - "entities", - ], - ) - - # Call the Multi-Index Drift Search API - if dataframe_dict["multi-index"]: - final_entities_list = dataframe_dict["entities"] - final_communities_list = dataframe_dict["communities"] - final_community_reports_list = dataframe_dict["community_reports"] - final_text_units_list = dataframe_dict["text_units"] - final_relationships_list = dataframe_dict["relationships"] - index_names = dataframe_dict["index_names"] - - response, context_data = asyncio.run( - api.multi_index_drift_search( - config=config, - entities_list=final_entities_list, - communities_list=final_communities_list, - community_reports_list=final_community_reports_list, - text_units_list=final_text_units_list, - relationships_list=final_relationships_list, - index_names=index_names, - community_level=community_level, - response_type=response_type, - streaming=streaming, - query=query, - verbose=verbose, - ) - ) - print(response) - - return response, context_data - - # Otherwise, call the Single-Index Drift Search API - final_communities: pd.DataFrame = dataframe_dict["communities"] - final_community_reports: pd.DataFrame = dataframe_dict["community_reports"] - final_text_units: pd.DataFrame = dataframe_dict["text_units"] - final_relationships: pd.DataFrame = dataframe_dict["relationships"] - final_entities: pd.DataFrame = dataframe_dict["entities"] - - if streaming: - - async def run_streaming_search(): - full_response = "" - context_data = {} - - def on_context(context: Any) -> None: - nonlocal context_data - context_data = context - - callbacks = NoopQueryCallbacks() - callbacks.on_context = on_context - - async for stream_chunk in api.drift_search_streaming( - config=config, - entities=final_entities, - communities=final_communities, - community_reports=final_community_reports, - text_units=final_text_units, - relationships=final_relationships, - community_level=community_level, - response_type=response_type, - query=query, - callbacks=[callbacks], - verbose=verbose, - ): - full_response += stream_chunk - print(stream_chunk, end="") - sys.stdout.flush() - print() - return full_response, context_data - - return asyncio.run(run_streaming_search()) - - # not streaming - response, context_data = asyncio.run( - api.drift_search( - config=config, - entities=final_entities, - communities=final_communities, - community_reports=final_community_reports, - text_units=final_text_units, - relationships=final_relationships, - community_level=community_level, - response_type=response_type, - query=query, - verbose=verbose, - ) - ) - print(response) - - return response, context_data - - -def run_basic_search( - config_filepath: Path | None, - data_dir: Path | None, - root_dir: Path, - streaming: bool, - query: str, - verbose: bool, -): - """Perform a basics search with a given query. - - Loads index files required for basic search and calls the Query API. - """ - root = root_dir.resolve() - cli_overrides = {} - if data_dir: - cli_overrides["output.base_dir"] = str(data_dir) - config = load_config(root, config_filepath, cli_overrides) - - dataframe_dict = _resolve_output_files( - config=config, - output_list=[ - "text_units", - ], - ) - - # Call the Multi-Index Basic Search API - if dataframe_dict["multi-index"]: - final_text_units_list = dataframe_dict["text_units"] - index_names = dataframe_dict["index_names"] - - response, context_data = asyncio.run( - api.multi_index_basic_search( - config=config, - text_units_list=final_text_units_list, - index_names=index_names, - streaming=streaming, - query=query, - verbose=verbose, - ) - ) - print(response) - - return response, context_data - - # Otherwise, call the Single-Index Basic Search API - final_text_units: pd.DataFrame = dataframe_dict["text_units"] - - if streaming: - - async def run_streaming_search(): - full_response = "" - context_data = {} - - def on_context(context: Any) -> None: - nonlocal context_data - context_data = context - - callbacks = NoopQueryCallbacks() - callbacks.on_context = on_context - - async for stream_chunk in api.basic_search_streaming( - config=config, - text_units=final_text_units, - query=query, - callbacks=[callbacks], - verbose=verbose, - ): - full_response += stream_chunk - print(stream_chunk, end="") - sys.stdout.flush() - print() - return full_response, context_data - - return asyncio.run(run_streaming_search()) - # not streaming - response, context_data = asyncio.run( - api.basic_search( - config=config, - text_units=final_text_units, - query=query, - verbose=verbose, - ) - ) - print(response) - - return response, context_data - - -def _resolve_output_files( - config: GraphRagConfig, - output_list: list[str], - optional_list: list[str] | None = None, -) -> dict[str, Any]: - """Read indexing output files to a dataframe dict.""" - dataframe_dict = {} - - # Loading output files for multi-index search - if config.outputs: - dataframe_dict["multi-index"] = True - dataframe_dict["num_indexes"] = len(config.outputs) - dataframe_dict["index_names"] = config.outputs.keys() - for output in config.outputs.values(): - storage_obj = create_storage_from_config(output) - for name in output_list: - if name not in dataframe_dict: - dataframe_dict[name] = [] - df_value = asyncio.run( - load_table_from_storage(name=name, storage=storage_obj) - ) - dataframe_dict[name].append(df_value) - - # for optional output files, do not append if the dataframe does not exist - if optional_list: - for optional_file in optional_list: - if optional_file not in dataframe_dict: - dataframe_dict[optional_file] = [] - file_exists = asyncio.run( - storage_has_table(optional_file, storage_obj) - ) - if file_exists: - df_value = asyncio.run( - load_table_from_storage( - name=optional_file, storage=storage_obj - ) - ) - dataframe_dict[optional_file].append(df_value) - return dataframe_dict - # Loading output files for single-index search - dataframe_dict["multi-index"] = False - storage_obj = create_storage_from_config(config.output) - for name in output_list: - df_value = asyncio.run(load_table_from_storage(name=name, storage=storage_obj)) - dataframe_dict[name] = df_value - - # for optional output files, set the dict entry to None instead of erroring out if it does not exist - if optional_list: - for optional_file in optional_list: - file_exists = asyncio.run(storage_has_table(optional_file, storage_obj)) - if file_exists: - df_value = asyncio.run( - load_table_from_storage(name=optional_file, storage=storage_obj) - ) - dataframe_dict[optional_file] = df_value - else: - dataframe_dict[optional_file] = None - return dataframe_dict diff --git a/graphrag/config/create_graphrag_config.py b/graphrag/config/create_graphrag_config.py deleted file mode 100644 index 59d7699e71..0000000000 --- a/graphrag/config/create_graphrag_config.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Parameterization settings for the default configuration, loaded from environment variables.""" - -from pathlib import Path -from typing import Any - -from graphrag.config.models.graph_rag_config import GraphRagConfig - - -def create_graphrag_config( - values: dict[str, Any] | None = None, - root_dir: str | None = None, -) -> GraphRagConfig: - """Load Configuration Parameters from a dictionary. - - Parameters - ---------- - values : dict[str, Any] | None - Dictionary of configuration values to pass into pydantic model. - root_dir : str | None - Root directory for the project. - skip_validation : bool - Skip pydantic model validation of the configuration. - This is useful for testing and mocking purposes but - should not be used in the core code or API. - - Returns - ------- - GraphRagConfig - The configuration object. - - Raises - ------ - ValidationError - If the configuration values do not satisfy pydantic validation. - """ - values = values or {} - if root_dir: - root_path = Path(root_dir).resolve() - values["root_dir"] = str(root_path) - return GraphRagConfig(**values) diff --git a/graphrag/config/embeddings.py b/graphrag/config/embeddings.py deleted file mode 100644 index f150238576..0000000000 --- a/graphrag/config/embeddings.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing embeddings values.""" - -entity_title_embedding = "entity.title" -entity_description_embedding = "entity.description" -relationship_description_embedding = "relationship.description" -document_text_embedding = "document.text" -community_title_embedding = "community.title" -community_summary_embedding = "community.summary" -community_full_content_embedding = "community.full_content" -text_unit_text_embedding = "text_unit.text" - -all_embeddings: set[str] = { - entity_title_embedding, - entity_description_embedding, - relationship_description_embedding, - document_text_embedding, - community_title_embedding, - community_summary_embedding, - community_full_content_embedding, - text_unit_text_embedding, -} -default_embeddings: list[str] = [ - entity_description_embedding, - community_full_content_embedding, - text_unit_text_embedding, -] - - -def create_index_name( - container_name: str, embedding_name: str, validate: bool = True -) -> str: - """ - Create a index name for the embedding store. - - Within any given vector store, we can have multiple sets of embeddings organized into projects. - The `container` param is used for this partitioning, and is added as a prefix to the index name for differentiation. - - The embedding name is fixed, with the available list defined in graphrag.index.config.embeddings - - Note that we use dot notation in our names, but many vector stores do not support this - so we convert to dashes. - """ - if validate and embedding_name not in all_embeddings: - msg = f"Invalid embedding name: {embedding_name}" - raise KeyError(msg) - return f"{container_name}-{embedding_name}".replace(".", "-") diff --git a/graphrag/config/enums.py b/graphrag/config/enums.py deleted file mode 100644 index ef99ee6a1a..0000000000 --- a/graphrag/config/enums.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing config enums.""" - -from __future__ import annotations - -from enum import Enum - - -class CacheType(str, Enum): - """The cache configuration type for the pipeline.""" - - file = "file" - """The file cache configuration type.""" - memory = "memory" - """The memory cache configuration type.""" - none = "none" - """The none cache configuration type.""" - blob = "blob" - """The blob cache configuration type.""" - cosmosdb = "cosmosdb" - """The cosmosdb cache configuration type""" - - def __repr__(self): - """Get a string representation.""" - return f'"{self.value}"' - - -class InputFileType(str, Enum): - """The input file type for the pipeline.""" - - csv = "csv" - """The CSV input type.""" - text = "text" - """The text input type.""" - json = "json" - """The JSON input type.""" - - def __repr__(self): - """Get a string representation.""" - return f'"{self.value}"' - - -class StorageType(str, Enum): - """The output type for the pipeline.""" - - file = "file" - """The file output type.""" - memory = "memory" - """The memory output type.""" - blob = "blob" - """The blob output type.""" - cosmosdb = "cosmosdb" - """The cosmosdb output type""" - - def __repr__(self): - """Get a string representation.""" - return f'"{self.value}"' - - -class VectorStoreType(str, Enum): - """The supported vector store types.""" - - LanceDB = "lancedb" - AzureAISearch = "azure_ai_search" - CosmosDB = "cosmosdb" - - -class ReportingType(str, Enum): - """The reporting configuration type for the pipeline.""" - - file = "file" - """The file reporting configuration type.""" - blob = "blob" - """The blob reporting configuration type.""" - - def __repr__(self): - """Get a string representation.""" - return f'"{self.value}"' - - -class ModelType(str, Enum): - """LLMType enum class definition.""" - - # Embeddings - OpenAIEmbedding = "openai_embedding" - AzureOpenAIEmbedding = "azure_openai_embedding" - Embedding = "embedding" - - # Chat Completion - OpenAIChat = "openai_chat" - AzureOpenAIChat = "azure_openai_chat" - Chat = "chat" - - # Debug - MockChat = "mock_chat" - MockEmbedding = "mock_embedding" - - def __repr__(self): - """Get a string representation.""" - return f'"{self.value}"' - - -class AuthType(str, Enum): - """AuthType enum class definition.""" - - APIKey = "api_key" - AzureManagedIdentity = "azure_managed_identity" - - -class AsyncType(str, Enum): - """Enum for the type of async to use.""" - - AsyncIO = "asyncio" - Threaded = "threaded" - - -class ChunkStrategyType(str, Enum): - """ChunkStrategy class definition.""" - - tokens = "tokens" - sentence = "sentence" - - def __repr__(self): - """Get a string representation.""" - return f'"{self.value}"' - - -class SearchMethod(Enum): - """The type of search to run.""" - - LOCAL = "local" - GLOBAL = "global" - DRIFT = "drift" - BASIC = "basic" - - def __str__(self): - """Return the string representation of the enum value.""" - return self.value - - -class IndexingMethod(str, Enum): - """Enum for the type of indexing to perform.""" - - Standard = "standard" - """Traditional GraphRAG indexing, with all graph construction and summarization performed by a language model.""" - Fast = "fast" - """Fast indexing, using NLP for graph construction and language model for summarization.""" - StandardUpdate = "standard-update" - """Incremental update with standard indexing.""" - FastUpdate = "fast-update" - """Incremental update with fast indexing.""" - - -class NounPhraseExtractorType(str, Enum): - """Enum for the noun phrase extractor options.""" - - RegexEnglish = "regex_english" - """Standard extractor using regex. Fastest, but limited to English.""" - Syntactic = "syntactic_parser" - """Noun phrase extractor based on dependency parsing and NER using SpaCy.""" - CFG = "cfg" - """Noun phrase extractor combining CFG-based noun-chunk extraction and NER.""" - - -class ModularityMetric(str, Enum): - """Enum for the modularity metric to use.""" - - Graph = "graph" - """Graph modularity metric.""" - - LCC = "lcc" - - WeightedComponents = "weighted_components" - """Weighted components modularity metric.""" diff --git a/graphrag/config/environment_reader.py b/graphrag/config/environment_reader.py deleted file mode 100644 index 258422666c..0000000000 --- a/graphrag/config/environment_reader.py +++ /dev/null @@ -1,155 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A configuration reader utility class.""" - -from collections.abc import Callable -from contextlib import contextmanager -from enum import Enum -from typing import Any, TypeVar - -from environs import Env - -T = TypeVar("T") - -KeyValue = str | Enum -EnvKeySet = str | list[str] - - -def read_key(value: KeyValue) -> str: - """Read a key value.""" - if not isinstance(value, str): - return value.value.lower() - return value.lower() - - -class EnvironmentReader: - """A configuration reader utility class.""" - - _env: Env - _config_stack: list[dict] - - def __init__(self, env: Env): - self._env = env - self._config_stack = [] - - @property - def env(self): - """Get the environment object.""" - return self._env - - def _read_env( - self, env_key: str | list[str], default_value: T, read: Callable[[str, T], T] - ) -> T | None: - if isinstance(env_key, str): - env_key = [env_key] - - for k in env_key: - result = read(k.upper(), default_value) - if result is not default_value: - return result - - return default_value - - def envvar_prefix(self, prefix: KeyValue): - """Set the environment variable prefix.""" - prefix = read_key(prefix) - prefix = f"{prefix}_".upper() - return self._env.prefixed(prefix) - - def use(self, value: Any | None): - """Create a context manager to push the value into the config_stack.""" - - @contextmanager - def config_context(): - self._config_stack.append(value or {}) - try: - yield - finally: - self._config_stack.pop() - - return config_context() - - @property - def section(self) -> dict: - """Get the current section.""" - return self._config_stack[-1] if self._config_stack else {} - - def str( - self, - key: KeyValue, - env_key: EnvKeySet | None = None, - default_value: str | None = None, - ) -> str | None: - """Read a configuration value.""" - key = read_key(key) - if self.section and key in self.section: - return self.section[key] - - return self._read_env( - env_key or key, default_value, (lambda k, dv: self._env(k, dv)) - ) - - def int( - self, - key: KeyValue, - env_key: EnvKeySet | None = None, - default_value: int | None = None, - ) -> int | None: - """Read an integer configuration value.""" - key = read_key(key) - if self.section and key in self.section: - return int(self.section[key]) - return self._read_env( - env_key or key, default_value, lambda k, dv: self._env.int(k, dv) - ) - - def bool( - self, - key: KeyValue, - env_key: EnvKeySet | None = None, - default_value: bool | None = None, - ) -> bool | None: - """Read an integer configuration value.""" - key = read_key(key) - if self.section and key in self.section: - return bool(self.section[key]) - - return self._read_env( - env_key or key, default_value, lambda k, dv: self._env.bool(k, dv) - ) - - def float( - self, - key: KeyValue, - env_key: EnvKeySet | None = None, - default_value: float | None = None, - ) -> float | None: - """Read a float configuration value.""" - key = read_key(key) - if self.section and key in self.section: - return float(self.section[key]) - return self._read_env( - env_key or key, default_value, lambda k, dv: self._env.float(k, dv) - ) - - def list( - self, - key: KeyValue, - env_key: EnvKeySet | None = None, - default_value: list | None = None, - ) -> list | None: - """Parse an list configuration value.""" - key = read_key(key) - result = None - if self.section and key in self.section: - result = self.section[key] - if isinstance(result, list): - return result - - if result is None: - result = self.str(key, env_key) - if result is not None: - result = [s.strip() for s in result.split(",")] - return [s for s in result if s] - return default_value diff --git a/graphrag/config/get_embedding_settings.py b/graphrag/config/get_embedding_settings.py deleted file mode 100644 index 9522f31359..0000000000 --- a/graphrag/config/get_embedding_settings.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing get_embedding_settings.""" - -from graphrag.config.models.graph_rag_config import GraphRagConfig - - -def get_embedding_settings( - settings: GraphRagConfig, - vector_store_params: dict | None = None, -) -> dict: - """Transform GraphRAG config into settings for workflows.""" - embeddings_llm_settings = settings.get_language_model_config( - settings.embed_text.model_id - ) - vector_store_settings = settings.get_vector_store_config( - settings.embed_text.vector_store_id - ).model_dump() - - # - # If we get to this point, settings.vector_store is defined, and there's a specific setting for this embedding. - # settings.vector_store.base contains connection information, or may be undefined - # settings.vector_store. contains the specific settings for this embedding - # - strategy = settings.embed_text.resolved_strategy( - embeddings_llm_settings - ) # get the default strategy - strategy.update({ - "vector_store": { - **(vector_store_params or {}), - **(vector_store_settings), - } - }) # update the default strategy with the vector store settings - # This ensures the vector store config is part of the strategy and not the global config - return { - "strategy": strategy, - } diff --git a/graphrag/config/load_config.py b/graphrag/config/load_config.py deleted file mode 100644 index de9026037d..0000000000 --- a/graphrag/config/load_config.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Default method for loading config.""" - -import json -import os -from pathlib import Path -from string import Template -from typing import Any - -import yaml -from dotenv import load_dotenv - -from graphrag.config.create_graphrag_config import create_graphrag_config -from graphrag.config.models.graph_rag_config import GraphRagConfig - -_default_config_files = ["settings.yaml", "settings.yml", "settings.json"] - - -def _search_for_config_in_root_dir(root: str | Path) -> Path | None: - """Resolve the config path from the given root directory. - - Parameters - ---------- - root : str | Path - The path to the root directory containing the config file. - Searches for a default config file (settings.{yaml,yml,json}). - - Returns - ------- - Path | None - returns a Path if there is a config in the root directory - Otherwise returns None. - """ - root = Path(root) - - if not root.is_dir(): - msg = f"Invalid config path: {root} is not a directory" - raise FileNotFoundError(msg) - - for file in _default_config_files: - if (root / file).is_file(): - return root / file - - return None - - -def _parse_env_variables(text: str) -> str: - """Parse environment variables in the configuration text. - - Parameters - ---------- - text : str - The configuration text. - - Returns - ------- - str - The configuration text with environment variables parsed. - - Raises - ------ - KeyError - If an environment variable is not found. - """ - return Template(text).substitute(os.environ) - - -def _load_dotenv(config_path: Path | str) -> None: - """Load the .env file if it exists in the same directory as the config file. - - Parameters - ---------- - config_path : Path | str - The path to the config file. - """ - config_path = Path(config_path) - dotenv_path = config_path.parent / ".env" - if dotenv_path.exists(): - load_dotenv(dotenv_path) - - -def _get_config_path(root_dir: Path, config_filepath: Path | None) -> Path: - """Get the configuration file path. - - Parameters - ---------- - root_dir : str | Path - The root directory of the project. Will search for the config file in this directory. - config_filepath : str | None - The path to the config file. - If None, searches for config file in root. - - Returns - ------- - Path - The configuration file path. - """ - if config_filepath: - config_path = config_filepath.resolve() - if not config_path.exists(): - msg = f"Specified Config file not found: {config_path}" - raise FileNotFoundError(msg) - else: - config_path = _search_for_config_in_root_dir(root_dir) - - if not config_path: - msg = f"Config file not found in root directory: {root_dir}" - raise FileNotFoundError(msg) - - return config_path - - -def _apply_overrides(data: dict[str, Any], overrides: dict[str, Any]) -> None: - """Apply the overrides to the raw configuration.""" - for key, value in overrides.items(): - keys = key.split(".") - target = data - current_path = keys[0] - for k in keys[:-1]: - current_path += f".{k}" - target_obj = target.get(k, {}) - if not isinstance(target_obj, dict): - msg = f"Cannot override non-dict value: data[{current_path}] is not a dict." - raise TypeError(msg) - target[k] = target_obj - target = target[k] - target[keys[-1]] = value - - -def _parse(file_extension: str, contents: str) -> dict[str, Any]: - """Parse configuration.""" - match file_extension: - case ".yaml" | ".yml": - return yaml.safe_load(contents) - case ".json": - return json.loads(contents) - case _: - msg = ( - f"Unable to parse config. Unsupported file extension: {file_extension}" - ) - raise ValueError(msg) - - -def load_config( - root_dir: Path, - config_filepath: Path | None = None, - cli_overrides: dict[str, Any] | None = None, -) -> GraphRagConfig: - """Load configuration from a file. - - Parameters - ---------- - root_dir : str | Path - The root directory of the project. Will search for the config file in this directory. - config_filepath : str | None - The path to the config file. - If None, searches for config file in root. - cli_overrides : dict[str, Any] | None - A flat dictionary of cli overrides. - Example: {'output.base_dir': 'override_value'} - - Returns - ------- - GraphRagConfig - The loaded configuration. - - Raises - ------ - FileNotFoundError - If the config file is not found. - ValueError - If the config file extension is not supported. - TypeError - If applying cli overrides to the config fails. - KeyError - If config file references a non-existent environment variable. - ValidationError - If there are pydantic validation errors when instantiating the config. - """ - root = root_dir.resolve() - config_path = _get_config_path(root, config_filepath) - _load_dotenv(config_path) - config_extension = config_path.suffix - config_text = config_path.read_text(encoding="utf-8") - config_text = _parse_env_variables(config_text) - config_data = _parse(config_extension, config_text) - if cli_overrides: - _apply_overrides(config_data, cli_overrides) - return create_graphrag_config(config_data, root_dir=str(root)) diff --git a/graphrag/config/models/cache_config.py b/graphrag/config/models/cache_config.py deleted file mode 100644 index c301e9d3f6..0000000000 --- a/graphrag/config/models/cache_config.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Parameterization settings for the default configuration.""" - -from pydantic import BaseModel, Field - -from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.enums import CacheType - - -class CacheConfig(BaseModel): - """The default configuration section for Cache.""" - - type: CacheType | str = Field( - description="The cache type to use.", - default=graphrag_config_defaults.cache.type, - ) - base_dir: str = Field( - description="The base directory for the cache.", - default=graphrag_config_defaults.cache.base_dir, - ) - connection_string: str | None = Field( - description="The cache connection string to use.", - default=graphrag_config_defaults.cache.connection_string, - ) - container_name: str | None = Field( - description="The cache container name to use.", - default=graphrag_config_defaults.cache.container_name, - ) - storage_account_blob_url: str | None = Field( - description="The storage account blob url to use.", - default=graphrag_config_defaults.cache.storage_account_blob_url, - ) - cosmosdb_account_url: str | None = Field( - description="The cosmosdb account url to use.", - default=graphrag_config_defaults.cache.cosmosdb_account_url, - ) diff --git a/graphrag/config/models/chunking_config.py b/graphrag/config/models/chunking_config.py deleted file mode 100644 index 902bffcbb9..0000000000 --- a/graphrag/config/models/chunking_config.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Parameterization settings for the default configuration.""" - -from pydantic import BaseModel, Field - -from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.enums import ChunkStrategyType - - -class ChunkingConfig(BaseModel): - """Configuration section for chunking.""" - - size: int = Field( - description="The chunk size to use.", - default=graphrag_config_defaults.chunks.size, - ) - overlap: int = Field( - description="The chunk overlap to use.", - default=graphrag_config_defaults.chunks.overlap, - ) - group_by_columns: list[str] = Field( - description="The chunk by columns to use.", - default=graphrag_config_defaults.chunks.group_by_columns, - ) - strategy: ChunkStrategyType = Field( - description="The chunking strategy to use.", - default=graphrag_config_defaults.chunks.strategy, - ) - encoding_model: str = Field( - description="The encoding model to use.", - default=graphrag_config_defaults.chunks.encoding_model, - ) - prepend_metadata: bool = Field( - description="Prepend metadata into each chunk.", - default=graphrag_config_defaults.chunks.prepend_metadata, - ) - chunk_size_includes_metadata: bool = Field( - description="Count metadata in max tokens.", - default=graphrag_config_defaults.chunks.chunk_size_includes_metadata, - ) diff --git a/graphrag/config/models/embed_graph_config.py b/graphrag/config/models/embed_graph_config.py deleted file mode 100644 index 72827570e2..0000000000 --- a/graphrag/config/models/embed_graph_config.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Parameterization settings for the default configuration.""" - -from pydantic import BaseModel, Field - -from graphrag.config.defaults import graphrag_config_defaults - - -class EmbedGraphConfig(BaseModel): - """The default configuration section for Node2Vec.""" - - enabled: bool = Field( - description="A flag indicating whether to enable node2vec.", - default=graphrag_config_defaults.embed_graph.enabled, - ) - dimensions: int = Field( - description="The node2vec vector dimensions.", - default=graphrag_config_defaults.embed_graph.dimensions, - ) - num_walks: int = Field( - description="The node2vec number of walks.", - default=graphrag_config_defaults.embed_graph.num_walks, - ) - walk_length: int = Field( - description="The node2vec walk length.", - default=graphrag_config_defaults.embed_graph.walk_length, - ) - window_size: int = Field( - description="The node2vec window size.", - default=graphrag_config_defaults.embed_graph.window_size, - ) - iterations: int = Field( - description="The node2vec iterations.", - default=graphrag_config_defaults.embed_graph.iterations, - ) - random_seed: int = Field( - description="The node2vec random seed.", - default=graphrag_config_defaults.embed_graph.random_seed, - ) - use_lcc: bool = Field( - description="Whether to use the largest connected component.", - default=graphrag_config_defaults.embed_graph.use_lcc, - ) diff --git a/graphrag/config/models/graph_rag_config.py b/graphrag/config/models/graph_rag_config.py deleted file mode 100644 index 9959d56cd7..0000000000 --- a/graphrag/config/models/graph_rag_config.py +++ /dev/null @@ -1,416 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Parameterization settings for the default configuration.""" - -from dataclasses import asdict -from pathlib import Path - -from devtools import pformat -from pydantic import BaseModel, Field, model_validator - -import graphrag.config.defaults as defs -from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.enums import VectorStoreType -from graphrag.config.errors import LanguageModelConfigMissingError -from graphrag.config.models.basic_search_config import BasicSearchConfig -from graphrag.config.models.cache_config import CacheConfig -from graphrag.config.models.chunking_config import ChunkingConfig -from graphrag.config.models.cluster_graph_config import ClusterGraphConfig -from graphrag.config.models.community_reports_config import CommunityReportsConfig -from graphrag.config.models.drift_search_config import DRIFTSearchConfig -from graphrag.config.models.embed_graph_config import EmbedGraphConfig -from graphrag.config.models.extract_claims_config import ClaimExtractionConfig -from graphrag.config.models.extract_graph_config import ExtractGraphConfig -from graphrag.config.models.extract_graph_nlp_config import ExtractGraphNLPConfig -from graphrag.config.models.global_search_config import GlobalSearchConfig -from graphrag.config.models.input_config import InputConfig -from graphrag.config.models.language_model_config import LanguageModelConfig -from graphrag.config.models.local_search_config import LocalSearchConfig -from graphrag.config.models.prune_graph_config import PruneGraphConfig -from graphrag.config.models.reporting_config import ReportingConfig -from graphrag.config.models.snapshots_config import SnapshotsConfig -from graphrag.config.models.storage_config import StorageConfig -from graphrag.config.models.summarize_descriptions_config import ( - SummarizeDescriptionsConfig, -) -from graphrag.config.models.text_embedding_config import TextEmbeddingConfig -from graphrag.config.models.umap_config import UmapConfig -from graphrag.config.models.vector_store_config import VectorStoreConfig -from graphrag.language_model.providers.litellm.services.rate_limiter.rate_limiter_factory import ( - RateLimiterFactory, -) -from graphrag.language_model.providers.litellm.services.retry.retry_factory import ( - RetryFactory, -) - - -class GraphRagConfig(BaseModel): - """Base class for the Default-Configuration parameterization settings.""" - - def __repr__(self) -> str: - """Get a string representation.""" - return pformat(self, highlight=False) - - def __str__(self): - """Get a string representation.""" - return self.model_dump_json(indent=4) - - root_dir: str = Field( - description="The root directory for the configuration.", - default=graphrag_config_defaults.root_dir, - ) - - def _validate_root_dir(self) -> None: - """Validate the root directory.""" - if self.root_dir.strip() == "": - self.root_dir = str(Path.cwd()) - - root_dir = Path(self.root_dir).resolve() - if not root_dir.is_dir(): - msg = f"Invalid root directory: {self.root_dir} is not a directory." - raise FileNotFoundError(msg) - self.root_dir = str(root_dir) - - models: dict[str, LanguageModelConfig] = Field( - description="Available language model configurations.", - default=graphrag_config_defaults.models, - ) - - def _validate_models(self) -> None: - """Validate the models configuration. - - Ensure both a default chat model and default embedding model - have been defined. Other models may also be defined but - defaults are required for the time being as places of the - code fallback to default model configs instead - of specifying a specific model. - - TODO: Don't fallback to default models elsewhere in the code. - Forcing code to specify a model to use and allowing for any - names for model configurations. - """ - if defs.DEFAULT_CHAT_MODEL_ID not in self.models: - raise LanguageModelConfigMissingError(defs.DEFAULT_CHAT_MODEL_ID) - if defs.DEFAULT_EMBEDDING_MODEL_ID not in self.models: - raise LanguageModelConfigMissingError(defs.DEFAULT_EMBEDDING_MODEL_ID) - - def _validate_retry_services(self) -> None: - """Validate the retry services configuration.""" - retry_factory = RetryFactory() - - for model_id, model in self.models.items(): - if model.retry_strategy != "none": - if model.retry_strategy not in retry_factory: - msg = f"Retry strategy '{model.retry_strategy}' for model '{model_id}' is not registered. Available strategies: {', '.join(retry_factory.keys())}" - raise ValueError(msg) - - _ = retry_factory.create( - strategy=model.retry_strategy, - max_retries=model.max_retries, - max_retry_wait=model.max_retry_wait, - ) - - def _validate_rate_limiter_services(self) -> None: - """Validate the rate limiter services configuration.""" - rate_limiter_factory = RateLimiterFactory() - - for model_id, model in self.models.items(): - if model.rate_limit_strategy is not None: - if model.rate_limit_strategy not in rate_limiter_factory: - msg = f"Rate Limiter strategy '{model.rate_limit_strategy}' for model '{model_id}' is not registered. Available strategies: {', '.join(rate_limiter_factory.keys())}" - raise ValueError(msg) - - rpm = ( - model.requests_per_minute - if type(model.requests_per_minute) is int - else None - ) - tpm = ( - model.tokens_per_minute - if type(model.tokens_per_minute) is int - else None - ) - if rpm is not None or tpm is not None: - _ = rate_limiter_factory.create( - strategy=model.rate_limit_strategy, rpm=rpm, tpm=tpm - ) - - input: InputConfig = Field( - description="The input configuration.", default=InputConfig() - ) - """The input configuration.""" - - def _validate_input_pattern(self) -> None: - """Validate the input file pattern based on the specified type.""" - if len(self.input.file_pattern) == 0: - if self.input.file_type == defs.InputFileType.text: - self.input.file_pattern = ".*\\.txt$" - else: - self.input.file_pattern = f".*\\.{self.input.file_type.value}$" - - def _validate_input_base_dir(self) -> None: - """Validate the input base directory.""" - if self.input.storage.type == defs.StorageType.file: - if self.input.storage.base_dir.strip() == "": - msg = "input storage base directory is required for file input storage. Please rerun `graphrag init` and set the input storage configuration." - raise ValueError(msg) - self.input.storage.base_dir = str( - (Path(self.root_dir) / self.input.storage.base_dir).resolve() - ) - - chunks: ChunkingConfig = Field( - description="The chunking configuration to use.", - default=ChunkingConfig(), - ) - """The chunking configuration to use.""" - - output: StorageConfig = Field( - description="The output configuration.", - default=StorageConfig(), - ) - """The output configuration.""" - - def _validate_output_base_dir(self) -> None: - """Validate the output base directory.""" - if self.output.type == defs.StorageType.file: - if self.output.base_dir.strip() == "": - msg = "output base directory is required for file output. Please rerun `graphrag init` and set the output configuration." - raise ValueError(msg) - self.output.base_dir = str( - (Path(self.root_dir) / self.output.base_dir).resolve() - ) - - outputs: dict[str, StorageConfig] | None = Field( - description="A list of output configurations used for multi-index query.", - default=graphrag_config_defaults.outputs, - ) - - def _validate_multi_output_base_dirs(self) -> None: - """Validate the outputs dict base directories.""" - if self.outputs: - for output in self.outputs.values(): - if output.type == defs.StorageType.file: - if output.base_dir.strip() == "": - msg = "Output base directory is required for file output. Please rerun `graphrag init` and set the output configuration." - raise ValueError(msg) - output.base_dir = str( - (Path(self.root_dir) / output.base_dir).resolve() - ) - - update_index_output: StorageConfig = Field( - description="The output configuration for the updated index.", - default=StorageConfig( - base_dir=graphrag_config_defaults.update_index_output.base_dir, - ), - ) - """The output configuration for the updated index.""" - - def _validate_update_index_output_base_dir(self) -> None: - """Validate the update index output base directory.""" - if self.update_index_output.type == defs.StorageType.file: - if self.update_index_output.base_dir.strip() == "": - msg = "update_index_output base directory is required for file output. Please rerun `graphrag init` and set the update_index_output configuration." - raise ValueError(msg) - self.update_index_output.base_dir = str( - (Path(self.root_dir) / self.update_index_output.base_dir).resolve() - ) - - cache: CacheConfig = Field( - description="The cache configuration.", default=CacheConfig() - ) - """The cache configuration.""" - - reporting: ReportingConfig = Field( - description="The reporting configuration.", default=ReportingConfig() - ) - """The reporting configuration.""" - - def _validate_reporting_base_dir(self) -> None: - """Validate the reporting base directory.""" - if self.reporting.type == defs.ReportingType.file: - if self.reporting.base_dir.strip() == "": - msg = "Reporting base directory is required for file reporting. Please rerun `graphrag init` and set the reporting configuration." - raise ValueError(msg) - self.reporting.base_dir = str( - (Path(self.root_dir) / self.reporting.base_dir).resolve() - ) - - vector_store: dict[str, VectorStoreConfig] = Field( - description="The vector store configuration.", - default_factory=lambda: { - k: VectorStoreConfig(**asdict(v)) - for k, v in graphrag_config_defaults.vector_store.items() - }, - ) - """The vector store configuration.""" - - workflows: list[str] | None = Field( - description="List of workflows to run, in execution order. This always overrides any built-in workflow methods.", - default=graphrag_config_defaults.workflows, - ) - """List of workflows to run, in execution order.""" - - embed_text: TextEmbeddingConfig = Field( - description="Text embedding configuration.", - default=TextEmbeddingConfig(), - ) - """Text embedding configuration.""" - - extract_graph: ExtractGraphConfig = Field( - description="The entity extraction configuration to use.", - default=ExtractGraphConfig(), - ) - """The entity extraction configuration to use.""" - - summarize_descriptions: SummarizeDescriptionsConfig = Field( - description="The description summarization configuration to use.", - default=SummarizeDescriptionsConfig(), - ) - """The description summarization configuration to use.""" - - extract_graph_nlp: ExtractGraphNLPConfig = Field( - description="The NLP-based graph extraction configuration to use.", - default=ExtractGraphNLPConfig(), - ) - """The NLP-based graph extraction configuration to use.""" - - prune_graph: PruneGraphConfig = Field( - description="The graph pruning configuration to use.", - default=PruneGraphConfig(), - ) - """The graph pruning configuration to use.""" - - cluster_graph: ClusterGraphConfig = Field( - description="The cluster graph configuration to use.", - default=ClusterGraphConfig(), - ) - """The cluster graph configuration to use.""" - - extract_claims: ClaimExtractionConfig = Field( - description="The claim extraction configuration to use.", - default=ClaimExtractionConfig( - enabled=graphrag_config_defaults.extract_claims.enabled, - ), - ) - """The claim extraction configuration to use.""" - - community_reports: CommunityReportsConfig = Field( - description="The community reports configuration to use.", - default=CommunityReportsConfig(), - ) - """The community reports configuration to use.""" - - embed_graph: EmbedGraphConfig = Field( - description="Graph embedding configuration.", - default=EmbedGraphConfig(), - ) - """Graph Embedding configuration.""" - - umap: UmapConfig = Field( - description="The UMAP configuration to use.", default=UmapConfig() - ) - """The UMAP configuration to use.""" - - snapshots: SnapshotsConfig = Field( - description="The snapshots configuration to use.", - default=SnapshotsConfig(), - ) - """The snapshots configuration to use.""" - - local_search: LocalSearchConfig = Field( - description="The local search configuration.", default=LocalSearchConfig() - ) - """The local search configuration.""" - - global_search: GlobalSearchConfig = Field( - description="The global search configuration.", default=GlobalSearchConfig() - ) - """The global search configuration.""" - - drift_search: DRIFTSearchConfig = Field( - description="The drift search configuration.", default=DRIFTSearchConfig() - ) - """The drift search configuration.""" - - basic_search: BasicSearchConfig = Field( - description="The basic search configuration.", default=BasicSearchConfig() - ) - """The basic search configuration.""" - - def _validate_vector_store_db_uri(self) -> None: - """Validate the vector store configuration.""" - for store in self.vector_store.values(): - if store.type == VectorStoreType.LanceDB: - if not store.db_uri or store.db_uri.strip == "": - msg = "Vector store URI is required for LanceDB. Please rerun `graphrag init` and set the vector store configuration." - raise ValueError(msg) - store.db_uri = str((Path(self.root_dir) / store.db_uri).resolve()) - - def _validate_factories(self) -> None: - """Validate the factories used in the configuration.""" - self._validate_retry_services() - self._validate_rate_limiter_services() - - def get_language_model_config(self, model_id: str) -> LanguageModelConfig: - """Get a model configuration by ID. - - Parameters - ---------- - model_id : str - The ID of the model to get. Should match an ID in the models list. - - Returns - ------- - LanguageModelConfig - The model configuration if found. - - Raises - ------ - ValueError - If the model ID is not found in the configuration. - """ - if model_id not in self.models: - err_msg = f"Model ID {model_id} not found in configuration. Please rerun `graphrag init` and set the model configuration." - raise ValueError(err_msg) - - return self.models[model_id] - - def get_vector_store_config(self, vector_store_id: str) -> VectorStoreConfig: - """Get a vector store configuration by ID. - - Parameters - ---------- - vector_store_id : str - The ID of the vector store to get. Should match an ID in the vector_store list. - - Returns - ------- - VectorStoreConfig - The vector store configuration if found. - - Raises - ------ - ValueError - If the vector store ID is not found in the configuration. - """ - if vector_store_id not in self.vector_store: - err_msg = f"Vector Store ID {vector_store_id} not found in configuration. Please rerun `graphrag init` and set the vector store configuration." - raise ValueError(err_msg) - - return self.vector_store[vector_store_id] - - @model_validator(mode="after") - def _validate_model(self): - """Validate the model configuration.""" - self._validate_root_dir() - self._validate_models() - self._validate_input_pattern() - self._validate_input_base_dir() - self._validate_reporting_base_dir() - self._validate_output_base_dir() - self._validate_multi_output_base_dirs() - self._validate_update_index_output_base_dir() - self._validate_vector_store_db_uri() - self._validate_factories() - return self diff --git a/graphrag/config/models/input_config.py b/graphrag/config/models/input_config.py deleted file mode 100644 index 139dd90c46..0000000000 --- a/graphrag/config/models/input_config.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Parameterization settings for the default configuration.""" - -from pydantic import BaseModel, Field - -import graphrag.config.defaults as defs -from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.enums import InputFileType -from graphrag.config.models.storage_config import StorageConfig - - -class InputConfig(BaseModel): - """The default configuration section for Input.""" - - storage: StorageConfig = Field( - description="The storage configuration to use for reading input documents.", - default=StorageConfig( - base_dir=graphrag_config_defaults.input.storage.base_dir, - ), - ) - file_type: InputFileType = Field( - description="The input file type to use.", - default=graphrag_config_defaults.input.file_type, - ) - encoding: str = Field( - description="The input file encoding to use.", - default=defs.graphrag_config_defaults.input.encoding, - ) - file_pattern: str = Field( - description="The input file pattern to use.", - default=graphrag_config_defaults.input.file_pattern, - ) - file_filter: dict[str, str] | None = Field( - description="The optional file filter for the input files.", - default=graphrag_config_defaults.input.file_filter, - ) - text_column: str = Field( - description="The input text column to use.", - default=graphrag_config_defaults.input.text_column, - ) - title_column: str | None = Field( - description="The input title column to use.", - default=graphrag_config_defaults.input.title_column, - ) - metadata: list[str] | None = Field( - description="The document attribute columns to use.", - default=graphrag_config_defaults.input.metadata, - ) diff --git a/graphrag/config/models/language_model_config.py b/graphrag/config/models/language_model_config.py deleted file mode 100644 index e31e311762..0000000000 --- a/graphrag/config/models/language_model_config.py +++ /dev/null @@ -1,403 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Language model configuration.""" - -import logging -from typing import Literal - -import tiktoken -from pydantic import BaseModel, Field, model_validator - -from graphrag.config.defaults import language_model_defaults -from graphrag.config.enums import AsyncType, AuthType, ModelType -from graphrag.config.errors import ( - ApiKeyMissingError, - AzureApiBaseMissingError, - AzureApiVersionMissingError, - ConflictingSettingsError, -) -from graphrag.language_model.factory import ModelFactory - -logger = logging.getLogger(__name__) - - -class LanguageModelConfig(BaseModel): - """Language model configuration.""" - - api_key: str | None = Field( - description="The API key to use for the LLM service.", - default=language_model_defaults.api_key, - ) - - def _validate_api_key(self) -> None: - """Validate the API key. - - API Key is required when using OpenAI API - or when using Azure API with API Key authentication. - For the time being, this check is extra verbose for clarity. - It will also raise an exception if an API Key is provided - when one is not expected such as the case of using Azure - Managed Identity. - - Raises - ------ - ApiKeyMissingError - If the API key is missing and is required. - """ - if self.auth_type == AuthType.APIKey and ( - self.api_key is None or self.api_key.strip() == "" - ): - raise ApiKeyMissingError( - self.type, - self.auth_type.value, - ) - - if (self.auth_type == AuthType.AzureManagedIdentity) and ( - self.api_key is not None and self.api_key.strip() != "" - ): - msg = "API Key should not be provided when using Azure Managed Identity. Please rerun `graphrag init` and remove the api_key when using Azure Managed Identity." - raise ConflictingSettingsError(msg) - - auth_type: AuthType = Field( - description="The authentication type.", - default=language_model_defaults.auth_type, - ) - - def _validate_auth_type(self) -> None: - """Validate the authentication type. - - auth_type must be api_key when using OpenAI and - can be either api_key or azure_managed_identity when using AOI. - - Raises - ------ - ConflictingSettingsError - If the Azure authentication type conflicts with the model being used. - """ - if ( - self.auth_type == AuthType.AzureManagedIdentity - and self.type != ModelType.AzureOpenAIChat - and self.type != ModelType.AzureOpenAIEmbedding - and self.model_provider != "azure" # indicates Litellm + AOI - ): - msg = f"auth_type of azure_managed_identity is not supported for model type {self.type}. Please rerun `graphrag init` and set the auth_type to api_key." - raise ConflictingSettingsError(msg) - - type: ModelType | str = Field(description="The type of LLM model to use.") - - def _validate_type(self) -> None: - """Validate the model type. - - Raises - ------ - KeyError - If the model name is not recognized. - """ - # Type should be contained by the registered models - if not ModelFactory.is_supported_model(self.type): - msg = f"Model type {self.type} is not recognized, must be one of {ModelFactory.get_chat_models() + ModelFactory.get_embedding_models()}." - raise KeyError(msg) - if self.type in [ - "openai_chat", - "openai_embedding", - "azure_openai_chat", - "azure_openai_embedding", - ]: - msg = f"Model config based on fnllm is deprecated and will be removed in GraphRAG v3, please use {ModelType.Chat} or {ModelType.Embedding} instead to switch to LiteLLM config." - logger.warning(msg) - - model_provider: str | None = Field( - description="The model provider to use.", - default=language_model_defaults.model_provider, - ) - - def _validate_model_provider(self) -> None: - """Validate the model provider. - - Required when using Litellm. - - Raises - ------ - KeyError - If the model provider is not recognized. - """ - if (self.type == ModelType.Chat or self.type == ModelType.Embedding) and ( - self.model_provider is None or self.model_provider.strip() == "" - ): - msg = f"Model provider must be specified when using type == {self.type}." - raise KeyError(msg) - - model: str = Field(description="The LLM model to use.") - encoding_model: str = Field( - description="The encoding model to use", - default=language_model_defaults.encoding_model, - ) - - def _validate_encoding_model(self) -> None: - """Validate the encoding model. - - The default behavior is to use an encoding model that matches the LLM model. - LiteLLM supports 100+ models and their tokenization. There is no need to - set the encoding model when using the new LiteLLM provider as was done with fnllm provider. - - Users can still manually specify a tiktoken based encoding model to use even with the LiteLLM provider - in which case the specified encoding model will be used regardless of the LLM model being used, even if - it is not an openai based model. - - If not using LiteLLM provider, set the encoding model based on the LLM model name. - This is for backward compatibility with existing fnllm provider until fnllm is removed. - - Raises - ------ - KeyError - If the model name is not recognized. - """ - if ( - self.type != ModelType.Chat - and self.type != ModelType.Embedding - and self.encoding_model.strip() == "" - ): - self.encoding_model = tiktoken.encoding_name_for_model(self.model) - - api_base: str | None = Field( - description="The base URL for the LLM API.", - default=language_model_defaults.api_base, - ) - - def _validate_api_base(self) -> None: - """Validate the API base. - - Required when using AOI. - - Raises - ------ - AzureApiBaseMissingError - If the API base is missing and is required. - """ - if ( - self.type == ModelType.AzureOpenAIChat - or self.type == ModelType.AzureOpenAIEmbedding - or self.model_provider == "azure" # indicates Litellm + AOI - ) and (self.api_base is None or self.api_base.strip() == ""): - raise AzureApiBaseMissingError(self.type) - - api_version: str | None = Field( - description="The version of the LLM API to use.", - default=language_model_defaults.api_version, - ) - - def _validate_api_version(self) -> None: - """Validate the API version. - - Required when using AOI. - - Raises - ------ - AzureApiBaseMissingError - If the API base is missing and is required. - """ - if ( - self.type == ModelType.AzureOpenAIChat - or self.type == ModelType.AzureOpenAIEmbedding - or self.model_provider == "azure" # indicates Litellm + AOI - ) and (self.api_version is None or self.api_version.strip() == ""): - raise AzureApiVersionMissingError(self.type) - - deployment_name: str | None = Field( - description="The deployment name to use for the LLM service.", - default=language_model_defaults.deployment_name, - ) - - def _validate_deployment_name(self) -> None: - """Validate the deployment name. - - Required when using AOI. - - Raises - ------ - AzureDeploymentNameMissingError - If the deployment name is missing and is required. - """ - if ( - self.type == ModelType.AzureOpenAIChat - or self.type == ModelType.AzureOpenAIEmbedding - or self.model_provider == "azure" # indicates Litellm + AOI - ) and (self.deployment_name is None or self.deployment_name.strip() == ""): - msg = f"deployment_name is not set for Azure-hosted model. This will default to your model name ({self.model}). If different, this should be set." - logger.debug(msg) - - organization: str | None = Field( - description="The organization to use for the LLM service.", - default=language_model_defaults.organization, - ) - proxy: str | None = Field( - description="The proxy to use for the LLM service.", - default=language_model_defaults.proxy, - ) - audience: str | None = Field( - description="Azure resource URI to use with managed identity for the llm connection.", - default=language_model_defaults.audience, - ) - model_supports_json: bool | None = Field( - description="Whether the model supports JSON output mode.", - default=language_model_defaults.model_supports_json, - ) - request_timeout: float = Field( - description="The request timeout to use.", - default=language_model_defaults.request_timeout, - ) - tokens_per_minute: int | Literal["auto"] | None = Field( - description="The number of tokens per minute to use for the LLM service.", - default=language_model_defaults.tokens_per_minute, - ) - - def _validate_tokens_per_minute(self) -> None: - """Validate the tokens per minute. - - Raises - ------ - ValueError - If the tokens per minute is less than 0. - """ - # If the value is a number, check if it is less than 1 - if isinstance(self.tokens_per_minute, int) and self.tokens_per_minute < 1: - msg = f"Tokens per minute must be a non zero positive number, 'auto' or null. Suggested value: {language_model_defaults.tokens_per_minute}." - raise ValueError(msg) - - if ( - (self.type == ModelType.Chat or self.type == ModelType.Embedding) - and self.rate_limit_strategy is not None - and self.tokens_per_minute == "auto" - ): - msg = f"tokens_per_minute cannot be set to 'auto' when using type '{self.type}'. Please set it to a positive integer or null to disable." - raise ValueError(msg) - - requests_per_minute: int | Literal["auto"] | None = Field( - description="The number of requests per minute to use for the LLM service.", - default=language_model_defaults.requests_per_minute, - ) - - def _validate_requests_per_minute(self) -> None: - """Validate the requests per minute. - - Raises - ------ - ValueError - If the requests per minute is less than 0. - """ - # If the value is a number, check if it is less than 1 - if isinstance(self.requests_per_minute, int) and self.requests_per_minute < 1: - msg = f"Requests per minute must be a non zero positive number, 'auto' or null. Suggested value: {language_model_defaults.requests_per_minute}." - raise ValueError(msg) - - if ( - (self.type == ModelType.Chat or self.type == ModelType.Embedding) - and self.rate_limit_strategy is not None - and self.requests_per_minute == "auto" - ): - msg = f"requests_per_minute cannot be set to 'auto' when using type '{self.type}'. Please set it to a positive integer or null to disable." - raise ValueError(msg) - - rate_limit_strategy: str | None = Field( - description="The rate limit strategy to use for the LLM service.", - default=language_model_defaults.rate_limit_strategy, - ) - - retry_strategy: str = Field( - description="The retry strategy to use for the LLM service.", - default=language_model_defaults.retry_strategy, - ) - max_retries: int = Field( - description="The maximum number of retries to use for the LLM service.", - default=language_model_defaults.max_retries, - ) - - def _validate_max_retries(self) -> None: - """Validate the maximum retries. - - Raises - ------ - ValueError - If the maximum retries is less than 0. - """ - if self.max_retries < 1: - msg = f"Maximum retries must be greater than or equal to 1. Suggested value: {language_model_defaults.max_retries}." - raise ValueError(msg) - - max_retry_wait: float = Field( - description="The maximum retry wait to use for the LLM service.", - default=language_model_defaults.max_retry_wait, - ) - concurrent_requests: int = Field( - description="Whether to use concurrent requests for the LLM service.", - default=language_model_defaults.concurrent_requests, - ) - async_mode: AsyncType = Field( - description="The async mode to use.", default=language_model_defaults.async_mode - ) - responses: list[str | BaseModel] | None = Field( - default=language_model_defaults.responses, - description="Static responses to use in mock mode.", - ) - max_tokens: int | None = Field( - description="The maximum number of tokens to generate.", - default=language_model_defaults.max_tokens, - ) - temperature: float = Field( - description="The temperature to use for token generation.", - default=language_model_defaults.temperature, - ) - max_completion_tokens: int | None = Field( - description="The maximum number of tokens to consume. This includes reasoning tokens for the o* reasoning models.", - default=language_model_defaults.max_completion_tokens, - ) - reasoning_effort: str | None = Field( - description="Level of effort OpenAI reasoning models should expend. Supported options are 'low', 'medium', 'high'; and OAI defaults to 'medium'.", - default=language_model_defaults.reasoning_effort, - ) - top_p: float = Field( - description="The top-p value to use for token generation.", - default=language_model_defaults.top_p, - ) - n: int = Field( - description="The number of completions to generate.", - default=language_model_defaults.n, - ) - frequency_penalty: float = Field( - description="The frequency penalty to use for token generation.", - default=language_model_defaults.frequency_penalty, - ) - presence_penalty: float = Field( - description="The presence penalty to use for token generation.", - default=language_model_defaults.presence_penalty, - ) - - def _validate_azure_settings(self) -> None: - """Validate the Azure settings. - - Raises - ------ - AzureApiBaseMissingError - If the API base is missing and is required. - AzureApiVersionMissingError - If the API version is missing and is required. - AzureDeploymentNameMissingError - If the deployment name is missing and is required. - """ - self._validate_api_base() - self._validate_api_version() - self._validate_deployment_name() - - @model_validator(mode="after") - def _validate_model(self): - self._validate_type() - self._validate_model_provider() - self._validate_auth_type() - self._validate_api_key() - self._validate_tokens_per_minute() - self._validate_requests_per_minute() - self._validate_max_retries() - self._validate_azure_settings() - self._validate_encoding_model() - return self diff --git a/graphrag/config/models/storage_config.py b/graphrag/config/models/storage_config.py deleted file mode 100644 index 3f01448c66..0000000000 --- a/graphrag/config/models/storage_config.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Parameterization settings for the default configuration.""" - -from pathlib import Path - -from pydantic import BaseModel, Field, field_validator - -from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.enums import StorageType - - -class StorageConfig(BaseModel): - """The default configuration section for storage.""" - - type: StorageType | str = Field( - description="The storage type to use.", - default=graphrag_config_defaults.storage.type, - ) - base_dir: str = Field( - description="The base directory for the output.", - default=graphrag_config_defaults.storage.base_dir, - ) - - # Validate the base dir for multiple OS (use Path) - # if not using a cloud storage type. - @field_validator("base_dir", mode="before") - @classmethod - def validate_base_dir(cls, value, info): - """Ensure that base_dir is a valid filesystem path when using local storage.""" - # info.data contains other field values, including 'type' - if info.data.get("type") != StorageType.file: - return value - return str(Path(value)) - - connection_string: str | None = Field( - description="The storage connection string to use.", - default=graphrag_config_defaults.storage.connection_string, - ) - container_name: str | None = Field( - description="The storage container name to use.", - default=graphrag_config_defaults.storage.container_name, - ) - storage_account_blob_url: str | None = Field( - description="The storage account blob url to use.", - default=graphrag_config_defaults.storage.storage_account_blob_url, - ) - cosmosdb_account_url: str | None = Field( - description="The cosmosdb account url to use.", - default=graphrag_config_defaults.storage.cosmosdb_account_url, - ) diff --git a/graphrag/config/models/text_embedding_config.py b/graphrag/config/models/text_embedding_config.py deleted file mode 100644 index e154675a1e..0000000000 --- a/graphrag/config/models/text_embedding_config.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Parameterization settings for the default configuration.""" - -from pydantic import BaseModel, Field - -from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.models.language_model_config import LanguageModelConfig - - -class TextEmbeddingConfig(BaseModel): - """Configuration section for text embeddings.""" - - model_id: str = Field( - description="The model ID to use for text embeddings.", - default=graphrag_config_defaults.embed_text.model_id, - ) - vector_store_id: str = Field( - description="The vector store ID to use for text embeddings.", - default=graphrag_config_defaults.embed_text.vector_store_id, - ) - batch_size: int = Field( - description="The batch size to use.", - default=graphrag_config_defaults.embed_text.batch_size, - ) - batch_max_tokens: int = Field( - description="The batch max tokens to use.", - default=graphrag_config_defaults.embed_text.batch_max_tokens, - ) - names: list[str] = Field( - description="The specific embeddings to perform.", - default=graphrag_config_defaults.embed_text.names, - ) - strategy: dict | None = Field( - description="The override strategy to use.", - default=graphrag_config_defaults.embed_text.strategy, - ) - - def resolved_strategy(self, model_config: LanguageModelConfig) -> dict: - """Get the resolved text embedding strategy.""" - from graphrag.index.operations.embed_text.embed_text import ( - TextEmbedStrategyType, - ) - - return self.strategy or { - "type": TextEmbedStrategyType.openai, - "llm": model_config.model_dump(), - "num_threads": model_config.concurrent_requests, - "batch_size": self.batch_size, - "batch_max_tokens": self.batch_max_tokens, - } diff --git a/graphrag/config/models/umap_config.py b/graphrag/config/models/umap_config.py deleted file mode 100644 index e7a0c947bc..0000000000 --- a/graphrag/config/models/umap_config.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Parameterization settings for the default configuration.""" - -from pydantic import BaseModel, Field - -from graphrag.config.defaults import graphrag_config_defaults - - -class UmapConfig(BaseModel): - """Configuration section for UMAP.""" - - enabled: bool = Field( - description="A flag indicating whether to enable UMAP.", - default=graphrag_config_defaults.umap.enabled, - ) diff --git a/graphrag/config/models/vector_store_config.py b/graphrag/config/models/vector_store_config.py deleted file mode 100644 index 983165dcf1..0000000000 --- a/graphrag/config/models/vector_store_config.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Parameterization settings for the default configuration.""" - -from pydantic import BaseModel, Field, model_validator - -from graphrag.config.defaults import vector_store_defaults -from graphrag.config.embeddings import all_embeddings -from graphrag.config.enums import VectorStoreType -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig - - -class VectorStoreConfig(BaseModel): - """The default configuration section for Vector Store.""" - - type: str = Field( - description="The vector store type to use.", - default=vector_store_defaults.type, - ) - - db_uri: str | None = Field( - description="The database URI to use.", - default=None, - ) - - def _validate_db_uri(self) -> None: - """Validate the database URI.""" - if self.type == VectorStoreType.LanceDB.value and ( - self.db_uri is None or self.db_uri.strip() == "" - ): - self.db_uri = vector_store_defaults.db_uri - - if self.type != VectorStoreType.LanceDB.value and ( - self.db_uri is not None and self.db_uri.strip() != "" - ): - msg = "vector_store.db_uri is only used when vector_store.type == lancedb. Please rerun `graphrag init` and select the correct vector store type." - raise ValueError(msg) - - url: str | None = Field( - description="The database URL when type == azure_ai_search.", - default=vector_store_defaults.url, - ) - - def _validate_url(self) -> None: - """Validate the database URL.""" - if self.type == VectorStoreType.AzureAISearch and ( - self.url is None or self.url.strip() == "" - ): - msg = "vector_store.url is required when vector_store.type == azure_ai_search. Please rerun `graphrag init` and select the correct vector store type." - raise ValueError(msg) - - if self.type == VectorStoreType.CosmosDB and ( - self.url is None or self.url.strip() == "" - ): - msg = "vector_store.url is required when vector_store.type == cosmos_db. Please rerun `graphrag init` and select the correct vector store type." - raise ValueError(msg) - - if self.type == VectorStoreType.LanceDB and ( - self.url is not None and self.url.strip() != "" - ): - msg = "vector_store.url is only used when vector_store.type == azure_ai_search or vector_store.type == cosmos_db. Please rerun `graphrag init` and select the correct vector store type." - raise ValueError(msg) - - api_key: str | None = Field( - description="The database API key when type == azure_ai_search.", - default=vector_store_defaults.api_key, - ) - - audience: str | None = Field( - description="The database audience when type == azure_ai_search.", - default=vector_store_defaults.audience, - ) - - container_name: str = Field( - description="The container name to use.", - default=vector_store_defaults.container_name, - ) - - database_name: str | None = Field( - description="The database name to use when type == cosmos_db.", - default=vector_store_defaults.database_name, - ) - - overwrite: bool = Field( - description="Overwrite the existing data.", - default=vector_store_defaults.overwrite, - ) - - embeddings_schema: dict[str, VectorStoreSchemaConfig] = {} - - def _validate_embeddings_schema(self) -> None: - """Validate the embeddings schema.""" - for name in self.embeddings_schema: - if name not in all_embeddings: - msg = f"vector_store.embeddings_schema contains an invalid embedding schema name: {name}. Please update your settings.yaml and select the correct embedding schema names." - raise ValueError(msg) - - if self.type == VectorStoreType.CosmosDB: - for id_field in self.embeddings_schema: - if id_field != "id": - msg = "When using CosmosDB, the id_field in embeddings_schema must be 'id'. Please update your settings.yaml and set the id_field to 'id'." - raise ValueError(msg) - - @model_validator(mode="after") - def _validate_model(self): - """Validate the model.""" - self._validate_db_uri() - self._validate_url() - self._validate_embeddings_schema() - return self diff --git a/graphrag/config/read_dotenv.py b/graphrag/config/read_dotenv.py deleted file mode 100644 index a2da5d43fe..0000000000 --- a/graphrag/config/read_dotenv.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing the read_dotenv utility.""" - -import logging -import os -from pathlib import Path - -from dotenv import dotenv_values - -logger = logging.getLogger(__name__) - - -def read_dotenv(root: str) -> None: - """Read a .env file in the given root path.""" - env_path = Path(root) / ".env" - if env_path.exists(): - logger.info("Loading pipeline .env file") - env_config = dotenv_values(f"{env_path}") - for key, value in env_config.items(): - if key not in os.environ: - os.environ[key] = value or "" - else: - logger.info("No .env file found at %s", root) diff --git a/graphrag/factory/factory.py b/graphrag/factory/factory.py deleted file mode 100644 index 0624e9bfa1..0000000000 --- a/graphrag/factory/factory.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Factory ABC.""" - -from abc import ABC -from collections.abc import Callable -from typing import Any, ClassVar, Generic, TypeVar - -T = TypeVar("T", covariant=True) - - -class Factory(ABC, Generic[T]): - """Abstract base class for factories.""" - - _instance: ClassVar["Factory | None"] = None - - def __new__(cls, *args: Any, **kwargs: Any) -> "Factory": - """Create a new instance of Factory if it does not exist.""" - if cls._instance is None: - cls._instance = super().__new__(cls, *args, **kwargs) - return cls._instance - - def __init__(self): - if not hasattr(self, "_initialized"): - self._services: dict[str, Callable[..., T]] = {} - self._initialized = True - - def __contains__(self, strategy: str) -> bool: - """Check if a strategy is registered.""" - return strategy in self._services - - def keys(self) -> list[str]: - """Get a list of registered strategy names.""" - return list(self._services.keys()) - - def register(self, *, strategy: str, service_initializer: Callable[..., T]) -> None: - """ - Register a new service. - - Args - ---- - strategy: The name of the strategy. - service_initializer: A callable that creates an instance of T. - """ - self._services[strategy] = service_initializer - - def create(self, *, strategy: str, **kwargs: Any) -> T: - """ - Create a service instance based on the strategy. - - Args - ---- - strategy: The name of the strategy. - **kwargs: Additional arguments to pass to the service initializer. - - Returns - ------- - An instance of T. - - Raises - ------ - ValueError: If the strategy is not registered. - """ - if strategy not in self._services: - msg = f"Strategy '{strategy}' is not registered." - raise ValueError(msg) - return self._services[strategy](**kwargs) diff --git a/graphrag/index/input/__init__.py b/graphrag/index/input/__init__.py deleted file mode 100644 index 15177c91db..0000000000 --- a/graphrag/index/input/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""The Indexing Engine input package root.""" diff --git a/graphrag/index/input/csv.py b/graphrag/index/input/csv.py deleted file mode 100644 index eea4864b17..0000000000 --- a/graphrag/index/input/csv.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing load method definition.""" - -import logging -from io import BytesIO - -import pandas as pd - -from graphrag.config.models.input_config import InputConfig -from graphrag.index.input.util import load_files, process_data_columns -from graphrag.storage.pipeline_storage import PipelineStorage - -logger = logging.getLogger(__name__) - - -async def load_csv( - config: InputConfig, - storage: PipelineStorage, -) -> pd.DataFrame: - """Load csv inputs from a directory.""" - logger.info("Loading csv files from %s", config.storage.base_dir) - - async def load_file(path: str, group: dict | None) -> pd.DataFrame: - if group is None: - group = {} - buffer = BytesIO(await storage.get(path, as_bytes=True)) - data = pd.read_csv(buffer, encoding=config.encoding) - additional_keys = group.keys() - if len(additional_keys) > 0: - data[[*additional_keys]] = data.apply( - lambda _row: pd.Series([group[key] for key in additional_keys]), axis=1 - ) - - data = process_data_columns(data, config, path) - - creation_date = await storage.get_creation_date(path) - data["creation_date"] = data.apply(lambda _: creation_date, axis=1) - - return data - - return await load_files(load_file, config, storage) diff --git a/graphrag/index/input/factory.py b/graphrag/index/input/factory.py deleted file mode 100644 index bc4da8c7a1..0000000000 --- a/graphrag/index/input/factory.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing create_input method definition.""" - -import logging -from collections.abc import Awaitable, Callable -from typing import cast - -import pandas as pd - -from graphrag.config.enums import InputFileType -from graphrag.config.models.input_config import InputConfig -from graphrag.index.input.csv import load_csv -from graphrag.index.input.json import load_json -from graphrag.index.input.text import load_text -from graphrag.storage.pipeline_storage import PipelineStorage - -logger = logging.getLogger(__name__) -loaders: dict[str, Callable[..., Awaitable[pd.DataFrame]]] = { - InputFileType.text: load_text, - InputFileType.csv: load_csv, - InputFileType.json: load_json, -} - - -async def create_input( - config: InputConfig, - storage: PipelineStorage, -) -> pd.DataFrame: - """Instantiate input data for a pipeline.""" - logger.info("loading input from root_dir=%s", config.storage.base_dir) - - if config.file_type in loaders: - logger.info("Loading Input %s", config.file_type) - loader = loaders[config.file_type] - result = await loader(config, storage) - # Convert metadata columns to strings and collapse them into a JSON object - if config.metadata: - if all(col in result.columns for col in config.metadata): - # Collapse the metadata columns into a single JSON object column - result["metadata"] = result[config.metadata].apply( - lambda row: row.to_dict(), axis=1 - ) - else: - value_error_msg = ( - "One or more metadata columns not found in the DataFrame." - ) - raise ValueError(value_error_msg) - - result[config.metadata] = result[config.metadata].astype(str) - - return cast("pd.DataFrame", result) - - msg = f"Unknown input type {config.file_type}" - raise ValueError(msg) diff --git a/graphrag/index/input/json.py b/graphrag/index/input/json.py deleted file mode 100644 index e2880bc888..0000000000 --- a/graphrag/index/input/json.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing load method definition.""" - -import json -import logging - -import pandas as pd - -from graphrag.config.models.input_config import InputConfig -from graphrag.index.input.util import load_files, process_data_columns -from graphrag.storage.pipeline_storage import PipelineStorage - -logger = logging.getLogger(__name__) - - -async def load_json( - config: InputConfig, - storage: PipelineStorage, -) -> pd.DataFrame: - """Load json inputs from a directory.""" - logger.info("Loading json files from %s", config.storage.base_dir) - - async def load_file(path: str, group: dict | None) -> pd.DataFrame: - if group is None: - group = {} - text = await storage.get(path, encoding=config.encoding) - as_json = json.loads(text) - # json file could just be a single object, or an array of objects - rows = as_json if isinstance(as_json, list) else [as_json] - data = pd.DataFrame(rows) - - additional_keys = group.keys() - if len(additional_keys) > 0: - data[[*additional_keys]] = data.apply( - lambda _row: pd.Series([group[key] for key in additional_keys]), axis=1 - ) - - data = process_data_columns(data, config, path) - - creation_date = await storage.get_creation_date(path) - data["creation_date"] = data.apply(lambda _: creation_date, axis=1) - - return data - - return await load_files(load_file, config, storage) diff --git a/graphrag/index/input/text.py b/graphrag/index/input/text.py deleted file mode 100644 index 1834a532eb..0000000000 --- a/graphrag/index/input/text.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing load method definition.""" - -import logging -from pathlib import Path - -import pandas as pd - -from graphrag.config.models.input_config import InputConfig -from graphrag.index.input.util import load_files -from graphrag.index.utils.hashing import gen_sha512_hash -from graphrag.storage.pipeline_storage import PipelineStorage - -logger = logging.getLogger(__name__) - - -async def load_text( - config: InputConfig, - storage: PipelineStorage, -) -> pd.DataFrame: - """Load text inputs from a directory.""" - - async def load_file(path: str, group: dict | None = None) -> pd.DataFrame: - if group is None: - group = {} - text = await storage.get(path, encoding=config.encoding) - new_item = {**group, "text": text} - new_item["id"] = gen_sha512_hash(new_item, new_item.keys()) - new_item["title"] = str(Path(path).name) - new_item["creation_date"] = await storage.get_creation_date(path) - return pd.DataFrame([new_item]) - - return await load_files(load_file, config, storage) diff --git a/graphrag/index/input/util.py b/graphrag/index/input/util.py deleted file mode 100644 index cc15183673..0000000000 --- a/graphrag/index/input/util.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Shared column processing for structured input files.""" - -import logging -import re -from typing import Any - -import pandas as pd - -from graphrag.config.models.input_config import InputConfig -from graphrag.index.utils.hashing import gen_sha512_hash -from graphrag.storage.pipeline_storage import PipelineStorage - -logger = logging.getLogger(__name__) - - -async def load_files( - loader: Any, - config: InputConfig, - storage: PipelineStorage, -) -> pd.DataFrame: - """Load files from storage and apply a loader function.""" - files = list( - storage.find( - re.compile(config.file_pattern), - file_filter=config.file_filter, - ) - ) - - if len(files) == 0: - msg = f"No {config.file_type} files found in {config.storage.base_dir}" - raise ValueError(msg) - - files_loaded = [] - - for file, group in files: - try: - files_loaded.append(await loader(file, group)) - except Exception as e: # noqa: BLE001 (catching Exception is fine here) - logger.warning("Warning! Error loading file %s. Skipping...", file) - logger.warning("Error: %s", e) - - logger.info( - "Found %d %s files, loading %d", len(files), config.file_type, len(files_loaded) - ) - result = pd.concat(files_loaded) - total_files_log = ( - f"Total number of unfiltered {config.file_type} rows: {len(result)}" - ) - logger.info(total_files_log) - return result - - -def process_data_columns( - documents: pd.DataFrame, config: InputConfig, path: str -) -> pd.DataFrame: - """Process configured data columns of a DataFrame.""" - if "id" not in documents.columns: - documents["id"] = documents.apply( - lambda x: gen_sha512_hash(x, x.keys()), axis=1 - ) - if config.text_column is not None and "text" not in documents.columns: - if config.text_column not in documents.columns: - logger.warning( - "text_column %s not found in csv file %s", - config.text_column, - path, - ) - else: - documents["text"] = documents.apply(lambda x: x[config.text_column], axis=1) - if config.title_column is not None: - if config.title_column not in documents.columns: - logger.warning( - "title_column %s not found in csv file %s", - config.title_column, - path, - ) - else: - documents["title"] = documents.apply( - lambda x: x[config.title_column], axis=1 - ) - else: - documents["title"] = documents.apply(lambda _: path, axis=1) - return documents diff --git a/graphrag/index/operations/chunk_text/__init__.py b/graphrag/index/operations/chunk_text/__init__.py deleted file mode 100644 index 1e000e6aa7..0000000000 --- a/graphrag/index/operations/chunk_text/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""The Indexing Engine text chunk package root.""" diff --git a/graphrag/index/operations/chunk_text/chunk_text.py b/graphrag/index/operations/chunk_text/chunk_text.py deleted file mode 100644 index fc50370e28..0000000000 --- a/graphrag/index/operations/chunk_text/chunk_text.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing _get_num_total, chunk, run_strategy and load_strategy methods definitions.""" - -from typing import Any, cast - -import pandas as pd - -from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks -from graphrag.config.models.chunking_config import ChunkingConfig, ChunkStrategyType -from graphrag.index.operations.chunk_text.typing import ( - ChunkInput, - ChunkStrategy, -) -from graphrag.logger.progress import ProgressTicker, progress_ticker - - -def chunk_text( - input: pd.DataFrame, - column: str, - size: int, - overlap: int, - encoding_model: str, - strategy: ChunkStrategyType, - callbacks: WorkflowCallbacks, -) -> pd.Series: - """ - Chunk a piece of text into smaller pieces. - - ## Usage - ```yaml - args: - column: # The name of the column containing the text to chunk, this can either be a column with text, or a column with a list[tuple[doc_id, str]] - strategy: # The strategy to use to chunk the text, see below for more details - ``` - - ## Strategies - The text chunk verb uses a strategy to chunk the text. The strategy is an object which defines the strategy to use. The following strategies are available: - - ### tokens - This strategy uses the [tokens] library to chunk a piece of text. The strategy config is as follows: - - ```yaml - strategy: tokens - size: 1200 # Optional, The chunk size to use, default: 1200 - overlap: 100 # Optional, The chunk overlap to use, default: 100 - ``` - - ### sentence - This strategy uses the nltk library to chunk a piece of text into sentences. The strategy config is as follows: - - ```yaml - strategy: sentence - ``` - """ - strategy_exec = load_strategy(strategy) - - num_total = _get_num_total(input, column) - tick = progress_ticker(callbacks.progress, num_total) - - # collapse the config back to a single object to support "polymorphic" function call - config = ChunkingConfig(size=size, overlap=overlap, encoding_model=encoding_model) - - return cast( - "pd.Series", - input.apply( - cast( - "Any", - lambda x: run_strategy( - strategy_exec, - x[column], - config, - tick, - ), - ), - axis=1, - ), - ) - - -def run_strategy( - strategy_exec: ChunkStrategy, - input: ChunkInput, - config: ChunkingConfig, - tick: ProgressTicker, -) -> list[str | tuple[list[str] | None, str, int]]: - """Run strategy method definition.""" - if isinstance(input, str): - return [item.text_chunk for item in strategy_exec([input], config, tick)] - - # We can work with both just a list of text content - # or a list of tuples of (document_id, text content) - # text_to_chunk = ''' - texts = [item if isinstance(item, str) else item[1] for item in input] - - strategy_results = strategy_exec(texts, config, tick) - - results = [] - for strategy_result in strategy_results: - doc_indices = strategy_result.source_doc_indices - if isinstance(input[doc_indices[0]], str): - results.append(strategy_result.text_chunk) - else: - doc_ids = [input[doc_idx][0] for doc_idx in doc_indices] - results.append(( - doc_ids, - strategy_result.text_chunk, - strategy_result.n_tokens, - )) - return results - - -def load_strategy(strategy: ChunkStrategyType) -> ChunkStrategy: - """Load strategy method definition.""" - match strategy: - case ChunkStrategyType.tokens: - from graphrag.index.operations.chunk_text.strategies import run_tokens - - return run_tokens - case ChunkStrategyType.sentence: - # NLTK - from graphrag.index.operations.chunk_text.bootstrap import bootstrap - from graphrag.index.operations.chunk_text.strategies import run_sentences - - bootstrap() - return run_sentences - case _: - msg = f"Unknown strategy: {strategy}" - raise ValueError(msg) - - -def _get_num_total(output: pd.DataFrame, column: str) -> int: - num_total = 0 - for row in output[column]: - if isinstance(row, str): - num_total += 1 - else: - num_total += len(row) - return num_total diff --git a/graphrag/index/operations/chunk_text/strategies.py b/graphrag/index/operations/chunk_text/strategies.py deleted file mode 100644 index 4be14e408d..0000000000 --- a/graphrag/index/operations/chunk_text/strategies.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing chunk strategies.""" - -from collections.abc import Iterable - -import nltk -import tiktoken - -from graphrag.config.models.chunking_config import ChunkingConfig -from graphrag.index.operations.chunk_text.typing import TextChunk -from graphrag.index.text_splitting.text_splitting import ( - TokenChunkerOptions, - split_multiple_texts_on_tokens, -) -from graphrag.logger.progress import ProgressTicker - - -def get_encoding_fn(encoding_name): - """Get the encoding model.""" - enc = tiktoken.get_encoding(encoding_name) - - def encode(text: str) -> list[int]: - if not isinstance(text, str): - text = f"{text}" - return enc.encode(text) - - def decode(tokens: list[int]) -> str: - return enc.decode(tokens) - - return encode, decode - - -def run_tokens( - input: list[str], - config: ChunkingConfig, - tick: ProgressTicker, -) -> Iterable[TextChunk]: - """Chunks text into chunks based on encoding tokens.""" - tokens_per_chunk = config.size - chunk_overlap = config.overlap - encoding_name = config.encoding_model - - encode, decode = get_encoding_fn(encoding_name) - return split_multiple_texts_on_tokens( - input, - TokenChunkerOptions( - chunk_overlap=chunk_overlap, - tokens_per_chunk=tokens_per_chunk, - encode=encode, - decode=decode, - ), - tick, - ) - - -def run_sentences( - input: list[str], _config: ChunkingConfig, tick: ProgressTicker -) -> Iterable[TextChunk]: - """Chunks text into multiple parts by sentence.""" - for doc_idx, text in enumerate(input): - sentences = nltk.sent_tokenize(text) - for sentence in sentences: - yield TextChunk( - text_chunk=sentence, - source_doc_indices=[doc_idx], - ) - tick(1) diff --git a/graphrag/index/operations/chunk_text/typing.py b/graphrag/index/operations/chunk_text/typing.py deleted file mode 100644 index bf58ef5ec1..0000000000 --- a/graphrag/index/operations/chunk_text/typing.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing 'TextChunk' model.""" - -from collections.abc import Callable, Iterable -from dataclasses import dataclass - -from graphrag.config.models.chunking_config import ChunkingConfig -from graphrag.logger.progress import ProgressTicker - - -@dataclass -class TextChunk: - """Text chunk class definition.""" - - text_chunk: str - source_doc_indices: list[int] - n_tokens: int | None = None - - -ChunkInput = str | list[str] | list[tuple[str, str]] -"""Input to a chunking strategy. Can be a string, a list of strings, or a list of tuples of (id, text).""" - -ChunkStrategy = Callable[ - [list[str], ChunkingConfig, ProgressTicker], Iterable[TextChunk] -] diff --git a/graphrag/index/operations/embed_graph/__init__.py b/graphrag/index/operations/embed_graph/__init__.py deleted file mode 100644 index 3c3e4b1ca4..0000000000 --- a/graphrag/index/operations/embed_graph/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""The Indexing Engine graph embed package root.""" diff --git a/graphrag/index/operations/embed_graph/embed_graph.py b/graphrag/index/operations/embed_graph/embed_graph.py deleted file mode 100644 index 0328402db2..0000000000 --- a/graphrag/index/operations/embed_graph/embed_graph.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing embed_graph and run_embeddings methods definition.""" - -import networkx as nx - -from graphrag.config.models.embed_graph_config import EmbedGraphConfig -from graphrag.index.operations.embed_graph.embed_node2vec import embed_node2vec -from graphrag.index.operations.embed_graph.typing import ( - NodeEmbeddings, -) -from graphrag.index.utils.stable_lcc import stable_largest_connected_component - - -def embed_graph( - graph: nx.Graph, - config: EmbedGraphConfig, -) -> NodeEmbeddings: - """ - Embed a graph into a vector space using node2vec. The graph is expected to be in nx.Graph format. The operation outputs a mapping between node name and vector. - - ## Usage - ```yaml - dimensions: 1536 # Optional, The number of dimensions to use for the embedding, default: 1536 - num_walks: 10 # Optional, The number of walks to use for the embedding, default: 10 - walk_length: 40 # Optional, The walk length to use for the embedding, default: 40 - window_size: 2 # Optional, The window size to use for the embedding, default: 2 - iterations: 3 # Optional, The number of iterations to use for the embedding, default: 3 - random_seed: 86 # Optional, The random seed to use for the embedding, default: 86 - ``` - """ - if config.use_lcc: - graph = stable_largest_connected_component(graph) - - # create graph embedding using node2vec - embeddings = embed_node2vec( - graph=graph, - dimensions=config.dimensions, - num_walks=config.num_walks, - walk_length=config.walk_length, - window_size=config.window_size, - iterations=config.iterations, - random_seed=config.random_seed, - ) - - pairs = zip(embeddings.nodes, embeddings.embeddings.tolist(), strict=True) - sorted_pairs = sorted(pairs, key=lambda x: x[0]) - - return dict(sorted_pairs) diff --git a/graphrag/index/operations/embed_graph/embed_node2vec.py b/graphrag/index/operations/embed_graph/embed_node2vec.py deleted file mode 100644 index a009c670f6..0000000000 --- a/graphrag/index/operations/embed_graph/embed_node2vec.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Utilities to generate graph embeddings.""" - -from dataclasses import dataclass - -import networkx as nx -import numpy as np - - -@dataclass -class NodeEmbeddings: - """Node embeddings class definition.""" - - nodes: list[str] - embeddings: np.ndarray - - -def embed_node2vec( - graph: nx.Graph | nx.DiGraph, - dimensions: int = 1536, - num_walks: int = 10, - walk_length: int = 40, - window_size: int = 2, - iterations: int = 3, - random_seed: int = 86, -) -> NodeEmbeddings: - """Generate node embeddings using Node2Vec.""" - # NOTE: This import is done here to reduce the initial import time of the graphrag package - import graspologic as gc - - # generate embedding - lcc_tensors = gc.embed.node2vec_embed( # type: ignore - graph=graph, - dimensions=dimensions, - window_size=window_size, - iterations=iterations, - num_walks=num_walks, - walk_length=walk_length, - random_seed=random_seed, - ) - return NodeEmbeddings(embeddings=lcc_tensors[0], nodes=lcc_tensors[1]) diff --git a/graphrag/index/operations/embed_graph/typing.py b/graphrag/index/operations/embed_graph/typing.py deleted file mode 100644 index fea792c9b1..0000000000 --- a/graphrag/index/operations/embed_graph/typing.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing different lists and dictionaries.""" - -# Use this for now instead of a wrapper -from typing import Any - -NodeList = list[str] -EmbeddingList = list[Any] -NodeEmbeddings = dict[str, list[float]] -"""Label -> Embedding""" diff --git a/graphrag/index/operations/embed_text/embed_text.py b/graphrag/index/operations/embed_text/embed_text.py deleted file mode 100644 index 8e6cbbcbdb..0000000000 --- a/graphrag/index/operations/embed_text/embed_text.py +++ /dev/null @@ -1,246 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing embed_text, load_strategy and create_row_from_embedding_data methods definition.""" - -import logging -from enum import Enum -from typing import Any - -import numpy as np -import pandas as pd - -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks -from graphrag.config.embeddings import create_index_name -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig -from graphrag.index.operations.embed_text.strategies.typing import TextEmbeddingStrategy -from graphrag.vector_stores.base import BaseVectorStore, VectorStoreDocument -from graphrag.vector_stores.factory import VectorStoreFactory - -logger = logging.getLogger(__name__) - -# Per Azure OpenAI Limits -# https://learn.microsoft.com/en-us/azure/ai-services/openai/reference -DEFAULT_EMBEDDING_BATCH_SIZE = 500 - - -class TextEmbedStrategyType(str, Enum): - """TextEmbedStrategyType class definition.""" - - openai = "openai" - mock = "mock" - - def __repr__(self): - """Get a string representation.""" - return f'"{self.value}"' - - -async def embed_text( - input: pd.DataFrame, - callbacks: WorkflowCallbacks, - cache: PipelineCache, - embed_column: str, - strategy: dict, - embedding_name: str, - id_column: str = "id", - title_column: str | None = None, -): - """Embed a piece of text into a vector space. The operation outputs a new column containing a mapping between doc_id and vector.""" - vector_store_config = strategy.get("vector_store") - - if vector_store_config: - index_name = _get_index_name(vector_store_config, embedding_name) - vector_store: BaseVectorStore = _create_vector_store( - vector_store_config, index_name, embedding_name - ) - vector_store_workflow_config = vector_store_config.get( - embedding_name, vector_store_config - ) - return await _text_embed_with_vector_store( - input=input, - callbacks=callbacks, - cache=cache, - embed_column=embed_column, - strategy=strategy, - vector_store=vector_store, - vector_store_config=vector_store_workflow_config, - id_column=id_column, - title_column=title_column, - ) - - return await _text_embed_in_memory( - input=input, - callbacks=callbacks, - cache=cache, - embed_column=embed_column, - strategy=strategy, - ) - - -async def _text_embed_in_memory( - input: pd.DataFrame, - callbacks: WorkflowCallbacks, - cache: PipelineCache, - embed_column: str, - strategy: dict, -): - strategy_type = strategy["type"] - strategy_exec = load_strategy(strategy_type) - strategy_config = {**strategy} - - texts: list[str] = input[embed_column].to_numpy().tolist() - result = await strategy_exec(texts, callbacks, cache, strategy_config) - - return result.embeddings - - -async def _text_embed_with_vector_store( - input: pd.DataFrame, - callbacks: WorkflowCallbacks, - cache: PipelineCache, - embed_column: str, - strategy: dict[str, Any], - vector_store: BaseVectorStore, - vector_store_config: dict, - id_column: str = "id", - title_column: str | None = None, -): - strategy_type = strategy["type"] - strategy_exec = load_strategy(strategy_type) - strategy_config = {**strategy} - - # Get vector-storage configuration - insert_batch_size: int = ( - vector_store_config.get("batch_size") or DEFAULT_EMBEDDING_BATCH_SIZE - ) - - overwrite: bool = vector_store_config.get("overwrite", True) - - if embed_column not in input.columns: - msg = f"Column {embed_column} not found in input dataframe with columns {input.columns}" - raise ValueError(msg) - title = title_column or embed_column - if title not in input.columns: - msg = ( - f"Column {title} not found in input dataframe with columns {input.columns}" - ) - raise ValueError(msg) - if id_column not in input.columns: - msg = f"Column {id_column} not found in input dataframe with columns {input.columns}" - raise ValueError(msg) - - total_rows = 0 - for row in input[embed_column]: - if isinstance(row, list): - total_rows += len(row) - else: - total_rows += 1 - - i = 0 - starting_index = 0 - - all_results = [] - - num_total_batches = (input.shape[0] + insert_batch_size - 1) // insert_batch_size - while insert_batch_size * i < input.shape[0]: - logger.info( - "uploading text embeddings batch %d/%d of size %d to vector store", - i + 1, - num_total_batches, - insert_batch_size, - ) - batch = input.iloc[insert_batch_size * i : insert_batch_size * (i + 1)] - texts: list[str] = batch[embed_column].to_numpy().tolist() - titles: list[str] = batch[title].to_numpy().tolist() - ids: list[str] = batch[id_column].to_numpy().tolist() - result = await strategy_exec(texts, callbacks, cache, strategy_config) - if result.embeddings: - embeddings = [ - embedding for embedding in result.embeddings if embedding is not None - ] - all_results.extend(embeddings) - - vectors = result.embeddings or [] - documents: list[VectorStoreDocument] = [] - for doc_id, doc_text, doc_title, doc_vector in zip( - ids, texts, titles, vectors, strict=True - ): - if type(doc_vector) is np.ndarray: - doc_vector = doc_vector.tolist() - document = VectorStoreDocument( - id=doc_id, - text=doc_text, - vector=doc_vector, - attributes={"title": doc_title}, - ) - documents.append(document) - - vector_store.load_documents(documents, overwrite and i == 0) - starting_index += len(documents) - i += 1 - - return all_results - - -def _create_vector_store( - vector_store_config: dict, index_name: str, embedding_name: str | None = None -) -> BaseVectorStore: - vector_store_type: str = str(vector_store_config.get("type")) - - embeddings_schema: dict[str, VectorStoreSchemaConfig] = vector_store_config.get( - "embeddings_schema", {} - ) - single_embedding_config: VectorStoreSchemaConfig = VectorStoreSchemaConfig() - - if ( - embeddings_schema is not None - and embedding_name is not None - and embedding_name in embeddings_schema - ): - raw_config = embeddings_schema[embedding_name] - if isinstance(raw_config, dict): - single_embedding_config = VectorStoreSchemaConfig(**raw_config) - else: - single_embedding_config = raw_config - - if single_embedding_config.index_name is None: - single_embedding_config.index_name = index_name - - vector_store = VectorStoreFactory().create_vector_store( - vector_store_schema_config=single_embedding_config, - vector_store_type=vector_store_type, - **vector_store_config, - ) - - vector_store.connect(**vector_store_config) - return vector_store - - -def _get_index_name(vector_store_config: dict, embedding_name: str) -> str: - container_name = vector_store_config.get("container_name", "default") - index_name = create_index_name(container_name, embedding_name) - - msg = f"using vector store {vector_store_config.get('type')} with container_name {container_name} for embedding {embedding_name}: {index_name}" - logger.info(msg) - return index_name - - -def load_strategy(strategy: TextEmbedStrategyType) -> TextEmbeddingStrategy: - """Load strategy method definition.""" - match strategy: - case TextEmbedStrategyType.openai: - from graphrag.index.operations.embed_text.strategies.openai import ( - run as run_openai, - ) - - return run_openai - case TextEmbedStrategyType.mock: - from graphrag.index.operations.embed_text.strategies.mock import ( - run as run_mock, - ) - - return run_mock - case _: - msg = f"Unknown strategy: {strategy}" - raise ValueError(msg) diff --git a/graphrag/index/operations/embed_text/strategies/__init__.py b/graphrag/index/operations/embed_text/strategies/__init__.py deleted file mode 100644 index 8cbe7a580e..0000000000 --- a/graphrag/index/operations/embed_text/strategies/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""The Indexing Engine embed strategies package root.""" diff --git a/graphrag/index/operations/embed_text/strategies/mock.py b/graphrag/index/operations/embed_text/strategies/mock.py deleted file mode 100644 index a65ad9721f..0000000000 --- a/graphrag/index/operations/embed_text/strategies/mock.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing run and _embed_text methods definitions.""" - -import random -from collections.abc import Iterable -from typing import Any - -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks -from graphrag.index.operations.embed_text.strategies.typing import TextEmbeddingResult -from graphrag.logger.progress import ProgressTicker, progress_ticker - - -async def run( # noqa RUF029 async is required for interface - input: list[str], - callbacks: WorkflowCallbacks, - cache: PipelineCache, - _args: dict[str, Any], -) -> TextEmbeddingResult: - """Run the Claim extraction chain.""" - input = input if isinstance(input, Iterable) else [input] - ticker = progress_ticker( - callbacks.progress, len(input), description="generate embeddings progress: " - ) - return TextEmbeddingResult( - embeddings=[_embed_text(cache, text, ticker) for text in input] - ) - - -def _embed_text(_cache: PipelineCache, _text: str, tick: ProgressTicker) -> list[float]: - """Embed a single piece of text.""" - tick(1) - return [random.random(), random.random(), random.random()] # noqa S311 diff --git a/graphrag/index/operations/embed_text/strategies/typing.py b/graphrag/index/operations/embed_text/strategies/typing.py deleted file mode 100644 index f45a7eb36e..0000000000 --- a/graphrag/index/operations/embed_text/strategies/typing.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing 'TextEmbeddingResult' model.""" - -from collections.abc import Awaitable, Callable -from dataclasses import dataclass - -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks - - -@dataclass -class TextEmbeddingResult: - """Text embedding result class definition.""" - - embeddings: list[list[float] | None] | None - - -TextEmbeddingStrategy = Callable[ - [ - list[str], - WorkflowCallbacks, - PipelineCache, - dict, - ], - Awaitable[TextEmbeddingResult], -] diff --git a/graphrag/index/operations/extract_covariates/claim_extractor.py b/graphrag/index/operations/extract_covariates/claim_extractor.py deleted file mode 100644 index e50d05ce83..0000000000 --- a/graphrag/index/operations/extract_covariates/claim_extractor.py +++ /dev/null @@ -1,236 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing 'ClaimExtractorResult' and 'ClaimExtractor' models.""" - -import logging -import traceback -from dataclasses import dataclass -from typing import Any - -from graphrag.config.defaults import graphrag_config_defaults -from graphrag.index.typing.error_handler import ErrorHandlerFn -from graphrag.language_model.protocol.base import ChatModel -from graphrag.prompts.index.extract_claims import ( - CONTINUE_PROMPT, - EXTRACT_CLAIMS_PROMPT, - LOOP_PROMPT, -) - -DEFAULT_TUPLE_DELIMITER = "<|>" -DEFAULT_RECORD_DELIMITER = "##" -DEFAULT_COMPLETION_DELIMITER = "<|COMPLETE|>" -logger = logging.getLogger(__name__) - - -@dataclass -class ClaimExtractorResult: - """Claim extractor result class definition.""" - - output: list[dict] - source_docs: dict[str, Any] - - -class ClaimExtractor: - """Claim extractor class definition.""" - - _model: ChatModel - _extraction_prompt: str - _summary_prompt: str - _output_formatter_prompt: str - _input_text_key: str - _input_entity_spec_key: str - _input_claim_description_key: str - _tuple_delimiter_key: str - _record_delimiter_key: str - _completion_delimiter_key: str - _max_gleanings: int - _on_error: ErrorHandlerFn - - def __init__( - self, - model_invoker: ChatModel, - extraction_prompt: str | None = None, - input_text_key: str | None = None, - input_entity_spec_key: str | None = None, - input_claim_description_key: str | None = None, - input_resolved_entities_key: str | None = None, - tuple_delimiter_key: str | None = None, - record_delimiter_key: str | None = None, - completion_delimiter_key: str | None = None, - max_gleanings: int | None = None, - on_error: ErrorHandlerFn | None = None, - ): - """Init method definition.""" - self._model = model_invoker - self._extraction_prompt = extraction_prompt or EXTRACT_CLAIMS_PROMPT - self._input_text_key = input_text_key or "input_text" - self._input_entity_spec_key = input_entity_spec_key or "entity_specs" - self._tuple_delimiter_key = tuple_delimiter_key or "tuple_delimiter" - self._record_delimiter_key = record_delimiter_key or "record_delimiter" - self._completion_delimiter_key = ( - completion_delimiter_key or "completion_delimiter" - ) - self._input_claim_description_key = ( - input_claim_description_key or "claim_description" - ) - self._input_resolved_entities_key = ( - input_resolved_entities_key or "resolved_entities" - ) - self._max_gleanings = ( - max_gleanings - if max_gleanings is not None - else graphrag_config_defaults.extract_claims.max_gleanings - ) - self._on_error = on_error or (lambda _e, _s, _d: None) - - async def __call__( - self, inputs: dict[str, Any], prompt_variables: dict | None = None - ) -> ClaimExtractorResult: - """Call method definition.""" - if prompt_variables is None: - prompt_variables = {} - texts = inputs[self._input_text_key] - entity_spec = str(inputs[self._input_entity_spec_key]) - claim_description = inputs[self._input_claim_description_key] - resolved_entities = inputs.get(self._input_resolved_entities_key, {}) - source_doc_map = {} - - prompt_args = { - self._input_entity_spec_key: entity_spec, - self._input_claim_description_key: claim_description, - self._tuple_delimiter_key: prompt_variables.get(self._tuple_delimiter_key) - or DEFAULT_TUPLE_DELIMITER, - self._record_delimiter_key: prompt_variables.get(self._record_delimiter_key) - or DEFAULT_RECORD_DELIMITER, - self._completion_delimiter_key: prompt_variables.get( - self._completion_delimiter_key - ) - or DEFAULT_COMPLETION_DELIMITER, - } - - all_claims: list[dict] = [] - for doc_index, text in enumerate(texts): - document_id = f"d{doc_index}" - try: - claims = await self._process_document(prompt_args, text, doc_index) - all_claims += [ - self._clean_claim(c, document_id, resolved_entities) for c in claims - ] - source_doc_map[document_id] = text - except Exception as e: - logger.exception("error extracting claim") - self._on_error( - e, - traceback.format_exc(), - {"doc_index": doc_index, "text": text}, - ) - continue - - return ClaimExtractorResult( - output=all_claims, - source_docs=source_doc_map, - ) - - def _clean_claim( - self, claim: dict, document_id: str, resolved_entities: dict - ) -> dict: - # clean the parsed claims to remove any claims with status = False - obj = claim.get("object_id", claim.get("object")) - subject = claim.get("subject_id", claim.get("subject")) - - # If subject or object in resolved entities, then replace with resolved entity - obj = resolved_entities.get(obj, obj) - subject = resolved_entities.get(subject, subject) - claim["object_id"] = obj - claim["subject_id"] = subject - return claim - - async def _process_document( - self, prompt_args: dict, doc, doc_index: int - ) -> list[dict]: - record_delimiter = prompt_args.get( - self._record_delimiter_key, DEFAULT_RECORD_DELIMITER - ) - completion_delimiter = prompt_args.get( - self._completion_delimiter_key, DEFAULT_COMPLETION_DELIMITER - ) - - response = await self._model.achat( - self._extraction_prompt.format(**{ - self._input_text_key: doc, - **prompt_args, - }) - ) - results = response.output.content or "" - claims = results.strip().removesuffix(completion_delimiter) - - # if gleanings are specified, enter a loop to extract more claims - # there are two exit criteria: (a) we hit the configured max, (b) the model says there are no more claims - if self._max_gleanings > 0: - for i in range(self._max_gleanings): - response = await self._model.achat( - CONTINUE_PROMPT, - name=f"extract-continuation-{i}", - history=response.history, - ) - extension = response.output.content or "" - claims += record_delimiter + extension.strip().removesuffix( - completion_delimiter - ) - - # If this isn't the last loop, check to see if we should continue - if i >= self._max_gleanings - 1: - break - - response = await self._model.achat( - LOOP_PROMPT, - name=f"extract-loopcheck-{i}", - history=response.history, - ) - - if response.output.content != "Y": - break - - return self._parse_claim_tuples(results, prompt_args) - - def _parse_claim_tuples( - self, claims: str, prompt_variables: dict - ) -> list[dict[str, Any]]: - """Parse claim tuples.""" - record_delimiter = prompt_variables.get( - self._record_delimiter_key, DEFAULT_RECORD_DELIMITER - ) - completion_delimiter = prompt_variables.get( - self._completion_delimiter_key, DEFAULT_COMPLETION_DELIMITER - ) - tuple_delimiter = prompt_variables.get( - self._tuple_delimiter_key, DEFAULT_TUPLE_DELIMITER - ) - - def pull_field(index: int, fields: list[str]) -> str | None: - return fields[index].strip() if len(fields) > index else None - - result: list[dict[str, Any]] = [] - claims_values = ( - claims.strip().removesuffix(completion_delimiter).split(record_delimiter) - ) - for claim in claims_values: - claim = claim.strip().removeprefix("(").removesuffix(")") - - # Ignore the completion delimiter - if claim == completion_delimiter: - continue - - claim_fields = claim.split(tuple_delimiter) - result.append({ - "subject_id": pull_field(0, claim_fields), - "object_id": pull_field(1, claim_fields), - "type": pull_field(2, claim_fields), - "status": pull_field(3, claim_fields), - "start_date": pull_field(4, claim_fields), - "end_date": pull_field(5, claim_fields), - "description": pull_field(6, claim_fields), - "source_text": pull_field(7, claim_fields), - }) - return result diff --git a/graphrag/index/operations/extract_graph/graph_extractor.py b/graphrag/index/operations/extract_graph/graph_extractor.py deleted file mode 100644 index d1f66c3e81..0000000000 --- a/graphrag/index/operations/extract_graph/graph_extractor.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing 'GraphExtractionResult' and 'GraphExtractor' models.""" - -import logging -import re -import traceback -from collections.abc import Mapping -from dataclasses import dataclass -from typing import Any - -import networkx as nx - -from graphrag.config.defaults import graphrag_config_defaults -from graphrag.index.typing.error_handler import ErrorHandlerFn -from graphrag.index.utils.string import clean_str -from graphrag.language_model.protocol.base import ChatModel -from graphrag.prompts.index.extract_graph import ( - CONTINUE_PROMPT, - GRAPH_EXTRACTION_PROMPT, - LOOP_PROMPT, -) - -DEFAULT_TUPLE_DELIMITER = "<|>" -DEFAULT_RECORD_DELIMITER = "##" -DEFAULT_COMPLETION_DELIMITER = "<|COMPLETE|>" -DEFAULT_ENTITY_TYPES = ["organization", "person", "geo", "event"] - -logger = logging.getLogger(__name__) - - -@dataclass -class GraphExtractionResult: - """Unipartite graph extraction result class definition.""" - - output: nx.Graph - source_docs: dict[Any, Any] - - -class GraphExtractor: - """Unipartite graph extractor class definition.""" - - _model: ChatModel - _join_descriptions: bool - _tuple_delimiter_key: str - _record_delimiter_key: str - _entity_types_key: str - _input_text_key: str - _completion_delimiter_key: str - _entity_name_key: str - _input_descriptions_key: str - _extraction_prompt: str - _summarization_prompt: str - _max_gleanings: int - _on_error: ErrorHandlerFn - - def __init__( - self, - model_invoker: ChatModel, - tuple_delimiter_key: str | None = None, - record_delimiter_key: str | None = None, - input_text_key: str | None = None, - entity_types_key: str | None = None, - completion_delimiter_key: str | None = None, - prompt: str | None = None, - join_descriptions=True, - max_gleanings: int | None = None, - on_error: ErrorHandlerFn | None = None, - ): - """Init method definition.""" - # TODO: streamline construction - self._model = model_invoker - self._join_descriptions = join_descriptions - self._input_text_key = input_text_key or "input_text" - self._tuple_delimiter_key = tuple_delimiter_key or "tuple_delimiter" - self._record_delimiter_key = record_delimiter_key or "record_delimiter" - self._completion_delimiter_key = ( - completion_delimiter_key or "completion_delimiter" - ) - self._entity_types_key = entity_types_key or "entity_types" - self._extraction_prompt = prompt or GRAPH_EXTRACTION_PROMPT - self._max_gleanings = ( - max_gleanings - if max_gleanings is not None - else graphrag_config_defaults.extract_graph.max_gleanings - ) - self._on_error = on_error or (lambda _e, _s, _d: None) - - async def __call__( - self, texts: list[str], prompt_variables: dict[str, Any] | None = None - ) -> GraphExtractionResult: - """Call method definition.""" - if prompt_variables is None: - prompt_variables = {} - all_records: dict[int, str] = {} - source_doc_map: dict[int, str] = {} - - # Wire defaults into the prompt variables - prompt_variables = { - **prompt_variables, - self._tuple_delimiter_key: prompt_variables.get(self._tuple_delimiter_key) - or DEFAULT_TUPLE_DELIMITER, - self._record_delimiter_key: prompt_variables.get(self._record_delimiter_key) - or DEFAULT_RECORD_DELIMITER, - self._completion_delimiter_key: prompt_variables.get( - self._completion_delimiter_key - ) - or DEFAULT_COMPLETION_DELIMITER, - self._entity_types_key: ",".join( - prompt_variables[self._entity_types_key] or DEFAULT_ENTITY_TYPES - ), - } - - for doc_index, text in enumerate(texts): - try: - # Invoke the entity extraction - result = await self._process_document(text, prompt_variables) - source_doc_map[doc_index] = text - all_records[doc_index] = result - except Exception as e: - logger.exception("error extracting graph") - self._on_error( - e, - traceback.format_exc(), - { - "doc_index": doc_index, - "text": text, - }, - ) - - output = await self._process_results( - all_records, - prompt_variables.get(self._tuple_delimiter_key, DEFAULT_TUPLE_DELIMITER), - prompt_variables.get(self._record_delimiter_key, DEFAULT_RECORD_DELIMITER), - ) - - return GraphExtractionResult( - output=output, - source_docs=source_doc_map, - ) - - async def _process_document( - self, text: str, prompt_variables: dict[str, str] - ) -> str: - response = await self._model.achat( - self._extraction_prompt.format(**{ - **prompt_variables, - self._input_text_key: text, - }), - ) - results = response.output.content or "" - - # if gleanings are specified, enter a loop to extract more entities - # there are two exit criteria: (a) we hit the configured max, (b) the model says there are no more entities - if self._max_gleanings > 0: - for i in range(self._max_gleanings): - response = await self._model.achat( - CONTINUE_PROMPT, - name=f"extract-continuation-{i}", - history=response.history, - ) - results += response.output.content or "" - - # if this is the final glean, don't bother updating the continuation flag - if i >= self._max_gleanings - 1: - break - - response = await self._model.achat( - LOOP_PROMPT, - name=f"extract-loopcheck-{i}", - history=response.history, - ) - if response.output.content != "Y": - break - - return results - - async def _process_results( - self, - results: dict[int, str], - tuple_delimiter: str, - record_delimiter: str, - ) -> nx.Graph: - """Parse the result string to create an undirected unipartite graph. - - Args: - - results - dict of results from the extraction chain - - tuple_delimiter - delimiter between tuples in an output record, default is '<|>' - - record_delimiter - delimiter between records, default is '##' - Returns: - - output - unipartite graph in graphML format - """ - graph = nx.Graph() - for source_doc_id, extracted_data in results.items(): - records = [r.strip() for r in extracted_data.split(record_delimiter)] - - for record in records: - record = re.sub(r"^\(|\)$", "", record.strip()) - record_attributes = record.split(tuple_delimiter) - - if record_attributes[0] == '"entity"' and len(record_attributes) >= 4: - # add this record as a node in the G - entity_name = clean_str(record_attributes[1].upper()) - entity_type = clean_str(record_attributes[2].upper()) - entity_description = clean_str(record_attributes[3]) - - if entity_name in graph.nodes(): - node = graph.nodes[entity_name] - if self._join_descriptions: - node["description"] = "\n".join( - list({ - *_unpack_descriptions(node), - entity_description, - }) - ) - else: - if len(entity_description) > len(node["description"]): - node["description"] = entity_description - node["source_id"] = ", ".join( - list({ - *_unpack_source_ids(node), - str(source_doc_id), - }) - ) - node["type"] = ( - entity_type if entity_type != "" else node["type"] - ) - else: - graph.add_node( - entity_name, - type=entity_type, - description=entity_description, - source_id=str(source_doc_id), - ) - - if ( - record_attributes[0] == '"relationship"' - and len(record_attributes) >= 5 - ): - # add this record as edge - source = clean_str(record_attributes[1].upper()) - target = clean_str(record_attributes[2].upper()) - edge_description = clean_str(record_attributes[3]) - edge_source_id = clean_str(str(source_doc_id)) - try: - weight = float(record_attributes[-1]) - except ValueError: - weight = 1.0 - - if source not in graph.nodes(): - graph.add_node( - source, - type="", - description="", - source_id=edge_source_id, - ) - if target not in graph.nodes(): - graph.add_node( - target, - type="", - description="", - source_id=edge_source_id, - ) - if graph.has_edge(source, target): - edge_data = graph.get_edge_data(source, target) - if edge_data is not None: - weight += edge_data["weight"] - if self._join_descriptions: - edge_description = "\n".join( - list({ - *_unpack_descriptions(edge_data), - edge_description, - }) - ) - edge_source_id = ", ".join( - list({ - *_unpack_source_ids(edge_data), - str(source_doc_id), - }) - ) - graph.add_edge( - source, - target, - weight=weight, - description=edge_description, - source_id=edge_source_id, - ) - - return graph - - -def _unpack_descriptions(data: Mapping) -> list[str]: - value = data.get("description", None) - return [] if value is None else value.split("\n") - - -def _unpack_source_ids(data: Mapping) -> list[str]: - value = data.get("source_id", None) - return [] if value is None else value.split(", ") diff --git a/graphrag/index/operations/extract_graph/graph_intelligence_strategy.py b/graphrag/index/operations/extract_graph/graph_intelligence_strategy.py deleted file mode 100644 index b335d191a6..0000000000 --- a/graphrag/index/operations/extract_graph/graph_intelligence_strategy.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing run_graph_intelligence, run_extract_graph and _create_text_splitter methods to run graph intelligence.""" - -import logging - -import networkx as nx - -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.models.language_model_config import LanguageModelConfig -from graphrag.index.operations.extract_graph.graph_extractor import GraphExtractor -from graphrag.index.operations.extract_graph.typing import ( - Document, - EntityExtractionResult, - EntityTypes, - StrategyConfig, -) -from graphrag.language_model.manager import ModelManager -from graphrag.language_model.protocol.base import ChatModel - -logger = logging.getLogger(__name__) - - -async def run_graph_intelligence( - docs: list[Document], - entity_types: EntityTypes, - cache: PipelineCache, - args: StrategyConfig, -) -> EntityExtractionResult: - """Run the graph intelligence entity extraction strategy.""" - llm_config = LanguageModelConfig(**args["llm"]) - - llm = ModelManager().get_or_create_chat_model( - name="extract_graph", - model_type=llm_config.type, - config=llm_config, - cache=cache, - ) - - return await run_extract_graph(llm, docs, entity_types, args) - - -async def run_extract_graph( - model: ChatModel, - docs: list[Document], - entity_types: EntityTypes, - args: StrategyConfig, -) -> EntityExtractionResult: - """Run the entity extraction chain.""" - tuple_delimiter = args.get("tuple_delimiter", None) - record_delimiter = args.get("record_delimiter", None) - completion_delimiter = args.get("completion_delimiter", None) - extraction_prompt = args.get("extraction_prompt", None) - max_gleanings = args.get( - "max_gleanings", graphrag_config_defaults.extract_graph.max_gleanings - ) - - extractor = GraphExtractor( - model_invoker=model, - prompt=extraction_prompt, - max_gleanings=max_gleanings, - on_error=lambda e, s, d: logger.error( - "Entity Extraction Error", exc_info=e, extra={"stack": s, "details": d} - ), - ) - text_list = [doc.text.strip() for doc in docs] - - results = await extractor( - list(text_list), - { - "entity_types": entity_types, - "tuple_delimiter": tuple_delimiter, - "record_delimiter": record_delimiter, - "completion_delimiter": completion_delimiter, - }, - ) - - graph = results.output - # Map the "source_id" back to the "id" field - for _, node in graph.nodes(data=True): # type: ignore - if node is not None: - node["source_id"] = ",".join( - docs[int(id)].id for id in node["source_id"].split(",") - ) - - for _, _, edge in graph.edges(data=True): # type: ignore - if edge is not None: - edge["source_id"] = ",".join( - docs[int(id)].id for id in edge["source_id"].split(",") - ) - - entities = [ - ({"title": item[0], **(item[1] or {})}) - for item in graph.nodes(data=True) - if item is not None - ] - - relationships = nx.to_pandas_edgelist(graph) - - return EntityExtractionResult(entities, relationships, graph) diff --git a/graphrag/index/operations/extract_graph/typing.py b/graphrag/index/operations/extract_graph/typing.py deleted file mode 100644 index 3c7c134753..0000000000 --- a/graphrag/index/operations/extract_graph/typing.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing 'Document' and 'EntityExtractionResult' models.""" - -from collections.abc import Awaitable, Callable -from dataclasses import dataclass -from enum import Enum -from typing import Any - -import networkx as nx - -from graphrag.cache.pipeline_cache import PipelineCache - -ExtractedEntity = dict[str, Any] -ExtractedRelationship = dict[str, Any] -StrategyConfig = dict[str, Any] -EntityTypes = list[str] - - -@dataclass -class Document: - """Document class definition.""" - - text: str - id: str - - -@dataclass -class EntityExtractionResult: - """Entity extraction result class definition.""" - - entities: list[ExtractedEntity] - relationships: list[ExtractedRelationship] - graph: nx.Graph | None - - -EntityExtractStrategy = Callable[ - [ - list[Document], - EntityTypes, - PipelineCache, - StrategyConfig, - ], - Awaitable[EntityExtractionResult], -] - - -class ExtractEntityStrategyType(str, Enum): - """ExtractEntityStrategyType class definition.""" - - graph_intelligence = "graph_intelligence" - nltk = "nltk" - - def __repr__(self): - """Get a string representation.""" - return f'"{self.value}"' diff --git a/graphrag/index/operations/layout_graph/__init__.py b/graphrag/index/operations/layout_graph/__init__.py deleted file mode 100644 index cc80f99a97..0000000000 --- a/graphrag/index/operations/layout_graph/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""The Indexing Engine graph layout package root.""" diff --git a/graphrag/index/operations/layout_graph/layout_graph.py b/graphrag/index/operations/layout_graph/layout_graph.py deleted file mode 100644 index 433ffc8a10..0000000000 --- a/graphrag/index/operations/layout_graph/layout_graph.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing layout_graph, _run_layout and _apply_layout_to_graph methods definition.""" - -import logging - -import networkx as nx -import pandas as pd - -from graphrag.index.operations.embed_graph.typing import NodeEmbeddings -from graphrag.index.operations.layout_graph.typing import GraphLayout - -logger = logging.getLogger(__name__) - - -def layout_graph( - graph: nx.Graph, - enabled: bool, - embeddings: NodeEmbeddings | None, -): - """ - Apply a layout algorithm to a nx.Graph. The method returns a dataframe containing the node positions. - - ## Usage - ```yaml - args: - graph: The nx.Graph to layout - embeddings: Embeddings for each node in the graph - strategy: # See strategies section below - ``` - - ## Strategies - The layout graph verb uses a strategy to layout the graph. The strategy is a json object which defines the strategy to use. The following strategies are available: - - ### umap - This strategy uses the umap algorithm to layout a graph. The strategy config is as follows: - ```yaml - strategy: - type: umap - n_neighbors: 5 # Optional, The number of neighbors to use for the umap algorithm, default: 5 - min_dist: 0.75 # Optional, The min distance to use for the umap algorithm, default: 0.75 - ``` - """ - layout = _run_layout( - graph, - enabled, - embeddings if embeddings is not None else {}, - ) - - layout_df = pd.DataFrame(layout) - return layout_df.loc[ - :, - ["label", "x", "y", "size"], - ] - - -def _run_layout( - graph: nx.Graph, - enabled: bool, - embeddings: NodeEmbeddings, -) -> GraphLayout: - if enabled: - from graphrag.index.operations.layout_graph.umap import ( - run as run_umap, - ) - - return run_umap( - graph, - embeddings, - lambda e, stack, d: logger.error( - "Error in Umap", exc_info=e, extra={"stack": stack, "details": d} - ), - ) - from graphrag.index.operations.layout_graph.zero import ( - run as run_zero, - ) - - return run_zero( - graph, - lambda e, stack, d: logger.error( - "Error in Zero", exc_info=e, extra={"stack": stack, "details": d} - ), - ) diff --git a/graphrag/index/operations/layout_graph/typing.py b/graphrag/index/operations/layout_graph/typing.py deleted file mode 100644 index ae46afa928..0000000000 --- a/graphrag/index/operations/layout_graph/typing.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -# Use this for now instead of a wrapper -"""A module containing 'NodePosition' model.""" - -from dataclasses import dataclass - - -@dataclass -class NodePosition: - """Node position class definition.""" - - label: str - cluster: str - size: float - - x: float - y: float - z: float | None = None - - def to_pandas(self) -> tuple[str, float, float, str, float]: - """To pandas method definition.""" - return self.label, self.x, self.y, self.cluster, self.size - - -GraphLayout = list[NodePosition] diff --git a/graphrag/index/operations/layout_graph/umap.py b/graphrag/index/operations/layout_graph/umap.py deleted file mode 100644 index 6550188030..0000000000 --- a/graphrag/index/operations/layout_graph/umap.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing run and _create_node_position methods definitions.""" - -import logging -import traceback - -import networkx as nx -import numpy as np - -from graphrag.index.operations.embed_graph.typing import NodeEmbeddings -from graphrag.index.operations.layout_graph.typing import ( - GraphLayout, - NodePosition, -) -from graphrag.index.typing.error_handler import ErrorHandlerFn - -# TODO: This could be handled more elegantly, like what columns to use -# for "size" or "cluster" -# We could also have a boolean to indicate to use node sizes or clusters - -logger = logging.getLogger(__name__) - - -def run( - graph: nx.Graph, - embeddings: NodeEmbeddings, - on_error: ErrorHandlerFn, -) -> GraphLayout: - """Run method definition.""" - node_clusters = [] - node_sizes = [] - - embeddings = _filter_raw_embeddings(embeddings) - nodes = list(embeddings.keys()) - embedding_vectors = [embeddings[node_id] for node_id in nodes] - - for node_id in nodes: - node = graph.nodes[node_id] - cluster = node.get("cluster", node.get("community", -1)) - node_clusters.append(cluster) - size = node.get("degree", node.get("size", 0)) - node_sizes.append(size) - - additional_args = {} - if len(node_clusters) > 0: - additional_args["node_categories"] = node_clusters - if len(node_sizes) > 0: - additional_args["node_sizes"] = node_sizes - - try: - return compute_umap_positions( - embedding_vectors=np.array(embedding_vectors), - node_labels=nodes, - **additional_args, - ) - except Exception as e: - logger.exception("Error running UMAP") - on_error(e, traceback.format_exc(), None) - # Umap may fail due to input sparseness or memory pressure. - # For now, in these cases, we'll just return a layout with all nodes at (0, 0) - result = [] - for i in range(len(nodes)): - cluster = node_clusters[i] if len(node_clusters) > 0 else 1 - result.append( - NodePosition(x=0, y=0, label=nodes[i], size=0, cluster=str(cluster)) - ) - return result - - -def _filter_raw_embeddings(embeddings: NodeEmbeddings) -> NodeEmbeddings: - return { - node_id: embedding - for node_id, embedding in embeddings.items() - if embedding is not None - } - - -def compute_umap_positions( - embedding_vectors: np.ndarray, - node_labels: list[str], - node_categories: list[int] | None = None, - node_sizes: list[int] | None = None, - min_dist: float = 0.75, - n_neighbors: int = 5, - spread: int = 1, - metric: str = "euclidean", - n_components: int = 2, - random_state: int = 86, -) -> list[NodePosition]: - """Project embedding vectors down to 2D/3D using UMAP.""" - # NOTE: This import is done here to reduce the initial import time of the graphrag package - import umap - - embedding_positions = umap.UMAP( - min_dist=min_dist, - n_neighbors=n_neighbors, - spread=spread, - n_components=n_components, - metric=metric, - random_state=random_state, - ).fit_transform(embedding_vectors) - - embedding_position_data: list[NodePosition] = [] - for index, node_name in enumerate(node_labels): - node_points = embedding_positions[index] # type: ignore - node_category = 1 if node_categories is None else node_categories[index] - node_size = 1 if node_sizes is None else node_sizes[index] - - if len(node_points) == 2: - embedding_position_data.append( - NodePosition( - label=str(node_name), - x=float(node_points[0]), - y=float(node_points[1]), - cluster=str(int(node_category)), - size=int(node_size), - ) - ) - else: - embedding_position_data.append( - NodePosition( - label=str(node_name), - x=float(node_points[0]), - y=float(node_points[1]), - z=float(node_points[2]), - cluster=str(int(node_category)), - size=int(node_size), - ) - ) - return embedding_position_data diff --git a/graphrag/index/operations/layout_graph/zero.py b/graphrag/index/operations/layout_graph/zero.py deleted file mode 100644 index 934df0030f..0000000000 --- a/graphrag/index/operations/layout_graph/zero.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing run and _create_node_position methods definitions.""" - -import logging -import traceback - -import networkx as nx - -from graphrag.index.operations.layout_graph.typing import ( - GraphLayout, - NodePosition, -) -from graphrag.index.typing.error_handler import ErrorHandlerFn - -# TODO: This could be handled more elegantly, like what columns to use -# for "size" or "cluster" -# We could also have a boolean to indicate to use node sizes or clusters - -logger = logging.getLogger(__name__) - - -def run( - graph: nx.Graph, - on_error: ErrorHandlerFn, -) -> GraphLayout: - """Run method definition.""" - node_clusters = [] - node_sizes = [] - - nodes = list(graph.nodes) - - for node_id in nodes: - node = graph.nodes[node_id] - cluster = node.get("cluster", node.get("community", -1)) - node_clusters.append(cluster) - size = node.get("degree", node.get("size", 0)) - node_sizes.append(size) - - additional_args = {} - if len(node_clusters) > 0: - additional_args["node_categories"] = node_clusters - if len(node_sizes) > 0: - additional_args["node_sizes"] = node_sizes - - try: - return get_zero_positions(node_labels=nodes, **additional_args) - except Exception as e: - logger.exception("Error running zero-position") - on_error(e, traceback.format_exc(), None) - # Umap may fail due to input sparseness or memory pressure. - # For now, in these cases, we'll just return a layout with all nodes at (0, 0) - result = [] - for i in range(len(nodes)): - cluster = node_clusters[i] if len(node_clusters) > 0 else 1 - result.append( - NodePosition(x=0, y=0, label=nodes[i], size=0, cluster=str(cluster)) - ) - return result - - -def get_zero_positions( - node_labels: list[str], - node_categories: list[int] | None = None, - node_sizes: list[int] | None = None, - three_d: bool | None = False, -) -> list[NodePosition]: - """Project embedding vectors down to 2D/3D using UMAP.""" - embedding_position_data: list[NodePosition] = [] - for index, node_name in enumerate(node_labels): - node_category = 1 if node_categories is None else node_categories[index] - node_size = 1 if node_sizes is None else node_sizes[index] - - if not three_d: - embedding_position_data.append( - NodePosition( - label=str(node_name), - x=0, - y=0, - cluster=str(int(node_category)), - size=int(node_size), - ) - ) - else: - embedding_position_data.append( - NodePosition( - label=str(node_name), - x=0, - y=0, - z=0, - cluster=str(int(node_category)), - size=int(node_size), - ) - ) - return embedding_position_data diff --git a/graphrag/index/operations/summarize_communities/strategies.py b/graphrag/index/operations/summarize_communities/strategies.py deleted file mode 100644 index 06ce44b27d..0000000000 --- a/graphrag/index/operations/summarize_communities/strategies.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing run, _run_extractor and _load_nodes_edges_for_claim_chain methods definition.""" - -import logging - -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks -from graphrag.config.models.language_model_config import LanguageModelConfig -from graphrag.index.operations.summarize_communities.community_reports_extractor import ( - CommunityReportsExtractor, -) -from graphrag.index.operations.summarize_communities.typing import ( - CommunityReport, - Finding, - StrategyConfig, -) -from graphrag.language_model.manager import ModelManager -from graphrag.language_model.protocol.base import ChatModel - -logger = logging.getLogger(__name__) - - -async def run_graph_intelligence( - community: str | int, - input: str, - level: int, - callbacks: WorkflowCallbacks, - cache: PipelineCache, - args: StrategyConfig, -) -> CommunityReport | None: - """Run the graph intelligence entity extraction strategy.""" - llm_config = LanguageModelConfig(**args["llm"]) - llm = ModelManager().get_or_create_chat_model( - name="community_reporting", - model_type=llm_config.type, - config=llm_config, - callbacks=callbacks, - cache=cache, - ) - - return await _run_extractor(llm, community, input, level, args) - - -async def _run_extractor( - model: ChatModel, - community: str | int, - input: str, - level: int, - args: StrategyConfig, -) -> CommunityReport | None: - extractor = CommunityReportsExtractor( - model, - extraction_prompt=args.get("extraction_prompt", None), - max_report_length=args.get("max_report_length", None), - on_error=lambda e, stack, _data: logger.error( - "Community Report Extraction Error", exc_info=e, extra={"stack": stack} - ), - ) - - try: - results = await extractor(input) - report = results.structured_output - if report is None: - logger.warning("No report found for community: %s", community) - return None - - return CommunityReport( - community=community, - full_content=results.output, - level=level, - rank=report.rating, - title=report.title, - rating_explanation=report.rating_explanation, - summary=report.summary, - findings=[ - Finding(explanation=f.explanation, summary=f.summary) - for f in report.findings - ], - full_content_json=report.model_dump_json(indent=4), - ) - except Exception: - logger.exception("Error processing community: %s", community) - return None diff --git a/graphrag/index/operations/summarize_descriptions/graph_intelligence_strategy.py b/graphrag/index/operations/summarize_descriptions/graph_intelligence_strategy.py deleted file mode 100644 index d3259b290f..0000000000 --- a/graphrag/index/operations/summarize_descriptions/graph_intelligence_strategy.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing run_graph_intelligence, run_resolve_entities and _create_text_list_splitter methods to run graph intelligence.""" - -import logging - -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.config.models.language_model_config import LanguageModelConfig -from graphrag.index.operations.summarize_descriptions.description_summary_extractor import ( - SummarizeExtractor, -) -from graphrag.index.operations.summarize_descriptions.typing import ( - StrategyConfig, - SummarizedDescriptionResult, -) -from graphrag.language_model.manager import ModelManager -from graphrag.language_model.protocol.base import ChatModel - -logger = logging.getLogger(__name__) - - -async def run_graph_intelligence( - id: str | tuple[str, str], - descriptions: list[str], - cache: PipelineCache, - args: StrategyConfig, -) -> SummarizedDescriptionResult: - """Run the graph intelligence entity extraction strategy.""" - llm_config = LanguageModelConfig(**args["llm"]) - llm = ModelManager().get_or_create_chat_model( - name="summarize_descriptions", - model_type=llm_config.type, - config=llm_config, - cache=cache, - ) - - return await run_summarize_descriptions(llm, id, descriptions, args) - - -async def run_summarize_descriptions( - model: ChatModel, - id: str | tuple[str, str], - descriptions: list[str], - args: StrategyConfig, -) -> SummarizedDescriptionResult: - """Run the entity extraction chain.""" - # Extraction Arguments - summarize_prompt = args.get("summarize_prompt", None) - max_input_tokens = args["max_input_tokens"] - max_summary_length = args["max_summary_length"] - extractor = SummarizeExtractor( - model_invoker=model, - summarization_prompt=summarize_prompt, - on_error=lambda e, stack, details: logger.error( - "Entity Extraction Error", - exc_info=e, - extra={"stack": stack, "details": details}, - ), - max_summary_length=max_summary_length, - max_input_tokens=max_input_tokens, - ) - - result = await extractor(id=id, descriptions=descriptions) - return SummarizedDescriptionResult(id=result.id, description=result.description) diff --git a/graphrag/index/operations/summarize_descriptions/typing.py b/graphrag/index/operations/summarize_descriptions/typing.py deleted file mode 100644 index 55b079090d..0000000000 --- a/graphrag/index/operations/summarize_descriptions/typing.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing 'SummarizedDescriptionResult' model.""" - -from collections.abc import Awaitable, Callable -from dataclasses import dataclass -from enum import Enum -from typing import Any, NamedTuple - -from graphrag.cache.pipeline_cache import PipelineCache - -StrategyConfig = dict[str, Any] - - -@dataclass -class SummarizedDescriptionResult: - """Entity summarization result class definition.""" - - id: str | tuple[str, str] - description: str - - -SummarizationStrategy = Callable[ - [ - str | tuple[str, str], - list[str], - PipelineCache, - StrategyConfig, - ], - Awaitable[SummarizedDescriptionResult], -] - - -class DescriptionSummarizeRow(NamedTuple): - """DescriptionSummarizeRow class definition.""" - - graph: Any - - -class SummarizeStrategyType(str, Enum): - """SummarizeStrategyType class definition.""" - - graph_intelligence = "graph_intelligence" - - def __repr__(self): - """Get a string representation.""" - return f'"{self.value}"' diff --git a/graphrag/index/text_splitting/check_token_limit.py b/graphrag/index/text_splitting/check_token_limit.py deleted file mode 100644 index 7b6a139e02..0000000000 --- a/graphrag/index/text_splitting/check_token_limit.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Token limit method definition.""" - -from graphrag.index.text_splitting.text_splitting import TokenTextSplitter - - -def check_token_limit(text, max_token): - """Check token limit.""" - text_splitter = TokenTextSplitter(chunk_size=max_token, chunk_overlap=0) - docs = text_splitter.split_text(text) - if len(docs) > 1: - return 0 - return 1 diff --git a/graphrag/index/text_splitting/text_splitting.py b/graphrag/index/text_splitting/text_splitting.py deleted file mode 100644 index f1f0785e99..0000000000 --- a/graphrag/index/text_splitting/text_splitting.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing the 'Tokenizer', 'TextSplitter', 'NoopTextSplitter' and 'TokenTextSplitter' models.""" - -import logging -from abc import ABC, abstractmethod -from collections.abc import Callable, Iterable -from dataclasses import dataclass -from typing import Any, cast - -import pandas as pd - -from graphrag.index.operations.chunk_text.typing import TextChunk -from graphrag.logger.progress import ProgressTicker -from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer - -EncodedText = list[int] -DecodeFn = Callable[[EncodedText], str] -EncodeFn = Callable[[str], EncodedText] -LengthFn = Callable[[str], int] - -logger = logging.getLogger(__name__) - - -@dataclass(frozen=True) -class TokenChunkerOptions: - """TokenChunkerOptions data class.""" - - chunk_overlap: int - """Overlap in tokens between chunks""" - tokens_per_chunk: int - """Maximum number of tokens per chunk""" - decode: DecodeFn - """ Function to decode a list of token ids to a string""" - encode: EncodeFn - """ Function to encode a string to a list of token ids""" - - -class TextSplitter(ABC): - """Text splitter class definition.""" - - _chunk_size: int - _chunk_overlap: int - _length_function: LengthFn - _keep_separator: bool - _add_start_index: bool - _strip_whitespace: bool - - def __init__( - self, - # based on text-ada-002-embedding max input buffer length - # https://platform.openai.com/docs/guides/embeddings/second-generation-models - chunk_size: int = 8191, - chunk_overlap: int = 100, - length_function: LengthFn = len, - keep_separator: bool = False, - add_start_index: bool = False, - strip_whitespace: bool = True, - ): - """Init method definition.""" - self._chunk_size = chunk_size - self._chunk_overlap = chunk_overlap - self._length_function = length_function - self._keep_separator = keep_separator - self._add_start_index = add_start_index - self._strip_whitespace = strip_whitespace - - @abstractmethod - def split_text(self, text: str | list[str]) -> Iterable[str]: - """Split text method definition.""" - - -class NoopTextSplitter(TextSplitter): - """Noop text splitter class definition.""" - - def split_text(self, text: str | list[str]) -> Iterable[str]: - """Split text method definition.""" - return [text] if isinstance(text, str) else text - - -class TokenTextSplitter(TextSplitter): - """Token text splitter class definition.""" - - def __init__( - self, - tokenizer: Tokenizer | None = None, - **kwargs: Any, - ): - """Init method definition.""" - super().__init__(**kwargs) - self._tokenizer = tokenizer or get_tokenizer() - - def num_tokens(self, text: str) -> int: - """Return the number of tokens in a string.""" - return self._tokenizer.num_tokens(text) - - def split_text(self, text: str | list[str]) -> list[str]: - """Split text method.""" - if isinstance(text, list): - text = " ".join(text) - elif cast("bool", pd.isna(text)) or text == "": - return [] - if not isinstance(text, str): - msg = f"Attempting to split a non-string value, actual is {type(text)}" - raise TypeError(msg) - - token_chunker_options = TokenChunkerOptions( - chunk_overlap=self._chunk_overlap, - tokens_per_chunk=self._chunk_size, - decode=self._tokenizer.decode, - encode=self._tokenizer.encode, - ) - - return split_single_text_on_tokens(text=text, tokenizer=token_chunker_options) - - -def split_single_text_on_tokens(text: str, tokenizer: TokenChunkerOptions) -> list[str]: - """Split a single text and return chunks using the tokenizer.""" - result = [] - input_ids = tokenizer.encode(text) - - start_idx = 0 - cur_idx = min(start_idx + tokenizer.tokens_per_chunk, len(input_ids)) - chunk_ids = input_ids[start_idx:cur_idx] - - while start_idx < len(input_ids): - chunk_text = tokenizer.decode(list(chunk_ids)) - result.append(chunk_text) # Append chunked text as string - if cur_idx == len(input_ids): - break - start_idx += tokenizer.tokens_per_chunk - tokenizer.chunk_overlap - cur_idx = min(start_idx + tokenizer.tokens_per_chunk, len(input_ids)) - chunk_ids = input_ids[start_idx:cur_idx] - - return result - - -# Adapted from - https://github.com/langchain-ai/langchain/blob/77b359edf5df0d37ef0d539f678cf64f5557cb54/libs/langchain/langchain/text_splitter.py#L471 -# So we could have better control over the chunking process -def split_multiple_texts_on_tokens( - texts: list[str], tokenizer: TokenChunkerOptions, tick: ProgressTicker -) -> list[TextChunk]: - """Split multiple texts and return chunks with metadata using the tokenizer.""" - result = [] - mapped_ids = [] - - for source_doc_idx, text in enumerate(texts): - encoded = tokenizer.encode(text) - if tick: - tick(1) # Track progress if tick callback is provided - mapped_ids.append((source_doc_idx, encoded)) - - input_ids = [ - (source_doc_idx, id) for source_doc_idx, ids in mapped_ids for id in ids - ] - - start_idx = 0 - cur_idx = min(start_idx + tokenizer.tokens_per_chunk, len(input_ids)) - chunk_ids = input_ids[start_idx:cur_idx] - - while start_idx < len(input_ids): - chunk_text = tokenizer.decode([id for _, id in chunk_ids]) - doc_indices = list({doc_idx for doc_idx, _ in chunk_ids}) - result.append(TextChunk(chunk_text, doc_indices, len(chunk_ids))) - if cur_idx == len(input_ids): - break - start_idx += tokenizer.tokens_per_chunk - tokenizer.chunk_overlap - cur_idx = min(start_idx + tokenizer.tokens_per_chunk, len(input_ids)) - chunk_ids = input_ids[start_idx:cur_idx] - - return result diff --git a/graphrag/index/validate_config.py b/graphrag/index/validate_config.py deleted file mode 100644 index 2ba4ec1efe..0000000000 --- a/graphrag/index/validate_config.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing validate_config_names definition.""" - -import asyncio -import logging -import sys - -from graphrag.callbacks.noop_workflow_callbacks import NoopWorkflowCallbacks -from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.language_model.manager import ModelManager - -logger = logging.getLogger(__name__) - - -def validate_config_names(parameters: GraphRagConfig) -> None: - """Validate config file for model deployment name typos, by running a quick test message for each.""" - for id, config in parameters.models.items(): - if config.type in ["chat", "azure_openai", "openai"]: - llm = ModelManager().register_chat( - name="test-llm", - model_type=config.type, - config=config, - callbacks=NoopWorkflowCallbacks(), - cache=None, - ) - try: - asyncio.run( - llm.achat("This is an LLM connectivity test. Say Hello World") - ) - logger.info("LLM Config Params Validated") - except Exception as e: # noqa: BLE001 - logger.error(f"LLM configuration error detected.\n{e}") # noqa - print(f"Failed to validate language model ({id}) params", e) # noqa: T201 - sys.exit(1) - elif config.type in ["embedding", "azure_openai_embedding", "openai_embedding"]: - embed_llm = ModelManager().register_embedding( - name="test-embed-llm", - model_type=config.type, - config=config, - callbacks=NoopWorkflowCallbacks(), - cache=None, - ) - try: - asyncio.run( - embed_llm.aembed_batch(["This is an LLM Embedding Test String"]) - ) - logger.info("Embedding LLM Config Params Validated") - except Exception as e: # noqa: BLE001 - logger.error(f"Embedding configuration error detected.\n{e}") # noqa - print(f"Failed to validate embedding model ({id}) params", e) # noqa: T201 - sys.exit(1) diff --git a/graphrag/index/workflows/create_base_text_units.py b/graphrag/index/workflows/create_base_text_units.py deleted file mode 100644 index d94ee5951f..0000000000 --- a/graphrag/index/workflows/create_base_text_units.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing run_workflow method definition.""" - -import json -import logging -from typing import Any, cast - -import pandas as pd - -from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks -from graphrag.config.models.chunking_config import ChunkStrategyType -from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.index.operations.chunk_text.chunk_text import chunk_text -from graphrag.index.operations.chunk_text.strategies import get_encoding_fn -from graphrag.index.typing.context import PipelineRunContext -from graphrag.index.typing.workflow import WorkflowFunctionOutput -from graphrag.index.utils.hashing import gen_sha512_hash -from graphrag.utils.storage import load_table_from_storage, write_table_to_storage - -logger = logging.getLogger(__name__) - - -async def run_workflow( - config: GraphRagConfig, - context: PipelineRunContext, -) -> WorkflowFunctionOutput: - """All the steps to transform base text_units.""" - logger.info("Workflow started: create_base_text_units") - documents = await load_table_from_storage("documents", context.output_storage) - - chunks = config.chunks - - output = create_base_text_units( - documents, - context.callbacks, - chunks.group_by_columns, - chunks.size, - chunks.overlap, - chunks.encoding_model, - strategy=chunks.strategy, - prepend_metadata=chunks.prepend_metadata, - chunk_size_includes_metadata=chunks.chunk_size_includes_metadata, - ) - - await write_table_to_storage(output, "text_units", context.output_storage) - - logger.info("Workflow completed: create_base_text_units") - return WorkflowFunctionOutput(result=output) - - -def create_base_text_units( - documents: pd.DataFrame, - callbacks: WorkflowCallbacks, - group_by_columns: list[str], - size: int, - overlap: int, - encoding_model: str, - strategy: ChunkStrategyType, - prepend_metadata: bool = False, - chunk_size_includes_metadata: bool = False, -) -> pd.DataFrame: - """All the steps to transform base text_units.""" - sort = documents.sort_values(by=["id"], ascending=[True]) - - sort["text_with_ids"] = list( - zip(*[sort[col] for col in ["id", "text"]], strict=True) - ) - - agg_dict = {"text_with_ids": list} - if "metadata" in documents: - agg_dict["metadata"] = "first" # type: ignore - - aggregated = ( - ( - sort.groupby(group_by_columns, sort=False) - if len(group_by_columns) > 0 - else sort.groupby(lambda _x: True) - ) - .agg(agg_dict) - .reset_index() - ) - aggregated.rename(columns={"text_with_ids": "texts"}, inplace=True) - - def chunker(row: pd.Series) -> Any: - line_delimiter = ".\n" - metadata_str = "" - metadata_tokens = 0 - - if prepend_metadata and "metadata" in row: - metadata = row["metadata"] - if isinstance(metadata, str): - metadata = json.loads(metadata) - if isinstance(metadata, dict): - metadata_str = ( - line_delimiter.join(f"{k}: {v}" for k, v in metadata.items()) - + line_delimiter - ) - - if chunk_size_includes_metadata: - encode, _ = get_encoding_fn(encoding_model) - metadata_tokens = len(encode(metadata_str)) - if metadata_tokens >= size: - message = "Metadata tokens exceeds the maximum tokens per chunk. Please increase the tokens per chunk." - raise ValueError(message) - - chunked = chunk_text( - pd.DataFrame([row]).reset_index(drop=True), - column="texts", - size=size - metadata_tokens, - overlap=overlap, - encoding_model=encoding_model, - strategy=strategy, - callbacks=callbacks, - )[0] - - if prepend_metadata: - for index, chunk in enumerate(chunked): - if isinstance(chunk, str): - chunked[index] = metadata_str + chunk - else: - chunked[index] = ( - (chunk[0], metadata_str + chunk[1], chunk[2]) if chunk else None - ) - - row["chunks"] = chunked - return row - - # Track progress of row-wise apply operation - total_rows = len(aggregated) - logger.info("Starting chunking process for %d documents", total_rows) - - def chunker_with_logging(row: pd.Series, row_index: int) -> Any: - """Add logging to chunker execution.""" - result = chunker(row) - logger.info("chunker progress: %d/%d", row_index + 1, total_rows) - return result - - aggregated = aggregated.apply( - lambda row: chunker_with_logging(row, row.name), axis=1 - ) - - aggregated = cast("pd.DataFrame", aggregated[[*group_by_columns, "chunks"]]) - aggregated = aggregated.explode("chunks") - aggregated.rename( - columns={ - "chunks": "chunk", - }, - inplace=True, - ) - aggregated["id"] = aggregated.apply( - lambda row: gen_sha512_hash(row, ["chunk"]), axis=1 - ) - aggregated[["document_ids", "chunk", "n_tokens"]] = pd.DataFrame( - aggregated["chunk"].tolist(), index=aggregated.index - ) - # rename for downstream consumption - aggregated.rename(columns={"chunk": "text"}, inplace=True) - - return cast( - "pd.DataFrame", aggregated[aggregated["text"].notna()].reset_index(drop=True) - ) diff --git a/graphrag/language_model/__init__.py b/graphrag/language_model/__init__.py deleted file mode 100644 index 1c84bfd23a..0000000000 --- a/graphrag/language_model/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""GraphRAG Language Models module. Allows for provider registrations while providing some out-of-the-box solutions.""" diff --git a/graphrag/language_model/cache/__init__.py b/graphrag/language_model/cache/__init__.py deleted file mode 100644 index 41cca7905f..0000000000 --- a/graphrag/language_model/cache/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Cache provider definitions for Language Models.""" diff --git a/graphrag/language_model/cache/base.py b/graphrag/language_model/cache/base.py deleted file mode 100644 index 554d02c8c5..0000000000 --- a/graphrag/language_model/cache/base.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Base cache protocol definition.""" - -from typing import Any, Protocol - - -class ModelCache(Protocol): - """Base cache protocol.""" - - async def has(self, key: str) -> bool: - """Check if the cache has a value.""" - ... - - async def get(self, key: str) -> Any | None: - """Retrieve a value from the cache.""" - ... - - async def set( - self, key: str, value: Any, metadata: dict[str, Any] | None = None - ) -> None: - """Write a value into the cache.""" - ... - - async def remove(self, key: str) -> None: - """Remove a value from the cache.""" - ... - - async def clear(self) -> None: - """Clear the cache.""" - ... - - def child(self, key: str) -> Any: - """Create a child cache.""" - ... diff --git a/graphrag/language_model/events/__init__.py b/graphrag/language_model/events/__init__.py deleted file mode 100644 index c6abec322f..0000000000 --- a/graphrag/language_model/events/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Model Event handler modules.""" diff --git a/graphrag/language_model/events/base.py b/graphrag/language_model/events/base.py deleted file mode 100644 index 940da3f1ff..0000000000 --- a/graphrag/language_model/events/base.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Base model events protocol.""" - -from typing import Any, Protocol - - -class ModelEventHandler(Protocol): - """Protocol for Model event handling.""" - - async def on_error( - self, - error: BaseException | None, - traceback: str | None = None, - arguments: dict[str, Any] | None = None, - ) -> None: - """Handle an model error.""" - ... diff --git a/graphrag/language_model/factory.py b/graphrag/language_model/factory.py deleted file mode 100644 index 92bee9dde8..0000000000 --- a/graphrag/language_model/factory.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""A package containing a factory for supported llm types.""" - -from collections.abc import Callable -from typing import Any, ClassVar - -from graphrag.config.enums import ModelType -from graphrag.language_model.protocol.base import ChatModel, EmbeddingModel -from graphrag.language_model.providers.fnllm.models import ( - AzureOpenAIChatFNLLM, - AzureOpenAIEmbeddingFNLLM, - OpenAIChatFNLLM, - OpenAIEmbeddingFNLLM, -) -from graphrag.language_model.providers.litellm.chat_model import LitellmChatModel -from graphrag.language_model.providers.litellm.embedding_model import ( - LitellmEmbeddingModel, -) - - -class ModelFactory: - """A factory for creating Model instances.""" - - _chat_registry: ClassVar[dict[str, Callable[..., ChatModel]]] = {} - _embedding_registry: ClassVar[dict[str, Callable[..., EmbeddingModel]]] = {} - - @classmethod - def register_chat(cls, model_type: str, creator: Callable[..., ChatModel]) -> None: - """Register a ChatModel implementation.""" - cls._chat_registry[model_type] = creator - - @classmethod - def register_embedding( - cls, model_type: str, creator: Callable[..., EmbeddingModel] - ) -> None: - """Register an EmbeddingModel implementation.""" - cls._embedding_registry[model_type] = creator - - @classmethod - def create_chat_model(cls, model_type: str, **kwargs: Any) -> ChatModel: - """ - Create a ChatModel instance. - - Args: - model_type: The type of ChatModel to create. - **kwargs: Additional keyword arguments for the ChatModel constructor. - - Returns - ------- - A ChatModel instance. - """ - if model_type not in cls._chat_registry: - msg = f"ChatMOdel implementation '{model_type}' is not registered." - raise ValueError(msg) - return cls._chat_registry[model_type](**kwargs) - - @classmethod - def create_embedding_model(cls, model_type: str, **kwargs: Any) -> EmbeddingModel: - """ - Create an EmbeddingModel instance. - - Args: - model_type: The type of EmbeddingModel to create. - **kwargs: Additional keyword arguments for the EmbeddingLLM constructor. - - Returns - ------- - An EmbeddingLLM instance. - """ - if model_type not in cls._embedding_registry: - msg = f"EmbeddingModel implementation '{model_type}' is not registered." - raise ValueError(msg) - return cls._embedding_registry[model_type](**kwargs) - - @classmethod - def get_chat_models(cls) -> list[str]: - """Get the registered ChatModel implementations.""" - return list(cls._chat_registry.keys()) - - @classmethod - def get_embedding_models(cls) -> list[str]: - """Get the registered EmbeddingModel implementations.""" - return list(cls._embedding_registry.keys()) - - @classmethod - def is_supported_chat_model(cls, model_type: str) -> bool: - """Check if the given model type is supported.""" - return model_type in cls._chat_registry - - @classmethod - def is_supported_embedding_model(cls, model_type: str) -> bool: - """Check if the given model type is supported.""" - return model_type in cls._embedding_registry - - @classmethod - def is_supported_model(cls, model_type: str) -> bool: - """Check if the given model type is supported.""" - return cls.is_supported_chat_model( - model_type - ) or cls.is_supported_embedding_model(model_type) - - -# --- Register default implementations --- -ModelFactory.register_chat( - ModelType.AzureOpenAIChat.value, lambda **kwargs: AzureOpenAIChatFNLLM(**kwargs) -) -ModelFactory.register_chat( - ModelType.OpenAIChat.value, lambda **kwargs: OpenAIChatFNLLM(**kwargs) -) -ModelFactory.register_chat(ModelType.Chat, lambda **kwargs: LitellmChatModel(**kwargs)) - -ModelFactory.register_embedding( - ModelType.AzureOpenAIEmbedding.value, - lambda **kwargs: AzureOpenAIEmbeddingFNLLM(**kwargs), -) -ModelFactory.register_embedding( - ModelType.OpenAIEmbedding.value, lambda **kwargs: OpenAIEmbeddingFNLLM(**kwargs) -) -ModelFactory.register_embedding( - ModelType.Embedding, lambda **kwargs: LitellmEmbeddingModel(**kwargs) -) diff --git a/graphrag/language_model/manager.py b/graphrag/language_model/manager.py deleted file mode 100644 index bc41235dda..0000000000 --- a/graphrag/language_model/manager.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Singleton LLM Manager for ChatLLM and EmbeddingsLLM instances. - -This manager lets you register chat and embeddings LLMs independently. -It leverages the LLMFactory for instantiation. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, ClassVar - -from typing_extensions import Self - -from graphrag.language_model.factory import ModelFactory - -if TYPE_CHECKING: - from graphrag.language_model.protocol.base import ChatModel, EmbeddingModel - - -class ModelManager: - """Singleton manager for LLM instances.""" - - _instance: ClassVar[ModelManager | None] = None - - def __new__(cls) -> Self: - """Create a new instance of LLMManager if it does not exist.""" - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance # type: ignore[return-value] - - def __init__(self) -> None: - # Avoid reinitialization in the singleton. - if not hasattr(self, "_initialized"): - self.chat_models: dict[str, ChatModel] = {} - self.embedding_models: dict[str, EmbeddingModel] = {} - self._initialized = True - - @classmethod - def get_instance(cls) -> ModelManager: - """Return the singleton instance of LLMManager.""" - return cls.__new__(cls) - - def register_chat( - self, name: str, model_type: str, **chat_kwargs: Any - ) -> ChatModel: - """ - Register a ChatLLM instance under a unique name. - - Args: - name: Unique identifier for the ChatLLM instance. - model_type: Key for the ChatLLM implementation in LLMFactory. - **chat_kwargs: Additional parameters for instantiation. - """ - chat_kwargs["name"] = name - self.chat_models[name] = ModelFactory.create_chat_model( - model_type, **chat_kwargs - ) - return self.chat_models[name] - - def register_embedding( - self, name: str, model_type: str, **embedding_kwargs: Any - ) -> EmbeddingModel: - """ - Register an EmbeddingsLLM instance under a unique name. - - Args: - name: Unique identifier for the EmbeddingsLLM instance. - embedding_key: Key for the EmbeddingsLLM implementation in LLMFactory. - **embedding_kwargs: Additional parameters for instantiation. - """ - embedding_kwargs["name"] = name - self.embedding_models[name] = ModelFactory.create_embedding_model( - model_type, **embedding_kwargs - ) - return self.embedding_models[name] - - def get_chat_model(self, name: str) -> ChatModel | None: - """ - Retrieve the ChatLLM instance registered under the given name. - - Raises - ------ - ValueError: If no ChatLLM is registered under the name. - """ - if name not in self.chat_models: - msg = f"No ChatLLM registered under the name '{name}'." - raise ValueError(msg) - return self.chat_models[name] - - def get_embedding_model(self, name: str) -> EmbeddingModel | None: - """ - Retrieve the EmbeddingsLLM instance registered under the given name. - - Raises - ------ - ValueError: If no EmbeddingsLLM is registered under the name. - """ - if name not in self.embedding_models: - msg = f"No EmbeddingsLLM registered under the name '{name}'." - raise ValueError(msg) - return self.embedding_models[name] - - def get_or_create_chat_model( - self, name: str, model_type: str, **chat_kwargs: Any - ) -> ChatModel: - """ - Retrieve the ChatLLM instance registered under the given name. - - If the ChatLLM does not exist, it is created and registered. - - Args: - name: Unique identifier for the ChatLLM instance. - model_type: Key for the ChatModel implementation in LLMFactory. - **chat_kwargs: Additional parameters for instantiation. - """ - if name not in self.chat_models: - return self.register_chat(name, model_type, **chat_kwargs) - return self.chat_models[name] - - def get_or_create_embedding_model( - self, name: str, model_type: str, **embedding_kwargs: Any - ) -> EmbeddingModel: - """ - Retrieve the EmbeddingsLLM instance registered under the given name. - - If the EmbeddingsLLM does not exist, it is created and registered. - - Args: - name: Unique identifier for the EmbeddingsLLM instance. - model_type: Key for the EmbeddingsLLM implementation in LLMFactory. - **embedding_kwargs: Additional parameters for instantiation. - """ - if name not in self.embedding_models: - return self.register_embedding(name, model_type, **embedding_kwargs) - return self.embedding_models[name] - - def remove_chat(self, name: str) -> None: - """Remove the ChatLLM instance registered under the given name.""" - self.chat_models.pop(name, None) - - def remove_embedding(self, name: str) -> None: - """Remove the EmbeddingsLLM instance registered under the given name.""" - self.embedding_models.pop(name, None) - - def list_chat_models(self) -> dict[str, ChatModel]: - """Return a copy of all registered ChatLLM instances.""" - return dict(self.chat_models) - - def list_embedding_models(self) -> dict[str, EmbeddingModel]: - """Return a copy of all registered EmbeddingsLLM instances.""" - return dict(self.embedding_models) diff --git a/graphrag/language_model/protocol/__init__.py b/graphrag/language_model/protocol/__init__.py deleted file mode 100644 index 12432bd1f5..0000000000 --- a/graphrag/language_model/protocol/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Base protocol definitions for LLMs.""" diff --git a/graphrag/language_model/protocol/base.py b/graphrag/language_model/protocol/base.py deleted file mode 100644 index 74cd38746e..0000000000 --- a/graphrag/language_model/protocol/base.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Base llm protocol definitions.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Protocol - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Generator - - from graphrag.config.models.language_model_config import LanguageModelConfig - from graphrag.language_model.response.base import ModelResponse - - -class EmbeddingModel(Protocol): - """ - Protocol for an embedding-based Language Model (LM). - - This protocol defines the methods required for an embedding-based LM. - """ - - config: LanguageModelConfig - """Passthrough of the config used to create the model instance.""" - - async def aembed_batch( - self, text_list: list[str], **kwargs: Any - ) -> list[list[float]]: - """ - Generate an embedding vector for the given list of strings. - - Args: - text: The text to generate an embedding for. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - A collections of list of floats representing the embedding vector for each item in the batch. - """ - ... - - async def aembed(self, text: str, **kwargs: Any) -> list[float]: - """ - Generate an embedding vector for the given text. - - Args: - text: The text to generate an embedding for. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - A list of floats representing the embedding vector. - """ - ... - - def embed_batch(self, text_list: list[str], **kwargs: Any) -> list[list[float]]: - """ - Generate an embedding vector for the given list of strings. - - Args: - text: The text to generate an embedding for. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - A collections of list of floats representing the embedding vector for each item in the batch. - """ - ... - - def embed(self, text: str, **kwargs: Any) -> list[float]: - """ - Generate an embedding vector for the given text. - - Args: - text: The text to generate an embedding for. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - A list of floats representing the embedding vector. - """ - ... - - -class ChatModel(Protocol): - """ - Protocol for a chat-based Language Model (LM). - - This protocol defines the methods required for a chat-based LM. - Prompt is always required for the chat method, and any other keyword arguments are forwarded to the Model provider. - """ - - config: LanguageModelConfig - """Passthrough of the config used to create the model instance.""" - - async def achat( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> ModelResponse: - """ - Generate a response for the given text. - - Args: - prompt: The text to generate a response for. - history: The conversation history. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - A string representing the response. - - """ - ... - - async def achat_stream( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> AsyncGenerator[str, None]: - """ - Generate a response for the given text using a streaming interface. - - Args: - prompt: The text to generate a response for. - history: The conversation history. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - A generator that yields strings representing the response. - """ - yield "" # Yield an empty string so that the function is recognized as a generator - ... - - def chat( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> ModelResponse: - """ - Generate a response for the given text. - - Args: - prompt: The text to generate a response for. - history: The conversation history. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - A string representing the response. - - """ - ... - - def chat_stream( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> Generator[str, None]: - """ - Generate a response for the given text using a streaming interface. - - Args: - prompt: The text to generate a response for. - history: The conversation history. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - A generator that yields strings representing the response. - """ - ... diff --git a/graphrag/language_model/providers/__init__.py b/graphrag/language_model/providers/__init__.py deleted file mode 100644 index d635f898ba..0000000000 --- a/graphrag/language_model/providers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Model Providers module.""" diff --git a/graphrag/language_model/providers/fnllm/cache.py b/graphrag/language_model/providers/fnllm/cache.py deleted file mode 100644 index 4554f1d158..0000000000 --- a/graphrag/language_model/providers/fnllm/cache.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""FNLLM Cache provider.""" - -from typing import Any - -from fnllm.caching import Cache as FNLLMCache - -from graphrag.cache.pipeline_cache import PipelineCache - - -class FNLLMCacheProvider(FNLLMCache): - """A cache for the pipeline.""" - - def __init__(self, cache: PipelineCache): - self._cache = cache - - async def has(self, key: str) -> bool: - """Check if the cache has a value.""" - return await self._cache.has(key) - - async def get(self, key: str) -> Any | None: - """Retrieve a value from the cache.""" - return await self._cache.get(key) - - async def set( - self, key: str, value: Any, metadata: dict[str, Any] | None = None - ) -> None: - """Write a value into the cache.""" - await self._cache.set(key, value, metadata) - - async def remove(self, key: str) -> None: - """Remove a value from the cache.""" - await self._cache.delete(key) - - async def clear(self) -> None: - """Clear the cache.""" - await self._cache.clear() - - def child(self, key: str) -> "FNLLMCacheProvider": - """Create a child cache.""" - child_cache = self._cache.child(key) - return FNLLMCacheProvider(child_cache) diff --git a/graphrag/language_model/providers/fnllm/events.py b/graphrag/language_model/providers/fnllm/events.py deleted file mode 100644 index e6f75eafe8..0000000000 --- a/graphrag/language_model/providers/fnllm/events.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""FNLLM llm events provider.""" - -from typing import Any - -from fnllm.events import LLMEvents - -from graphrag.index.typing.error_handler import ErrorHandlerFn - - -class FNLLMEvents(LLMEvents): - """FNLLM events handler that calls the error handler.""" - - def __init__(self, on_error: ErrorHandlerFn): - self._on_error = on_error - - async def on_error( - self, - error: BaseException | None, - traceback: str | None = None, - arguments: dict[str, Any] | None = None, - ) -> None: - """Handle an fnllm error.""" - self._on_error(error, traceback, arguments) diff --git a/graphrag/language_model/providers/fnllm/models.py b/graphrag/language_model/providers/fnllm/models.py deleted file mode 100644 index 059b0412ca..0000000000 --- a/graphrag/language_model/providers/fnllm/models.py +++ /dev/null @@ -1,443 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing fnllm model provider definitions.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from fnllm.openai import ( - create_openai_chat_llm, - create_openai_client, - create_openai_embeddings_llm, -) - -from graphrag.language_model.providers.fnllm.events import FNLLMEvents -from graphrag.language_model.providers.fnllm.utils import ( - _create_cache, - _create_error_handler, - _create_openai_config, - run_coroutine_sync, -) -from graphrag.language_model.response.base import ( - BaseModelOutput, - BaseModelResponse, - ModelResponse, -) - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Generator - - from fnllm.openai.types.client import OpenAIChatLLM as FNLLMChatLLM - from fnllm.openai.types.client import OpenAIEmbeddingsLLM as FNLLMEmbeddingLLM - - from graphrag.cache.pipeline_cache import PipelineCache - from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks - from graphrag.config.models.language_model_config import ( - LanguageModelConfig, - ) - - -class OpenAIChatFNLLM: - """An OpenAI Chat Model provider using the fnllm library.""" - - model: FNLLMChatLLM - - def __init__( - self, - *, - name: str, - config: LanguageModelConfig, - callbacks: WorkflowCallbacks | None = None, - cache: PipelineCache | None = None, - ) -> None: - model_config = _create_openai_config(config, azure=False) - error_handler = _create_error_handler(callbacks) if callbacks else None - model_cache = _create_cache(cache, name) - client = create_openai_client(model_config) - self.model = create_openai_chat_llm( - model_config, - client=client, - cache=model_cache, - events=FNLLMEvents(error_handler) if error_handler else None, - ) - self.config = config - - async def achat( - self, prompt: str, history: list | None = None, **kwargs - ) -> ModelResponse: - """ - Chat with the Model using the given prompt. - - Args: - prompt: The prompt to chat with. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - The response from the Model. - """ - if history is None: - response = await self.model(prompt, **kwargs) - else: - response = await self.model(prompt, history=history, **kwargs) - return BaseModelResponse( - output=BaseModelOutput( - content=response.output.content, - full_response=response.output.raw_model.to_dict(), - ), - parsed_response=response.parsed_json, - history=response.history, - cache_hit=response.cache_hit, - tool_calls=response.tool_calls, - metrics=response.metrics, - ) - - async def achat_stream( - self, prompt: str, history: list | None = None, **kwargs - ) -> AsyncGenerator[str, None]: - """ - Stream Chat with the Model using the given prompt. - - Args: - prompt: The prompt to chat with. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - A generator that yields strings representing the response. - """ - if history is None: - response = await self.model(prompt, stream=True, **kwargs) - else: - response = await self.model(prompt, history=history, stream=True, **kwargs) - async for chunk in response.output.content: - if chunk is not None: - yield chunk - - def chat(self, prompt: str, history: list | None = None, **kwargs) -> ModelResponse: - """ - Chat with the Model using the given prompt. - - Args: - prompt: The prompt to chat with. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - The response from the Model. - """ - return run_coroutine_sync(self.achat(prompt, history=history, **kwargs)) - - def chat_stream( - self, prompt: str, history: list | None = None, **kwargs - ) -> Generator[str, None]: - """ - Stream Chat with the Model using the given prompt. - - Args: - prompt: The prompt to chat with. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - A generator that yields strings representing the response. - """ - msg = "chat_stream is not supported for synchronous execution" - raise NotImplementedError(msg) - - -class OpenAIEmbeddingFNLLM: - """An OpenAI Embedding Model provider using the fnllm library.""" - - model: FNLLMEmbeddingLLM - - def __init__( - self, - *, - name: str, - config: LanguageModelConfig, - callbacks: WorkflowCallbacks | None = None, - cache: PipelineCache | None = None, - ) -> None: - model_config = _create_openai_config(config, azure=False) - error_handler = _create_error_handler(callbacks) if callbacks else None - model_cache = _create_cache(cache, name) - client = create_openai_client(model_config) - self.model = create_openai_embeddings_llm( - model_config, - client=client, - cache=model_cache, - events=FNLLMEvents(error_handler) if error_handler else None, - ) - self.config = config - - async def aembed_batch(self, text_list: list[str], **kwargs) -> list[list[float]]: - """ - Embed the given text using the Model. - - Args: - text: The text to embed. - kwargs: Additional arguments to pass to the LLM. - - Returns - ------- - The embeddings of the text. - """ - response = await self.model(text_list, **kwargs) - if response.output.embeddings is None: - msg = "No embeddings found in response" - raise ValueError(msg) - embeddings: list[list[float]] = response.output.embeddings - return embeddings - - async def aembed(self, text: str, **kwargs) -> list[float]: - """ - Embed the given text using the Model. - - Args: - text: The text to embed. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - The embeddings of the text. - """ - response = await self.model([text], **kwargs) - if response.output.embeddings is None: - msg = "No embeddings found in response" - raise ValueError(msg) - embeddings: list[float] = response.output.embeddings[0] - return embeddings - - def embed_batch(self, text_list: list[str], **kwargs) -> list[list[float]]: - """ - Embed the given text using the Model. - - Args: - text: The text to embed. - kwargs: Additional arguments to pass to the LLM. - - Returns - ------- - The embeddings of the text. - """ - return run_coroutine_sync(self.aembed_batch(text_list, **kwargs)) - - def embed(self, text: str, **kwargs) -> list[float]: - """ - Embed the given text using the Model. - - Args: - text: The text to embed. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - The embeddings of the text. - """ - return run_coroutine_sync(self.aembed(text, **kwargs)) - - -class AzureOpenAIChatFNLLM: - """An Azure OpenAI Chat LLM provider using the fnllm library.""" - - model: FNLLMChatLLM - - def __init__( - self, - *, - name: str, - config: LanguageModelConfig, - callbacks: WorkflowCallbacks | None = None, - cache: PipelineCache | None = None, - ) -> None: - model_config = _create_openai_config(config, azure=True) - error_handler = _create_error_handler(callbacks) if callbacks else None - model_cache = _create_cache(cache, name) - client = create_openai_client(model_config) - self.model = create_openai_chat_llm( - model_config, - client=client, - cache=model_cache, - events=FNLLMEvents(error_handler) if error_handler else None, - ) - self.config = config - - async def achat( - self, prompt: str, history: list | None = None, **kwargs - ) -> ModelResponse: - """ - Chat with the Model using the given prompt. - - Args: - prompt: The prompt to chat with. - history: The conversation history. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - The response from the Model. - """ - if history is None: - response = await self.model(prompt, **kwargs) - else: - response = await self.model(prompt, history=history, **kwargs) - return BaseModelResponse( - output=BaseModelOutput( - content=response.output.content, - full_response=response.output.raw_model.to_dict(), - ), - parsed_response=response.parsed_json, - history=response.history, - cache_hit=response.cache_hit, - tool_calls=response.tool_calls, - metrics=response.metrics, - ) - - async def achat_stream( - self, prompt: str, history: list | None = None, **kwargs - ) -> AsyncGenerator[str, None]: - """ - Stream Chat with the Model using the given prompt. - - Args: - prompt: The prompt to chat with. - history: The conversation history. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - A generator that yields strings representing the response. - """ - if history is None: - response = await self.model(prompt, stream=True, **kwargs) - else: - response = await self.model(prompt, history=history, stream=True, **kwargs) - async for chunk in response.output.content: - if chunk is not None: - yield chunk - - def chat(self, prompt: str, history: list | None = None, **kwargs) -> ModelResponse: - """ - Chat with the Model using the given prompt. - - Args: - prompt: The prompt to chat with. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - The response from the Model. - """ - return run_coroutine_sync(self.achat(prompt, history=history, **kwargs)) - - def chat_stream( - self, prompt: str, history: list | None = None, **kwargs - ) -> Generator[str, None]: - """ - Stream Chat with the Model using the given prompt. - - Args: - prompt: The prompt to chat with. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - A generator that yields strings representing the response. - """ - msg = "chat_stream is not supported for synchronous execution" - raise NotImplementedError(msg) - - -class AzureOpenAIEmbeddingFNLLM: - """An Azure OpenAI Embedding Model provider using the fnllm library.""" - - model: FNLLMEmbeddingLLM - - def __init__( - self, - *, - name: str, - config: LanguageModelConfig, - callbacks: WorkflowCallbacks | None = None, - cache: PipelineCache | None = None, - ) -> None: - model_config = _create_openai_config(config, azure=True) - error_handler = _create_error_handler(callbacks) if callbacks else None - model_cache = _create_cache(cache, name) - client = create_openai_client(model_config) - self.model = create_openai_embeddings_llm( - model_config, - client=client, - cache=model_cache, - events=FNLLMEvents(error_handler) if error_handler else None, - ) - self.config = config - - async def aembed_batch(self, text_list: list[str], **kwargs) -> list[list[float]]: - """ - Embed the given text using the Model. - - Args: - text: The text to embed. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - The embeddings of the text. - """ - response = await self.model(text_list, **kwargs) - if response.output.embeddings is None: - msg = "No embeddings found in response" - raise ValueError(msg) - embeddings: list[list[float]] = response.output.embeddings - return embeddings - - async def aembed(self, text: str, **kwargs) -> list[float]: - """ - Embed the given text using the Model. - - Args: - text: The text to embed. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - The embeddings of the text. - """ - response = await self.model([text], **kwargs) - if response.output.embeddings is None: - msg = "No embeddings found in response" - raise ValueError(msg) - embeddings: list[float] = response.output.embeddings[0] - return embeddings - - def embed_batch(self, text_list: list[str], **kwargs) -> list[list[float]]: - """ - Embed the given text using the Model. - - Args: - text: The text to embed. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - The embeddings of the text. - """ - return run_coroutine_sync(self.aembed_batch(text_list, **kwargs)) - - def embed(self, text: str, **kwargs) -> list[float]: - """ - Embed the given text using the Model. - - Args: - text: The text to embed. - kwargs: Additional arguments to pass to the Model. - - Returns - ------- - The embeddings of the text. - """ - return run_coroutine_sync(self.aembed(text, **kwargs)) diff --git a/graphrag/language_model/providers/fnllm/utils.py b/graphrag/language_model/providers/fnllm/utils.py deleted file mode 100644 index b12b0e27b0..0000000000 --- a/graphrag/language_model/providers/fnllm/utils.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing utils for fnllm.""" - -from __future__ import annotations - -import asyncio -import logging -import threading -from typing import TYPE_CHECKING, Any, TypeVar - -from fnllm.base.config import JsonStrategy, RetryStrategy -from fnllm.openai import AzureOpenAIConfig, OpenAIConfig, PublicOpenAIConfig -from fnllm.openai.types.chat.parameters import OpenAIChatParameters - -import graphrag.config.defaults as defs -from graphrag.language_model.providers.fnllm.cache import FNLLMCacheProvider - -if TYPE_CHECKING: - from collections.abc import Coroutine - - from graphrag.cache.pipeline_cache import PipelineCache - from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks - from graphrag.config.models.language_model_config import ( - LanguageModelConfig, - ) - from graphrag.index.typing.error_handler import ErrorHandlerFn - -logger = logging.getLogger(__name__) - - -def _create_cache(cache: PipelineCache | None, name: str) -> FNLLMCacheProvider | None: - """Create an FNLLM cache from a pipeline cache.""" - if cache is None: - return None - return FNLLMCacheProvider(cache).child(name) - - -def _create_error_handler(callbacks: WorkflowCallbacks) -> ErrorHandlerFn: # noqa: ARG001 - """Create an error handler from a WorkflowCallbacks.""" - - def on_error( - error: BaseException | None = None, - stack: str | None = None, - details: dict | None = None, - ) -> None: - logger.error( - "Error Invoking LLM", - exc_info=error, - extra={"stack": stack, "details": details}, - ) - - return on_error - - -def _create_openai_config(config: LanguageModelConfig, azure: bool) -> OpenAIConfig: - """Create an OpenAIConfig from a LanguageModelConfig.""" - encoding_model = config.encoding_model - json_strategy = ( - JsonStrategy.VALID if config.model_supports_json else JsonStrategy.LOOSE - ) - chat_parameters = OpenAIChatParameters( - **get_openai_model_parameters_from_config(config) - ) - - if azure: - if config.api_base is None: - msg = "Azure OpenAI Chat LLM requires an API base" - raise ValueError(msg) - - audience = config.audience or defs.COGNITIVE_SERVICES_AUDIENCE - return AzureOpenAIConfig( - api_key=config.api_key, - endpoint=config.api_base, - json_strategy=json_strategy, - api_version=config.api_version, - organization=config.organization, - max_retries=config.max_retries, - max_retry_wait=config.max_retry_wait, - requests_per_minute=config.requests_per_minute, - tokens_per_minute=config.tokens_per_minute, - audience=audience, - retry_strategy=RetryStrategy(config.retry_strategy), - timeout=config.request_timeout, - max_concurrency=config.concurrent_requests, - model=config.model, - encoding=encoding_model, - deployment=config.deployment_name, - chat_parameters=chat_parameters, - ) - return PublicOpenAIConfig( - api_key=config.api_key, - base_url=config.api_base, - json_strategy=json_strategy, - organization=config.organization, - retry_strategy=RetryStrategy(config.retry_strategy), - max_retries=config.max_retries, - max_retry_wait=config.max_retry_wait, - requests_per_minute=config.requests_per_minute, - tokens_per_minute=config.tokens_per_minute, - timeout=config.request_timeout, - max_concurrency=config.concurrent_requests, - model=config.model, - encoding=encoding_model, - chat_parameters=chat_parameters, - ) - - -# FNLLM does not support sync operations, so we workaround running in an available loop/thread. -T = TypeVar("T") - -_loop = asyncio.new_event_loop() - -_thr = threading.Thread(target=_loop.run_forever, name="Async Runner", daemon=True) - - -def run_coroutine_sync(coroutine: Coroutine[Any, Any, T]) -> T: - """ - Run a coroutine synchronously. - - Args: - coroutine: The coroutine to run. - - Returns - ------- - The result of the coroutine. - """ - if not _thr.is_alive(): - _thr.start() - future = asyncio.run_coroutine_threadsafe(coroutine, _loop) - return future.result() - - -def is_reasoning_model(model: str) -> bool: - """Return whether the model uses a known OpenAI reasoning model.""" - return model.lower() in {"o1", "o1-mini", "o3-mini"} - - -def get_openai_model_parameters_from_config( - config: LanguageModelConfig, -) -> dict[str, Any]: - """Get the model parameters for a given config, adjusting for reasoning API differences.""" - return get_openai_model_parameters_from_dict(config.model_dump()) - - -def get_openai_model_parameters_from_dict(config: dict[str, Any]) -> dict[str, Any]: - """Get the model parameters for a given config, adjusting for reasoning API differences.""" - params = { - "n": config.get("n"), - } - if is_reasoning_model(config["model"]): - params["max_completion_tokens"] = config.get("max_completion_tokens") - params["reasoning_effort"] = config.get("reasoning_effort") - else: - params["max_tokens"] = config.get("max_tokens") - params["temperature"] = config.get("temperature") - params["frequency_penalty"] = config.get("frequency_penalty") - params["presence_penalty"] = config.get("presence_penalty") - params["top_p"] = config.get("top_p") - - if config.get("response_format"): - params["response_format"] = config["response_format"] - - return params diff --git a/graphrag/language_model/providers/litellm/__init__.py b/graphrag/language_model/providers/litellm/__init__.py deleted file mode 100644 index a1f948aba8..0000000000 --- a/graphrag/language_model/providers/litellm/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""GraphRAG LiteLLM module. Provides LiteLLM-based implementations of chat and embedding models.""" diff --git a/graphrag/language_model/providers/litellm/chat_model.py b/graphrag/language_model/providers/litellm/chat_model.py deleted file mode 100644 index 43cb7ece98..0000000000 --- a/graphrag/language_model/providers/litellm/chat_model.py +++ /dev/null @@ -1,414 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Chat model implementation using Litellm.""" - -import inspect -import json -from collections.abc import AsyncGenerator, Generator -from typing import TYPE_CHECKING, Any, cast - -import litellm -from azure.identity import DefaultAzureCredential, get_bearer_token_provider -from litellm import ( - CustomStreamWrapper, - ModelResponse, # type: ignore - acompletion, - completion, -) -from pydantic import BaseModel, Field - -from graphrag.config.defaults import COGNITIVE_SERVICES_AUDIENCE -from graphrag.config.enums import AuthType -from graphrag.language_model.providers.litellm.request_wrappers.with_cache import ( - with_cache, -) -from graphrag.language_model.providers.litellm.request_wrappers.with_logging import ( - with_logging, -) -from graphrag.language_model.providers.litellm.request_wrappers.with_rate_limiter import ( - with_rate_limiter, -) -from graphrag.language_model.providers.litellm.request_wrappers.with_retries import ( - with_retries, -) -from graphrag.language_model.providers.litellm.types import ( - AFixedModelCompletion, - FixedModelCompletion, -) - -if TYPE_CHECKING: - from graphrag.cache.pipeline_cache import PipelineCache - from graphrag.config.models.language_model_config import LanguageModelConfig - from graphrag.language_model.response.base import ModelResponse as MR # noqa: N817 - -litellm.suppress_debug_info = True - - -def _create_base_completions( - model_config: "LanguageModelConfig", -) -> tuple[FixedModelCompletion, AFixedModelCompletion]: - """Wrap the base litellm completion function with the model configuration. - - Args - ---- - model_config: The configuration for the language model. - - Returns - ------- - A tuple containing the synchronous and asynchronous completion functions. - """ - model_provider = model_config.model_provider - model = model_config.deployment_name or model_config.model - - base_args: dict[str, Any] = { - "drop_params": True, # LiteLLM drop unsupported params for selected model. - "model": f"{model_provider}/{model}", - "timeout": model_config.request_timeout, - "top_p": model_config.top_p, - "n": model_config.n, - "temperature": model_config.temperature, - "frequency_penalty": model_config.frequency_penalty, - "presence_penalty": model_config.presence_penalty, - "api_base": model_config.api_base, - "api_version": model_config.api_version, - "api_key": model_config.api_key, - "organization": model_config.organization, - "proxy": model_config.proxy, - "audience": model_config.audience, - "max_tokens": model_config.max_tokens, - "max_completion_tokens": model_config.max_completion_tokens, - "reasoning_effort": model_config.reasoning_effort, - } - - if model_config.auth_type == AuthType.AzureManagedIdentity: - if model_config.model_provider != "azure": - msg = "Azure Managed Identity authentication is only supported for Azure models." - raise ValueError(msg) - - base_args["azure_scope"] = base_args.pop("audience") - base_args["azure_ad_token_provider"] = get_bearer_token_provider( - DefaultAzureCredential(), - model_config.audience or COGNITIVE_SERVICES_AUDIENCE, - ) - - def _base_completion(**kwargs: Any) -> ModelResponse | CustomStreamWrapper: - new_args = {**base_args, **kwargs} - - if "name" in new_args: - new_args.pop("name") - - return completion(**new_args) - - async def _base_acompletion(**kwargs: Any) -> ModelResponse | CustomStreamWrapper: - new_args = {**base_args, **kwargs} - - if "name" in new_args: - new_args.pop("name") - - return await acompletion(**new_args) - - return (_base_completion, _base_acompletion) - - -def _create_completions( - model_config: "LanguageModelConfig", - cache: "PipelineCache | None", - cache_key_prefix: str, -) -> tuple[FixedModelCompletion, AFixedModelCompletion]: - """Wrap the base litellm completion function with the model configuration and additional features. - - Wrap the base litellm completion function with instance variables based on the model configuration. - Then wrap additional features such as rate limiting, retries, and caching, if enabled. - - Final function composition order: - - Logging(Cache(Retries(RateLimiter(ModelCompletion())))) - - Args - ---- - model_config: The configuration for the language model. - cache: Optional cache for storing responses. - cache_key_prefix: Prefix for cache keys. - - Returns - ------- - A tuple containing the synchronous and asynchronous completion functions. - - """ - completion, acompletion = _create_base_completions(model_config) - - # TODO: For v2.x release, rpm/tpm can be int or str (auto) for backwards compatibility with fnllm. - # LiteLLM does not support "auto", so we have to check those values here. - # For v3 release, force rpm/tpm to be int and remove the type checks below - # and just check if rate_limit_strategy is enabled. - if model_config.rate_limit_strategy is not None: - rpm = ( - model_config.requests_per_minute - if type(model_config.requests_per_minute) is int - else None - ) - tpm = ( - model_config.tokens_per_minute - if type(model_config.tokens_per_minute) is int - else None - ) - if rpm is not None or tpm is not None: - completion, acompletion = with_rate_limiter( - sync_fn=completion, - async_fn=acompletion, - model_config=model_config, - rpm=rpm, - tpm=tpm, - ) - - if model_config.retry_strategy != "none": - completion, acompletion = with_retries( - sync_fn=completion, - async_fn=acompletion, - model_config=model_config, - ) - - if cache is not None: - completion, acompletion = with_cache( - sync_fn=completion, - async_fn=acompletion, - model_config=model_config, - cache=cache, - request_type="chat", - cache_key_prefix=cache_key_prefix, - ) - - completion, acompletion = with_logging( - sync_fn=completion, - async_fn=acompletion, - ) - - return (completion, acompletion) - - -class LitellmModelOutput(BaseModel): - """A model representing the output from a language model.""" - - content: str = Field(description="The generated text content") - full_response: None = Field( - default=None, description="The full response from the model, if available" - ) - - -class LitellmModelResponse(BaseModel): - """A model representing the response from a language model.""" - - output: LitellmModelOutput = Field(description="The output from the model") - parsed_response: BaseModel | None = Field( - default=None, description="Parsed response from the model" - ) - history: list = Field( - default_factory=list, - description="Conversation history including the prompt and response", - ) - - -class LitellmChatModel: - """LiteLLM-based Chat Model.""" - - def __init__( - self, - name: str, - config: "LanguageModelConfig", - cache: "PipelineCache | None" = None, - **kwargs: Any, - ): - self.name = name - self.config = config - self.cache = cache.child(self.name) if cache else None - self.completion, self.acompletion = _create_completions( - config, self.cache, "chat" - ) - - def _get_kwargs(self, **kwargs: Any) -> dict[str, Any]: - """Get model arguments supported by litellm.""" - args_to_include = [ - "name", - "modalities", - "prediction", - "audio", - "logit_bias", - "metadata", - "user", - "response_format", - "seed", - "tools", - "tool_choice", - "logprobs", - "top_logprobs", - "parallel_tool_calls", - "web_search_options", - "extra_headers", - "functions", - "function_call", - "thinking", - ] - new_args = {k: v for k, v in kwargs.items() if k in args_to_include} - - # If using JSON, check if response_format should be a Pydantic model or just a general JSON object - if kwargs.get("json"): - new_args["response_format"] = {"type": "json_object"} - - if ( - "json_model" in kwargs - and inspect.isclass(kwargs["json_model"]) - and issubclass(kwargs["json_model"], BaseModel) - ): - new_args["response_format"] = kwargs["json_model"] - - return new_args - - async def achat( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> "MR": - """ - Generate a response for the given prompt and history. - - Args - ---- - prompt: The prompt to generate a response for. - history: Optional chat history. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - LitellmModelResponse: The generated model response. - """ - new_kwargs = self._get_kwargs(**kwargs) - messages: list[dict[str, str]] = history or [] - messages.append({"role": "user", "content": prompt}) - - response = await self.acompletion(messages=messages, stream=False, **new_kwargs) # type: ignore - - messages.append({ - "role": "assistant", - "content": response.choices[0].message.content or "", # type: ignore - }) - - parsed_response: BaseModel | None = None - if "response_format" in new_kwargs: - parsed_dict: dict[str, Any] = json.loads( - response.choices[0].message.content or "{}" # type: ignore - ) - parsed_response = parsed_dict # type: ignore - if inspect.isclass(new_kwargs["response_format"]) and issubclass( - new_kwargs["response_format"], BaseModel - ): - # If response_format is a pydantic model, instantiate it - model_initializer = cast( - "type[BaseModel]", new_kwargs["response_format"] - ) - parsed_response = model_initializer(**parsed_dict) - - return LitellmModelResponse( - output=LitellmModelOutput( - content=response.choices[0].message.content or "" # type: ignore - ), - parsed_response=parsed_response, - history=messages, - ) - - async def achat_stream( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> AsyncGenerator[str, None]: - """ - Generate a response for the given prompt and history. - - Args - ---- - prompt: The prompt to generate a response for. - history: Optional chat history. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - AsyncGenerator[str, None]: The generated response as a stream of strings. - """ - new_kwargs = self._get_kwargs(**kwargs) - messages: list[dict[str, str]] = history or [] - messages.append({"role": "user", "content": prompt}) - - response = await self.acompletion(messages=messages, stream=True, **new_kwargs) # type: ignore - - async for chunk in response: # type: ignore - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content - - def chat(self, prompt: str, history: list | None = None, **kwargs: Any) -> "MR": - """ - Generate a response for the given prompt and history. - - Args - ---- - prompt: The prompt to generate a response for. - history: Optional chat history. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - LitellmModelResponse: The generated model response. - """ - new_kwargs = self._get_kwargs(**kwargs) - messages: list[dict[str, str]] = history or [] - messages.append({"role": "user", "content": prompt}) - - response = self.completion(messages=messages, stream=False, **new_kwargs) # type: ignore - - messages.append({ - "role": "assistant", - "content": response.choices[0].message.content or "", # type: ignore - }) - - parsed_response: BaseModel | None = None - if "response_format" in new_kwargs: - parsed_dict: dict[str, Any] = json.loads( - response.choices[0].message.content or "{}" # type: ignore - ) - parsed_response = parsed_dict # type: ignore - if inspect.isclass(new_kwargs["response_format"]) and issubclass( - new_kwargs["response_format"], BaseModel - ): - # If response_format is a pydantic model, instantiate it - model_initializer = cast( - "type[BaseModel]", new_kwargs["response_format"] - ) - parsed_response = model_initializer(**parsed_dict) - - return LitellmModelResponse( - output=LitellmModelOutput( - content=response.choices[0].message.content or "" # type: ignore - ), - parsed_response=parsed_response, - history=messages, - ) - - def chat_stream( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> Generator[str, None]: - """ - Generate a response for the given prompt and history. - - Args - ---- - prompt: The prompt to generate a response for. - history: Optional chat history. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - Generator[str, None]: The generated response as a stream of strings. - """ - new_kwargs = self._get_kwargs(**kwargs) - messages: list[dict[str, str]] = history or [] - messages.append({"role": "user", "content": prompt}) - - response = self.completion(messages=messages, stream=True, **new_kwargs) # type: ignore - - for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: # type: ignore - yield chunk.choices[0].delta.content # type: ignore diff --git a/graphrag/language_model/providers/litellm/embedding_model.py b/graphrag/language_model/providers/litellm/embedding_model.py deleted file mode 100644 index 779222c37e..0000000000 --- a/graphrag/language_model/providers/litellm/embedding_model.py +++ /dev/null @@ -1,280 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Embedding model implementation using Litellm.""" - -from typing import TYPE_CHECKING, Any - -import litellm -from azure.identity import DefaultAzureCredential, get_bearer_token_provider -from litellm import ( - EmbeddingResponse, # type: ignore - aembedding, - embedding, -) - -from graphrag.config.defaults import COGNITIVE_SERVICES_AUDIENCE -from graphrag.config.enums import AuthType -from graphrag.language_model.providers.litellm.request_wrappers.with_cache import ( - with_cache, -) -from graphrag.language_model.providers.litellm.request_wrappers.with_logging import ( - with_logging, -) -from graphrag.language_model.providers.litellm.request_wrappers.with_rate_limiter import ( - with_rate_limiter, -) -from graphrag.language_model.providers.litellm.request_wrappers.with_retries import ( - with_retries, -) -from graphrag.language_model.providers.litellm.types import ( - AFixedModelEmbedding, - FixedModelEmbedding, -) - -if TYPE_CHECKING: - from graphrag.cache.pipeline_cache import PipelineCache - from graphrag.config.models.language_model_config import LanguageModelConfig - -litellm.suppress_debug_info = True - - -def _create_base_embeddings( - model_config: "LanguageModelConfig", -) -> tuple[FixedModelEmbedding, AFixedModelEmbedding]: - """Wrap the base litellm embedding function with the model configuration. - - Args - ---- - model_config: The configuration for the language model. - - Returns - ------- - A tuple containing the synchronous and asynchronous embedding functions. - """ - model_provider = model_config.model_provider - model = model_config.deployment_name or model_config.model - - base_args: dict[str, Any] = { - "drop_params": True, # LiteLLM drop unsupported params for selected model. - "model": f"{model_provider}/{model}", - "timeout": model_config.request_timeout, - "api_base": model_config.api_base, - "api_version": model_config.api_version, - "api_key": model_config.api_key, - "organization": model_config.organization, - "proxy": model_config.proxy, - "audience": model_config.audience, - } - - if model_config.auth_type == AuthType.AzureManagedIdentity: - if model_config.model_provider != "azure": - msg = "Azure Managed Identity authentication is only supported for Azure models." - raise ValueError(msg) - - base_args["azure_scope"] = base_args.pop("audience") - base_args["azure_ad_token_provider"] = get_bearer_token_provider( - DefaultAzureCredential(), - model_config.audience or COGNITIVE_SERVICES_AUDIENCE, - ) - - def _base_embedding(**kwargs: Any) -> EmbeddingResponse: - new_args = {**base_args, **kwargs} - - if "name" in new_args: - new_args.pop("name") - - return embedding(**new_args) - - async def _base_aembedding(**kwargs: Any) -> EmbeddingResponse: - new_args = {**base_args, **kwargs} - - if "name" in new_args: - new_args.pop("name") - - return await aembedding(**new_args) - - return (_base_embedding, _base_aembedding) - - -def _create_embeddings( - model_config: "LanguageModelConfig", - cache: "PipelineCache | None", - cache_key_prefix: str, -) -> tuple[FixedModelEmbedding, AFixedModelEmbedding]: - """Wrap the base litellm embedding function with the model configuration and additional features. - - Wrap the base litellm embedding function with instance variables based on the model configuration. - Then wrap additional features such as rate limiting, retries, and caching, if enabled. - - Final function composition order: - - Logging(Cache(Retries(RateLimiter(ModelEmbedding())))) - - Args - ---- - model_config: The configuration for the language model. - cache: Optional cache for storing responses. - cache_key_prefix: Prefix for cache keys. - - Returns - ------- - A tuple containing the synchronous and asynchronous embedding functions. - - """ - embedding, aembedding = _create_base_embeddings(model_config) - - # TODO: For v2.x release, rpm/tpm can be int or str (auto) for backwards compatibility with fnllm. - # LiteLLM does not support "auto", so we have to check those values here. - # For v3 release, force rpm/tpm to be int and remove the type checks below - # and just check if rate_limit_strategy is enabled. - if model_config.rate_limit_strategy is not None: - rpm = ( - model_config.requests_per_minute - if type(model_config.requests_per_minute) is int - else None - ) - tpm = ( - model_config.tokens_per_minute - if type(model_config.tokens_per_minute) is int - else None - ) - if rpm is not None or tpm is not None: - embedding, aembedding = with_rate_limiter( - sync_fn=embedding, - async_fn=aembedding, - model_config=model_config, - rpm=rpm, - tpm=tpm, - ) - - if model_config.retry_strategy != "none": - embedding, aembedding = with_retries( - sync_fn=embedding, - async_fn=aembedding, - model_config=model_config, - ) - - if cache is not None: - embedding, aembedding = with_cache( - sync_fn=embedding, - async_fn=aembedding, - model_config=model_config, - cache=cache, - request_type="embedding", - cache_key_prefix=cache_key_prefix, - ) - - embedding, aembedding = with_logging( - sync_fn=embedding, - async_fn=aembedding, - ) - - return (embedding, aembedding) - - -class LitellmEmbeddingModel: - """LiteLLM-based Embedding Model.""" - - def __init__( - self, - name: str, - config: "LanguageModelConfig", - cache: "PipelineCache | None" = None, - **kwargs: Any, - ): - self.name = name - self.config = config - self.cache = cache.child(self.name) if cache else None - self.embedding, self.aembedding = _create_embeddings( - config, self.cache, "embeddings" - ) - - def _get_kwargs(self, **kwargs: Any) -> dict[str, Any]: - """Get model arguments supported by litellm.""" - args_to_include = [ - "name", - "dimensions", - "encoding_format", - "timeout", - "user", - ] - return {k: v for k, v in kwargs.items() if k in args_to_include} - - async def aembed_batch( - self, text_list: list[str], **kwargs: Any - ) -> list[list[float]]: - """ - Batch generate embeddings. - - Args - ---- - text_list: A batch of text inputs to generate embeddings for. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - A Batch of embeddings. - """ - new_kwargs = self._get_kwargs(**kwargs) - response = await self.aembedding(input=text_list, **new_kwargs) - - return [emb.get("embedding", []) for emb in response.data] - - async def aembed(self, text: str, **kwargs: Any) -> list[float]: - """ - Async embed. - - Args: - text: The text to generate an embedding for. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - An embedding. - """ - new_kwargs = self._get_kwargs(**kwargs) - response = await self.aembedding(input=[text], **new_kwargs) - - return ( - response.data[0].get("embedding", []) - if response.data and response.data[0] - else [] - ) - - def embed_batch(self, text_list: list[str], **kwargs: Any) -> list[list[float]]: - """ - Batch generate embeddings. - - Args: - text_list: A batch of text inputs to generate embeddings for. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - A Batch of embeddings. - """ - new_kwargs = self._get_kwargs(**kwargs) - response = self.embedding(input=text_list, **new_kwargs) - - return [emb.get("embedding", []) for emb in response.data] - - def embed(self, text: str, **kwargs: Any) -> list[float]: - """ - Embed a single text input. - - Args: - text: The text to generate an embedding for. - **kwargs: Additional keyword arguments (e.g., model parameters). - - Returns - ------- - An embedding. - """ - new_kwargs = self._get_kwargs(**kwargs) - response = self.embedding(input=[text], **new_kwargs) - - return ( - response.data[0].get("embedding", []) - if response.data and response.data[0] - else [] - ) diff --git a/graphrag/language_model/providers/litellm/get_cache_key.py b/graphrag/language_model/providers/litellm/get_cache_key.py deleted file mode 100644 index 0d6938d45a..0000000000 --- a/graphrag/language_model/providers/litellm/get_cache_key.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -""" -LiteLLM cache key generation. - -Modeled after the fnllm cache key generation. -https://github.com/microsoft/essex-toolkit/blob/23d3077b65c0e8f1d89c397a2968fe570a25f790/python/fnllm/fnllm/caching/base.py#L50 -""" - -import hashlib -import inspect -import json -from typing import TYPE_CHECKING, Any - -from pydantic import BaseModel - -if TYPE_CHECKING: - from graphrag.config.models.language_model_config import LanguageModelConfig - - -_CACHE_VERSION = 3 -""" -If there's a breaking change in what we cache, we should increment this version number to invalidate existing caches. - -fnllm was on cache version 2 and though we generate -similar cache keys, the objects stored in cache by fnllm and litellm are different. -Using litellm model providers will not be able to reuse caches generated by fnllm -thus we start with version 3 for litellm. -""" - - -def get_cache_key( - model_config: "LanguageModelConfig", - prefix: str, - messages: str | None = None, - input: str | None = None, - **kwargs: Any, -) -> str: - """Generate a cache key based on the model configuration and input arguments. - - Modeled after the fnllm cache key generation. - https://github.com/microsoft/essex-toolkit/blob/23d3077b65c0e8f1d89c397a2968fe570a25f790/python/fnllm/fnllm/caching/base.py#L50 - - Args - ____ - model_config: The configuration of the language model. - prefix: A prefix for the cache key. - **kwargs: Additional model input parameters. - - Returns - ------- - `{prefix}_{data_hash}_v{version}` if prefix is provided. - """ - cache_key: dict[str, Any] = { - "parameters": _get_parameters(model_config, **kwargs), - } - - if messages is not None and input is not None: - msg = "Only one of 'messages' or 'input' should be provided." - raise ValueError(msg) - - if messages is not None: - cache_key["messages"] = messages - elif input is not None: - cache_key["input"] = input - else: - msg = "Either 'messages' or 'input' must be provided." - raise ValueError(msg) - - data_hash = _hash(json.dumps(cache_key, sort_keys=True)) - - name = kwargs.get("name") - - if name: - prefix += f"_{name}" - - return f"{prefix}_{data_hash}_v{_CACHE_VERSION}" - - -def _get_parameters( - model_config: "LanguageModelConfig", - **kwargs: Any, -) -> dict[str, Any]: - """Pluck out the parameters that define a cache key. - - Use the same parameters as fnllm except request timeout. - - embeddings: https://github.com/microsoft/essex-toolkit/blob/main/python/fnllm/fnllm/openai/types/embeddings/parameters.py#L12 - - chat: https://github.com/microsoft/essex-toolkit/blob/main/python/fnllm/fnllm/openai/types/chat/parameters.py#L25 - - Args - ____ - model_config: The configuration of the language model. - **kwargs: Additional model input parameters. - - Returns - ------- - dict[str, Any]: A dictionary of parameters that define the cache key. - """ - parameters = { - "model": model_config.deployment_name or model_config.model, - "frequency_penalty": model_config.frequency_penalty, - "max_tokens": model_config.max_tokens, - "max_completion_tokens": model_config.max_completion_tokens, - "n": model_config.n, - "presence_penalty": model_config.presence_penalty, - "temperature": model_config.temperature, - "top_p": model_config.top_p, - "reasoning_effort": model_config.reasoning_effort, - } - keys_to_cache = [ - "function_call", - "functions", - "logit_bias", - "logprobs", - "parallel_tool_calls", - "seed", - "service_tier", - "stop", - "tool_choice", - "tools", - "top_logprobs", - "user", - "dimensions", - "encoding_format", - ] - parameters.update({key: kwargs.get(key) for key in keys_to_cache if key in kwargs}) - - response_format = kwargs.get("response_format") - if inspect.isclass(response_format) and issubclass(response_format, BaseModel): - parameters["response_format"] = str(response_format) - elif response_format is not None: - parameters["response_format"] = response_format - - return parameters - - -def _hash(input: str) -> str: - """Generate a hash for the input string.""" - return hashlib.sha256(input.encode()).hexdigest() diff --git a/graphrag/language_model/providers/litellm/request_wrappers/__init__.py b/graphrag/language_model/providers/litellm/request_wrappers/__init__.py deleted file mode 100644 index b1ba631645..0000000000 --- a/graphrag/language_model/providers/litellm/request_wrappers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM completion/embedding function wrappers.""" diff --git a/graphrag/language_model/providers/litellm/request_wrappers/with_cache.py b/graphrag/language_model/providers/litellm/request_wrappers/with_cache.py deleted file mode 100644 index d14c97206e..0000000000 --- a/graphrag/language_model/providers/litellm/request_wrappers/with_cache.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM completion/embedding cache wrapper.""" - -import asyncio -from typing import TYPE_CHECKING, Any, Literal - -from litellm import EmbeddingResponse, ModelResponse # type: ignore - -from graphrag.language_model.providers.litellm.get_cache_key import get_cache_key -from graphrag.language_model.providers.litellm.types import ( - AsyncLitellmRequestFunc, - LitellmRequestFunc, -) - -if TYPE_CHECKING: - from graphrag.cache.pipeline_cache import PipelineCache - from graphrag.config.models.language_model_config import LanguageModelConfig - - -def with_cache( - *, - sync_fn: LitellmRequestFunc, - async_fn: AsyncLitellmRequestFunc, - model_config: "LanguageModelConfig", - cache: "PipelineCache", - request_type: Literal["chat", "embedding"], - cache_key_prefix: str, -) -> tuple[LitellmRequestFunc, AsyncLitellmRequestFunc]: - """ - Wrap the synchronous and asynchronous request functions with caching. - - Args - ---- - sync_fn: The synchronous chat/embedding request function to wrap. - async_fn: The asynchronous chat/embedding request function to wrap. - model_config: The configuration for the language model. - cache: The cache to use for storing responses. - request_type: The type of request being made, either "chat" or "embedding". - cache_key_prefix: The prefix to use for cache keys. - - Returns - ------- - A tuple containing the wrapped synchronous and asynchronous chat/embedding request functions. - """ - - def _wrapped_with_cache(**kwargs: Any) -> Any: - is_streaming = kwargs.get("stream", False) - if is_streaming: - return sync_fn(**kwargs) - cache_key = get_cache_key( - model_config=model_config, prefix=cache_key_prefix, **kwargs - ) - event_loop = asyncio.get_event_loop() - cached_response = event_loop.run_until_complete(cache.get(cache_key)) - if ( - cached_response is not None - and isinstance(cached_response, dict) - and "response" in cached_response - and cached_response["response"] is not None - and isinstance(cached_response["response"], dict) - ): - try: - if request_type == "chat": - return ModelResponse(**cached_response["response"]) - return EmbeddingResponse(**cached_response["response"]) - except Exception: # noqa: BLE001 - # Try to retrieve value from cache but if it fails, continue - # to make the request. - ... - response = sync_fn(**kwargs) - event_loop.run_until_complete( - cache.set(cache_key, {"response": response.model_dump()}) - ) - return response - - async def _wrapped_with_cache_async( - **kwargs: Any, - ) -> Any: - is_streaming = kwargs.get("stream", False) - if is_streaming: - return await async_fn(**kwargs) - cache_key = get_cache_key( - model_config=model_config, prefix=cache_key_prefix, **kwargs - ) - cached_response = await cache.get(cache_key) - if ( - cached_response is not None - and isinstance(cached_response, dict) - and "response" in cached_response - and cached_response["response"] is not None - and isinstance(cached_response["response"], dict) - ): - try: - if request_type == "chat": - return ModelResponse(**cached_response["response"]) - return EmbeddingResponse(**cached_response["response"]) - except Exception: # noqa: BLE001 - # Try to retrieve value from cache but if it fails, continue - # to make the request. - ... - response = await async_fn(**kwargs) - await cache.set(cache_key, {"response": response.model_dump()}) - return response - - return (_wrapped_with_cache, _wrapped_with_cache_async) diff --git a/graphrag/language_model/providers/litellm/request_wrappers/with_logging.py b/graphrag/language_model/providers/litellm/request_wrappers/with_logging.py deleted file mode 100644 index a353f455fc..0000000000 --- a/graphrag/language_model/providers/litellm/request_wrappers/with_logging.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM completion/embedding logging wrapper.""" - -import logging -from typing import Any - -from graphrag.language_model.providers.litellm.types import ( - AsyncLitellmRequestFunc, - LitellmRequestFunc, -) - -logger = logging.getLogger(__name__) - - -def with_logging( - *, - sync_fn: LitellmRequestFunc, - async_fn: AsyncLitellmRequestFunc, -) -> tuple[LitellmRequestFunc, AsyncLitellmRequestFunc]: - """ - Wrap the synchronous and asynchronous request functions with retries. - - Args - ---- - sync_fn: The synchronous chat/embedding request function to wrap. - async_fn: The asynchronous chat/embedding request function to wrap. - model_config: The configuration for the language model. - - Returns - ------- - A tuple containing the wrapped synchronous and asynchronous chat/embedding request functions. - """ - - def _wrapped_with_logging(**kwargs: Any) -> Any: - try: - return sync_fn(**kwargs) - except Exception as e: - logger.exception( - f"with_logging: Request failed with exception={e}", # noqa: G004, TRY401 - ) - raise - - async def _wrapped_with_logging_async( - **kwargs: Any, - ) -> Any: - try: - return await async_fn(**kwargs) - except Exception as e: - logger.exception( - f"with_logging: Async request failed with exception={e}", # noqa: G004, TRY401 - ) - raise - - return (_wrapped_with_logging, _wrapped_with_logging_async) diff --git a/graphrag/language_model/providers/litellm/request_wrappers/with_rate_limiter.py b/graphrag/language_model/providers/litellm/request_wrappers/with_rate_limiter.py deleted file mode 100644 index c0e0728f2e..0000000000 --- a/graphrag/language_model/providers/litellm/request_wrappers/with_rate_limiter.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM completion/embedding rate limiter wrapper.""" - -from typing import TYPE_CHECKING, Any - -from litellm import token_counter # type: ignore - -from graphrag.language_model.providers.litellm.services.rate_limiter.rate_limiter_factory import ( - RateLimiterFactory, -) -from graphrag.language_model.providers.litellm.types import ( - AsyncLitellmRequestFunc, - LitellmRequestFunc, -) - -if TYPE_CHECKING: - from graphrag.config.models.language_model_config import LanguageModelConfig - - -def with_rate_limiter( - *, - sync_fn: LitellmRequestFunc, - async_fn: AsyncLitellmRequestFunc, - model_config: "LanguageModelConfig", - rpm: int | None = None, - tpm: int | None = None, -) -> tuple[LitellmRequestFunc, AsyncLitellmRequestFunc]: - """ - Wrap the synchronous and asynchronous request functions with rate limiting. - - Args - ---- - sync_fn: The synchronous chat/embedding request function to wrap. - async_fn: The asynchronous chat/embedding request function to wrap. - model_config: The configuration for the language model. - processing_event: A threading event that can be used to pause the rate limiter. - rpm: An optional requests per minute limit. - tpm: An optional tokens per minute limit. - - If `rpm` and `tpm` is set to 0 or None, rate limiting is disabled. - - Returns - ------- - A tuple containing the wrapped synchronous and asynchronous chat/embedding request functions. - """ - rate_limiter_factory = RateLimiterFactory() - - if ( - model_config.rate_limit_strategy is None - or model_config.rate_limit_strategy not in rate_limiter_factory - ): - msg = f"Rate Limiter strategy '{model_config.rate_limit_strategy}' is none or not registered. Available strategies: {', '.join(rate_limiter_factory.keys())}" - raise ValueError(msg) - - rate_limiter_service = rate_limiter_factory.create( - strategy=model_config.rate_limit_strategy, rpm=rpm, tpm=tpm - ) - - max_tokens = model_config.max_completion_tokens or model_config.max_tokens or 0 - - def _wrapped_with_rate_limiter(**kwargs: Any) -> Any: - token_count = max_tokens - if "messages" in kwargs: - token_count += token_counter( - model=model_config.model, - messages=kwargs["messages"], - ) - elif "input" in kwargs: - token_count += token_counter( - model=model_config.model, - text=kwargs["input"], - ) - - with rate_limiter_service.acquire(token_count=token_count): - return sync_fn(**kwargs) - - async def _wrapped_with_rate_limiter_async( - **kwargs: Any, - ) -> Any: - token_count = max_tokens - if "messages" in kwargs: - token_count += token_counter( - model=model_config.model, - messages=kwargs["messages"], - ) - elif "input" in kwargs: - token_count += token_counter( - model=model_config.model, - text=kwargs["input"], - ) - - with rate_limiter_service.acquire(token_count=token_count): - return await async_fn(**kwargs) - - return (_wrapped_with_rate_limiter, _wrapped_with_rate_limiter_async) diff --git a/graphrag/language_model/providers/litellm/request_wrappers/with_retries.py b/graphrag/language_model/providers/litellm/request_wrappers/with_retries.py deleted file mode 100644 index 1279f9e820..0000000000 --- a/graphrag/language_model/providers/litellm/request_wrappers/with_retries.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM completion/embedding retries wrapper.""" - -from typing import TYPE_CHECKING, Any - -from graphrag.language_model.providers.litellm.services.retry.retry_factory import ( - RetryFactory, -) -from graphrag.language_model.providers.litellm.types import ( - AsyncLitellmRequestFunc, - LitellmRequestFunc, -) - -if TYPE_CHECKING: - from graphrag.config.models.language_model_config import LanguageModelConfig - - -def with_retries( - *, - sync_fn: LitellmRequestFunc, - async_fn: AsyncLitellmRequestFunc, - model_config: "LanguageModelConfig", -) -> tuple[LitellmRequestFunc, AsyncLitellmRequestFunc]: - """ - Wrap the synchronous and asynchronous request functions with retries. - - Args - ---- - sync_fn: The synchronous chat/embedding request function to wrap. - async_fn: The asynchronous chat/embedding request function to wrap. - model_config: The configuration for the language model. - - Returns - ------- - A tuple containing the wrapped synchronous and asynchronous chat/embedding request functions. - """ - retry_factory = RetryFactory() - retry_service = retry_factory.create( - strategy=model_config.retry_strategy, - max_retries=model_config.max_retries, - max_retry_wait=model_config.max_retry_wait, - ) - - def _wrapped_with_retries(**kwargs: Any) -> Any: - return retry_service.retry(func=sync_fn, **kwargs) - - async def _wrapped_with_retries_async( - **kwargs: Any, - ) -> Any: - return await retry_service.aretry(func=async_fn, **kwargs) - - return (_wrapped_with_retries, _wrapped_with_retries_async) diff --git a/graphrag/language_model/providers/litellm/services/__init__.py b/graphrag/language_model/providers/litellm/services/__init__.py deleted file mode 100644 index dd0cf9fe2a..0000000000 --- a/graphrag/language_model/providers/litellm/services/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Services.""" diff --git a/graphrag/language_model/providers/litellm/services/rate_limiter/__init__.py b/graphrag/language_model/providers/litellm/services/rate_limiter/__init__.py deleted file mode 100644 index 3c80d9f3f1..0000000000 --- a/graphrag/language_model/providers/litellm/services/rate_limiter/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Rate Limiter.""" diff --git a/graphrag/language_model/providers/litellm/services/rate_limiter/rate_limiter_factory.py b/graphrag/language_model/providers/litellm/services/rate_limiter/rate_limiter_factory.py deleted file mode 100644 index a6ef6880ef..0000000000 --- a/graphrag/language_model/providers/litellm/services/rate_limiter/rate_limiter_factory.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Rate Limiter Factory.""" - -from graphrag.config.defaults import DEFAULT_RATE_LIMITER_SERVICES -from graphrag.factory.factory import Factory -from graphrag.language_model.providers.litellm.services.rate_limiter.rate_limiter import ( - RateLimiter, -) - - -class RateLimiterFactory(Factory[RateLimiter]): - """Singleton factory for creating rate limiter services.""" - - -rate_limiter_factory = RateLimiterFactory() - -for service_name, service_cls in DEFAULT_RATE_LIMITER_SERVICES.items(): - rate_limiter_factory.register( - strategy=service_name, service_initializer=service_cls - ) diff --git a/graphrag/language_model/providers/litellm/services/rate_limiter/static_rate_limiter.py b/graphrag/language_model/providers/litellm/services/rate_limiter/static_rate_limiter.py deleted file mode 100644 index 43681ceb10..0000000000 --- a/graphrag/language_model/providers/litellm/services/rate_limiter/static_rate_limiter.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Static Rate Limiter.""" - -import threading -import time -from collections import deque -from collections.abc import Iterator -from contextlib import contextmanager -from typing import Any - -from graphrag.language_model.providers.litellm.services.rate_limiter.rate_limiter import ( - RateLimiter, -) - - -class StaticRateLimiter(RateLimiter): - """Static Rate Limiter implementation.""" - - def __init__( - self, - *, - rpm: int | None = None, - tpm: int | None = None, - default_stagger: float = 0.0, - period_in_seconds: int = 60, - **kwargs: Any, - ): - if rpm is None and tpm is None: - msg = "Both TPM and RPM cannot be None (disabled), one or both must be set to a positive integer." - raise ValueError(msg) - if (rpm is not None and rpm <= 0) or (tpm is not None and tpm <= 0): - msg = "RPM and TPM must be either None (disabled) or positive integers." - raise ValueError(msg) - if default_stagger < 0: - msg = "Default stagger must be a >= 0." - raise ValueError(msg) - if period_in_seconds <= 0: - msg = "Period in seconds must be a positive integer." - raise ValueError(msg) - self.rpm = rpm - self.tpm = tpm - self._lock = threading.Lock() - self.rate_queue: deque[float] = deque() - self.token_queue: deque[int] = deque() - self.period_in_seconds = period_in_seconds - self._last_time: float | None = None - - self.stagger = default_stagger - if self.rpm is not None and self.rpm > 0: - self.stagger = self.period_in_seconds / self.rpm - - @contextmanager - def acquire(self, *, token_count: int) -> Iterator[None]: - """ - Acquire Rate Limiter. - - Args - ---- - token_count: The estimated number of tokens for the current request. - - Yields - ------ - None: This context manager does not return any value. - """ - while True: - with self._lock: - current_time = time.time() - - # Use two sliding windows to keep track of #requests and tokens per period - # Drop old requests and tokens out of the sliding windows - while ( - len(self.rate_queue) > 0 - and self.rate_queue[0] < current_time - self.period_in_seconds - ): - self.rate_queue.popleft() - self.token_queue.popleft() - - # If sliding window still exceed request limit, wait again - # Waiting requires reacquiring the lock, allowing other threads - # to see if their request fits within the rate limiting windows - # Makes more sense for token limit than request limit - if ( - self.rpm is not None - and self.rpm > 0 - and len(self.rate_queue) >= self.rpm - ): - continue - - # Check if current token window exceeds token limit - # If it does, wait again - # This does not account for the tokens from the current request - # This is intentional, as we want to allow the current request - # to be processed if it is larger than the tpm but smaller than context window. - # tpm is a rate/soft limit and not the hard limit of context window limits. - if ( - self.tpm is not None - and self.tpm > 0 - and sum(self.token_queue) >= self.tpm - ): - continue - - # This check accounts for the current request token usage - # is within the token limits bound. - # If the current requests token limit exceeds the token limit, - # Then let it be processed. - if ( - self.tpm is not None - and self.tpm > 0 - and token_count <= self.tpm - and sum(self.token_queue) + token_count > self.tpm - ): - continue - - # If there was a previous call, check if we need to stagger - if ( - self.stagger > 0 - and ( - self._last_time # is None if this is the first hit to the rate limiter - and current_time - self._last_time - < self.stagger # If more time has passed than the stagger time, we can proceed - ) - ): - time.sleep(self.stagger - (current_time - self._last_time)) - current_time = time.time() - - # Add the current request to the sliding window - self.rate_queue.append(current_time) - self.token_queue.append(token_count) - self._last_time = current_time - break - yield diff --git a/graphrag/language_model/providers/litellm/services/retry/__init__.py b/graphrag/language_model/providers/litellm/services/retry/__init__.py deleted file mode 100644 index f01e0020e8..0000000000 --- a/graphrag/language_model/providers/litellm/services/retry/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Retry Services.""" diff --git a/graphrag/language_model/providers/litellm/services/retry/exponential_retry.py b/graphrag/language_model/providers/litellm/services/retry/exponential_retry.py deleted file mode 100644 index e008322be0..0000000000 --- a/graphrag/language_model/providers/litellm/services/retry/exponential_retry.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Exponential Retry Service.""" - -import asyncio -import logging -import random -import time -from collections.abc import Awaitable, Callable -from typing import Any - -from graphrag.language_model.providers.litellm.services.retry.retry import Retry - -logger = logging.getLogger(__name__) - - -class ExponentialRetry(Retry): - """LiteLLM Exponential Retry Service.""" - - def __init__( - self, - *, - max_retries: int = 5, - base_delay: float = 2.0, - jitter: bool = True, - **kwargs: Any, - ): - if max_retries <= 0: - msg = "max_retries must be greater than 0." - raise ValueError(msg) - - if base_delay <= 1.0: - msg = "base_delay must be greater than 1.0." - raise ValueError(msg) - - self._max_retries = max_retries - self._base_delay = base_delay - self._jitter = jitter - - def retry(self, func: Callable[..., Any], **kwargs: Any) -> Any: - """Retry a synchronous function.""" - retries = 0 - delay = 1.0 # Initial delay in seconds - while True: - try: - return func(**kwargs) - except Exception as e: - if retries >= self._max_retries: - logger.exception( - f"ExponentialRetry: Max retries exceeded, retries={retries}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - raise - retries += 1 - delay *= self._base_delay - logger.exception( - f"ExponentialRetry: Request failed, retrying, retries={retries}, delay={delay}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - time.sleep(delay + (self._jitter * random.uniform(0, 1))) # noqa: S311 - - async def aretry( - self, - func: Callable[..., Awaitable[Any]], - **kwargs: Any, - ) -> Any: - """Retry an asynchronous function.""" - retries = 0 - delay = 1.0 # Initial delay in seconds - while True: - try: - return await func(**kwargs) - except Exception as e: - if retries >= self._max_retries: - logger.exception( - f"ExponentialRetry: Max retries exceeded, retries={retries}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - raise - retries += 1 - delay *= self._base_delay - logger.exception( - f"ExponentialRetry: Request failed, retrying, retries={retries}, delay={delay}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - await asyncio.sleep(delay + (self._jitter * random.uniform(0, 1))) # noqa: S311 diff --git a/graphrag/language_model/providers/litellm/services/retry/incremental_wait_retry.py b/graphrag/language_model/providers/litellm/services/retry/incremental_wait_retry.py deleted file mode 100644 index 97fbdbf9c9..0000000000 --- a/graphrag/language_model/providers/litellm/services/retry/incremental_wait_retry.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Incremental Wait Retry Service.""" - -import asyncio -import logging -import time -from collections.abc import Awaitable, Callable -from typing import Any - -from graphrag.language_model.providers.litellm.services.retry.retry import Retry - -logger = logging.getLogger(__name__) - - -class IncrementalWaitRetry(Retry): - """LiteLLM Incremental Wait Retry Service.""" - - def __init__( - self, - *, - max_retry_wait: float, - max_retries: int = 5, - **kwargs: Any, - ): - if max_retries <= 0: - msg = "max_retries must be greater than 0." - raise ValueError(msg) - - if max_retry_wait <= 0: - msg = "max_retry_wait must be greater than 0." - raise ValueError(msg) - - self._max_retries = max_retries - self._max_retry_wait = max_retry_wait - self._increment = max_retry_wait / max_retries - - def retry(self, func: Callable[..., Any], **kwargs: Any) -> Any: - """Retry a synchronous function.""" - retries = 0 - delay = 0.0 - while True: - try: - return func(**kwargs) - except Exception as e: - if retries >= self._max_retries: - logger.exception( - f"IncrementalWaitRetry: Max retries exceeded, retries={retries}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - raise - retries += 1 - delay += self._increment - logger.exception( - f"IncrementalWaitRetry: Request failed, retrying after incremental delay, retries={retries}, delay={delay}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - time.sleep(delay) - - async def aretry( - self, - func: Callable[..., Awaitable[Any]], - **kwargs: Any, - ) -> Any: - """Retry an asynchronous function.""" - retries = 0 - delay = 0.0 - while True: - try: - return await func(**kwargs) - except Exception as e: - if retries >= self._max_retries: - logger.exception( - f"IncrementalWaitRetry: Max retries exceeded, retries={retries}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - raise - retries += 1 - delay += self._increment - logger.exception( - f"IncrementalWaitRetry: Request failed, retrying after incremental delay, retries={retries}, delay={delay}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - await asyncio.sleep(delay) diff --git a/graphrag/language_model/providers/litellm/services/retry/native_wait_retry.py b/graphrag/language_model/providers/litellm/services/retry/native_wait_retry.py deleted file mode 100644 index 088f454213..0000000000 --- a/graphrag/language_model/providers/litellm/services/retry/native_wait_retry.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Native Retry Service.""" - -import logging -from collections.abc import Awaitable, Callable -from typing import Any - -from graphrag.language_model.providers.litellm.services.retry.retry import Retry - -logger = logging.getLogger(__name__) - - -class NativeRetry(Retry): - """LiteLLM Native Retry Service.""" - - def __init__( - self, - *, - max_retries: int = 5, - **kwargs: Any, - ): - if max_retries <= 0: - msg = "max_retries must be greater than 0." - raise ValueError(msg) - - self._max_retries = max_retries - - def retry(self, func: Callable[..., Any], **kwargs: Any) -> Any: - """Retry a synchronous function.""" - retries = 0 - while True: - try: - return func(**kwargs) - except Exception as e: - if retries >= self._max_retries: - logger.exception( - f"NativeRetry: Max retries exceeded, retries={retries}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - raise - retries += 1 - logger.exception( - f"NativeRetry: Request failed, immediately retrying, retries={retries}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - - async def aretry( - self, - func: Callable[..., Awaitable[Any]], - **kwargs: Any, - ) -> Any: - """Retry an asynchronous function.""" - retries = 0 - while True: - try: - return await func(**kwargs) - except Exception as e: - if retries >= self._max_retries: - logger.exception( - f"NativeRetry: Max retries exceeded, retries={retries}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - raise - retries += 1 - logger.exception( - f"NativeRetry: Request failed, immediately retrying, retries={retries}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) diff --git a/graphrag/language_model/providers/litellm/services/retry/random_wait_retry.py b/graphrag/language_model/providers/litellm/services/retry/random_wait_retry.py deleted file mode 100644 index 603f439d1f..0000000000 --- a/graphrag/language_model/providers/litellm/services/retry/random_wait_retry.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Random Wait Retry Service.""" - -import asyncio -import logging -import random -import time -from collections.abc import Awaitable, Callable -from typing import Any - -from graphrag.language_model.providers.litellm.services.retry.retry import Retry - -logger = logging.getLogger(__name__) - - -class RandomWaitRetry(Retry): - """LiteLLM Random Wait Retry Service.""" - - def __init__( - self, - *, - max_retry_wait: float, - max_retries: int = 5, - **kwargs: Any, - ): - if max_retries <= 0: - msg = "max_retries must be greater than 0." - raise ValueError(msg) - - if max_retry_wait <= 0: - msg = "max_retry_wait must be greater than 0." - raise ValueError(msg) - - self._max_retries = max_retries - self._max_retry_wait = max_retry_wait - - def retry(self, func: Callable[..., Any], **kwargs: Any) -> Any: - """Retry a synchronous function.""" - retries = 0 - while True: - try: - return func(**kwargs) - except Exception as e: - if retries >= self._max_retries: - logger.exception( - f"RandomWaitRetry: Max retries exceeded, retries={retries}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - raise - retries += 1 - delay = random.uniform(0, self._max_retry_wait) # noqa: S311 - logger.exception( - f"RandomWaitRetry: Request failed, retrying after random delay, retries={retries}, delay={delay}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - time.sleep(delay) - - async def aretry( - self, - func: Callable[..., Awaitable[Any]], - **kwargs: Any, - ) -> Any: - """Retry an asynchronous function.""" - retries = 0 - while True: - try: - return await func(**kwargs) - except Exception as e: - if retries >= self._max_retries: - logger.exception( - f"RandomWaitRetry: Max retries exceeded, retries={retries}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - raise - retries += 1 - delay = random.uniform(0, self._max_retry_wait) # noqa: S311 - logger.exception( - f"RandomWaitRetry: Request failed, retrying after random delay, retries={retries}, delay={delay}, max_retries={self._max_retries}, exception={e}", # noqa: G004, TRY401 - ) - await asyncio.sleep(delay) diff --git a/graphrag/language_model/providers/litellm/services/retry/retry.py b/graphrag/language_model/providers/litellm/services/retry/retry.py deleted file mode 100644 index 4f53e598c6..0000000000 --- a/graphrag/language_model/providers/litellm/services/retry/retry.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Retry Abstract Base Class.""" - -from abc import ABC, abstractmethod -from collections.abc import Awaitable, Callable -from typing import Any - - -class Retry(ABC): - """LiteLLM Retry Abstract Base Class.""" - - @abstractmethod - def __init__(self, /, **kwargs: Any): - msg = "Retry subclasses must implement the __init__ method." - raise NotImplementedError(msg) - - @abstractmethod - def retry(self, func: Callable[..., Any], **kwargs: Any) -> Any: - """Retry a synchronous function.""" - msg = "Subclasses must implement this method" - raise NotImplementedError(msg) - - @abstractmethod - async def aretry( - self, - func: Callable[..., Awaitable[Any]], - **kwargs: Any, - ) -> Any: - """Retry an asynchronous function.""" - msg = "Subclasses must implement this method" - raise NotImplementedError(msg) diff --git a/graphrag/language_model/providers/litellm/services/retry/retry_factory.py b/graphrag/language_model/providers/litellm/services/retry/retry_factory.py deleted file mode 100644 index 15b3318630..0000000000 --- a/graphrag/language_model/providers/litellm/services/retry/retry_factory.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM Retry Factory.""" - -from graphrag.config.defaults import DEFAULT_RETRY_SERVICES -from graphrag.factory.factory import Factory -from graphrag.language_model.providers.litellm.services.retry.retry import Retry - - -class RetryFactory(Factory[Retry]): - """Singleton factory for creating retry services.""" - - -retry_factory = RetryFactory() - -for service_name, service_cls in DEFAULT_RETRY_SERVICES.items(): - retry_factory.register(strategy=service_name, service_initializer=service_cls) diff --git a/graphrag/language_model/providers/litellm/types.py b/graphrag/language_model/providers/litellm/types.py deleted file mode 100644 index cec39b13e2..0000000000 --- a/graphrag/language_model/providers/litellm/types.py +++ /dev/null @@ -1,235 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""LiteLLM types.""" - -from typing import ( - Any, - Protocol, - runtime_checkable, -) - -from litellm import ( - AnthropicThinkingParam, - BaseModel, - ChatCompletionAudioParam, - ChatCompletionModality, - ChatCompletionPredictionContentParam, - CustomStreamWrapper, - EmbeddingResponse, # type: ignore - ModelResponse, # type: ignore - OpenAIWebSearchOptions, -) -from openai.types.chat.chat_completion import ( - ChatCompletion, - Choice, -) -from openai.types.chat.chat_completion_chunk import ChatCompletionChunk, ChoiceDelta -from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice -from openai.types.chat.chat_completion_message import ChatCompletionMessage -from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam -from openai.types.completion_usage import ( - CompletionTokensDetails, - CompletionUsage, - PromptTokensDetails, -) -from openai.types.create_embedding_response import CreateEmbeddingResponse, Usage -from openai.types.embedding import Embedding - -LMChatCompletionMessageParam = ChatCompletionMessageParam | dict[str, str] - -LMChatCompletion = ChatCompletion -LMChoice = Choice -LMChatCompletionMessage = ChatCompletionMessage - -LMChatCompletionChunk = ChatCompletionChunk -LMChoiceChunk = ChunkChoice -LMChoiceDelta = ChoiceDelta - -LMCompletionUsage = CompletionUsage -LMPromptTokensDetails = PromptTokensDetails -LMCompletionTokensDetails = CompletionTokensDetails - - -LMEmbeddingResponse = CreateEmbeddingResponse -LMEmbedding = Embedding -LMEmbeddingUsage = Usage - - -@runtime_checkable -class FixedModelCompletion(Protocol): - """ - Synchronous chat completion function. - - Same signature as litellm.completion but without the `model` parameter - as this is already set in the model configuration. - """ - - def __call__( - self, - *, - messages: list = [], # type: ignore # noqa: B006 - stream: bool | None = None, - stream_options: dict | None = None, # type: ignore - stop=None, # type: ignore - max_completion_tokens: int | None = None, - max_tokens: int | None = None, - modalities: list[ChatCompletionModality] | None = None, - prediction: ChatCompletionPredictionContentParam | None = None, - audio: ChatCompletionAudioParam | None = None, - logit_bias: dict | None = None, # type: ignore - user: str | None = None, - # openai v1.0+ new params - response_format: dict | type[BaseModel] | None = None, # type: ignore - seed: int | None = None, - tools: list | None = None, # type: ignore - tool_choice: str | dict | None = None, # type: ignore - logprobs: bool | None = None, - top_logprobs: int | None = None, - parallel_tool_calls: bool | None = None, - web_search_options: OpenAIWebSearchOptions | None = None, - deployment_id=None, # type: ignore - extra_headers: dict | None = None, # type: ignore - # soon to be deprecated params by OpenAI - functions: list | None = None, # type: ignore - function_call: str | None = None, - # Optional liteLLM function params - thinking: AnthropicThinkingParam | None = None, - **kwargs: Any, - ) -> ModelResponse | CustomStreamWrapper: - """Chat completion function.""" - ... - - -@runtime_checkable -class AFixedModelCompletion(Protocol): - """ - Asynchronous chat completion function. - - Same signature as litellm.acompletion but without the `model` parameter - as this is already set in the model configuration. - """ - - async def __call__( - self, - *, - # Optional OpenAI params: see https://platform.openai.com/docs/api-reference/chat/create - messages: list = [], # type: ignore # noqa: B006 - stream: bool | None = None, - stream_options: dict | None = None, # type: ignore - stop=None, # type: ignore - max_completion_tokens: int | None = None, - max_tokens: int | None = None, - modalities: list[ChatCompletionModality] | None = None, - prediction: ChatCompletionPredictionContentParam | None = None, - audio: ChatCompletionAudioParam | None = None, - logit_bias: dict | None = None, # type: ignore - user: str | None = None, - # openai v1.0+ new params - response_format: dict | type[BaseModel] | None = None, # type: ignore - seed: int | None = None, - tools: list | None = None, # type: ignore - tool_choice: str | dict | None = None, # type: ignore - logprobs: bool | None = None, - top_logprobs: int | None = None, - parallel_tool_calls: bool | None = None, - web_search_options: OpenAIWebSearchOptions | None = None, - deployment_id=None, # type: ignore - extra_headers: dict | None = None, # type: ignore - # soon to be deprecated params by OpenAI - functions: list | None = None, # type: ignore - function_call: str | None = None, - # Optional liteLLM function params - thinking: AnthropicThinkingParam | None = None, - **kwargs: Any, - ) -> ModelResponse | CustomStreamWrapper: - """Chat completion function.""" - ... - - -@runtime_checkable -class FixedModelEmbedding(Protocol): - """ - Synchronous embedding function. - - Same signature as litellm.embedding but without the `model` parameter - as this is already set in the model configuration. - """ - - def __call__( - self, - *, - request_id: str | None = None, - input: list = [], # type: ignore # noqa: B006 - # Optional params - dimensions: int | None = None, - encoding_format: str | None = None, - timeout: int = 600, # default to 10 minutes - # set api_base, api_version, api_key - api_base: str | None = None, - api_version: str | None = None, - api_key: str | None = None, - api_type: str | None = None, - caching: bool = False, - user: str | None = None, - **kwargs: Any, - ) -> EmbeddingResponse: - """Embedding function.""" - ... - - -@runtime_checkable -class AFixedModelEmbedding(Protocol): - """ - Asynchronous embedding function. - - Same signature as litellm.embedding but without the `model` parameter - as this is already set in the model configuration. - """ - - async def __call__( - self, - *, - request_id: str | None = None, - input: list = [], # type: ignore # noqa: B006 - # Optional params - dimensions: int | None = None, - encoding_format: str | None = None, - timeout: int = 600, # default to 10 minutes - # set api_base, api_version, api_key - api_base: str | None = None, - api_version: str | None = None, - api_key: str | None = None, - api_type: str | None = None, - caching: bool = False, - user: str | None = None, - **kwargs: Any, - ) -> EmbeddingResponse: - """Embedding function.""" - ... - - -@runtime_checkable -class LitellmRequestFunc(Protocol): - """ - Synchronous request function. - - Represents either a chat completion or embedding function. - """ - - def __call__(self, /, **kwargs: Any) -> Any: - """Request function.""" - ... - - -@runtime_checkable -class AsyncLitellmRequestFunc(Protocol): - """ - Asynchronous request function. - - Represents either a chat completion or embedding function. - """ - - async def __call__(self, /, **kwargs: Any) -> Any: - """Request function.""" - ... diff --git a/graphrag/language_model/response/__init__.py b/graphrag/language_model/response/__init__.py deleted file mode 100644 index 3c4721caab..0000000000 --- a/graphrag/language_model/response/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing Model response definitions.""" diff --git a/graphrag/language_model/response/base.py b/graphrag/language_model/response/base.py deleted file mode 100644 index 178259c4b7..0000000000 --- a/graphrag/language_model/response/base.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""Base llm response protocol.""" - -from typing import Any, Generic, Protocol, TypeVar - -from pydantic import BaseModel, Field - -T = TypeVar("T", bound=BaseModel, covariant=True) - - -class ModelOutput(Protocol): - """Protocol for Model response's output object.""" - - @property - def content(self) -> str: - """Return the textual content of the output.""" - ... - - @property - def full_response(self) -> dict[str, Any] | None: - """Return the complete JSON response returned by the model.""" - ... - - -class ModelResponse(Protocol, Generic[T]): - """Protocol for LLM response.""" - - @property - def output(self) -> ModelOutput: - """Return the output of the response.""" - ... - - @property - def parsed_response(self) -> T | None: - """Return the parsed response.""" - ... - - @property - def history(self) -> list: - """Return the history of the response.""" - ... - - -class BaseModelOutput(BaseModel): - """Base class for LLM output.""" - - content: str = Field(..., description="The textual content of the output.") - """The textual content of the output.""" - full_response: dict[str, Any] | None = Field( - None, description="The complete JSON response returned by the LLM provider." - ) - """The complete JSON response returned by the LLM provider.""" - - -class BaseModelResponse(BaseModel, Generic[T]): - """Base class for a Model response.""" - - output: BaseModelOutput - """""" - parsed_response: T | None = None - """Parsed response.""" - history: list[Any] = Field(default_factory=list) - """History of the response.""" - tool_calls: list = Field(default_factory=list) - """Tool calls required by the Model. These will be instances of the LLM tools (with filled parameters).""" - metrics: Any | None = None - """Request/response metrics.""" - cache_hit: bool | None = None - """Whether the response was a cache hit.""" diff --git a/graphrag/language_model/response/base.pyi b/graphrag/language_model/response/base.pyi deleted file mode 100644 index 7a33b0a304..0000000000 --- a/graphrag/language_model/response/base.pyi +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -from typing import Any, Generic, Protocol, TypeVar - -from pydantic import BaseModel - -_T = TypeVar("_T", bound=BaseModel, covariant=True) - -class ModelOutput(Protocol): - @property - def content(self) -> str: ... - @property - def full_response(self) -> dict[str, Any] | None: ... - -class ModelResponse(Protocol, Generic[_T]): - @property - def output(self) -> ModelOutput: ... - @property - def parsed_response(self) -> _T | None: ... - @property - def history(self) -> list[Any]: ... - -class BaseModelOutput(BaseModel): - content: str - full_response: dict[str, Any] | None - - def __init__( - self, - content: str, - full_response: dict[str, Any] | None = None, - ) -> None: ... - -class BaseModelResponse(BaseModel, Generic[_T]): - output: BaseModelOutput - parsed_response: _T | None - history: list[Any] - tool_calls: list[Any] - metrics: Any | None - cache_hit: bool | None - - def __init__( - self, - output: BaseModelOutput, - parsed_response: _T | None = None, - history: list[Any] = ..., # default provided by Pydantic - tool_calls: list[Any] = ..., # default provided by Pydantic - metrics: Any | None = None, - cache_hit: bool | None = None, - ) -> None: ... diff --git a/graphrag/logger/factory.py b/graphrag/logger/factory.py deleted file mode 100644 index 03245d1bb0..0000000000 --- a/graphrag/logger/factory.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Factory functions for creating a logger.""" - -from __future__ import annotations - -import logging -from pathlib import Path -from typing import TYPE_CHECKING, ClassVar - -from graphrag.config.enums import ReportingType - -if TYPE_CHECKING: - from collections.abc import Callable - -LOG_FORMAT = "%(asctime)s.%(msecs)04d - %(levelname)s - %(name)s - %(message)s" -DATE_FORMAT = "%Y-%m-%d %H:%M:%S" - - -class LoggerFactory: - """A factory class for logger implementations. - - Includes a method for users to register a custom logger implementation. - - Configuration arguments are passed to each logger implementation as kwargs - for individual enforcement of required/optional arguments. - - Note that because we rely on the built-in Python logging architecture, this factory does not return an instance, - it merely configures the logger to your specified storage location. - """ - - _registry: ClassVar[dict[str, Callable[..., logging.Handler]]] = {} - - @classmethod - def register( - cls, reporting_type: str, creator: Callable[..., logging.Handler] - ) -> None: - """Register a custom logger implementation. - - Args: - reporting_type: The type identifier for the logger. - creator: A class or callable that initializes logging. - """ - cls._registry[reporting_type] = creator - - @classmethod - def create_logger(cls, reporting_type: str, kwargs: dict) -> logging.Handler: - """Create a logger from the provided type. - - Args: - reporting_type: The type of logger to create. - logger: The logger instance for the application. - kwargs: Additional keyword arguments for the constructor. - - Returns - ------- - A logger instance. - - Raises - ------ - ValueError: If the logger type is not registered. - """ - if reporting_type not in cls._registry: - msg = f"Unknown reporting type: {reporting_type}" - raise ValueError(msg) - - return cls._registry[reporting_type](**kwargs) - - @classmethod - def get_logger_types(cls) -> list[str]: - """Get the registered logger implementations.""" - return list(cls._registry.keys()) - - @classmethod - def is_supported_type(cls, reporting_type: str) -> bool: - """Check if the given logger type is supported.""" - return reporting_type in cls._registry - - -# --- register built-in logger implementations --- -def create_file_logger(**kwargs) -> logging.Handler: - """Create a file-based logger.""" - root_dir = kwargs["root_dir"] - base_dir = kwargs["base_dir"] - filename = kwargs["filename"] - log_dir = Path(root_dir) / base_dir - log_dir.mkdir(parents=True, exist_ok=True) - log_file_path = log_dir / filename - - handler = logging.FileHandler(str(log_file_path), mode="a") - - formatter = logging.Formatter(fmt=LOG_FORMAT, datefmt=DATE_FORMAT) - handler.setFormatter(formatter) - - return handler - - -def create_blob_logger(**kwargs) -> logging.Handler: - """Create a blob storage-based logger.""" - from graphrag.logger.blob_workflow_logger import BlobWorkflowLogger - - return BlobWorkflowLogger( - connection_string=kwargs["connection_string"], - container_name=kwargs["container_name"], - base_dir=kwargs["base_dir"], - storage_account_blob_url=kwargs["storage_account_blob_url"], - ) - - -# --- register built-in implementations --- -LoggerFactory.register(ReportingType.file.value, create_file_logger) -LoggerFactory.register(ReportingType.blob.value, create_blob_logger) diff --git a/graphrag/prompts/index/extract_graph.py b/graphrag/prompts/index/extract_graph.py deleted file mode 100644 index a94b36142e..0000000000 --- a/graphrag/prompts/index/extract_graph.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A file containing prompts definition.""" - -GRAPH_EXTRACTION_PROMPT = """ --Goal- -Given a text document that is potentially relevant to this activity and a list of entity types, identify all entities of those types from the text and all relationships among the identified entities. - --Steps- -1. Identify all entities. For each identified entity, extract the following information: -- entity_name: Name of the entity, capitalized -- entity_type: One of the following types: [{entity_types}] -- entity_description: Comprehensive description of the entity's attributes and activities -Format each entity as ("entity"{tuple_delimiter}{tuple_delimiter}{tuple_delimiter}) - -2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other. -For each pair of related entities, extract the following information: -- source_entity: name of the source entity, as identified in step 1 -- target_entity: name of the target entity, as identified in step 1 -- relationship_description: explanation as to why you think the source entity and the target entity are related to each other -- relationship_strength: a numeric score indicating strength of the relationship between the source entity and target entity - Format each relationship as ("relationship"{tuple_delimiter}{tuple_delimiter}{tuple_delimiter}{tuple_delimiter}) - -3. Return output in English as a single list of all the entities and relationships identified in steps 1 and 2. Use **{record_delimiter}** as the list delimiter. - -4. When finished, output {completion_delimiter} - -###################### --Examples- -###################### -Example 1: -Entity_types: ORGANIZATION,PERSON -Text: -The Verdantis's Central Institution is scheduled to meet on Monday and Thursday, with the institution planning to release its latest policy decision on Thursday at 1:30 p.m. PDT, followed by a press conference where Central Institution Chair Martin Smith will take questions. Investors expect the Market Strategy Committee to hold its benchmark interest rate steady in a range of 3.5%-3.75%. -###################### -Output: -("entity"{tuple_delimiter}CENTRAL INSTITUTION{tuple_delimiter}ORGANIZATION{tuple_delimiter}The Central Institution is the Federal Reserve of Verdantis, which is setting interest rates on Monday and Thursday) -{record_delimiter} -("entity"{tuple_delimiter}MARTIN SMITH{tuple_delimiter}PERSON{tuple_delimiter}Martin Smith is the chair of the Central Institution) -{record_delimiter} -("entity"{tuple_delimiter}MARKET STRATEGY COMMITTEE{tuple_delimiter}ORGANIZATION{tuple_delimiter}The Central Institution committee makes key decisions about interest rates and the growth of Verdantis's money supply) -{record_delimiter} -("relationship"{tuple_delimiter}MARTIN SMITH{tuple_delimiter}CENTRAL INSTITUTION{tuple_delimiter}Martin Smith is the Chair of the Central Institution and will answer questions at a press conference{tuple_delimiter}9) -{completion_delimiter} - -###################### -Example 2: -Entity_types: ORGANIZATION -Text: -TechGlobal's (TG) stock skyrocketed in its opening day on the Global Exchange Thursday. But IPO experts warn that the semiconductor corporation's debut on the public markets isn't indicative of how other newly listed companies may perform. - -TechGlobal, a formerly public company, was taken private by Vision Holdings in 2014. The well-established chip designer says it powers 85% of premium smartphones. -###################### -Output: -("entity"{tuple_delimiter}TECHGLOBAL{tuple_delimiter}ORGANIZATION{tuple_delimiter}TechGlobal is a stock now listed on the Global Exchange which powers 85% of premium smartphones) -{record_delimiter} -("entity"{tuple_delimiter}VISION HOLDINGS{tuple_delimiter}ORGANIZATION{tuple_delimiter}Vision Holdings is a firm that previously owned TechGlobal) -{record_delimiter} -("relationship"{tuple_delimiter}TECHGLOBAL{tuple_delimiter}VISION HOLDINGS{tuple_delimiter}Vision Holdings formerly owned TechGlobal from 2014 until present{tuple_delimiter}5) -{completion_delimiter} - -###################### -Example 3: -Entity_types: ORGANIZATION,GEO,PERSON -Text: -Five Aurelians jailed for 8 years in Firuzabad and widely regarded as hostages are on their way home to Aurelia. - -The swap orchestrated by Quintara was finalized when $8bn of Firuzi funds were transferred to financial institutions in Krohaara, the capital of Quintara. - -The exchange initiated in Firuzabad's capital, Tiruzia, led to the four men and one woman, who are also Firuzi nationals, boarding a chartered flight to Krohaara. - -They were welcomed by senior Aurelian officials and are now on their way to Aurelia's capital, Cashion. - -The Aurelians include 39-year-old businessman Samuel Namara, who has been held in Tiruzia's Alhamia Prison, as well as journalist Durke Bataglani, 59, and environmentalist Meggie Tazbah, 53, who also holds Bratinas nationality. -###################### -Output: -("entity"{tuple_delimiter}FIRUZABAD{tuple_delimiter}GEO{tuple_delimiter}Firuzabad held Aurelians as hostages) -{record_delimiter} -("entity"{tuple_delimiter}AURELIA{tuple_delimiter}GEO{tuple_delimiter}Country seeking to release hostages) -{record_delimiter} -("entity"{tuple_delimiter}QUINTARA{tuple_delimiter}GEO{tuple_delimiter}Country that negotiated a swap of money in exchange for hostages) -{record_delimiter} -{record_delimiter} -("entity"{tuple_delimiter}TIRUZIA{tuple_delimiter}GEO{tuple_delimiter}Capital of Firuzabad where the Aurelians were being held) -{record_delimiter} -("entity"{tuple_delimiter}KROHAARA{tuple_delimiter}GEO{tuple_delimiter}Capital city in Quintara) -{record_delimiter} -("entity"{tuple_delimiter}CASHION{tuple_delimiter}GEO{tuple_delimiter}Capital city in Aurelia) -{record_delimiter} -("entity"{tuple_delimiter}SAMUEL NAMARA{tuple_delimiter}PERSON{tuple_delimiter}Aurelian who spent time in Tiruzia's Alhamia Prison) -{record_delimiter} -("entity"{tuple_delimiter}ALHAMIA PRISON{tuple_delimiter}GEO{tuple_delimiter}Prison in Tiruzia) -{record_delimiter} -("entity"{tuple_delimiter}DURKE BATAGLANI{tuple_delimiter}PERSON{tuple_delimiter}Aurelian journalist who was held hostage) -{record_delimiter} -("entity"{tuple_delimiter}MEGGIE TAZBAH{tuple_delimiter}PERSON{tuple_delimiter}Bratinas national and environmentalist who was held hostage) -{record_delimiter} -("relationship"{tuple_delimiter}FIRUZABAD{tuple_delimiter}AURELIA{tuple_delimiter}Firuzabad negotiated a hostage exchange with Aurelia{tuple_delimiter}2) -{record_delimiter} -("relationship"{tuple_delimiter}QUINTARA{tuple_delimiter}AURELIA{tuple_delimiter}Quintara brokered the hostage exchange between Firuzabad and Aurelia{tuple_delimiter}2) -{record_delimiter} -("relationship"{tuple_delimiter}QUINTARA{tuple_delimiter}FIRUZABAD{tuple_delimiter}Quintara brokered the hostage exchange between Firuzabad and Aurelia{tuple_delimiter}2) -{record_delimiter} -("relationship"{tuple_delimiter}SAMUEL NAMARA{tuple_delimiter}ALHAMIA PRISON{tuple_delimiter}Samuel Namara was a prisoner at Alhamia prison{tuple_delimiter}8) -{record_delimiter} -("relationship"{tuple_delimiter}SAMUEL NAMARA{tuple_delimiter}MEGGIE TAZBAH{tuple_delimiter}Samuel Namara and Meggie Tazbah were exchanged in the same hostage release{tuple_delimiter}2) -{record_delimiter} -("relationship"{tuple_delimiter}SAMUEL NAMARA{tuple_delimiter}DURKE BATAGLANI{tuple_delimiter}Samuel Namara and Durke Bataglani were exchanged in the same hostage release{tuple_delimiter}2) -{record_delimiter} -("relationship"{tuple_delimiter}MEGGIE TAZBAH{tuple_delimiter}DURKE BATAGLANI{tuple_delimiter}Meggie Tazbah and Durke Bataglani were exchanged in the same hostage release{tuple_delimiter}2) -{record_delimiter} -("relationship"{tuple_delimiter}SAMUEL NAMARA{tuple_delimiter}FIRUZABAD{tuple_delimiter}Samuel Namara was a hostage in Firuzabad{tuple_delimiter}2) -{record_delimiter} -("relationship"{tuple_delimiter}MEGGIE TAZBAH{tuple_delimiter}FIRUZABAD{tuple_delimiter}Meggie Tazbah was a hostage in Firuzabad{tuple_delimiter}2) -{record_delimiter} -("relationship"{tuple_delimiter}DURKE BATAGLANI{tuple_delimiter}FIRUZABAD{tuple_delimiter}Durke Bataglani was a hostage in Firuzabad{tuple_delimiter}2) -{completion_delimiter} - -###################### --Real Data- -###################### -Entity_types: {entity_types} -Text: {input_text} -###################### -Output:""" - -CONTINUE_PROMPT = "MANY entities and relationships were missed in the last extraction. Remember to ONLY emit entities that match any of the previously extracted types. Add them below using the same format:\n" -LOOP_PROMPT = "It appears some entities and relationships may have still been missed. Answer Y if there are still entities or relationships that need to be added, or N if there are none. Please answer with a single letter Y or N.\n" diff --git a/graphrag/storage/factory.py b/graphrag/storage/factory.py deleted file mode 100644 index 89c4eeee0d..0000000000 --- a/graphrag/storage/factory.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Factory functions for creating storage.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, ClassVar - -from graphrag.config.enums import StorageType -from graphrag.storage.blob_pipeline_storage import BlobPipelineStorage -from graphrag.storage.cosmosdb_pipeline_storage import CosmosDBPipelineStorage -from graphrag.storage.file_pipeline_storage import FilePipelineStorage -from graphrag.storage.memory_pipeline_storage import MemoryPipelineStorage - -if TYPE_CHECKING: - from collections.abc import Callable - - from graphrag.storage.pipeline_storage import PipelineStorage - - -class StorageFactory: - """A factory class for storage implementations. - - Includes a method for users to register a custom storage implementation. - - Configuration arguments are passed to each storage implementation as kwargs - for individual enforcement of required/optional arguments. - """ - - _registry: ClassVar[dict[str, Callable[..., PipelineStorage]]] = {} - - @classmethod - def register( - cls, storage_type: str, creator: Callable[..., PipelineStorage] - ) -> None: - """Register a custom storage implementation. - - Args: - storage_type: The type identifier for the storage. - creator: A class or callable that creates an instance of PipelineStorage. - - """ - cls._registry[storage_type] = creator - - @classmethod - def create_storage(cls, storage_type: str, kwargs: dict) -> PipelineStorage: - """Create a storage object from the provided type. - - Args: - storage_type: The type of storage to create. - kwargs: Additional keyword arguments for the storage constructor. - - Returns - ------- - A PipelineStorage instance. - - Raises - ------ - ValueError: If the storage type is not registered. - """ - if storage_type not in cls._registry: - msg = f"Unknown storage type: {storage_type}" - raise ValueError(msg) - - return cls._registry[storage_type](**kwargs) - - @classmethod - def get_storage_types(cls) -> list[str]: - """Get the registered storage implementations.""" - return list(cls._registry.keys()) - - @classmethod - def is_supported_type(cls, storage_type: str) -> bool: - """Check if the given storage type is supported.""" - return storage_type in cls._registry - - -# --- register built-in storage implementations --- -StorageFactory.register(StorageType.blob.value, BlobPipelineStorage) -StorageFactory.register(StorageType.cosmosdb.value, CosmosDBPipelineStorage) -StorageFactory.register(StorageType.file.value, FilePipelineStorage) -StorageFactory.register(StorageType.memory.value, MemoryPipelineStorage) diff --git a/graphrag/storage/pipeline_storage.py b/graphrag/storage/pipeline_storage.py deleted file mode 100644 index ed117a577d..0000000000 --- a/graphrag/storage/pipeline_storage.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing 'PipelineStorage' model.""" - -import re -from abc import ABCMeta, abstractmethod -from collections.abc import Iterator -from datetime import datetime -from typing import Any - - -class PipelineStorage(metaclass=ABCMeta): - """Provide a storage interface for the pipeline. This is where the pipeline will store its output data.""" - - @abstractmethod - def find( - self, - file_pattern: re.Pattern[str], - base_dir: str | None = None, - file_filter: dict[str, Any] | None = None, - max_count=-1, - ) -> Iterator[tuple[str, dict[str, Any]]]: - """Find files in the storage using a file pattern, as well as a custom filter function.""" - - @abstractmethod - async def get( - self, key: str, as_bytes: bool | None = None, encoding: str | None = None - ) -> Any: - """Get the value for the given key. - - Args: - - key - The key to get the value for. - - as_bytes - Whether or not to return the value as bytes. - - Returns - ------- - - output - The value for the given key. - """ - - @abstractmethod - async def set(self, key: str, value: Any, encoding: str | None = None) -> None: - """Set the value for the given key. - - Args: - - key - The key to set the value for. - - value - The value to set. - """ - - @abstractmethod - async def has(self, key: str) -> bool: - """Return True if the given key exists in the storage. - - Args: - - key - The key to check for. - - Returns - ------- - - output - True if the key exists in the storage, False otherwise. - """ - - @abstractmethod - async def delete(self, key: str) -> None: - """Delete the given key from the storage. - - Args: - - key - The key to delete. - """ - - @abstractmethod - async def clear(self) -> None: - """Clear the storage.""" - - @abstractmethod - def child(self, name: str | None) -> "PipelineStorage": - """Create a child storage instance.""" - - @abstractmethod - def keys(self) -> list[str]: - """List all keys in the storage.""" - - @abstractmethod - async def get_creation_date(self, key: str) -> str: - """Get the creation date for the given key. - - Args: - - key - The key to get the creation date for. - - Returns - ------- - - output - The creation date for the given key. - """ - - -def get_timestamp_formatted_with_local_tz(timestamp: datetime) -> str: - """Get the formatted timestamp with the local time zone.""" - creation_time_local = timestamp.astimezone() - - return creation_time_local.strftime("%Y-%m-%d %H:%M:%S %z") diff --git a/graphrag/tokenizer/tokenizer.py b/graphrag/tokenizer/tokenizer.py deleted file mode 100644 index 32c0b2bd23..0000000000 --- a/graphrag/tokenizer/tokenizer.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Tokenizer Abstract Base Class.""" - -from abc import ABC, abstractmethod - - -class Tokenizer(ABC): - """Tokenizer Abstract Base Class.""" - - @abstractmethod - def encode(self, text: str) -> list[int]: - """Encode the given text into a list of tokens. - - Args - ---- - text (str): The input text to encode. - - Returns - ------- - list[int]: A list of tokens representing the encoded text. - """ - msg = "The encode method must be implemented by subclasses." - raise NotImplementedError(msg) - - @abstractmethod - def decode(self, tokens: list[int]) -> str: - """Decode a list of tokens back into a string. - - Args - ---- - tokens (list[int]): A list of tokens to decode. - - Returns - ------- - str: The decoded string from the list of tokens. - """ - msg = "The decode method must be implemented by subclasses." - raise NotImplementedError(msg) - - def num_tokens(self, text: str) -> int: - """Return the number of tokens in the given text. - - Args - ---- - text (str): The input text to analyze. - - Returns - ------- - int: The number of tokens in the input text. - """ - return len(self.encode(text)) diff --git a/graphrag/utils/api.py b/graphrag/utils/api.py deleted file mode 100644 index db3d94790d..0000000000 --- a/graphrag/utils/api.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""API functions for the GraphRAG module.""" - -from pathlib import Path -from typing import Any - -from graphrag.cache.factory import CacheFactory -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.config.embeddings import create_index_name -from graphrag.config.models.cache_config import CacheConfig -from graphrag.config.models.storage_config import StorageConfig -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig -from graphrag.data_model.types import TextEmbedder -from graphrag.storage.factory import StorageFactory -from graphrag.storage.pipeline_storage import PipelineStorage -from graphrag.vector_stores.base import ( - BaseVectorStore, - VectorStoreDocument, - VectorStoreSearchResult, -) -from graphrag.vector_stores.factory import VectorStoreFactory - - -class MultiVectorStore(BaseVectorStore): - """Multi Vector Store wrapper implementation.""" - - def __init__( - self, - embedding_stores: list[BaseVectorStore], - index_names: list[str], - ): - self.embedding_stores = embedding_stores - self.index_names = index_names - - def load_documents( - self, documents: list[VectorStoreDocument], overwrite: bool = True - ) -> None: - """Load documents into the vector store.""" - msg = "load_documents method not implemented" - raise NotImplementedError(msg) - - def connect(self, **kwargs: Any) -> Any: - """Connect to vector storage.""" - msg = "connect method not implemented" - raise NotImplementedError(msg) - - def filter_by_id(self, include_ids: list[str] | list[int]) -> Any: - """Build a query filter to filter documents by id.""" - msg = "filter_by_id method not implemented" - raise NotImplementedError(msg) - - def search_by_id(self, id: str) -> VectorStoreDocument: - """Search for a document by id.""" - search_index_id = id.split("-")[0] - search_index_name = id.split("-")[1] - for index_name, embedding_store in zip( - self.index_names, self.embedding_stores, strict=False - ): - if index_name == search_index_name: - return embedding_store.search_by_id(search_index_id) - else: - message = f"Index {search_index_name} not found." - raise ValueError(message) - - def similarity_search_by_vector( - self, query_embedding: list[float], k: int = 10, **kwargs: Any - ) -> list[VectorStoreSearchResult]: - """Perform a vector-based similarity search.""" - all_results = [] - for index_name, embedding_store in zip( - self.index_names, self.embedding_stores, strict=False - ): - results = embedding_store.similarity_search_by_vector( - query_embedding=query_embedding, k=k - ) - mod_results = [] - for r in results: - r.document.id = str(r.document.id) + f"-{index_name}" - mod_results += [r] - all_results += mod_results - return sorted(all_results, key=lambda x: x.score, reverse=True)[:k] - - def similarity_search_by_text( - self, text: str, text_embedder: TextEmbedder, k: int = 10, **kwargs: Any - ) -> list[VectorStoreSearchResult]: - """Perform a text-based similarity search.""" - query_embedding = text_embedder(text) - if query_embedding: - return self.similarity_search_by_vector( - query_embedding=query_embedding, k=k - ) - return [] - - -def get_embedding_store( - config_args: dict[str, dict], - embedding_name: str, -) -> BaseVectorStore: - """Get the embedding description store.""" - num_indexes = len(config_args) - embedding_stores = [] - index_names = [] - for index, store in config_args.items(): - vector_store_type = store["type"] - index_name = create_index_name( - store.get("container_name", "default"), embedding_name - ) - - embeddings_schema: dict[str, VectorStoreSchemaConfig] = store.get( - "embeddings_schema", {} - ) - single_embedding_config: VectorStoreSchemaConfig = VectorStoreSchemaConfig() - - if ( - embeddings_schema is not None - and embedding_name is not None - and embedding_name in embeddings_schema - ): - raw_config = embeddings_schema[embedding_name] - if isinstance(raw_config, dict): - single_embedding_config = VectorStoreSchemaConfig(**raw_config) - else: - single_embedding_config = raw_config - - if single_embedding_config.index_name is None: - single_embedding_config.index_name = index_name - - embedding_store = VectorStoreFactory().create_vector_store( - vector_store_type=vector_store_type, - vector_store_schema_config=single_embedding_config, - **store, - ) - embedding_store.connect(**store) - # If there is only a single index, return the embedding store directly - if num_indexes == 1: - return embedding_store - embedding_stores.append(embedding_store) - index_names.append(index) - return MultiVectorStore(embedding_stores, index_names) - - -def reformat_context_data(context_data: dict) -> dict: - """ - Reformats context_data for all query responses. - - Reformats a dictionary of dataframes into a dictionary of lists. - One list entry for each record. Records are grouped by original - dictionary keys. - - Note: depending on which query algorithm is used, the context_data may not - contain the same information (keys). In this case, the default behavior will be to - set these keys as empty lists to preserve a standard output format. - """ - final_format = { - "reports": [], - "entities": [], - "relationships": [], - "claims": [], - "sources": [], - } - for key in context_data: - records = ( - context_data[key].to_dict(orient="records") - if context_data[key] is not None and not isinstance(context_data[key], dict) - else context_data[key] - ) - if len(records) < 1: - continue - final_format[key] = records - return final_format - - -def update_context_data( - context_data: Any, - links: dict[str, Any], -) -> Any: - """ - Update context data with the links dict so that it contains both the index name and community id. - - Parameters - ---------- - - context_data (str | list[pd.DataFrame] | dict[str, pd.DataFrame]): The context data to update. - - links (dict[str, Any]): A dictionary of links to the original dataframes. - - Returns - ------- - str | list[pd.DataFrame] | dict[str, pd.DataFrame]: The updated context data. - """ - updated_context_data = {} - for key in context_data: - entries = context_data[key].to_dict(orient="records") - updated_entry = [] - if key == "reports": - updated_entry = [ - dict( - entry, - index_name=links["community_reports"][int(entry["id"])][ - "index_name" - ], - index_id=links["community_reports"][int(entry["id"])]["id"], - ) - for entry in entries - ] - if key == "entities": - updated_entry = [ - dict( - entry, - entity=entry["entity"].split("-")[0], - index_name=links["entities"][int(entry["id"])]["index_name"], - index_id=links["entities"][int(entry["id"])]["id"], - ) - for entry in entries - ] - if key == "relationships": - updated_entry = [ - dict( - entry, - source=entry["source"].split("-")[0], - target=entry["target"].split("-")[0], - index_name=links["relationships"][int(entry["id"])]["index_name"], - index_id=links["relationships"][int(entry["id"])]["id"], - ) - for entry in entries - ] - if key == "claims": - updated_entry = [ - dict( - entry, - entity=entry["entity"].split("-")[0], - index_name=links["covariates"][int(entry["id"])]["index_name"], - index_id=links["covariates"][int(entry["id"])]["id"], - ) - for entry in entries - ] - if key == "sources": - updated_entry = [ - dict( - entry, - index_name=links["text_units"][int(entry["id"])]["index_name"], - index_id=links["text_units"][int(entry["id"])]["id"], - ) - for entry in entries - ] - updated_context_data[key] = updated_entry - return updated_context_data - - -def load_search_prompt(root_dir: str, prompt_config: str | None) -> str | None: - """ - Load the search prompt from disk if configured. - - If not, leave it empty - the search functions will load their defaults. - - """ - if prompt_config: - prompt_file = Path(root_dir) / prompt_config - if prompt_file.exists(): - return prompt_file.read_bytes().decode(encoding="utf-8") - return None - - -def create_storage_from_config(output: StorageConfig) -> PipelineStorage: - """Create a storage object from the config.""" - storage_config = output.model_dump() - return StorageFactory().create_storage( - storage_type=storage_config["type"], - kwargs=storage_config, - ) - - -def create_cache_from_config(cache: CacheConfig, root_dir: str) -> PipelineCache: - """Create a cache object from the config.""" - cache_config = cache.model_dump() - kwargs = {**cache_config, "root_dir": root_dir} - return CacheFactory().create_cache( - cache_type=cache_config["type"], - kwargs=kwargs, - ) - - -def truncate(text: str, max_length: int) -> str: - """Truncate a string to a maximum length.""" - if len(text) <= max_length: - return text - return text[:max_length] + "...[truncated]" diff --git a/graphrag/vector_stores/__init__.py b/graphrag/vector_stores/__init__.py deleted file mode 100644 index 4f137d07bb..0000000000 --- a/graphrag/vector_stores/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A package containing vector store implementations.""" diff --git a/graphrag/vector_stores/azure_ai_search.py b/graphrag/vector_stores/azure_ai_search.py deleted file mode 100644 index 3adce873ef..0000000000 --- a/graphrag/vector_stores/azure_ai_search.py +++ /dev/null @@ -1,214 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""A package containing the Azure AI Search vector store implementation.""" - -import json -from typing import Any - -from azure.core.credentials import AzureKeyCredential -from azure.identity import DefaultAzureCredential -from azure.search.documents import SearchClient -from azure.search.documents.indexes import SearchIndexClient -from azure.search.documents.indexes.models import ( - HnswAlgorithmConfiguration, - HnswParameters, - SearchableField, - SearchField, - SearchFieldDataType, - SearchIndex, - SimpleField, - VectorSearch, - VectorSearchAlgorithmMetric, - VectorSearchProfile, -) -from azure.search.documents.models import VectorizedQuery - -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig -from graphrag.data_model.types import TextEmbedder -from graphrag.vector_stores.base import ( - BaseVectorStore, - VectorStoreDocument, - VectorStoreSearchResult, -) - - -class AzureAISearchVectorStore(BaseVectorStore): - """Azure AI Search vector storage implementation.""" - - index_client: SearchIndexClient - - def __init__( - self, vector_store_schema_config: VectorStoreSchemaConfig, **kwargs: Any - ) -> None: - super().__init__( - vector_store_schema_config=vector_store_schema_config, **kwargs - ) - - def connect(self, **kwargs: Any) -> Any: - """Connect to AI search vector storage.""" - url = kwargs["url"] - api_key = kwargs.get("api_key") - audience = kwargs.get("audience") - - self.vector_search_profile_name = kwargs.get( - "vector_search_profile_name", "vectorSearchProfile" - ) - - if url: - audience_arg = {"audience": audience} if audience and not api_key else {} - self.db_connection = SearchClient( - endpoint=url, - index_name=self.index_name if self.index_name else "", - credential=( - AzureKeyCredential(api_key) if api_key else DefaultAzureCredential() - ), - **audience_arg, - ) - self.index_client = SearchIndexClient( - endpoint=url, - credential=( - AzureKeyCredential(api_key) if api_key else DefaultAzureCredential() - ), - **audience_arg, - ) - else: - not_supported_error = "Azure AI Search expects `url`." - raise ValueError(not_supported_error) - - def load_documents( - self, documents: list[VectorStoreDocument], overwrite: bool = True - ) -> None: - """Load documents into an Azure AI Search index.""" - if overwrite: - if ( - self.index_name is not None - and self.index_name in self.index_client.list_index_names() - ): - self.index_client.delete_index(self.index_name) - - # Configure vector search profile - vector_search = VectorSearch( - algorithms=[ - HnswAlgorithmConfiguration( - name="HnswAlg", - parameters=HnswParameters( - metric=VectorSearchAlgorithmMetric.COSINE - ), - ) - ], - profiles=[ - VectorSearchProfile( - name=self.vector_search_profile_name, - algorithm_configuration_name="HnswAlg", - ) - ], - ) - # Configure the index - index = SearchIndex( - name=self.index_name if self.index_name else "", - fields=[ - SimpleField( - name=self.id_field, - type=SearchFieldDataType.String, - key=True, - ), - SearchField( - name=self.vector_field, - type=SearchFieldDataType.Collection(SearchFieldDataType.Single), - searchable=True, - hidden=False, # DRIFT needs to return the vector for client-side similarity - vector_search_dimensions=self.vector_size, - vector_search_profile_name=self.vector_search_profile_name, - ), - SearchableField( - name=self.text_field, type=SearchFieldDataType.String - ), - SimpleField( - name=self.attributes_field, - type=SearchFieldDataType.String, - ), - ], - vector_search=vector_search, - ) - self.index_client.create_or_update_index( - index, - ) - - batch = [ - { - self.id_field: doc.id, - self.vector_field: doc.vector, - self.text_field: doc.text, - self.attributes_field: json.dumps(doc.attributes), - } - for doc in documents - if doc.vector is not None - ] - - if len(batch) > 0: - self.db_connection.upload_documents(batch) - - def filter_by_id(self, include_ids: list[str] | list[int]) -> Any: - """Build a query filter to filter documents by a list of ids.""" - if include_ids is None or len(include_ids) == 0: - self.query_filter = None - # Returning to keep consistency with other methods, but not needed - return self.query_filter - - # More info about odata filtering here: https://learn.microsoft.com/en-us/azure/search/search-query-odata-search-in-function - # search.in is faster that joined and/or conditions - id_filter = ",".join([f"{id!s}" for id in include_ids]) - self.query_filter = f"search.in({self.id_field}, '{id_filter}', ',')" - - # Returning to keep consistency with other methods, but not needed - # TODO: Refactor on a future PR - return self.query_filter - - def similarity_search_by_vector( - self, query_embedding: list[float], k: int = 10, **kwargs: Any - ) -> list[VectorStoreSearchResult]: - """Perform a vector-based similarity search.""" - vectorized_query = VectorizedQuery( - vector=query_embedding, k_nearest_neighbors=k, fields=self.vector_field - ) - - response = self.db_connection.search( - vector_queries=[vectorized_query], - ) - - return [ - VectorStoreSearchResult( - document=VectorStoreDocument( - id=doc.get(self.id_field, ""), - text=doc.get(self.text_field, ""), - vector=doc.get(self.vector_field, []), - attributes=(json.loads(doc.get(self.attributes_field, "{}"))), - ), - # Cosine similarity between 0.333 and 1.000 - # https://learn.microsoft.com/en-us/azure/search/hybrid-search-ranking#scores-in-a-hybrid-search-results - score=doc["@search.score"], - ) - for doc in response - ] - - def similarity_search_by_text( - self, text: str, text_embedder: TextEmbedder, k: int = 10, **kwargs: Any - ) -> list[VectorStoreSearchResult]: - """Perform a text-based similarity search.""" - query_embedding = text_embedder(text) - if query_embedding: - return self.similarity_search_by_vector( - query_embedding=query_embedding, k=k - ) - return [] - - def search_by_id(self, id: str) -> VectorStoreDocument: - """Search for a document by id.""" - response = self.db_connection.get_document(id) - return VectorStoreDocument( - id=response.get(self.id_field, ""), - text=response.get(self.text_field, ""), - vector=response.get(self.vector_field, []), - attributes=(json.loads(response.get(self.attributes_field, "{}"))), - ) diff --git a/graphrag/vector_stores/base.py b/graphrag/vector_stores/base.py deleted file mode 100644 index 762bc93232..0000000000 --- a/graphrag/vector_stores/base.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Base classes for vector stores.""" - -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from typing import Any - -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig -from graphrag.data_model.types import TextEmbedder - - -@dataclass -class VectorStoreDocument: - """A document that is stored in vector storage.""" - - id: str | int - """unique id for the document""" - - text: str | None - vector: list[float] | None - - attributes: dict[str, Any] = field(default_factory=dict) - """store any additional metadata, e.g. title, date ranges, etc""" - - -@dataclass -class VectorStoreSearchResult: - """A vector storage search result.""" - - document: VectorStoreDocument - """Document that was found.""" - - score: float - """Similarity score between -1 and 1. Higher is more similar.""" - - -class BaseVectorStore(ABC): - """The base class for vector storage data-access classes.""" - - def __init__( - self, - vector_store_schema_config: VectorStoreSchemaConfig, - db_connection: Any | None = None, - document_collection: Any | None = None, - query_filter: Any | None = None, - **kwargs: Any, - ): - self.db_connection = db_connection - self.document_collection = document_collection - self.query_filter = query_filter - self.kwargs = kwargs - - self.index_name = vector_store_schema_config.index_name - self.id_field = vector_store_schema_config.id_field - self.text_field = vector_store_schema_config.text_field - self.vector_field = vector_store_schema_config.vector_field - self.attributes_field = vector_store_schema_config.attributes_field - self.vector_size = vector_store_schema_config.vector_size - - @abstractmethod - def connect(self, **kwargs: Any) -> None: - """Connect to vector storage.""" - - @abstractmethod - def load_documents( - self, documents: list[VectorStoreDocument], overwrite: bool = True - ) -> None: - """Load documents into the vector-store.""" - - @abstractmethod - def similarity_search_by_vector( - self, query_embedding: list[float], k: int = 10, **kwargs: Any - ) -> list[VectorStoreSearchResult]: - """Perform ANN search by vector.""" - - @abstractmethod - def similarity_search_by_text( - self, text: str, text_embedder: TextEmbedder, k: int = 10, **kwargs: Any - ) -> list[VectorStoreSearchResult]: - """Perform ANN search by text.""" - - @abstractmethod - def filter_by_id(self, include_ids: list[str] | list[int]) -> Any: - """Build a query filter to filter documents by id.""" - - @abstractmethod - def search_by_id(self, id: str) -> VectorStoreDocument: - """Search for a document by id.""" diff --git a/graphrag/vector_stores/factory.py b/graphrag/vector_stores/factory.py deleted file mode 100644 index 8e4f7baa30..0000000000 --- a/graphrag/vector_stores/factory.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Factory functions for creating a vector store.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, ClassVar - -from graphrag.config.enums import VectorStoreType -from graphrag.vector_stores.azure_ai_search import AzureAISearchVectorStore -from graphrag.vector_stores.cosmosdb import CosmosDBVectorStore -from graphrag.vector_stores.lancedb import LanceDBVectorStore - -if TYPE_CHECKING: - from collections.abc import Callable - - from graphrag.config.models.vector_store_schema_config import ( - VectorStoreSchemaConfig, - ) - from graphrag.vector_stores.base import BaseVectorStore - - -class VectorStoreFactory: - """A factory for vector stores. - - Includes a method for users to register a custom vector store implementation. - - Configuration arguments are passed to each vector store implementation as kwargs - for individual enforcement of required/optional arguments. - """ - - _registry: ClassVar[dict[str, Callable[..., BaseVectorStore]]] = {} - - @classmethod - def register( - cls, vector_store_type: str, creator: Callable[..., BaseVectorStore] - ) -> None: - """Register a custom vector store implementation. - - Args: - vector_store_type: The type identifier for the vector store. - creator: A class or callable that creates an instance of BaseVectorStore. - - Raises - ------ - TypeError: If creator is a class type instead of a factory function. - """ - cls._registry[vector_store_type] = creator - - @classmethod - def create_vector_store( - cls, - vector_store_type: str, - vector_store_schema_config: VectorStoreSchemaConfig, - **kwargs: dict, - ) -> BaseVectorStore: - """Create a vector store object from the provided type. - - Args: - vector_store_type: The type of vector store to create. - kwargs: Additional keyword arguments for the vector store constructor. - - Returns - ------- - A BaseVectorStore instance. - - Raises - ------ - ValueError: If the vector store type is not registered. - """ - if vector_store_type not in cls._registry: - msg = f"Unknown vector store type: {vector_store_type}" - raise ValueError(msg) - - return cls._registry[vector_store_type]( - vector_store_schema_config=vector_store_schema_config, **kwargs - ) - - @classmethod - def get_vector_store_types(cls) -> list[str]: - """Get the registered vector store implementations.""" - return list(cls._registry.keys()) - - @classmethod - def is_supported_type(cls, vector_store_type: str) -> bool: - """Check if the given vector store type is supported.""" - return vector_store_type in cls._registry - - -# --- register built-in vector store implementations --- -VectorStoreFactory.register(VectorStoreType.LanceDB.value, LanceDBVectorStore) -VectorStoreFactory.register( - VectorStoreType.AzureAISearch.value, AzureAISearchVectorStore -) -VectorStoreFactory.register(VectorStoreType.CosmosDB.value, CosmosDBVectorStore) diff --git a/graphrag/vector_stores/lancedb.py b/graphrag/vector_stores/lancedb.py deleted file mode 100644 index e2b52858ec..0000000000 --- a/graphrag/vector_stores/lancedb.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""The LanceDB vector storage implementation package.""" - -import json # noqa: I001 -from typing import Any -import pyarrow as pa -import numpy as np -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig -from graphrag.data_model.types import TextEmbedder - -from graphrag.vector_stores.base import ( - BaseVectorStore, - VectorStoreDocument, - VectorStoreSearchResult, -) -import lancedb - - -class LanceDBVectorStore(BaseVectorStore): - """LanceDB vector storage implementation.""" - - def __init__( - self, vector_store_schema_config: VectorStoreSchemaConfig, **kwargs: Any - ) -> None: - super().__init__( - vector_store_schema_config=vector_store_schema_config, **kwargs - ) - - def connect(self, **kwargs: Any) -> Any: - """Connect to the vector storage.""" - self.db_connection = lancedb.connect(kwargs["db_uri"]) - - if self.index_name and self.index_name in self.db_connection.table_names(): - self.document_collection = self.db_connection.open_table(self.index_name) - - def load_documents( - self, documents: list[VectorStoreDocument], overwrite: bool = True - ) -> None: - """Load documents into vector storage.""" - # Step 1: Prepare data columns manually - ids = [] - texts = [] - vectors = [] - attributes = [] - - for document in documents: - self.vector_size = ( - len(document.vector) if document.vector else self.vector_size - ) - if document.vector is not None and len(document.vector) == self.vector_size: - ids.append(document.id) - texts.append(document.text) - vectors.append(np.array(document.vector, dtype=np.float32)) - attributes.append(json.dumps(document.attributes)) - - # Step 2: Handle empty case - if len(ids) == 0: - data = None - else: - # Step 3: Flatten the vectors and build FixedSizeListArray manually - flat_vector = np.concatenate(vectors).astype(np.float32) - flat_array = pa.array(flat_vector, type=pa.float32()) - vector_column = pa.FixedSizeListArray.from_arrays( - flat_array, self.vector_size - ) - - # Step 4: Create PyArrow table (let schema be inferred) - data = pa.table({ - self.id_field: pa.array(ids, type=pa.string()), - self.text_field: pa.array(texts, type=pa.string()), - self.vector_field: vector_column, - self.attributes_field: pa.array(attributes, type=pa.string()), - }) - - # NOTE: If modifying the next section of code, ensure that the schema remains the same. - # The pyarrow format of the 'vector' field may change if the order of operations is changed - # and will break vector search. - if overwrite: - if data: - self.document_collection = self.db_connection.create_table( - self.index_name if self.index_name else "", - data=data, - mode="overwrite", - schema=data.schema, - ) - else: - self.document_collection = self.db_connection.create_table( - self.index_name if self.index_name else "", mode="overwrite" - ) - self.document_collection.create_index( - vector_column_name=self.vector_field, index_type="IVF_FLAT" - ) - else: - # add data to existing table - self.document_collection = self.db_connection.open_table( - self.index_name if self.index_name else "" - ) - if data: - self.document_collection.add(data) - - def filter_by_id(self, include_ids: list[str] | list[int]) -> Any: - """Build a query filter to filter documents by id.""" - if len(include_ids) == 0: - self.query_filter = None - else: - if isinstance(include_ids[0], str): - id_filter = ", ".join([f"'{id}'" for id in include_ids]) - self.query_filter = f"{self.id_field} in ({id_filter})" - else: - self.query_filter = ( - f"{self.id_field} in ({', '.join([str(id) for id in include_ids])})" - ) - return self.query_filter - - def similarity_search_by_vector( - self, query_embedding: list[float] | np.ndarray, k: int = 10, **kwargs: Any - ) -> list[VectorStoreSearchResult]: - """Perform a vector-based similarity search.""" - if self.query_filter: - docs = ( - self.document_collection.search( - query=query_embedding, vector_column_name=self.vector_field - ) - .where(self.query_filter, prefilter=True) - .limit(k) - .to_list() - ) - else: - query_embedding = np.array(query_embedding, dtype=np.float32) - - docs = ( - self.document_collection.search( - query=query_embedding, vector_column_name=self.vector_field - ) - .limit(k) - .to_list() - ) - return [ - VectorStoreSearchResult( - document=VectorStoreDocument( - id=doc[self.id_field], - text=doc[self.text_field], - vector=doc[self.vector_field], - attributes=json.loads(doc[self.attributes_field]), - ), - score=1 - abs(float(doc["_distance"])), - ) - for doc in docs - ] - - def similarity_search_by_text( - self, text: str, text_embedder: TextEmbedder, k: int = 10, **kwargs: Any - ) -> list[VectorStoreSearchResult]: - """Perform a similarity search using a given input text.""" - query_embedding = text_embedder(text) - if query_embedding: - return self.similarity_search_by_vector(query_embedding, k) - return [] - - def search_by_id(self, id: str) -> VectorStoreDocument: - """Search for a document by id.""" - doc = ( - self.document_collection.search() - .where(f"{self.id_field} == '{id}'", prefilter=True) - .to_list() - ) - if doc: - return VectorStoreDocument( - id=doc[0][self.id_field], - text=doc[0][self.text_field], - vector=doc[0][self.vector_field], - attributes=json.loads(doc[0][self.attributes_field]), - ) - return VectorStoreDocument(id=id, text=None, vector=None) diff --git a/packages/graphrag-cache/README.md b/packages/graphrag-cache/README.md new file mode 100644 index 0000000000..d700900b2d --- /dev/null +++ b/packages/graphrag-cache/README.md @@ -0,0 +1,109 @@ +# GraphRAG Cache + +This package contains a collection of utilities to handle GraphRAG caching implementation. + +### Basic + +This example shows how to create a JSON cache with file storage using the GraphRAG cache package's configuration system. + +```python +import asyncio +from graphrag_storage import StorageConfig, create_storage, StorageType +from graphrag_cache import CacheConfig, create_cache, CacheType, create_cache_key + +async def run(): + cache = create_cache() + + # The above is equivalent to the following: + cache = create_cache( + CacheConfig( + type=CacheType.Json, + storage=StorageConfig( + type=StorageType.File, + base_dir="cache" + ) + ), + ) + + await cache.set("my_key", {"some": "object to cache"}) + print(await cache.get("my_key")) + + # create cache key from data dict. + cache_key = create_cache_key({ + "some_arg": "some_value", + "something_else": 5 + }) + await cache.set(cache_key, {"some": "object to cache"}) + print(await cache.get(cache_key)) + +if __name__ == "__main__": + asyncio.run(run()) +``` + +### Custom Cache + +This demonstrates how to create a custom cache implementation by extending the base Cache class and registering it with the GraphRAG cache system. Once registered, the custom cache can be instantiated through the factory pattern using either CacheConfig or directly via cache_factory, allowing for extensible caching solutions tailored to specific needs. + +```python +import asyncio +from typing import Any +from graphrag_storage import Storage +from graphrag_cache import Cache, CacheConfig, create_cache, register_cache + +class MyCache(Cache): + def __init__(self, some_setting: str, optional_setting: str = "default setting", **kwargs: Any): + # Validate settings and initialize + # View the JsonCache implementation to see how to create a cache that relies on a Storage provider. + ... + + #Implement rest of interface + ... + +register_cache("MyCache", MyCache) + +async def run(): + cache = create_cache( + CacheConfig( + type="MyCache", + some_setting="My Setting" + ) + ) + + # Or use the factory directly to instantiate with a dict instead of using + # CacheConfig + create_factory + # from graphrag_cache.cache_factory import cache_factory + # cache = cache_factory.create(strategy="MyCache", init_args={"some_setting": "My Setting"}) + + await cache.set("my_key", {"some": "object to cache"}) + print(await cache.get("my_key")) + +if __name__ == "__main__": + asyncio.run(run()) +``` + +#### Details + +By default, the `create_cache` comes with the following cache providers registered that correspond to the entries in the `CacheType` enum. + +- `JsonCache` +- `MemoryCache` +- `NoopCache` + +The preregistration happens dynamically, e.g., `JsonCache` is only imported and registered if you request a `JsonCache` with `create_cache(CacheType.Json, ...)`. There is no need to manually import and register builtin cache providers when using `create_cache`. + +If you want a clean factory with no preregistered cache providers then directly import `cache_factory` and bypass using `create_cache`. The downside is that `cache_factory.create` uses a dict for init args instead of the strongly typed `CacheConfig` used with `create_cache`. + +```python +from graphrag_cache.cache_factory import cache_factory +from graphrag_cache.json_cache import JsonCache + +# cache_factory has no preregistered providers so you must register any +# providers you plan on using. +# May also register a custom implementation, see above for example. +cache_factory.register("my_cache_impl", JsonCache) + +cache = cache_factory.create(strategy="my_cache_impl", init_args={"some_setting": "..."}) + +... + +``` \ No newline at end of file diff --git a/packages/graphrag-cache/graphrag_cache/__init__.py b/packages/graphrag-cache/graphrag_cache/__init__.py new file mode 100644 index 0000000000..41caa0c72e --- /dev/null +++ b/packages/graphrag-cache/graphrag_cache/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""The GraphRAG Cache package.""" + +from graphrag_cache.cache import Cache +from graphrag_cache.cache_config import CacheConfig +from graphrag_cache.cache_factory import create_cache, register_cache +from graphrag_cache.cache_key import CacheKeyCreator, create_cache_key +from graphrag_cache.cache_type import CacheType + +__all__ = [ + "Cache", + "CacheConfig", + "CacheKeyCreator", + "CacheType", + "create_cache", + "create_cache_key", + "register_cache", +] diff --git a/graphrag/cache/pipeline_cache.py b/packages/graphrag-cache/graphrag_cache/cache.py similarity index 80% rename from graphrag/cache/pipeline_cache.py rename to packages/graphrag-cache/graphrag_cache/cache.py index c68c5cfb4b..8395bb4b4c 100644 --- a/graphrag/cache/pipeline_cache.py +++ b/packages/graphrag-cache/graphrag_cache/cache.py @@ -1,17 +1,24 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing 'PipelineCache' model.""" +"""Abstract base class for cache.""" from __future__ import annotations -from abc import ABCMeta, abstractmethod -from typing import Any +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any +if TYPE_CHECKING: + from graphrag_storage import Storage -class PipelineCache(metaclass=ABCMeta): + +class Cache(ABC): """Provide a cache interface for the pipeline.""" + @abstractmethod + def __init__(self, *, storage: Storage | None, **kwargs: Any) -> None: + """Create a cache instance.""" + @abstractmethod async def get(self, key: str) -> Any: """Get the value for the given key. @@ -59,7 +66,7 @@ async def clear(self) -> None: """Clear the cache.""" @abstractmethod - def child(self, name: str) -> PipelineCache: + def child(self, name: str) -> Cache: """Create a child cache with the given name. Args: diff --git a/packages/graphrag-cache/graphrag_cache/cache_config.py b/packages/graphrag-cache/graphrag_cache/cache_config.py new file mode 100644 index 0000000000..93bcabed6d --- /dev/null +++ b/packages/graphrag-cache/graphrag_cache/cache_config.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Cache configuration model.""" + +from graphrag_storage import StorageConfig, StorageType +from pydantic import BaseModel, ConfigDict, Field + +from graphrag_cache.cache_type import CacheType + + +class CacheConfig(BaseModel): + """The configuration section for cache.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom cache implementations.""" + + type: str = Field( + description="The cache type to use. Builtin types include 'Json', 'Memory', and 'Noop'.", + default=CacheType.Json, + ) + + storage: StorageConfig | None = Field( + description="The storage configuration to use for file-based caches such as 'Json'.", + default_factory=lambda: StorageConfig(type=StorageType.File, base_dir="cache"), + ) diff --git a/packages/graphrag-cache/graphrag_cache/cache_factory.py b/packages/graphrag-cache/graphrag_cache/cache_factory.py new file mode 100644 index 0000000000..6b1310754c --- /dev/null +++ b/packages/graphrag-cache/graphrag_cache/cache_factory.py @@ -0,0 +1,95 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + + +"""Cache factory implementation.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from graphrag_common.factory import Factory +from graphrag_storage import create_storage + +from graphrag_cache.cache_config import CacheConfig +from graphrag_cache.cache_type import CacheType + +if TYPE_CHECKING: + from graphrag_common.factory import ServiceScope + from graphrag_storage import Storage + + from graphrag_cache.cache import Cache + + +class CacheFactory(Factory["Cache"]): + """A factory class for cache implementations.""" + + +cache_factory = CacheFactory() + + +def register_cache( + cache_type: str, + cache_initializer: Callable[..., "Cache"], + scope: "ServiceScope" = "transient", +) -> None: + """Register a custom cache implementation. + + Args + ---- + - cache_type: str + The cache id to register. + - cache_initializer: Callable[..., Cache] + The cache initializer to register. + """ + cache_factory.register(cache_type, cache_initializer, scope) + + +def create_cache( + config: CacheConfig | None = None, storage: "Storage | None" = None +) -> "Cache": + """Create a cache implementation based on the given configuration. + + Args + ---- + - config: CacheConfig + The cache configuration to use. + - storage: Storage | None + The storage implementation to use for file-based caches such as 'Json'. + + Returns + ------- + Cache + The created cache implementation. + """ + config = config or CacheConfig() + config_model = config.model_dump() + cache_strategy = config.type + + if not storage and config.storage: + storage = create_storage(config.storage) + + if cache_strategy not in cache_factory: + match cache_strategy: + case CacheType.Json: + from graphrag_cache.json_cache import JsonCache + + register_cache(CacheType.Json, JsonCache) + + case CacheType.Memory: + from graphrag_cache.memory_cache import MemoryCache + + register_cache(CacheType.Memory, MemoryCache) + + case CacheType.Noop: + from graphrag_cache.noop_cache import NoopCache + + register_cache(CacheType.Noop, NoopCache) + + case _: + msg = f"CacheConfig.type '{cache_strategy}' is not registered in the CacheFactory. Registered types: {', '.join(cache_factory.keys())}." + raise ValueError(msg) + + if storage: + config_model["storage"] = storage + + return cache_factory.create(strategy=cache_strategy, init_args=config_model) diff --git a/packages/graphrag-cache/graphrag_cache/cache_key.py b/packages/graphrag-cache/graphrag_cache/cache_key.py new file mode 100644 index 0000000000..6bf4930b97 --- /dev/null +++ b/packages/graphrag-cache/graphrag_cache/cache_key.py @@ -0,0 +1,36 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Create cache key.""" + +from typing import Any, Protocol, runtime_checkable + +from graphrag_common.hasher import hash_data + + +@runtime_checkable +class CacheKeyCreator(Protocol): + """Create cache key function protocol. + + Args + ---- + input_args: dict[str, Any] + The input arguments for creating the cache key. + + Returns + ------- + str + The generated cache key. + """ + + def __call__( + self, + input_args: dict[str, Any], + ) -> str: + """Create cache key.""" + ... + + +def create_cache_key(input_args: dict[str, Any]) -> str: + """Create a cache key based on the input arguments.""" + return hash_data(input_args) diff --git a/packages/graphrag-cache/graphrag_cache/cache_type.py b/packages/graphrag-cache/graphrag_cache/cache_type.py new file mode 100644 index 0000000000..4b1fe966e9 --- /dev/null +++ b/packages/graphrag-cache/graphrag_cache/cache_type.py @@ -0,0 +1,15 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + + +"""Builtin cache implementation types.""" + +from enum import StrEnum + + +class CacheType(StrEnum): + """Enum for cache types.""" + + Json = "json" + Memory = "memory" + Noop = "none" diff --git a/graphrag/cache/json_pipeline_cache.py b/packages/graphrag-cache/graphrag_cache/json_cache.py similarity index 58% rename from graphrag/cache/json_pipeline_cache.py rename to packages/graphrag-cache/graphrag_cache/json_cache.py index 84cd180c52..dc78551cf2 100644 --- a/graphrag/cache/json_pipeline_cache.py +++ b/packages/graphrag-cache/graphrag_cache/json_cache.py @@ -1,31 +1,40 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing 'JsonPipelineCache' model.""" +"""A module containing 'JsonCache' model.""" import json from typing import Any -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.storage.pipeline_storage import PipelineStorage +from graphrag_storage import Storage, StorageConfig, create_storage +from graphrag_cache.cache import Cache -class JsonPipelineCache(PipelineCache): + +class JsonCache(Cache): """File pipeline cache class definition.""" - _storage: PipelineStorage - _encoding: str + _storage: Storage - def __init__(self, storage: PipelineStorage, encoding="utf-8"): + def __init__( + self, + storage: Storage | dict[str, Any] | None = None, + **kwargs: Any, + ) -> None: """Init method definition.""" - self._storage = storage - self._encoding = encoding + if storage is None: + msg = "JsonCache requires either a Storage instance to be provided or a StorageConfig to create one." + raise ValueError(msg) + if isinstance(storage, Storage): + self._storage = storage + else: + self._storage = create_storage(StorageConfig(**storage)) - async def get(self, key: str) -> str | None: + async def get(self, key: str) -> Any | None: """Get method definition.""" if await self.has(key): try: - data = await self._storage.get(key, encoding=self._encoding) + data = await self._storage.get(key) data = json.loads(data) except UnicodeDecodeError: await self._storage.delete(key) @@ -43,9 +52,7 @@ async def set(self, key: str, value: Any, debug_data: dict | None = None) -> Non if value is None: return data = {"result": value, **(debug_data or {})} - await self._storage.set( - key, json.dumps(data, ensure_ascii=False), encoding=self._encoding - ) + await self._storage.set(key, json.dumps(data, ensure_ascii=False)) async def has(self, key: str) -> bool: """Has method definition.""" @@ -60,6 +67,6 @@ async def clear(self) -> None: """Clear method definition.""" await self._storage.clear() - def child(self, name: str) -> "JsonPipelineCache": + def child(self, name: str) -> "Cache": """Child method definition.""" - return JsonPipelineCache(self._storage.child(name), encoding=self._encoding) + return JsonCache(storage=self._storage.child(name)) diff --git a/graphrag/cache/memory_pipeline_cache.py b/packages/graphrag-cache/graphrag_cache/memory_cache.py similarity index 71% rename from graphrag/cache/memory_pipeline_cache.py rename to packages/graphrag-cache/graphrag_cache/memory_cache.py index 62de552f96..0ab3ca4838 100644 --- a/graphrag/cache/memory_pipeline_cache.py +++ b/packages/graphrag-cache/graphrag_cache/memory_cache.py @@ -1,23 +1,22 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing 'InMemoryCache' model.""" +"""MemoryCache implementation.""" from typing import Any -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag_cache.cache import Cache -class InMemoryCache(PipelineCache): +class MemoryCache(Cache): """In memory cache class definition.""" _cache: dict[str, Any] _name: str - def __init__(self, name: str | None = None): + def __init__(self, **kwargs: Any) -> None: """Init method definition.""" self._cache = {} - self._name = name or "" async def get(self, key: str) -> Any: """Get the value for the given key. @@ -30,7 +29,6 @@ async def get(self, key: str) -> Any: ------- - output - The value for the given key. """ - key = self._create_cache_key(key) return self._cache.get(key) async def set(self, key: str, value: Any, debug_data: dict | None = None) -> None: @@ -40,7 +38,6 @@ async def set(self, key: str, value: Any, debug_data: dict | None = None) -> Non - key - The key to set the value for. - value - The value to set. """ - key = self._create_cache_key(key) self._cache[key] = value async def has(self, key: str) -> bool: @@ -53,7 +50,6 @@ async def has(self, key: str) -> bool: ------- - output - True if the key exists in the storage, False otherwise. """ - key = self._create_cache_key(key) return key in self._cache async def delete(self, key: str) -> None: @@ -62,17 +58,12 @@ async def delete(self, key: str) -> None: Args: - key - The key to delete. """ - key = self._create_cache_key(key) del self._cache[key] async def clear(self) -> None: """Clear the storage.""" self._cache.clear() - def child(self, name: str) -> PipelineCache: + def child(self, name: str) -> "Cache": """Create a sub cache with the given name.""" - return InMemoryCache(name) - - def _create_cache_key(self, key: str) -> str: - """Create a cache key for the given key.""" - return f"{self._name}{key}" + return MemoryCache() diff --git a/graphrag/cache/noop_pipeline_cache.py b/packages/graphrag-cache/graphrag_cache/noop_cache.py similarity index 82% rename from graphrag/cache/noop_pipeline_cache.py rename to packages/graphrag-cache/graphrag_cache/noop_cache.py index 227ef687b8..5c3fe1368a 100644 --- a/graphrag/cache/noop_pipeline_cache.py +++ b/packages/graphrag-cache/graphrag_cache/noop_cache.py @@ -1,15 +1,18 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""Module containing the NoopPipelineCache implementation.""" +"""NoopCache implementation.""" from typing import Any -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag_cache.cache import Cache -class NoopPipelineCache(PipelineCache): - """A no-op implementation of the pipeline cache, usually useful for testing.""" +class NoopCache(Cache): + """A no-op implementation of Cache, usually useful for testing.""" + + def __init__(self, **kwargs: Any) -> None: + """Init method definition.""" async def get(self, key: str) -> Any: """Get the value for the given key. @@ -56,7 +59,7 @@ async def delete(self, key: str) -> None: async def clear(self) -> None: """Clear the cache.""" - def child(self, name: str) -> PipelineCache: + def child(self, name: str) -> "Cache": """Create a child cache with the given name. Args: diff --git a/packages/graphrag-cache/graphrag_cache/py.typed b/packages/graphrag-cache/graphrag_cache/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/graphrag-cache/pyproject.toml b/packages/graphrag-cache/pyproject.toml new file mode 100644 index 0000000000..65bed4921f --- /dev/null +++ b/packages/graphrag-cache/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "graphrag-cache" +version = "2.7.1" +description = "GraphRAG cache package." +authors = [ + {name = "Alonso Guevara Fernández", email = "alonsog@microsoft.com"}, + {name = "Andrés Morales Esquivel", email = "andresmor@microsoft.com"}, + {name = "Chris Trevino", email = "chtrevin@microsoft.com"}, + {name = "David Tittsworth", email = "datittsw@microsoft.com"}, + {name = "Dayenne de Souza", email = "ddesouza@microsoft.com"}, + {name = "Derek Worthen", email = "deworthe@microsoft.com"}, + {name = "Gaudy Blanco Meneses", email = "gaudyb@microsoft.com"}, + {name = "Ha Trinh", email = "trinhha@microsoft.com"}, + {name = "Jonathan Larson", email = "jolarso@microsoft.com"}, + {name = "Josh Bradley", email = "joshbradley@microsoft.com"}, + {name = "Kate Lytvynets", email = "kalytv@microsoft.com"}, + {name = "Kenny Zhang", email = "zhangken@microsoft.com"}, + {name = "Mónica Carvajal"}, + {name = "Nathan Evans", email = "naevans@microsoft.com"}, + {name = "Rodrigo Racanicci", email = "rracanicci@microsoft.com"}, + {name = "Sarah Smith", email = "smithsarah@microsoft.com"}, +] +license = "MIT" +readme = "README.md" +license-files = ["LICENSE"] +requires-python = ">=3.11,<3.14" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "graphrag-common==2.7.1", + "graphrag-storage==2.7.1", +] + +[project.urls] +Source = "https://github.com/microsoft/graphrag" + +[build-system] +requires = ["hatchling>=1.27.0,<2.0.0"] +build-backend = "hatchling.build" diff --git a/packages/graphrag-chunking/README.md b/packages/graphrag-chunking/README.md new file mode 100644 index 0000000000..64b8e13998 --- /dev/null +++ b/packages/graphrag-chunking/README.md @@ -0,0 +1,41 @@ +# GraphRAG Chunking + +This package contains a collection of text chunkers, a core config model, and a factory for acquiring instances. + +## Examples + +### Basic sentence chunking with nltk + +The SentenceChunker class splits text into individual sentences by identifying sentence boundaries. It takes input text and returns a list where each element is a separate sentence, making it easy to process text at the sentence level. + +```python +chunker = SentenceChunker() +chunks = chunker.chunk("This is a test. Another sentence.") +print(chunks) # ["This is a test.", "Another sentence."] +``` + +### Token chunking + +The TokenChunker splits text into fixed-size chunks based on token count rather than sentence boundaries. It uses a tokenizer to encode text into tokens, then creates chunks of a specified size with configurable overlap between chunks. + +```python +tokenizer = tiktoken.get_encoding("o200k_base") +chunker = TokenChunker(size=3, overlap=0, encode=tokenizer.encode, decode=tokenizer.decode) +chunks = chunker.chunk("This is a random test fragment of some text") +print(chunks) # ["This is a", " random test fragment", " of some text"] +``` + +### Using the factory via helper util + +The create_chunker factory function provides a configuration-driven approach to instantiate chunkers by accepting a ChunkingConfig object that specifies the chunking strategy and parameters. This allows for more flexible and maintainable code by separating chunker configuration from direct instantiation. + +```python +tokenizer = tiktoken.get_encoding("o200k_base") +config = ChunkingConfig( + strategy="tokens", + size=3, + overlap=0 +) +chunker = create_chunker(config, tokenizer.encode, tokenizer.decode) +... +``` \ No newline at end of file diff --git a/tests/unit/litellm_services/__init__.py b/packages/graphrag-chunking/graphrag_chunking/__init__.py similarity index 66% rename from tests/unit/litellm_services/__init__.py rename to packages/graphrag-chunking/graphrag_chunking/__init__.py index 0a3e38adfb..6610d3a82f 100644 --- a/tests/unit/litellm_services/__init__.py +++ b/packages/graphrag-chunking/graphrag_chunking/__init__.py @@ -1,2 +1,4 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License + +"""System-level chunking package.""" diff --git a/graphrag/index/operations/chunk_text/bootstrap.py b/packages/graphrag-chunking/graphrag_chunking/bootstrap_nltk.py similarity index 100% rename from graphrag/index/operations/chunk_text/bootstrap.py rename to packages/graphrag-chunking/graphrag_chunking/bootstrap_nltk.py diff --git a/packages/graphrag-chunking/graphrag_chunking/chunk_strategy_type.py b/packages/graphrag-chunking/graphrag_chunking/chunk_strategy_type.py new file mode 100644 index 0000000000..5de6321519 --- /dev/null +++ b/packages/graphrag-chunking/graphrag_chunking/chunk_strategy_type.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Chunk strategy type enumeration.""" + +from enum import StrEnum + + +class ChunkerType(StrEnum): + """ChunkerType class definition.""" + + Tokens = "tokens" + Sentence = "sentence" diff --git a/packages/graphrag-chunking/graphrag_chunking/chunker.py b/packages/graphrag-chunking/graphrag_chunking/chunker.py new file mode 100644 index 0000000000..3e44ce46f4 --- /dev/null +++ b/packages/graphrag-chunking/graphrag_chunking/chunker.py @@ -0,0 +1,24 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing the 'Chunker' class.""" + +from abc import ABC, abstractmethod +from collections.abc import Callable +from typing import Any + +from graphrag_chunking.text_chunk import TextChunk + + +class Chunker(ABC): + """Abstract base class for document chunkers.""" + + @abstractmethod + def __init__(self, **kwargs: Any) -> None: + """Create a chunker instance.""" + + @abstractmethod + def chunk( + self, text: str, transform: Callable[[str], str] | None = None + ) -> list[TextChunk]: + """Chunk method definition.""" diff --git a/packages/graphrag-chunking/graphrag_chunking/chunker_factory.py b/packages/graphrag-chunking/graphrag_chunking/chunker_factory.py new file mode 100644 index 0000000000..636d0909f2 --- /dev/null +++ b/packages/graphrag-chunking/graphrag_chunking/chunker_factory.py @@ -0,0 +1,77 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'ChunkerFactory', 'register_chunker', and 'create_chunker'.""" + +from collections.abc import Callable + +from graphrag_common.factory.factory import Factory, ServiceScope + +from graphrag_chunking.chunk_strategy_type import ChunkerType +from graphrag_chunking.chunker import Chunker +from graphrag_chunking.chunking_config import ChunkingConfig + + +class ChunkerFactory(Factory[Chunker]): + """Factory for creating Chunker instances.""" + + +chunker_factory = ChunkerFactory() + + +def register_chunker( + chunker_type: str, + chunker_initializer: Callable[..., Chunker], + scope: ServiceScope = "transient", +) -> None: + """Register a custom chunker implementation. + + Args + ---- + - chunker_type: str + The chunker id to register. + - chunker_initializer: Callable[..., Chunker] + The chunker initializer to register. + """ + chunker_factory.register(chunker_type, chunker_initializer, scope) + + +def create_chunker( + config: ChunkingConfig, + encode: Callable[[str], list[int]] | None = None, + decode: Callable[[list[int]], str] | None = None, +) -> Chunker: + """Create a chunker implementation based on the given configuration. + + Args + ---- + - config: ChunkingConfig + The chunker configuration to use. + + Returns + ------- + Chunker + The created chunker implementation. + """ + config_model = config.model_dump() + if encode is not None: + config_model["encode"] = encode + if decode is not None: + config_model["decode"] = decode + chunker_strategy = config.type + + if chunker_strategy not in chunker_factory: + match chunker_strategy: + case ChunkerType.Tokens: + from graphrag_chunking.token_chunker import TokenChunker + + register_chunker(ChunkerType.Tokens, TokenChunker) + case ChunkerType.Sentence: + from graphrag_chunking.sentence_chunker import SentenceChunker + + register_chunker(ChunkerType.Sentence, SentenceChunker) + case _: + msg = f"ChunkingConfig.strategy '{chunker_strategy}' is not registered in the ChunkerFactory. Registered types: {', '.join(chunker_factory.keys())}." + raise ValueError(msg) + + return chunker_factory.create(chunker_strategy, init_args=config_model) diff --git a/packages/graphrag-chunking/graphrag_chunking/chunking_config.py b/packages/graphrag-chunking/graphrag_chunking/chunking_config.py new file mode 100644 index 0000000000..69db1260a1 --- /dev/null +++ b/packages/graphrag-chunking/graphrag_chunking/chunking_config.py @@ -0,0 +1,36 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Parameterization settings for the default configuration.""" + +from pydantic import BaseModel, ConfigDict, Field + +from graphrag_chunking.chunk_strategy_type import ChunkerType + + +class ChunkingConfig(BaseModel): + """Configuration section for chunking.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom cache implementations.""" + + type: str = Field( + description="The chunking type to use.", + default=ChunkerType.Tokens, + ) + encoding_model: str | None = Field( + description="The encoding model to use.", + default=None, + ) + size: int = Field( + description="The chunk size to use.", + default=1200, + ) + overlap: int = Field( + description="The chunk overlap to use.", + default=100, + ) + prepend_metadata: list[str] | None = Field( + description="Metadata fields from the source document to prepend on each chunk.", + default=None, + ) diff --git a/packages/graphrag-chunking/graphrag_chunking/create_chunk_results.py b/packages/graphrag-chunking/graphrag_chunking/create_chunk_results.py new file mode 100644 index 0000000000..780de6a0bc --- /dev/null +++ b/packages/graphrag-chunking/graphrag_chunking/create_chunk_results.py @@ -0,0 +1,32 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'create_chunk_results' function.""" + +from collections.abc import Callable + +from graphrag_chunking.text_chunk import TextChunk + + +def create_chunk_results( + chunks: list[str], + transform: Callable[[str], str] | None = None, + encode: Callable[[str], list[int]] | None = None, +) -> list[TextChunk]: + """Create chunk results from a list of text chunks. The index assignments are 0-based and assume chunks were not stripped relative to the source text.""" + results = [] + start_char = 0 + for index, chunk in enumerate(chunks): + end_char = start_char + len(chunk) - 1 # 0-based indices + result = TextChunk( + original=chunk, + text=transform(chunk) if transform else chunk, + index=index, + start_char=start_char, + end_char=end_char, + ) + if encode: + result.token_count = len(encode(result.text)) + results.append(result) + start_char = end_char + 1 + return results diff --git a/packages/graphrag-chunking/graphrag_chunking/sentence_chunker.py b/packages/graphrag-chunking/graphrag_chunking/sentence_chunker.py new file mode 100644 index 0000000000..42d8a9a211 --- /dev/null +++ b/packages/graphrag-chunking/graphrag_chunking/sentence_chunker.py @@ -0,0 +1,48 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'SentenceChunker' class.""" + +from collections.abc import Callable +from typing import Any + +import nltk + +from graphrag_chunking.bootstrap_nltk import bootstrap +from graphrag_chunking.chunker import Chunker +from graphrag_chunking.create_chunk_results import create_chunk_results +from graphrag_chunking.text_chunk import TextChunk + + +class SentenceChunker(Chunker): + """A chunker that splits text into sentence-based chunks.""" + + def __init__( + self, encode: Callable[[str], list[int]] | None = None, **kwargs: Any + ) -> None: + """Create a sentence chunker instance.""" + self._encode = encode + bootstrap() + + def chunk( + self, text: str, transform: Callable[[str], str] | None = None + ) -> list[TextChunk]: + """Chunk the text into sentence-based chunks.""" + sentences = nltk.sent_tokenize(text.strip()) + results = create_chunk_results( + sentences, transform=transform, encode=self._encode + ) + # nltk sentence tokenizer may trim whitespace, so we need to adjust start/end chars + for index, result in enumerate(results): + txt = result.text + start = result.start_char + actual_start = text.find(txt, start) + delta = actual_start - start + if delta > 0: + result.start_char += delta + result.end_char += delta + # bump the next to keep the start check from falling too far behind + if index < len(results) - 1: + results[index + 1].start_char += delta + results[index + 1].end_char += delta + return results diff --git a/packages/graphrag-chunking/graphrag_chunking/text_chunk.py b/packages/graphrag-chunking/graphrag_chunking/text_chunk.py new file mode 100644 index 0000000000..876832f734 --- /dev/null +++ b/packages/graphrag-chunking/graphrag_chunking/text_chunk.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""The TextChunk dataclass.""" + +from dataclasses import dataclass + + +@dataclass +class TextChunk: + """Result of chunking a document.""" + + original: str + """Raw original text chunk before any transformation.""" + + text: str + """The final text content of this chunk.""" + + index: int + """Zero-based index of this chunk within the source document.""" + + start_char: int + """Character index where the raw chunk text begins in the source document.""" + + end_char: int + """Character index where the raw chunk text ends in the source document.""" + + token_count: int | None = None + """Number of tokens in the final chunk text, if computed.""" diff --git a/packages/graphrag-chunking/graphrag_chunking/token_chunker.py b/packages/graphrag-chunking/graphrag_chunking/token_chunker.py new file mode 100644 index 0000000000..41921a0415 --- /dev/null +++ b/packages/graphrag-chunking/graphrag_chunking/token_chunker.py @@ -0,0 +1,69 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'TokenChunker' class.""" + +from collections.abc import Callable +from typing import Any + +from graphrag_chunking.chunker import Chunker +from graphrag_chunking.create_chunk_results import create_chunk_results +from graphrag_chunking.text_chunk import TextChunk + + +class TokenChunker(Chunker): + """A chunker that splits text into token-based chunks.""" + + def __init__( + self, + size: int, + overlap: int, + encode: Callable[[str], list[int]], + decode: Callable[[list[int]], str], + **kwargs: Any, + ) -> None: + """Create a token chunker instance.""" + self._size = size + self._overlap = overlap + self._encode = encode + self._decode = decode + + def chunk( + self, text: str, transform: Callable[[str], str] | None = None + ) -> list[TextChunk]: + """Chunk the text into token-based chunks.""" + chunks = split_text_on_tokens( + text, + chunk_size=self._size, + chunk_overlap=self._overlap, + encode=self._encode, + decode=self._decode, + ) + return create_chunk_results(chunks, transform=transform, encode=self._encode) + + +def split_text_on_tokens( + text: str, + chunk_size: int, + chunk_overlap: int, + encode: Callable[[str], list[int]], + decode: Callable[[list[int]], str], +) -> list[str]: + """Split a single text and return chunks using the tokenizer.""" + result = [] + input_tokens = encode(text) + + start_idx = 0 + cur_idx = min(start_idx + chunk_size, len(input_tokens)) + chunk_tokens = input_tokens[start_idx:cur_idx] + + while start_idx < len(input_tokens): + chunk_text = decode(list(chunk_tokens)) + result.append(chunk_text) # Append chunked text as string + if cur_idx == len(input_tokens): + break + start_idx += chunk_size - chunk_overlap + cur_idx = min(start_idx + chunk_size, len(input_tokens)) + chunk_tokens = input_tokens[start_idx:cur_idx] + + return result diff --git a/packages/graphrag-chunking/graphrag_chunking/transformers.py b/packages/graphrag-chunking/graphrag_chunking/transformers.py new file mode 100644 index 0000000000..5d6f11e0e6 --- /dev/null +++ b/packages/graphrag-chunking/graphrag_chunking/transformers.py @@ -0,0 +1,25 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A collection of useful built-in transformers you can use for chunking.""" + +from collections.abc import Callable +from typing import Any + + +def add_metadata( + metadata: dict[str, Any], + delimiter: str = ": ", + line_delimiter: str = "\n", + append: bool = False, +) -> Callable[[str], str]: + """Add metadata to the given text, prepending by default. This utility writes the dict as rows of key/value pairs.""" + + def transformer(text: str) -> str: + metadata_str = ( + line_delimiter.join(f"{k}{delimiter}{v}" for k, v in metadata.items()) + + line_delimiter + ) + return text + metadata_str if append else metadata_str + text + + return transformer diff --git a/packages/graphrag-chunking/pyproject.toml b/packages/graphrag-chunking/pyproject.toml new file mode 100644 index 0000000000..da2b5fab7b --- /dev/null +++ b/packages/graphrag-chunking/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "graphrag-chunking" +version = "2.7.1" +description = "Chunking utilities for GraphRAG" +authors = [ + {name = "Alonso Guevara Fernández", email = "alonsog@microsoft.com"}, + {name = "Andrés Morales Esquivel", email = "andresmor@microsoft.com"}, + {name = "Chris Trevino", email = "chtrevin@microsoft.com"}, + {name = "David Tittsworth", email = "datittsw@microsoft.com"}, + {name = "Dayenne de Souza", email = "ddesouza@microsoft.com"}, + {name = "Derek Worthen", email = "deworthe@microsoft.com"}, + {name = "Gaudy Blanco Meneses", email = "gaudyb@microsoft.com"}, + {name = "Ha Trinh", email = "trinhha@microsoft.com"}, + {name = "Jonathan Larson", email = "jolarso@microsoft.com"}, + {name = "Josh Bradley", email = "joshbradley@microsoft.com"}, + {name = "Kate Lytvynets", email = "kalytv@microsoft.com"}, + {name = "Kenny Zhang", email = "zhangken@microsoft.com"}, + {name = "Mónica Carvajal"}, + {name = "Nathan Evans", email = "naevans@microsoft.com"}, + {name = "Rodrigo Racanicci", email = "rracanicci@microsoft.com"}, + {name = "Sarah Smith", email = "smithsarah@microsoft.com"}, +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.11,<3.14" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "graphrag-common==2.7.1", + "pydantic~=2.10", +] + +[project.urls] +Source = "https://github.com/microsoft/graphrag" + +[build-system] +requires = ["hatchling>=1.27.0,<2.0.0"] +build-backend = "hatchling.build" + diff --git a/packages/graphrag-common/README.md b/packages/graphrag-common/README.md new file mode 100644 index 0000000000..42dbde6d7a --- /dev/null +++ b/packages/graphrag-common/README.md @@ -0,0 +1,125 @@ +# GraphRAG Common + +This package provides utility modules for GraphRAG, including a flexible factory system for dependency injection and service registration, and a comprehensive configuration loading system with Pydantic model support, environment variable substitution, and automatic file discovery. + +## Factory module + +The Factory class provides a flexible dependency injection pattern that can register and create instances of classes implementing a common interface using string-based strategies. It supports both transient scope (creates new instances on each request) and singleton scope (returns the same instance after first creation). + +```python +from abc import ABC, abstractmethod + +from graphrag_common.factory import Factory + +class SampleABC(ABC): + + @abstractmethod + def get_value(self) -> str: + msg = "Subclasses must implement the get_value method." + raise NotImplementedError(msg) + + +class ConcreteClass(SampleABC): + def __init__(self, value: str): + self._value = value + + def get_value(self) -> str: + return self._value + +class SampleFactory(Factory[SampleABC]): +"""A Factory for SampleABC classes.""" + +factory = SampleFactory() + +# Registering transient services +# A new one is created for every request +factory.register("some_strategy", ConcreteTestClass) + +trans1 = factory.create("some_strategy", {"value": "test1"}) +trans2 = factory.create("some_strategy", {"value": "test2"}) + +assert trans1 is not trans2 +assert trans1.get_value() == "test1" +assert trans2.get_value() == "test2" + +# Registering singleton services +# After first creation, the same one is returned every time +factory.register("some_other_strategy", ConcreteTestClass, scope="singleton") + +single1 = factory.create("some_other_strategy", {"value": "singleton"}) +single2 = factory.create("some_other_strategy", {"value": "ignored"}) + +assert single1 is single2 +assert single1.get_value() == "singleton" +assert single2.get_value() == "singleton" +``` + +## Config module + +The load_config function provides a comprehensive configuration loading system that automatically discovers and parses YAML/JSON config files into Pydantic models with support for environment variable substitution and .env file loading. It offers flexible features like config overrides, custom parsers for different file formats, and automatically sets the working directory to the config file location for relative path resolution. + +```python +from pydantic import BaseModel, Field +from graphrag_common.config import load_config + +from pathlib import Path + +class Logging(BaseModel): + """Test nested model.""" + + directory: str = Field(default="output/logs") + filename: str = Field(default="logs.txt") + +class Config(BaseModel): + """Test configuration model.""" + + name: str = Field(description="Name field.") + logging: Logging = Field(description="Nested model field.") + +# Basic - by default: +# - searches for Path.cwd() / settings.[yaml|yml|json] +# - sets the CWD to the directory containing the config file. +# so if no custom config path is provided than CWD remains unchanged. +# - loads config_directory/.env file +# - parses ${env} in the config file +config = load_config(Config) + +# Custom file location +config = load_config(Config, "path_to_config_filename_or_directory_containing_settings.[yaml|yml|json]") + +# Using a custom file extension with +# custom config parser (str) -> dict[str, Any] +config = load_config( + config_initializer=Config, + config_path="config.toml", + config_parser=lambda contents: toml.loads(contents) # Needs toml pypi package +) + +# With overrides - provided values override whats in the config file +# Only overrides what is specified - recursively merges settings. +config = load_config( + config_initializer=Config, + overrides={ + "name": "some name", + "logging": { + "filename": "my_logs.txt" + } + }, +) + +# By default, sets CWD to directory containing config file +# So custom config paths will change the CWD. +config = load_config( + config_initializer=Config, + config_path="some/path/to/config.yaml", + set_cwd=True # default +) + +# now cwd == some/path/to +assert Path.cwd() == "some/path/to" + +# And now throughout the codebase resolving relative paths in config +# will resolve relative to the config directory +Path(config.logging.directory) == "some/path/to/output/logs" + +``` \ No newline at end of file diff --git a/graphrag/storage/__init__.py b/packages/graphrag-common/graphrag_common/__init__.py similarity index 70% rename from graphrag/storage/__init__.py rename to packages/graphrag-common/graphrag_common/__init__.py index b21f077cb1..e6f83e0260 100644 --- a/graphrag/storage/__init__.py +++ b/packages/graphrag-common/graphrag_common/__init__.py @@ -1,4 +1,4 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""The storage package root.""" +"""GraphRAG Common package.""" diff --git a/packages/graphrag-common/graphrag_common/config/__init__.py b/packages/graphrag-common/graphrag_common/config/__init__.py new file mode 100644 index 0000000000..71f24b79b7 --- /dev/null +++ b/packages/graphrag-common/graphrag_common/config/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""The GraphRAG config module.""" + +from graphrag_common.config.load_config import ConfigParsingError, load_config + +__all__ = ["ConfigParsingError", "load_config"] diff --git a/packages/graphrag-common/graphrag_common/config/load_config.py b/packages/graphrag-common/graphrag_common/config/load_config.py new file mode 100644 index 0000000000..c8929149f1 --- /dev/null +++ b/packages/graphrag-common/graphrag_common/config/load_config.py @@ -0,0 +1,205 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Load configuration.""" + +import json +import os +from collections.abc import Callable +from pathlib import Path +from string import Template +from typing import Any, TypeVar + +import yaml +from dotenv import load_dotenv + +T = TypeVar("T", covariant=True) + +_default_config_files = ["settings.yaml", "settings.yml", "settings.json"] + + +class ConfigParsingError(ValueError): + """Configuration Parsing Error.""" + + def __init__(self, msg: str) -> None: + """Initialize the ConfigParsingError.""" + super().__init__(msg) + + +def _get_config_file_path(config_dir_or_file: Path) -> Path: + """Resolve the config path from the given directory or file.""" + config_dir_or_file = Path(config_dir_or_file) + + if config_dir_or_file.is_file(): + return config_dir_or_file + + if not config_dir_or_file.is_dir(): + msg = f"Invalid config path: {config_dir_or_file} is not a directory" + raise FileNotFoundError(msg) + + for file in _default_config_files: + if (config_dir_or_file / file).is_file(): + return config_dir_or_file / file + + msg = f"No 'settings.[yaml|yml|json]' config file found in directory: {config_dir_or_file}" + raise FileNotFoundError(msg) + + +def _load_dotenv(env_file_path: Path, required: bool) -> None: + """Load the .env file if it exists.""" + if not env_file_path.is_file(): + if not required: + return + msg = f"dot_env_path not found: {env_file_path}" + raise FileNotFoundError(msg) + load_dotenv(env_file_path) + + +def _parse_json(data: str) -> dict[str, Any]: + """Parse JSON data.""" + return json.loads(data) + + +def _parse_yaml(data: str) -> dict[str, Any]: + """Parse YAML data.""" + return yaml.safe_load(data) + + +def _get_parser_for_file(file_path: str | Path) -> Callable[[str], dict[str, Any]]: + """Get the parser for the given file path.""" + file_path = Path(file_path).resolve() + match file_path.suffix.lower(): + case ".json": + return _parse_json + case ".yaml" | ".yml": + return _parse_yaml + case _: + msg = ( + f"Failed to parse, {file_path}. Unsupported file extension, " + + f"{file_path.suffix}. Pass in a custom config_parser argument or " + + "use one of the supported file extensions, .json, .yaml, .yml, .toml." + ) + raise ConfigParsingError(msg) + + +def _parse_env_variables(text: str) -> str: + """Parse environment variables in the configuration text.""" + try: + return Template(text).substitute(os.environ) + except KeyError as error: + msg = f"Environment variable not found: {error}" + raise ConfigParsingError(msg) from error + + +def _recursive_merge_dicts(dest: dict[str, Any], src: dict[str, Any]) -> None: + """Recursively merge two dictionaries in place.""" + for key, value in src.items(): + if isinstance(value, dict): + if isinstance(dest.get(key), dict): + _recursive_merge_dicts(dest[key], value) + else: + dest[key] = value + else: + dest[key] = value + + +def load_config( + config_initializer: Callable[..., T], + config_path: str | Path | None = None, + overrides: dict[str, Any] | None = None, + set_cwd: bool = True, + parse_env_vars: bool = True, + load_dot_env_file: bool = True, + dot_env_path: str | Path | None = None, + config_parser: Callable[[str], dict[str, Any]] | None = None, + file_encoding: str = "utf-8", +) -> T: + """Load configuration from a file. + + Parameters + ---------- + config_initializer : Callable[..., T] + Configuration constructor/initializer. + Should accept **kwargs to initialize the configuration, + e.g., Config(**kwargs). + config_path : str | Path | None, optional (default=None) + Path to the configuration directory containing settings.[yaml|yml|json]. + Or path to a configuration file itself. + If None, search the current working directory for + settings.[yaml|yml|json]. + overrides : dict[str, Any] | None, optional (default=None) + Configuration overrides. + Useful for overriding configuration settings programmatically, + perhaps from CLI flags. + set_cwd : bool, optional (default=True) + Whether to set the current working directory to the directory + containing the configuration file. Helpful for resolving relative paths + in the configuration file. + parse_env_vars : bool, optional (default=True) + Whether to parse environment variables in the configuration text. + load_dot_env_file : bool, optional (default=True) + Whether to load the .env file prior to parsing environment variables. + dot_env_path : str | Path | None, optional (default=None) + Optional .env file to load prior to parsing env variables. + If None and load_dot_env_file is True, looks for a .env file in the + same directory as the config file. + config_parser : Callable[[str], dict[str, Any]] | None, optional (default=None) + function to parse the configuration text, (str) -> dict[str, Any]. + If None, the parser is inferred from the file extension. + Supported extensions: .json, .yaml, .yml. + file_encoding : str, optional (default="utf-8") + File encoding to use when reading the configuration file. + + Returns + ------- + T + The initialized configuration object. + + Raises + ------ + FileNotFoundError + - If the config file is not found. + - If the .env file is not found when parse_env_vars is True and dot_env_path is provided. + + ConfigParsingError + - If an environment variable is not found when parsing env variables. + - If there was a problem merging the overrides with the configuration. + - If parser=None and load_config was unable to determine how to parse + the file based on the file extension. + - If the parser fails to parse the configuration text. + """ + config_path = Path(config_path).resolve() if config_path else Path.cwd() + config_path = _get_config_file_path(config_path) + + file_contents = config_path.read_text(encoding=file_encoding) + + if parse_env_vars: + if load_dot_env_file: + required = dot_env_path is not None + dot_env_path = ( + Path(dot_env_path) if dot_env_path else config_path.parent / ".env" + ) + _load_dotenv(dot_env_path, required=required) + file_contents = _parse_env_variables(file_contents) + + if config_parser is None: + config_parser = _get_parser_for_file(config_path) + + config_data: dict[str, Any] = {} + try: + config_data = config_parser(file_contents) + except Exception as error: + msg = f"Failed to parse config_path: {config_path}. Error: {error}" + raise ConfigParsingError(msg) from error + + if overrides is not None: + try: + _recursive_merge_dicts(config_data, overrides) + except Exception as error: + msg = f"Failed to merge overrides with config_path: {config_path}. Error: {error}" + raise ConfigParsingError(msg) from error + + if set_cwd: + os.chdir(config_path.parent) + + return config_initializer(**config_data) diff --git a/packages/graphrag-common/graphrag_common/factory/__init__.py b/packages/graphrag-common/graphrag_common/factory/__init__.py new file mode 100644 index 0000000000..86b102f265 --- /dev/null +++ b/packages/graphrag-common/graphrag_common/factory/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""The GraphRAG factory module.""" + +from graphrag_common.factory.factory import Factory, ServiceScope + +__all__ = ["Factory", "ServiceScope"] diff --git a/packages/graphrag-common/graphrag_common/factory/factory.py b/packages/graphrag-common/graphrag_common/factory/factory.py new file mode 100644 index 0000000000..ab001d101c --- /dev/null +++ b/packages/graphrag-common/graphrag_common/factory/factory.py @@ -0,0 +1,113 @@ +# Copyright (c) 2025 Microsoft Corporation. +# Licensed under the MIT License + +"""Factory ABC.""" + +from abc import ABC +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, ClassVar, Generic, Literal, TypeVar + +from graphrag_common.hasher import hash_data + +T = TypeVar("T", covariant=True) + +ServiceScope = Literal["singleton", "transient"] + + +@dataclass +class _ServiceDescriptor(Generic[T]): + """Descriptor for a service.""" + + scope: ServiceScope + initializer: Callable[..., T] + + +class Factory(ABC, Generic[T]): + """Abstract base class for factories.""" + + _instance: ClassVar["Factory | None"] = None + + def __new__(cls, *args: Any, **kwargs: Any) -> "Factory[T]": + """Create a new instance of Factory if it does not exist.""" + if cls._instance is None: + cls._instance = super().__new__(cls, *args, **kwargs) + return cls._instance + + def __init__(self): + if not hasattr(self, "_initialized"): + self._service_initializers: dict[str, _ServiceDescriptor[T]] = {} + self._initialized_services: dict[str, T] = {} + self._initialized = True + + def __contains__(self, strategy: str) -> bool: + """Check if a strategy is registered.""" + return strategy in self._service_initializers + + def keys(self) -> list[str]: + """Get a list of registered strategy names.""" + return list(self._service_initializers.keys()) + + def register( + self, + strategy: str, + initializer: Callable[..., T], + scope: ServiceScope = "transient", + ) -> None: + """ + Register a new service. + + Args + ---- + strategy: str + The name of the strategy. + initializer: Callable[..., T] + A callable that creates an instance of T. + scope: ServiceScope (default: "transient") + The scope of the service ("singleton" or "transient"). + Singleton services are cached based on their init args + so that the same instance is returned for the same init args. + """ + self._service_initializers[strategy] = _ServiceDescriptor(scope, initializer) + + def create(self, strategy: str, init_args: dict[str, Any] | None = None) -> T: + """ + Create a service instance based on the strategy. + + Args + ---- + strategy: str + The name of the strategy. + init_args: dict[str, Any] | None + A dictionary of keyword arguments to pass to the service initializer. + + Returns + ------- + An instance of T. + + Raises + ------ + ValueError: If the strategy is not registered. + """ + if strategy not in self._service_initializers: + msg = f"Strategy '{strategy}' is not registered. Registered strategies are: {', '.join(list(self._service_initializers.keys()))}" + raise ValueError(msg) + + # Delete entries with value None + # That way services can have default values + init_args = {k: v for k, v in (init_args or {}).items() if v is not None} + + service_descriptor = self._service_initializers[strategy] + if service_descriptor.scope == "singleton": + cache_key = hash_data({ + "strategy": strategy, + "init_args": init_args, + }) + + if cache_key not in self._initialized_services: + self._initialized_services[cache_key] = service_descriptor.initializer( + **init_args + ) + return self._initialized_services[cache_key] + + return service_descriptor.initializer(**(init_args or {})) diff --git a/packages/graphrag-common/graphrag_common/hasher/__init__.py b/packages/graphrag-common/graphrag_common/hasher/__init__.py new file mode 100644 index 0000000000..cdf27f8f80 --- /dev/null +++ b/packages/graphrag-common/graphrag_common/hasher/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""The GraphRAG hasher module.""" + +from graphrag_common.hasher.hasher import ( + Hasher, + hash_data, + make_yaml_serializable, + sha256_hasher, +) + +__all__ = [ + "Hasher", + "hash_data", + "make_yaml_serializable", + "sha256_hasher", +] diff --git a/packages/graphrag-common/graphrag_common/hasher/hasher.py b/packages/graphrag-common/graphrag_common/hasher/hasher.py new file mode 100644 index 0000000000..c4af285377 --- /dev/null +++ b/packages/graphrag-common/graphrag_common/hasher/hasher.py @@ -0,0 +1,59 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""The GraphRAG hasher module.""" + +import hashlib +from collections.abc import Callable +from typing import Any + +import yaml + +Hasher = Callable[[str], str] +"""Type alias for a hasher function (data: str) -> str.""" + + +def sha256_hasher(data: str) -> str: + """Generate a SHA-256 hash for the input data.""" + return hashlib.sha256(data.encode("utf-8")).hexdigest() + + +def make_yaml_serializable(data: Any) -> Any: + """Convert data to a YAML-serializable format.""" + if isinstance(data, (list, tuple)): + return tuple(make_yaml_serializable(item) for item in data) + + if isinstance(data, set): + return tuple(sorted(make_yaml_serializable(item) for item in data)) + + if isinstance(data, dict): + return tuple( + sorted((key, make_yaml_serializable(value)) for key, value in data.items()) + ) + + return str(data) + + +def hash_data(data: Any, *, hasher: Hasher | None = None) -> str: + """Hash the input data dictionary using the specified hasher function. + + Args + ---- + data: dict[str, Any] + The input data to be hashed. + The input data is serialized using yaml + to support complex data structures such as classes and functions. + hasher: Hasher | None (default: sha256_hasher) + The hasher function to use. (data: str) -> str + + Returns + ------- + str + The resulting hash of the input data. + + """ + hasher = hasher or sha256_hasher + try: + return hasher(yaml.dump(data, sort_keys=True)) + except TypeError: + return hasher(yaml.dump(make_yaml_serializable(data), sort_keys=True)) diff --git a/packages/graphrag-common/graphrag_common/py.typed b/packages/graphrag-common/graphrag_common/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/graphrag-common/pyproject.toml b/packages/graphrag-common/pyproject.toml new file mode 100644 index 0000000000..c8d221f76a --- /dev/null +++ b/packages/graphrag-common/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "graphrag-common" +version = "2.7.1" +description = "Common utilities and types for GraphRAG" +authors = [ + {name = "Alonso Guevara Fernández", email = "alonsog@microsoft.com"}, + {name = "Andrés Morales Esquivel", email = "andresmor@microsoft.com"}, + {name = "Chris Trevino", email = "chtrevin@microsoft.com"}, + {name = "David Tittsworth", email = "datittsw@microsoft.com"}, + {name = "Dayenne de Souza", email = "ddesouza@microsoft.com"}, + {name = "Derek Worthen", email = "deworthe@microsoft.com"}, + {name = "Gaudy Blanco Meneses", email = "gaudyb@microsoft.com"}, + {name = "Ha Trinh", email = "trinhha@microsoft.com"}, + {name = "Jonathan Larson", email = "jolarso@microsoft.com"}, + {name = "Josh Bradley", email = "joshbradley@microsoft.com"}, + {name = "Kate Lytvynets", email = "kalytv@microsoft.com"}, + {name = "Kenny Zhang", email = "zhangken@microsoft.com"}, + {name = "Mónica Carvajal"}, + {name = "Nathan Evans", email = "naevans@microsoft.com"}, + {name = "Rodrigo Racanicci", email = "rracanicci@microsoft.com"}, + {name = "Sarah Smith", email = "smithsarah@microsoft.com"}, +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.11,<3.14" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "python-dotenv~=1.0", + "pyyaml~=6.0", +] + +[project.urls] +Source = "https://github.com/microsoft/graphrag" + +[build-system] +requires = ["hatchling>=1.27.0,<2.0.0"] +build-backend = "hatchling.build" + diff --git a/packages/graphrag-input/README.md b/packages/graphrag-input/README.md new file mode 100644 index 0000000000..5dd6cb1497 --- /dev/null +++ b/packages/graphrag-input/README.md @@ -0,0 +1,72 @@ +# GraphRAG Inputs + +This package provides input document loading utilities for GraphRAG, supporting multiple file formats including CSV, JSON, JSON Lines, and plain text. + +## Supported File Types + +The following four standard file formats are supported out of the box: + +- **CSV** - Tabular data with configurable column mappings +- **JSON** - JSON files with configurable property paths +- **JSON Lines** - Line-delimited JSON records +- **Text** - Plain text files + +### Markitdown Support + +Additionally, we support the `InputType.MarkItDown` format, which uses the [MarkItDown](https://github.com/microsoft/markitdown) library to import any supported file type. The MarkItDown converter can handle a wide variety of file formats including Office documents, PDFs, HTML, and more. + +**Note:** Additional optional dependencies may need to be installed depending on the file type you're processing. The choice of converter is determined by MarkItDowns's processing logic, which primarily uses the file extension to select the appropriate converter. Please refer to the [MarkItDown repository](https://github.com/microsoft/markitdown) for installation instructions and detailed information about supported formats. + +## Examples + +Basic usage with the factory: +```python +from graphrag_input import create_input_reader, InputConfig, InputType +from graphrag_storage import StorageConfig, create_storage + +config = InputConfig( + type=InputType.Csv, + text_column="content", + title_column="title", +) +storage = create_storage(StorageConfig(base_dir="./input")) +reader = create_input_reader(config, storage) +documents = await reader.read_files() +``` + +Import a pdf with MarkItDown: + +```bash +pip install 'markitdown[pdf]' # required dependency for pdf processing +``` + +```python +from graphrag_input import create_input_reader, InputConfig, InputType +from graphrag_storage import StorageConfig, create_storage + +config = InputConfig( + type=InputType.MarkitDown, + file_pattern=".*\\.pdf$" +) +storage = create_storage(StorageConfig(base_dir="./input")) +reader = create_input_reader(config, storage) +documents = await reader.read_files() +``` + +YAML config example for above: +```yaml +input: + type: markitdown + file_pattern: ".*\\.pdf$$" +input_storage: + type: file + base_dir: "input" +``` + +Note that when specifying column names for data extraction, we can handle nested objects (e.g., in JSON) with dot notation: +```python +from graphrag_input import get_property + +data = {"user": {"profile": {"name": "Alice"}}} +name = get_property(data, "user.profile.name") # Returns "Alice" +``` diff --git a/packages/graphrag-input/graphrag_input/__init__.py b/packages/graphrag-input/graphrag_input/__init__.py new file mode 100644 index 0000000000..e742d7f06b --- /dev/null +++ b/packages/graphrag-input/graphrag_input/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""GraphRAG input document loading package.""" + +from graphrag_input.get_property import get_property +from graphrag_input.input_config import InputConfig +from graphrag_input.input_reader import InputReader +from graphrag_input.input_reader_factory import create_input_reader +from graphrag_input.input_type import InputType +from graphrag_input.text_document import TextDocument + +__all__ = [ + "InputConfig", + "InputReader", + "InputType", + "TextDocument", + "create_input_reader", + "get_property", +] diff --git a/packages/graphrag-input/graphrag_input/csv.py b/packages/graphrag-input/graphrag_input/csv.py new file mode 100644 index 0000000000..6c0f51dd3a --- /dev/null +++ b/packages/graphrag-input/graphrag_input/csv.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'CSVFileReader' model.""" + +import csv +import logging + +from graphrag_input.structured_file_reader import StructuredFileReader +from graphrag_input.text_document import TextDocument + +logger = logging.getLogger(__name__) + + +class CSVFileReader(StructuredFileReader): + """Reader implementation for csv files.""" + + def __init__(self, file_pattern: str | None = None, **kwargs): + super().__init__( + file_pattern=file_pattern if file_pattern is not None else ".*\\.csv$", + **kwargs, + ) + + async def read_file(self, path: str) -> list[TextDocument]: + """Read a csv file into a list of documents. + + Args: + - path - The path to read the file from. + + Returns + ------- + - output - list with a TextDocument for each row in the file. + """ + file = await self._storage.get(path, encoding=self._encoding) + + reader = csv.DictReader(file.splitlines()) + rows = list(reader) + return await self.process_data_columns(rows, path) diff --git a/packages/graphrag-input/graphrag_input/get_property.py b/packages/graphrag-input/graphrag_input/get_property.py new file mode 100644 index 0000000000..8c82d69885 --- /dev/null +++ b/packages/graphrag-input/graphrag_input/get_property.py @@ -0,0 +1,36 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Utility for retrieving properties from nested dictionaries.""" + +from typing import Any + + +def get_property(data: dict[str, Any], path: str) -> Any: + """Retrieve a property from a dictionary using dot notation. + + Parameters + ---------- + data : dict[str, Any] + The dictionary to retrieve the property from. + path : str + A dot-separated string representing the path to the property (e.g., "foo.bar.baz"). + + Returns + ------- + Any + The value at the specified path. + + Raises + ------ + KeyError + If the path does not exist in the dictionary. + """ + keys = path.split(".") + current = data + for key in keys: + if not isinstance(current, dict) or key not in current: + msg = f"Property '{path}' not found" + raise KeyError(msg) + current = current[key] + return current diff --git a/packages/graphrag-input/graphrag_input/hashing.py b/packages/graphrag-input/graphrag_input/hashing.py new file mode 100644 index 0000000000..915824cd9f --- /dev/null +++ b/packages/graphrag-input/graphrag_input/hashing.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Hashing utilities.""" + +from collections.abc import Iterable +from hashlib import sha512 +from typing import Any + + +def gen_sha512_hash(item: dict[str, Any], hashcode: Iterable[str]) -> str: + """Generate a SHA512 hash. + + Parameters + ---------- + item : dict[str, Any] + The dictionary containing values to hash. + hashcode : Iterable[str] + The keys to include in the hash. + + Returns + ------- + str + The SHA512 hash as a hexadecimal string. + """ + hashed = "".join([str(item[column]) for column in hashcode]) + return f"{sha512(hashed.encode('utf-8'), usedforsecurity=False).hexdigest()}" diff --git a/packages/graphrag-input/graphrag_input/input_config.py b/packages/graphrag-input/graphrag_input/input_config.py new file mode 100644 index 0000000000..792ce3f85a --- /dev/null +++ b/packages/graphrag-input/graphrag_input/input_config.py @@ -0,0 +1,40 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Parameterization settings for the default configuration.""" + +from pydantic import BaseModel, ConfigDict, Field + +from graphrag_input.input_type import InputType + + +class InputConfig(BaseModel): + """The default configuration section for Input.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom reader implementations.""" + + type: str = Field( + description="The input file type to use.", + default=InputType.Text, + ) + encoding: str | None = Field( + description="The input file encoding to use.", + default=None, + ) + file_pattern: str | None = Field( + description="The input file pattern to use.", + default=None, + ) + id_column: str | None = Field( + description="The input ID column to use.", + default=None, + ) + title_column: str | None = Field( + description="The input title column to use.", + default=None, + ) + text_column: str | None = Field( + description="The input text column to use.", + default=None, + ) diff --git a/packages/graphrag-input/graphrag_input/input_reader.py b/packages/graphrag-input/graphrag_input/input_reader.py new file mode 100644 index 0000000000..be95168336 --- /dev/null +++ b/packages/graphrag-input/graphrag_input/input_reader.py @@ -0,0 +1,75 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'InputReader' model.""" + +from __future__ import annotations + +import logging +import re +from abc import ABCMeta, abstractmethod +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from graphrag_storage import Storage + + from graphrag_input.text_document import TextDocument + +logger = logging.getLogger(__name__) + + +class InputReader(metaclass=ABCMeta): + """Provide a cache interface for the pipeline.""" + + def __init__( + self, + storage: Storage, + file_pattern: str, + encoding: str = "utf-8", + **kwargs, + ): + self._storage = storage + self._encoding = encoding + self._file_pattern = file_pattern + + async def read_files(self) -> list[TextDocument]: + """Load files from storage and apply a loader function based on file type. Process metadata on the results if needed.""" + files = list(self._storage.find(re.compile(self._file_pattern))) + if len(files) == 0: + msg = f"No {self._file_pattern} matches found in storage" + logger.warning(msg) + files = [] + + documents: list[TextDocument] = [] + + for file in files: + try: + documents.extend(await self.read_file(file)) + except Exception as e: # noqa: BLE001 (catching Exception is fine here) + logger.warning("Warning! Error loading file %s. Skipping...", file) + logger.warning("Error: %s", e) + + logger.info( + "Found %d %s files, loading %d", + len(files), + self._file_pattern, + len(documents), + ) + total_files_log = ( + f"Total number of unfiltered {self._file_pattern} rows: {len(documents)}" + ) + logger.info(total_files_log) + + return documents + + @abstractmethod + async def read_file(self, path: str) -> list[TextDocument]: + """Read a file into a list of documents. + + Args: + - path - The path to read the file from. + + Returns + ------- + - output - List with an entry for each document in the file. + """ diff --git a/packages/graphrag-input/graphrag_input/input_reader_factory.py b/packages/graphrag-input/graphrag_input/input_reader_factory.py new file mode 100644 index 0000000000..825665fa9d --- /dev/null +++ b/packages/graphrag-input/graphrag_input/input_reader_factory.py @@ -0,0 +1,90 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'InputReaderFactory' model.""" + +import logging +from collections.abc import Callable + +from graphrag_common.factory import Factory +from graphrag_common.factory.factory import ServiceScope +from graphrag_storage.storage import Storage + +from graphrag_input.input_config import InputConfig +from graphrag_input.input_reader import InputReader +from graphrag_input.input_type import InputType + +logger = logging.getLogger(__name__) + + +class InputReaderFactory(Factory[InputReader]): + """Factory for creating Input Reader instances.""" + + +input_reader_factory = InputReaderFactory() + + +def register_input_reader( + input_reader_type: str, + input_reader_initializer: Callable[..., InputReader], + scope: ServiceScope = "transient", +) -> None: + """Register a custom input reader implementation. + + Args + ---- + - input_reader_type: str + The input reader id to register. + - input_reader_initializer: Callable[..., InputReader] + The input reader initializer to register. + """ + input_reader_factory.register(input_reader_type, input_reader_initializer, scope) + + +def create_input_reader(config: InputConfig, storage: Storage) -> InputReader: + """Create an input reader implementation based on the given configuration. + + Args + ---- + - config: InputConfig + The input reader configuration to use. + - storage: Storage | None + The storage implementation to use for reading the files. + + Returns + ------- + InputReader + The created input reader implementation. + """ + config_model = config.model_dump() + input_strategy = config.type + + if input_strategy not in input_reader_factory: + match input_strategy: + case InputType.Csv: + from graphrag_input.csv import CSVFileReader + + register_input_reader(InputType.Csv, CSVFileReader) + case InputType.Text: + from graphrag_input.text import TextFileReader + + register_input_reader(InputType.Text, TextFileReader) + case InputType.Json: + from graphrag_input.json import JSONFileReader + + register_input_reader(InputType.Json, JSONFileReader) + case InputType.JsonLines: + from graphrag_input.jsonl import JSONLinesFileReader + + register_input_reader(InputType.JsonLines, JSONLinesFileReader) + case InputType.MarkItDown: + from graphrag_input.markitdown import MarkItDownFileReader + + register_input_reader(InputType.MarkItDown, MarkItDownFileReader) + case _: + msg = f"InputConfig.type '{input_strategy}' is not registered in the InputReaderFactory. Registered types: {', '.join(input_reader_factory.keys())}." + raise ValueError(msg) + + config_model["storage"] = storage + + return input_reader_factory.create(input_strategy, init_args=config_model) diff --git a/packages/graphrag-input/graphrag_input/input_type.py b/packages/graphrag-input/graphrag_input/input_type.py new file mode 100644 index 0000000000..39d0559c8e --- /dev/null +++ b/packages/graphrag-input/graphrag_input/input_type.py @@ -0,0 +1,25 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing input file type enum.""" + +from enum import StrEnum + + +class InputType(StrEnum): + """The input file type for the pipeline.""" + + Csv = "csv" + """The CSV input type.""" + Text = "text" + """The text input type.""" + Json = "json" + """The JSON input type.""" + JsonLines = "jsonl" + """The JSON Lines input type.""" + MarkItDown = "markitdown" + """The MarkItDown input type.""" + + def __repr__(self): + """Get a string representation.""" + return f'"{self.value}"' diff --git a/packages/graphrag-input/graphrag_input/json.py b/packages/graphrag-input/graphrag_input/json.py new file mode 100644 index 0000000000..1e4bdefe8d --- /dev/null +++ b/packages/graphrag-input/graphrag_input/json.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'JSONFileReader' model.""" + +import json +import logging + +from graphrag_input.structured_file_reader import StructuredFileReader +from graphrag_input.text_document import TextDocument + +logger = logging.getLogger(__name__) + + +class JSONFileReader(StructuredFileReader): + """Reader implementation for json files.""" + + def __init__(self, file_pattern: str | None = None, **kwargs): + super().__init__( + file_pattern=file_pattern if file_pattern is not None else ".*\\.json$", + **kwargs, + ) + + async def read_file(self, path: str) -> list[TextDocument]: + """Read a JSON file into a list of documents. + + Args: + - path - The path to read the file from. + + Returns + ------- + - output - list with a TextDocument for each row in the file. + """ + text = await self._storage.get(path, encoding=self._encoding) + as_json = json.loads(text) + # json file could just be a single object, or an array of objects + rows = as_json if isinstance(as_json, list) else [as_json] + return await self.process_data_columns(rows, path) diff --git a/packages/graphrag-input/graphrag_input/jsonl.py b/packages/graphrag-input/graphrag_input/jsonl.py new file mode 100644 index 0000000000..f038aafaa5 --- /dev/null +++ b/packages/graphrag-input/graphrag_input/jsonl.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'JSONLinesFileReader' model.""" + +import json +import logging + +from graphrag_input.structured_file_reader import StructuredFileReader +from graphrag_input.text_document import TextDocument + +logger = logging.getLogger(__name__) + + +class JSONLinesFileReader(StructuredFileReader): + """Reader implementation for json lines files.""" + + def __init__(self, file_pattern: str | None = None, **kwargs): + super().__init__( + file_pattern=file_pattern if file_pattern is not None else ".*\\.jsonl$", + **kwargs, + ) + + async def read_file(self, path: str) -> list[TextDocument]: + """Read a JSON lines file into a list of documents. + + This differs from standard JSON files in that each line is a separate JSON object. + + Args: + - path - The path to read the file from. + + Returns + ------- + - output - list with a TextDocument for each row in the file. + """ + text = await self._storage.get(path, encoding=self._encoding) + rows = [json.loads(line) for line in text.splitlines()] + return await self.process_data_columns(rows, path) diff --git a/packages/graphrag-input/graphrag_input/markitdown.py b/packages/graphrag-input/graphrag_input/markitdown.py new file mode 100644 index 0000000000..466a2ebdd9 --- /dev/null +++ b/packages/graphrag-input/graphrag_input/markitdown.py @@ -0,0 +1,49 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'TextFileReader' model.""" + +import logging +from io import BytesIO +from pathlib import Path + +from markitdown import MarkItDown, StreamInfo + +from graphrag_input.hashing import gen_sha512_hash +from graphrag_input.input_reader import InputReader +from graphrag_input.text_document import TextDocument + +logger = logging.getLogger(__name__) + + +class MarkItDownFileReader(InputReader): + """Reader implementation for any file type supported by markitdown. + + https://github.com/microsoft/markitdown + """ + + async def read_file(self, path: str) -> list[TextDocument]: + """Read a text file into a DataFrame of documents. + + Args: + - path - The path to read the file from. + + Returns + ------- + - output - list with a TextDocument for each row in the file. + """ + bytes = await self._storage.get(path, encoding=self._encoding, as_bytes=True) + md = MarkItDown() + result = md.convert_stream( + BytesIO(bytes), stream_info=StreamInfo(extension=Path(path).suffix) + ) + text = result.markdown + + document = TextDocument( + id=gen_sha512_hash({"text": text}, ["text"]), + title=result.title if result.title else str(Path(path).name), + text=text, + creation_date=await self._storage.get_creation_date(path), + raw_data=None, + ) + return [document] diff --git a/packages/graphrag-input/graphrag_input/structured_file_reader.py b/packages/graphrag-input/graphrag_input/structured_file_reader.py new file mode 100644 index 0000000000..45628ff94d --- /dev/null +++ b/packages/graphrag-input/graphrag_input/structured_file_reader.py @@ -0,0 +1,65 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'StructuredFileReader' model.""" + +import logging +from typing import Any + +from graphrag_input.get_property import get_property +from graphrag_input.hashing import gen_sha512_hash +from graphrag_input.input_reader import InputReader +from graphrag_input.text_document import TextDocument + +logger = logging.getLogger(__name__) + + +class StructuredFileReader(InputReader): + """Base reader implementation for structured files such as csv and json.""" + + def __init__( + self, + id_column: str | None = None, + title_column: str | None = None, + text_column: str = "text", + **kwargs, + ): + super().__init__(**kwargs) + self._id_column = id_column + self._title_column = title_column + self._text_column = text_column + + async def process_data_columns( + self, + rows: list[dict[str, Any]], + path: str, + ) -> list[TextDocument]: + """Process configured data columns from a list of loaded dicts.""" + documents = [] + for index, row in enumerate(rows): + # text column is required - harvest from dict + text = get_property(row, self._text_column) + # id is optional - harvest from dict or hash from text + id = ( + get_property(row, self._id_column) + if self._id_column + else gen_sha512_hash({"text": text}, ["text"]) + ) + # title is optional - harvest from dict or use filename + num = f" ({index})" if len(rows) > 1 else "" + title = ( + get_property(row, self._title_column) + if self._title_column + else f"{path}{num}" + ) + creation_date = await self._storage.get_creation_date(path) + documents.append( + TextDocument( + id=id, + title=title, + text=text, + creation_date=creation_date, + raw_data=row, + ) + ) + return documents diff --git a/packages/graphrag-input/graphrag_input/text.py b/packages/graphrag-input/graphrag_input/text.py new file mode 100644 index 0000000000..5d42924181 --- /dev/null +++ b/packages/graphrag-input/graphrag_input/text.py @@ -0,0 +1,43 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'TextFileReader' model.""" + +import logging +from pathlib import Path + +from graphrag_input.hashing import gen_sha512_hash +from graphrag_input.input_reader import InputReader +from graphrag_input.text_document import TextDocument + +logger = logging.getLogger(__name__) + + +class TextFileReader(InputReader): + """Reader implementation for text files.""" + + def __init__(self, file_pattern: str | None = None, **kwargs): + super().__init__( + file_pattern=file_pattern if file_pattern is not None else ".*\\.txt$", + **kwargs, + ) + + async def read_file(self, path: str) -> list[TextDocument]: + """Read a text file into a list of documents. + + Args: + - path - The path to read the file from. + + Returns + ------- + - output - list with a TextDocument for each row in the file. + """ + text = await self._storage.get(path, encoding=self._encoding) + document = TextDocument( + id=gen_sha512_hash({"text": text}, ["text"]), + title=str(Path(path).name), + text=text, + creation_date=await self._storage.get_creation_date(path), + raw_data=None, + ) + return [document] diff --git a/packages/graphrag-input/graphrag_input/text_document.py b/packages/graphrag-input/graphrag_input/text_document.py new file mode 100644 index 0000000000..a771f30d43 --- /dev/null +++ b/packages/graphrag-input/graphrag_input/text_document.py @@ -0,0 +1,59 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""TextDocument dataclass.""" + +import logging +from dataclasses import dataclass +from typing import Any + +from graphrag_input.get_property import get_property + +logger = logging.getLogger(__name__) + + +@dataclass +class TextDocument: + """The TextDocument holds relevant content for GraphRAG indexing.""" + + id: str + """Unique identifier for the document.""" + text: str + """The main text content of the document.""" + title: str + """The title of the document.""" + creation_date: str + """The creation date of the document, ISO-8601 format.""" + raw_data: dict[str, Any] | None = None + """Raw data from source document.""" + + def get(self, field: str, default_value: Any = None) -> Any: + """ + Get a single field from the TextDocument. + + Functions like the get method on a dictionary, returning default_value if the field is not found. + + Supports nested fields using dot notation. + + This takes a two step approach for flexibility: + 1. If the field is one of the standard text document fields (id, title, text, creation_date), just grab it directly. This accommodates unstructured text for example, which just has the standard fields. + 2. Otherwise. try to extract it from the raw_data dict. This allows users to specify any column from the original input file. + + """ + if field in ["id", "title", "text", "creation_date"]: + return getattr(self, field) + + raw = self.raw_data or {} + try: + return get_property(raw, field) + except KeyError: + return default_value + + def collect(self, fields: list[str]) -> dict[str, Any]: + """Extract data fields from a TextDocument into a dict.""" + data = {} + for field in fields: + value = self.get(field) + if value is not None: + data[field] = value + return data diff --git a/packages/graphrag-input/pyproject.toml b/packages/graphrag-input/pyproject.toml new file mode 100644 index 0000000000..54ea11b85c --- /dev/null +++ b/packages/graphrag-input/pyproject.toml @@ -0,0 +1,44 @@ +[project] +name = "graphrag-input" +version = "2.7.1" +description = "Input document loading utilities for GraphRAG" +authors = [ + {name = "Alonso Guevara Fernández", email = "alonsog@microsoft.com"}, + {name = "Andrés Morales Esquivel", email = "andresmor@microsoft.com"}, + {name = "Chris Trevino", email = "chtrevin@microsoft.com"}, + {name = "David Tittsworth", email = "datittsw@microsoft.com"}, + {name = "Dayenne de Souza", email = "ddesouza@microsoft.com"}, + {name = "Derek Worthen", email = "deworthe@microsoft.com"}, + {name = "Gaudy Blanco Meneses", email = "gaudyb@microsoft.com"}, + {name = "Ha Trinh", email = "trinhha@microsoft.com"}, + {name = "Jonathan Larson", email = "jolarso@microsoft.com"}, + {name = "Josh Bradley", email = "joshbradley@microsoft.com"}, + {name = "Kate Lytvynets", email = "kalytv@microsoft.com"}, + {name = "Kenny Zhang", email = "zhangken@microsoft.com"}, + {name = "Mónica Carvajal"}, + {name = "Nathan Evans", email = "naevans@microsoft.com"}, + {name = "Rodrigo Racanicci", email = "rracanicci@microsoft.com"}, + {name = "Sarah Smith", email = "smithsarah@microsoft.com"}, +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.11,<3.14" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "graphrag-common==2.7.1", + "graphrag-storage==2.7.1 ", + "pydantic~=2.10", + "markitdown~=0.1.0" +] + +[project.urls] +Source = "https://github.com/microsoft/graphrag" + +[build-system] +requires = ["hatchling>=1.27.0,<2.0.0"] +build-backend = "hatchling.build" diff --git a/packages/graphrag-llm/README.md b/packages/graphrag-llm/README.md new file mode 100644 index 0000000000..30f84431fc --- /dev/null +++ b/packages/graphrag-llm/README.md @@ -0,0 +1,87 @@ +# GraphRAG LLM + +## Basic Completion + +```python +import os +from collections.abc import AsyncIterator, Iterator + +from dotenv import load_dotenv +from graphrag_llm.completion import LLMCompletion, create_completion +from graphrag_llm.config import AuthMethod, ModelConfig +from graphrag_llm.types import LLMCompletionChunk, LLMCompletionResponse +from graphrag_llm.utils import ( + gather_completion_response, + gather_completion_response_async, +) + +load_dotenv() + +api_key = os.getenv("GRAPHRAG_API_KEY") +model_config = ModelConfig( + model_provider="azure", + model=os.getenv("GRAPHRAG_MODEL", "gpt-4o"), + azure_deployment_name=os.getenv("GRAPHRAG_MODEL", "gpt-4o"), + api_base=os.getenv("GRAPHRAG_API_BASE"), + api_version=os.getenv("GRAPHRAG_API_VERSION", "2025-04-01-preview"), + api_key=api_key, + auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey, +) +llm_completion: LLMCompletion = create_completion(model_config) + +response: LLMCompletionResponse | Iterator[LLMCompletionChunk] = ( + llm_completion.completion( + messages="What is the capital of France?", + ) +) + +if isinstance(response, Iterator): + # Streaming response + for chunk in response: + print(chunk.choices[0].delta.content or "", end="", flush=True) +else: + # Non-streaming response + print(response.choices[0].message.content) + +# Alternatively, you can use the utility function to gather the full response +# The following is equivalent to the above logic. If all you care about is +# the first choice response then you can use the gather_completion_response +# utility function. +response_text = gather_completion_response(response) +print(response_text) +``` + +## Basic Embedding + +```python +from graphrag_llm.embedding import LLMEmbedding, create_embedding +from graphrag_llm.types import LLMEmbeddingResponse +from graphrag_llm.utils import gather_embeddings + +embedding_config = ModelConfig( + model_provider="azure", + model=os.getenv("GRAPHRAG_EMBEDDING_MODEL", "text-embedding-3-small"), + azure_deployment_name=os.getenv( + "GRAPHRAG_LLM_EMBEDDING_MODEL", "text-embedding-3-small" + ), + api_base=os.getenv("GRAPHRAG_API_BASE"), + api_version=os.getenv("GRAPHRAG_API_VERSION", "2025-04-01-preview"), + api_key=api_key, + auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey, +) + +llm_embedding: LLMEmbedding = create_embedding(embedding_config) + +embeddings_batch: LLMEmbeddingResponse = llm_embedding.embedding( + input=["Hello world", "How are you?"] +) +for data in embeddings_batch.data: + print(data.embedding[0:3]) + +# OR +batch = gather_embeddings(embeddings_batch) +for embedding in batch: + print(embedding[0:3]) +``` + +View the [notebooks](notebooks/README.md) for more examples. \ No newline at end of file diff --git a/packages/graphrag-llm/graphrag_llm/README.md b/packages/graphrag-llm/graphrag_llm/README.md new file mode 100644 index 0000000000..4ed411515f --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/README.md @@ -0,0 +1,83 @@ +# GraphRAG LLM + +View the [notebooks](notebooks) for detailed examples. + +## Basic Completion + +```python +import os +from collections.abc import AsyncIterator, Iterator + +from graphrag_llm.completion import LLMCompletion, create_completion +from graphrag_llm.config import ModelConfig +from graphrag_llm.types import LLMCompletionChunk, LLMCompletionResponse +from graphrag_llm.utils import ( + gather_completion_response, +) + +api_key = os.getenv("GRAPHRAG_API_KEY") +model_config = ModelConfig( + model_provider="azure", + model=os.getenv("GRAPHRAG_MODEL"), + azure_deployment_name=os.getenv("GRAPHRAG_MODEL"), + api_base=os.getenv("GRAPHRAG_API_BASE"), + api_version=os.getenv("GRAPHRAG_API_VERSION"), + api_key=api_key, + azure_managed_identity=not api_key, +) +llm_completion: LLMCompletion = create_completion(model_config) + +response: LLMCompletionResponse | Iterator[LLMCompletionChunk] = ( + llm_completion.completion( + messages="What is the capital of France?", + ) +) + +if isinstance(response, Iterator): + # Streaming response + for chunk in response: + print(chunk.choices[0].delta.content or "", end="", flush=True) +else: + # Non-streaming response + print(response.choices[0].message.content) + +# Alternatively, you can use the utility function to gather the full response +# The following is equivalent to the above logic. If all you care about is +# the first choice response then you can use the gather_completion_response +# utility function. +response_text = gather_completion_response(response) +print(response_text) +``` + +## Basic Embedding + +```python +import os +from collections.abc import AsyncIterator, Iterator + +from graphrag_llm.embedding import LLMEmbedding, create_embedding +from graphrag_llm.config import ModelConfig +from graphrag_llm.types import LLMEmbeddingResponse +from graphrag_llm.utils import ( + gather_completion_response, +) + +api_key = os.getenv("GRAPHRAG_API_KEY") +embedding_config = ModelConfig( + model_provider="azure", + model=os.getenv("GRAPHRAG_EMBEDDING_MODEL"), # type: ignore + azure_deployment_name=os.getenv("GRAPHRAG_EMBEDDING_MODEL"), + api_base=os.getenv("GRAPHRAG_API_BASE"), + api_version=os.getenv("GRAPHRAG_API_VERSION"), + api_key=api_key, + azure_managed_identity=not api_key, +) + +llm_embedding: LLMEmbedding = create_embedding(embedding_config) + +embeddings: LLMEmbeddingResponse = llm_embedding.embedding( + input=["Hello world", "How are you?"] +) +for data in embeddings.data: + print(data.embedding[0:3]) +``` \ No newline at end of file diff --git a/packages/graphrag-llm/graphrag_llm/__init__.py b/packages/graphrag-llm/graphrag_llm/__init__.py new file mode 100644 index 0000000000..c7e7b60983 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""GraphRAG LLM Package.""" + +import nest_asyncio2 + +nest_asyncio2.apply() # noqa: RUF067 diff --git a/packages/graphrag-llm/graphrag_llm/cache/__init__.py b/packages/graphrag-llm/graphrag_llm/cache/__init__.py new file mode 100644 index 0000000000..20d8b56f08 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/cache/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Cache module.""" + +from graphrag_llm.cache.create_cache_key import create_cache_key + +__all__ = [ + "create_cache_key", +] diff --git a/packages/graphrag-llm/graphrag_llm/cache/create_cache_key.py b/packages/graphrag-llm/graphrag_llm/cache/create_cache_key.py new file mode 100644 index 0000000000..7fa7ec26dd --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/cache/create_cache_key.py @@ -0,0 +1,71 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Create cache key.""" + +from typing import Any + +from graphrag_cache import create_cache_key as default_create_cache_key + +_CACHE_VERSION = 4 +""" +If there's a breaking change in what we cache, we should increment this version number to invalidate existing caches. + +fnllm was on cache version 2 and though we generate +similar cache keys, the objects stored in cache by fnllm and litellm are different. +Using litellm model providers will not be able to reuse caches generated by fnllm +thus we start with version 3 for litellm. + +graphrag-llm package is now on version 4. +This is to account for changes to the ModelConfig that affect the cache key and +occurred when pulling this package out of graphrag. +graphrag-llm, now that is supports metrics, also caches metrics which were not cached before. +""" + + +def create_cache_key( + input_args: dict[str, Any], +) -> str: + """Generate a cache key based on the model configuration and input arguments. + + Args + ____ + input_args: dict[str, Any] + The input arguments for the model call. + + Returns + ------- + str + The generated cache key in the format + `{prefix}_{data_hash}_v{version}` if prefix is provided. + """ + cache_key_parameters = _get_parameters( + input_args=input_args, + ) + return default_create_cache_key(cache_key_parameters) + + +def _get_parameters( + # model_config: "ModelConfig", + input_args: dict[str, Any], +) -> dict[str, Any]: + """Pluck out the parameters that define a cache key.""" + excluded_keys = [ + "metrics", + "stream", + "stream_options", + "mock_response", + "timeout", + "base_url", + "api_base", + "api_version", + "api_key", + "azure_ad_token_provider", + "drop_params", + ] + + parameters: dict[str, Any] = { + k: v for k, v in input_args.items() if k not in excluded_keys + } + + return parameters diff --git a/packages/graphrag-llm/graphrag_llm/completion/__init__.py b/packages/graphrag-llm/graphrag_llm/completion/__init__.py new file mode 100644 index 0000000000..ceb3a43bfb --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/completion/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Completion module for graphrag-llm.""" + +from graphrag_llm.completion.completion import LLMCompletion +from graphrag_llm.completion.completion_factory import ( + create_completion, + register_completion, +) + +__all__ = [ + "LLMCompletion", + "create_completion", + "register_completion", +] diff --git a/packages/graphrag-llm/graphrag_llm/completion/completion.py b/packages/graphrag-llm/graphrag_llm/completion/completion.py new file mode 100644 index 0000000000..0debab2731 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/completion/completion.py @@ -0,0 +1,276 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Completion Abstract Base Class.""" + +from abc import ABC, abstractmethod +from contextlib import contextmanager +from typing import TYPE_CHECKING, Any, Unpack + +from graphrag_llm.threading.completion_thread_runner import completion_thread_runner + +if TYPE_CHECKING: + from collections.abc import AsyncIterator, Iterator + + from graphrag_cache import Cache, CacheKeyCreator + + from graphrag_llm.config import ModelConfig + from graphrag_llm.metrics import MetricsProcessor, MetricsStore + from graphrag_llm.rate_limit import RateLimiter + from graphrag_llm.retry import Retry + from graphrag_llm.threading.completion_thread_runner import ( + ThreadedLLMCompletionFunction, + ThreadedLLMCompletionResponseHandler, + ) + from graphrag_llm.tokenizer import Tokenizer + from graphrag_llm.types import ( + LLMCompletionArgs, + LLMCompletionChunk, + LLMCompletionResponse, + ResponseFormat, + ) + + +class LLMCompletion(ABC): + """Abstract base class for language model completions.""" + + @abstractmethod + def __init__( + self, + *, + model_id: str, + model_config: "ModelConfig", + tokenizer: "Tokenizer", + metrics_store: "MetricsStore", + metrics_processor: "MetricsProcessor | None" = None, + rate_limiter: "RateLimiter | None" = None, + retrier: "Retry | None" = None, + cache: "Cache | None" = None, + cache_key_creator: "CacheKeyCreator", + **kwargs: Any, + ): + """Initialize the LLMCompletion. + + Args + ---- + model_id: str + The model ID, e.g., "openai/gpt-4o". + model_config: ModelConfig + The configuration for the language model. + tokenizer: Tokenizer + The tokenizer to use. + metrics_store: MetricsStore | None (default=None) + The metrics store to use. + metrics_processor: MetricsProcessor | None (default: None) + The metrics processor to use. + rate_limiter: RateLimiter | None (default=None) + The rate limiter to use. + retrier: Retry | None (default=None) + The retry strategy to use. + cache: Cache | None (default=None) + Optional cache for embeddings. + cache_key_creator: CacheKeyCreator | None (default=None) + Optional cache key creator function. + (dict[str, Any]) -> str + **kwargs: Any + Additional keyword arguments. + """ + raise NotImplementedError + + @abstractmethod + def supports_structured_response(self) -> bool: + """Whether the completion supports structured responses. + + Returns + ------- + bool: + True if structured responses are supported, False otherwise. + """ + raise NotImplementedError + + @abstractmethod + def completion( + self, + /, + **kwargs: Unpack["LLMCompletionArgs[ResponseFormat]"], + ) -> "LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk]": + """Sync completion method. + + Args + ---- + messages: LLMCompletionMessagesParam + The messages to send to the LLM. + Can be str | list[dict[str, str]] | list[ChatCompletionMessageParam]. + response_format: BaseModel | None (default=None) + The structured response format. + Must extend pydantic BaseModel. + stream: bool (default=False) + Whether to stream the response. + streaming is not supported when using response_format. + max_completion_tokens: int | None (default=None) + The maximum number of tokens to generate in the completion. + temperature: float | None (default=None) + The temperature to control how deterministic vs. creative the responses are. + top_p: float | None (default=None) + top_p for nucleus sampling, where the model considers tokens with + cumulative probabilities up to top_p. Values range from 0 to 1. + n: int | None (default=None) + The number of completions to generate for each prompt. + tools: list[Tool] | None (default=None) + Optional tools to use during completion. + https://docs.litellm.ai/docs/completion/function_call + **kwargs: Any + Additional keyword arguments. + + Returns + ------- + LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk]: + The completion response or an iterator of completion chunks if streaming. + + """ + raise NotImplementedError + + @abstractmethod + async def completion_async( + self, + /, + **kwargs: Unpack["LLMCompletionArgs[ResponseFormat]"], + ) -> "LLMCompletionResponse[ResponseFormat] | AsyncIterator[LLMCompletionChunk]": + """Async completion method. + + Args + ---- + messages: LLMCompletionMessagesParam + The messages to send to the LLM. + Can be str | list[dict[str, str]] | list[ChatCompletionMessageParam]. + response_format: BaseModel | None (default=None) + The structured response format. + Must extend pydantic BaseModel. + stream: bool (default=False) + Whether to stream the response. + streaming is not supported when using response_format. + max_completion_tokens: int | None (default=None) + The maximum number of tokens to generate in the completion. + temperature: float | None (default=None) + The temperature to control how deterministic vs. creative the responses are. + top_p: float | None (default=None) + top_p for nucleus sampling, where the model considers tokens with + cumulative probabilities up to top_p. Values range from 0 to 1. + n: int | None (default=None) + The number of completions to generate for each prompt. + tools: list[Tool] | None (default=None) + Optional tools to use during completion. + https://docs.litellm.ai/docs/completion/function_call + **kwargs: Any + Additional keyword arguments. + + Returns + ------- + LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk]: + The completion response or an iterator of completion chunks if streaming. + """ + raise NotImplementedError + + @contextmanager + def completion_thread_pool( + self, + *, + response_handler: "ThreadedLLMCompletionResponseHandler", + concurrency: int, + queue_limit: int = 0, + ) -> "Iterator[ThreadedLLMCompletionFunction]": + """Run a completion thread pool. + + Args + ---- + response_handler: ThreadedLLMCompletionResponseHandler + The callback function to handle completion responses. + (request_id, response|exception) -> Awaitable[None] | None + concurrency: int + The number of threads to spin up in a thread pool. + queue_limit: int (default=0) + The maximum number of items allowed in the input queue. + 0 means unlimited. + Set this to a value to create backpressure on the caller. + + Yields + ------ + ThreadedLLMCompletionFunction: + A function that can be used to submit completion requests to the thread pool. + (messages, request_id, **kwargs) -> None + + The thread pool will process the requests and invoke the provided callback + with the responses. + + same signature as LLMCompletionFunction but requires a `request_id` parameter + to identify the request and does not return anything. + """ + with completion_thread_runner( + completion=self.completion, + response_handler=response_handler, + concurrency=concurrency, + queue_limit=queue_limit, + metrics_store=self.metrics_store, + ) as completion: + yield completion + + def completion_batch( + self, + completion_requests: list["LLMCompletionArgs[ResponseFormat]"], + *, + concurrency: int, + queue_limit: int = 0, + ) -> list[ + "LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk] | Exception" + ]: + """Process a batch of completion requests using a thread pool. + + Args + ---- + completion_requests: list[LLMCompletionArgs] + A list of completion request arguments to process in parallel. + concurrency: int + The number of threads to spin up in a thread pool. + queue_limit: int (default=0) + The maximum number of items allowed in the input queue. + 0 means unlimited. + Set this to a value to create backpressure on the caller. + + Returns + ------- + list[LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk] | Exception]: + A list of completion responses or exceptions corresponding to all the requests. + """ + responses: list[ + LLMCompletionResponse[ResponseFormat] + | Iterator[LLMCompletionChunk] + | Exception + ] = [None] * len(completion_requests) # type: ignore + + def handle_response( + request_id: str, + resp: "LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk] | Exception", + ): + responses[int(request_id)] = resp + + with self.completion_thread_pool( + response_handler=handle_response, + concurrency=concurrency, + queue_limit=queue_limit, + ) as threaded_completion: + for idx, request in enumerate(completion_requests): + threaded_completion(request_id=str(idx), **request) + + return responses + + @property + @abstractmethod + def metrics_store(self) -> "MetricsStore": + """Metrics store.""" + raise NotImplementedError + + @property + @abstractmethod + def tokenizer(self) -> "Tokenizer": + """Tokenizer.""" + raise NotImplementedError diff --git a/packages/graphrag-llm/graphrag_llm/completion/completion_factory.py b/packages/graphrag-llm/graphrag_llm/completion/completion_factory.py new file mode 100644 index 0000000000..e1923b9c63 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/completion/completion_factory.py @@ -0,0 +1,150 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Completion factory.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +from graphrag_common.factory import Factory + +from graphrag_llm.cache import create_cache_key +from graphrag_llm.config.tokenizer_config import TokenizerConfig +from graphrag_llm.config.types import LLMProviderType +from graphrag_llm.metrics.noop_metrics_store import NoopMetricsStore +from graphrag_llm.tokenizer.tokenizer_factory import create_tokenizer + +if TYPE_CHECKING: + from graphrag_cache import Cache, CacheKeyCreator + from graphrag_common.factory import ServiceScope + + from graphrag_llm.completion.completion import LLMCompletion + from graphrag_llm.config import ModelConfig + from graphrag_llm.metrics import MetricsProcessor, MetricsStore + from graphrag_llm.rate_limit import RateLimiter + from graphrag_llm.retry import Retry + from graphrag_llm.tokenizer import Tokenizer + + +class CompletionFactory(Factory["LLMCompletion"]): + """Factory for creating Completion instances.""" + + +completion_factory = CompletionFactory() + + +def register_completion( + completion_type: str, + completion_initializer: Callable[..., "LLMCompletion"], + scope: "ServiceScope" = "transient", +) -> None: + """Register a custom completion implementation. + + Args + ---- + completion_type: str + The completion id to register. + completion_initializer: Callable[..., LLMCompletion] + The completion initializer to register. + scope: ServiceScope (default: "transient") + The service scope for the completion. + """ + completion_factory.register(completion_type, completion_initializer, scope) + + +def create_completion( + model_config: "ModelConfig", + *, + cache: "Cache | None" = None, + cache_key_creator: "CacheKeyCreator | None" = None, + tokenizer: "Tokenizer | None" = None, +) -> "LLMCompletion": + """Create a Completion instance based on the model configuration. + + Args + ---- + model_config: ModelConfig + The configuration for the model. + cache: Cache | None (default: None) + An optional cache instance. + cache_key_creator: CacheKeyCreator | None (default: create_cache_key) + An optional cache key creator function. + (dict[str, Any]) -> str + tokenizer: Tokenizer | None (default: litellm) + An optional tokenizer instance. + + Returns + ------- + LLMCompletion: + An instance of a LLMCompletion subclass. + """ + cache_key_creator = cache_key_creator or create_cache_key + model_id = f"{model_config.model_provider}/{model_config.model}" + strategy = model_config.type + extra: dict[str, Any] = model_config.model_extra or {} + + if strategy not in completion_factory: + match strategy: + case LLMProviderType.LiteLLM: + from graphrag_llm.completion.lite_llm_completion import ( + LiteLLMCompletion, + ) + + register_completion( + completion_type=LLMProviderType.LiteLLM, + completion_initializer=LiteLLMCompletion, + scope="singleton", + ) + case LLMProviderType.MockLLM: + from graphrag_llm.completion.mock_llm_completion import ( + MockLLMCompletion, + ) + + register_completion( + completion_type=LLMProviderType.MockLLM, + completion_initializer=MockLLMCompletion, + ) + case _: + msg = f"ModelConfig.type '{strategy}' is not registered in the CompletionFactory. Registered strategies: {', '.join(completion_factory.keys())}" + raise ValueError(msg) + + tokenizer = tokenizer or create_tokenizer(TokenizerConfig(model_id=model_id)) + + rate_limiter: RateLimiter | None = None + if model_config.rate_limit: + from graphrag_llm.rate_limit.rate_limit_factory import create_rate_limiter + + rate_limiter = create_rate_limiter(rate_limit_config=model_config.rate_limit) + + retrier: Retry | None = None + if model_config.retry: + from graphrag_llm.retry.retry_factory import create_retry + + retrier = create_retry(retry_config=model_config.retry) + + metrics_store: MetricsStore = NoopMetricsStore() + metrics_processor: MetricsProcessor | None = None + if model_config.metrics: + from graphrag_llm.metrics import create_metrics_processor, create_metrics_store + + metrics_store = create_metrics_store( + config=model_config.metrics, + id=model_id, + ) + metrics_processor = create_metrics_processor(model_config.metrics) + + return completion_factory.create( + strategy=strategy, + init_args={ + **extra, + "model_id": model_id, + "model_config": model_config, + "tokenizer": tokenizer, + "metrics_store": metrics_store, + "metrics_processor": metrics_processor, + "rate_limiter": rate_limiter, + "retrier": retrier, + "cache": cache, + "cache_key_creator": cache_key_creator, + }, + ) diff --git a/packages/graphrag-llm/graphrag_llm/completion/lite_llm_completion.py b/packages/graphrag-llm/graphrag_llm/completion/lite_llm_completion.py new file mode 100644 index 0000000000..794296604c --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/completion/lite_llm_completion.py @@ -0,0 +1,314 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""LLMCompletion based on litellm.""" + +from collections.abc import AsyncIterator, Iterator +from typing import TYPE_CHECKING, Any, Unpack + +import litellm +from azure.identity import DefaultAzureCredential, get_bearer_token_provider +from litellm import ModelResponse, supports_response_schema # type: ignore + +from graphrag_llm.completion.completion import LLMCompletion +from graphrag_llm.config.types import AuthMethod +from graphrag_llm.middleware import ( + with_middleware_pipeline, +) +from graphrag_llm.types import LLMCompletionChunk, LLMCompletionResponse +from graphrag_llm.utils import ( + structure_completion_response, +) + +if TYPE_CHECKING: + from graphrag_cache import Cache, CacheKeyCreator + + from graphrag_llm.config import ModelConfig + from graphrag_llm.metrics import MetricsProcessor, MetricsStore + from graphrag_llm.rate_limit import RateLimiter + from graphrag_llm.retry import Retry + from graphrag_llm.tokenizer import Tokenizer + from graphrag_llm.types import ( + AsyncLLMCompletionFunction, + LLMCompletionArgs, + LLMCompletionFunction, + LLMCompletionMessagesParam, + Metrics, + ResponseFormat, + ) + + +litellm.suppress_debug_info = True + + +class LiteLLMCompletion(LLMCompletion): + """LLMCompletion based on litellm.""" + + _model_config: "ModelConfig" + _model_id: str + _track_metrics: bool = False + _metrics_store: "MetricsStore" + _metrics_processor: "MetricsProcessor | None" + _cache: "Cache | None" + _cache_key_creator: "CacheKeyCreator" + _tokenizer: "Tokenizer" + _rate_limiter: "RateLimiter | None" + _retrier: "Retry | None" + + def __init__( + self, + *, + model_id: str, + model_config: "ModelConfig", + tokenizer: "Tokenizer", + metrics_store: "MetricsStore", + metrics_processor: "MetricsProcessor | None" = None, + rate_limiter: "RateLimiter | None" = None, + retrier: "Retry | None" = None, + cache: "Cache | None" = None, + cache_key_creator: "CacheKeyCreator", + azure_cognitive_services_audience: str = "https://cognitiveservices.azure.com/.default", + drop_unsupported_params: bool = True, + **kwargs: Any, + ) -> None: + """Initialize LiteLLMCompletion. + + Args + ---- + model_id: str + The LiteLLM model ID, e.g., "openai/gpt-4o" + model_config: ModelConfig + The configuration for the model. + tokenizer: Tokenizer + The tokenizer to use. + metrics_store: MetricsStore | None (default: None) + The metrics store to use. + metrics_processor: MetricsProcessor | None (default: None) + The metrics processor to use. + cache: Cache | None (default: None) + An optional cache instance. + cache_key_prefix: str | None (default: "chat") + The cache key prefix. Required if cache is provided. + rate_limiter: RateLimiter | None (default: None) + The rate limiter to use. + retrier: Retry | None (default: None) + The retry strategy to use. + azure_cognitive_services_audience: str (default: "https://cognitiveservices.azure.com/.default") + The audience for Azure Cognitive Services when using Managed Identity. + drop_unsupported_params: bool (default: True) + Whether to drop unsupported parameters for the model provider. + """ + self._model_id = model_id + self._model_config = model_config + self._tokenizer = tokenizer + self._metrics_store = metrics_store + self._metrics_processor = metrics_processor + self._cache = cache + self._track_metrics = metrics_processor is not None + self._cache_key_creator = cache_key_creator + self._rate_limiter = rate_limiter + self._retrier = retrier + + self._completion, self._completion_async = _create_base_completions( + model_config=model_config, + drop_unsupported_params=drop_unsupported_params, + azure_cognitive_services_audience=azure_cognitive_services_audience, + ) + + self._completion, self._completion_async = with_middleware_pipeline( + model_config=self._model_config, + model_fn=self._completion, + async_model_fn=self._completion_async, + request_type="chat", + cache=self._cache, + cache_key_creator=self._cache_key_creator, + tokenizer=self._tokenizer, + metrics_processor=self._metrics_processor, + rate_limiter=self._rate_limiter, + retrier=self._retrier, + ) + + def supports_structured_response(self) -> bool: + """Check if the model supports structured response.""" + return supports_response_schema(self._model_id) + + def completion( + self, + /, + **kwargs: Unpack["LLMCompletionArgs[ResponseFormat]"], + ) -> "LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk]": + """Sync completion method.""" + messages: LLMCompletionMessagesParam = kwargs.pop("messages") + response_format = kwargs.pop("response_format", None) + if response_format and not self.supports_structured_response(): + msg = f"Model '{self._model_id}' does not support response schemas." + raise ValueError(msg) + + is_streaming = kwargs.get("stream") or False + + if response_format is not None and is_streaming: + msg = "response_format is not supported for streaming completions." + raise ValueError(msg) + + request_metrics: Metrics | None = kwargs.pop("metrics", None) or {} + if not self._track_metrics: + request_metrics = None + + if isinstance(messages, str): + messages = [{"role": "user", "content": messages}] + + try: + response = self._completion( + messages=messages, + metrics=request_metrics, + response_format=response_format, + **kwargs, # type: ignore + ) + if response_format is not None: + structured_response = structure_completion_response( + response.content, response_format + ) + response.formatted_response = structured_response + return response + finally: + if request_metrics is not None: + self._metrics_store.update_metrics(metrics=request_metrics) + + async def completion_async( + self, + /, + **kwargs: Unpack["LLMCompletionArgs[ResponseFormat]"], + ) -> "LLMCompletionResponse[ResponseFormat] | AsyncIterator[LLMCompletionChunk]": + """Async completion method.""" + messages: LLMCompletionMessagesParam = kwargs.pop("messages") + response_format = kwargs.pop("response_format", None) + if response_format and not supports_response_schema( + self._model_id, + ): + msg = f"Model '{self._model_id}' does not support response schemas." + raise ValueError(msg) + + is_streaming = kwargs.get("stream") or False + + if response_format is not None and is_streaming: + msg = "response_format is not supported for streaming completions." + raise ValueError(msg) + + request_metrics: Metrics | None = kwargs.pop("metrics", None) or {} + if not self._track_metrics: + request_metrics = None + + if isinstance(messages, str): + messages = [{"role": "user", "content": messages}] + + try: + response = await self._completion_async( + messages=messages, + metrics=request_metrics, + response_format=response_format, + **kwargs, # type: ignore + ) + if response_format is not None: + structured_response = structure_completion_response( + response.content, response_format + ) + response.formatted_response = structured_response + return response + finally: + if request_metrics is not None: + self._metrics_store.update_metrics(metrics=request_metrics) + + @property + def metrics_store(self) -> "MetricsStore": + """Get metrics store.""" + return self._metrics_store + + @property + def tokenizer(self) -> "Tokenizer": + """Get tokenizer.""" + return self._tokenizer + + +def _create_base_completions( + *, + model_config: "ModelConfig", + drop_unsupported_params: bool, + azure_cognitive_services_audience: str, +) -> tuple["LLMCompletionFunction", "AsyncLLMCompletionFunction"]: + """Create base completions for LiteLLM. + + Convert litellm completion functions to graphrag_llm LLMCompletionFunction. + LLMCompletionFunction is close to the litellm completion function signature, + but uses a few extra params such as metrics. Remove graphrag_llm LLMCompletionFunction + specific params before calling litellm completion functions. + """ + model_provider = model_config.model_provider + model = model_config.azure_deployment_name or model_config.model + + base_args: dict[str, Any] = { + "drop_params": drop_unsupported_params, + "model": f"{model_provider}/{model}", + "api_key": model_config.api_key, + "api_base": model_config.api_base, + "api_version": model_config.api_version, + **model_config.call_args, + } + + if model_config.auth_method == AuthMethod.AzureManagedIdentity: + base_args["azure_ad_token_provider"] = get_bearer_token_provider( + DefaultAzureCredential(), azure_cognitive_services_audience + ) + + def _base_completion( + **kwargs: Any, + ) -> LLMCompletionResponse | Iterator[LLMCompletionChunk]: + kwargs.pop("metrics", None) + mock_response: str | None = kwargs.pop("mock_response", None) + json_object: bool | None = kwargs.pop("response_format_json_object", None) + new_args: dict[str, Any] = {**base_args, **kwargs} + + if model_config.mock_responses and mock_response is not None: + new_args["mock_response"] = mock_response + + if json_object and "response_format" not in new_args: + new_args["response_format"] = {"type": "json_object"} + + response = litellm.completion( + **new_args, + ) + if isinstance(response, ModelResponse): + return LLMCompletionResponse(**response.model_dump()) + + def _run_iterator() -> Iterator[LLMCompletionChunk]: + for chunk in response: + yield LLMCompletionChunk(**chunk.model_dump()) + + return _run_iterator() + + async def _base_completion_async( + **kwargs: Any, + ) -> LLMCompletionResponse | AsyncIterator[LLMCompletionChunk]: + kwargs.pop("metrics", None) + mock_response: str | None = kwargs.pop("mock_response", None) + json_object: bool | None = kwargs.pop("response_format_json_object", None) + new_args: dict[str, Any] = {**base_args, **kwargs} + + if model_config.mock_responses and mock_response is not None: + new_args["mock_response"] = mock_response + + if json_object and "response_format" not in new_args: + new_args["response_format"] = {"type": "json_object"} + + response = await litellm.acompletion( + **new_args, + ) + if isinstance(response, ModelResponse): + return LLMCompletionResponse(**response.model_dump()) + + async def _run_iterator() -> AsyncIterator[LLMCompletionChunk]: + async for chunk in response: + yield LLMCompletionChunk(**chunk.model_dump()) # type: ignore + + return _run_iterator() + + return (_base_completion, _base_completion_async) diff --git a/packages/graphrag-llm/graphrag_llm/completion/mock_llm_completion.py b/packages/graphrag-llm/graphrag_llm/completion/mock_llm_completion.py new file mode 100644 index 0000000000..c1e29fcfc4 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/completion/mock_llm_completion.py @@ -0,0 +1,134 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Mock LLMCompletion.""" + +from typing import TYPE_CHECKING, Any, Unpack + +import litellm + +from graphrag_llm.completion.completion import LLMCompletion +from graphrag_llm.utils import ( + create_completion_response, + structure_completion_response, +) + +if TYPE_CHECKING: + from collections.abc import AsyncIterator, Iterator + + from graphrag_llm.config import ModelConfig + from graphrag_llm.metrics import MetricsStore + from graphrag_llm.tokenizer import Tokenizer + from graphrag_llm.types import ( + LLMCompletionArgs, + LLMCompletionChunk, + LLMCompletionResponse, + ResponseFormat, + ) + + +litellm.suppress_debug_info = True + + +class MockLLMCompletion(LLMCompletion): + """LLMCompletion based on litellm.""" + + _metrics_store: "MetricsStore" + _tokenizer: "Tokenizer" + _mock_responses: list[str] + _mock_index: int = 0 + + def __init__( + self, + *, + model_config: "ModelConfig", + tokenizer: "Tokenizer", + metrics_store: "MetricsStore", + **kwargs: Any, + ) -> None: + """Initialize LiteLLMCompletion. + + Args + ---- + model_id: str + The LiteLLM model ID, e.g., "openai/gpt-4o" + model_config: ModelConfig + The configuration for the model. + tokenizer: Tokenizer + The tokenizer to use. + metrics_store: MetricsStore | None (default: None) + The metrics store to use. + metrics_processor: MetricsProcessor | None (default: None) + The metrics processor to use. + cache: Cache | None (default: None) + An optional cache instance. + cache_key_prefix: str | None (default: "chat") + The cache key prefix. Required if cache is provided. + rate_limiter: RateLimiter | None (default: None) + The rate limiter to use. + retrier: Retry | None (default: None) + The retry strategy to use. + azure_cognitive_services_audience: str (default: "https://cognitiveservices.azure.com/.default") + The audience for Azure Cognitive Services when using Managed Identity. + drop_unsupported_params: bool (default: True) + Whether to drop unsupported parameters for the model provider. + """ + self._tokenizer = tokenizer + self._metrics_store = metrics_store + + mock_responses = model_config.mock_responses + if not isinstance(mock_responses, list) or len(mock_responses) == 0: + msg = "ModelConfig.mock_responses must be a non-empty list." + raise ValueError(msg) + + if not all(isinstance(resp, str) for resp in mock_responses): + msg = "Each item in ModelConfig.mock_responses must be a string." + raise ValueError(msg) + + self._mock_responses = mock_responses # type: ignore + + def supports_structured_response(self) -> bool: + """Check if the model supports structured response.""" + return True + + def completion( + self, + /, + **kwargs: Unpack["LLMCompletionArgs[ResponseFormat]"], + ) -> "LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk]": + """Sync completion method.""" + response_format = kwargs.pop("response_format", None) + + is_streaming = kwargs.get("stream", False) + if is_streaming: + msg = "MockLLMCompletion does not support streaming completions." + raise ValueError(msg) + + response = create_completion_response( + self._mock_responses[self._mock_index % len(self._mock_responses)] + ) + self._mock_index += 1 + if response_format is not None: + structured_response = structure_completion_response( + response.content, response_format + ) + response.formatted_response = structured_response + return response + + async def completion_async( + self, + /, + **kwargs: Unpack["LLMCompletionArgs[ResponseFormat]"], + ) -> "LLMCompletionResponse[ResponseFormat] | AsyncIterator[LLMCompletionChunk]": + """Async completion method.""" + return self.completion(**kwargs) # type: ignore + + @property + def metrics_store(self) -> "MetricsStore": + """Get metrics store.""" + return self._metrics_store + + @property + def tokenizer(self) -> "Tokenizer": + """Get tokenizer.""" + return self._tokenizer diff --git a/packages/graphrag-llm/graphrag_llm/config/__init__.py b/packages/graphrag-llm/graphrag_llm/config/__init__.py new file mode 100644 index 0000000000..fc9023dc1b --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/config/__init__.py @@ -0,0 +1,42 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Config module for graphrag-llm.""" + +from graphrag_llm.config.metrics_config import MetricsConfig +from graphrag_llm.config.model_config import ModelConfig +from graphrag_llm.config.rate_limit_config import RateLimitConfig +from graphrag_llm.config.retry_config import RetryConfig +from graphrag_llm.config.template_engine_config import TemplateEngineConfig +from graphrag_llm.config.tokenizer_config import TokenizerConfig +from graphrag_llm.config.types import ( + AuthMethod, + LLMProviderType, + MetricsProcessorType, + MetricsStoreType, + MetricsWriterType, + RateLimitType, + RetryType, + TemplateEngineType, + TemplateManagerType, + TokenizerType, +) + +__all__ = [ + "AuthMethod", + "LLMProviderType", + "MetricsConfig", + "MetricsProcessorType", + "MetricsStoreType", + "MetricsWriterType", + "ModelConfig", + "RateLimitConfig", + "RateLimitType", + "RetryConfig", + "RetryType", + "TemplateEngineConfig", + "TemplateEngineType", + "TemplateManagerType", + "TokenizerConfig", + "TokenizerType", +] diff --git a/packages/graphrag-llm/graphrag_llm/config/metrics_config.py b/packages/graphrag-llm/graphrag_llm/config/metrics_config.py new file mode 100644 index 0000000000..9d8f88a047 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/config/metrics_config.py @@ -0,0 +1,57 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Metrics configuration.""" + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from graphrag_llm.config.types import ( + MetricsProcessorType, + MetricsStoreType, + MetricsWriterType, +) + + +class MetricsConfig(BaseModel): + """Configuration for metrics.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom metrics implementations.""" + + type: str = Field( + default=MetricsProcessorType.Default, + description="MetricsProcessor implementation to use.", + ) + + store: str = Field( + default=MetricsStoreType.Memory, + description="MetricsStore implementation to use. [memory] (default: memory).", + ) + + writer: str | None = Field( + default=MetricsWriterType.Log, + description="MetricsWriter implementation to use. [log, file] (default: log).", + ) + + log_level: int | None = Field( + default=None, + description="Log level to use when using the 'Log' metrics writer. (default: INFO)", + ) + + base_dir: str | None = Field( + default=None, + description="Base directory for file-based metrics writer. (default: ./metrics)", + ) + + def _validate_file_metrics_writer_config(self) -> None: + """Validate parameters for file-based metrics writer.""" + if self.base_dir is not None and self.base_dir.strip() == "": + msg = "base_dir must be specified for file-based metrics writer." + raise ValueError(msg) + + @model_validator(mode="after") + def _validate_model(self): + """Validate the metrics configuration based on its writer type.""" + if self.writer == MetricsWriterType.File: + self._validate_file_metrics_writer_config() + return self diff --git a/packages/graphrag-llm/graphrag_llm/config/model_config.py b/packages/graphrag-llm/graphrag_llm/config/model_config.py new file mode 100644 index 0000000000..38cfb342d9 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/config/model_config.py @@ -0,0 +1,111 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Language model configuration.""" + +import logging +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from graphrag_llm.config.metrics_config import MetricsConfig +from graphrag_llm.config.rate_limit_config import RateLimitConfig +from graphrag_llm.config.retry_config import RetryConfig +from graphrag_llm.config.types import AuthMethod, LLMProviderType + +logger = logging.getLogger(__name__) + + +class ModelConfig(BaseModel): + """Configuration for a language model.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom LLM provider implementations.""" + + type: str = Field( + default=LLMProviderType.LiteLLM, + description="The type of LLM provider to use. (default: litellm)", + ) + + model_provider: str = Field( + description="The provider of the model, e.g., 'openai', 'azure', etc.", + ) + + model: str = Field( + description="The specific model to use, e.g., 'gpt-4o', 'gpt-3.5-turbo', etc.", + ) + + call_args: dict[str, Any] = Field( + default_factory=dict, + description="Base keyword arguments to pass to the model provider's API.", + ) + + api_base: str | None = Field( + default=None, + description="The base URL for the API, required for some providers like Azure.", + ) + + api_version: str | None = Field( + default=None, + description="The version of the API to use.", + ) + + api_key: str | None = Field( + default=None, + description="API key for authentication with the model provider.", + ) + + auth_method: AuthMethod = Field( + default=AuthMethod.ApiKey, + description="The authentication method to use. (default: api_key)", + ) + + azure_deployment_name: str | None = Field( + default=None, + description="The deployment name for Azure models.", + ) + + retry: RetryConfig | None = Field( + default=None, + description="Configuration for the retry strategy.", + ) + + rate_limit: RateLimitConfig | None = Field( + default=None, + description="Configuration for the rate limit behavior.", + ) + + metrics: MetricsConfig | None = Field( + default_factory=MetricsConfig, + description="Specify and configure the metric services.", + ) + + mock_responses: list[str] | list[float] = Field( + default_factory=list, + description="List of mock responses for testing.", + ) + + def _validate_lite_llm_config(self) -> None: + """Validate LiteLLM specific configuration.""" + if self.model_provider == "azure" and not self.api_base: + msg = "api_base must be specified with the 'azure' model provider." + raise ValueError(msg) + + if self.model_provider != "azure" and self.azure_deployment_name is not None: + msg = "azure_deployment_name should not be specified for non-Azure model providers." + raise ValueError(msg) + + if self.auth_method == AuthMethod.AzureManagedIdentity: + if self.api_key is not None: + msg = "api_key should not be set when using Azure Managed Identity." + raise ValueError(msg) + elif not self.api_key: + msg = "api_key must be set when auth_method=api_key." + raise ValueError(msg) + + @model_validator(mode="after") + def _validate_model(self): + """Validate model configuration after initialization.""" + if self.type == LLMProviderType.LiteLLM: + self._validate_lite_llm_config() + return self diff --git a/packages/graphrag-llm/graphrag_llm/config/rate_limit_config.py b/packages/graphrag-llm/graphrag_llm/config/rate_limit_config.py new file mode 100644 index 0000000000..df654b8e88 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/config/rate_limit_config.py @@ -0,0 +1,60 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""RateLimit configuration.""" + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from graphrag_llm.config.types import RateLimitType + + +class RateLimitConfig(BaseModel): + """Configuration for rate limit behavior.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom RateLimit implementations.""" + + type: str = Field( + default=RateLimitType.SlidingWindow, + description="The type of rate limit strategy to use. [sliding_window] (default: sliding_window).", + ) + + period_in_seconds: int | None = Field( + default=None, + description="The period in seconds for the rate limit window. (default: 60).", + ) + + requests_per_period: int | None = Field( + default=None, + description="The maximum number of requests allowed per period. (default: None, no limit).", + ) + + tokens_per_period: int | None = Field( + default=None, + description="The maximum number of tokens allowed per period. (default: None, no limit).", + ) + + def _validate_sliding_window_config(self) -> None: + """Validate Sliding Window rate limit configuration.""" + if self.period_in_seconds is not None and self.period_in_seconds <= 0: + msg = "period_in_seconds must be a positive integer for Sliding Window rate limit." + raise ValueError(msg) + + if not self.requests_per_period and not self.tokens_per_period: + msg = "At least one of requests_per_period or tokens_per_period must be specified for Sliding Window rate limit." + raise ValueError(msg) + + if self.requests_per_period is not None and self.requests_per_period <= 0: + msg = "requests_per_period must be a positive integer for Sliding Window rate limit." + raise ValueError(msg) + + if self.tokens_per_period is not None and self.tokens_per_period <= 0: + msg = "tokens_per_period must be a positive integer for Sliding Window rate limit." + raise ValueError(msg) + + @model_validator(mode="after") + def _validate_model(self): + """Validate the rate limit configuration based on its type.""" + if self.type == RateLimitType.SlidingWindow: + self._validate_sliding_window_config() + return self diff --git a/packages/graphrag-llm/graphrag_llm/config/retry_config.py b/packages/graphrag-llm/graphrag_llm/config/retry_config.py new file mode 100644 index 0000000000..01042da70b --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/config/retry_config.py @@ -0,0 +1,69 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Retry configuration.""" + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from graphrag_llm.config.types import RetryType + + +class RetryConfig(BaseModel): + """Configuration for retry behavior.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom Retry implementations.""" + + type: str = Field( + default=RetryType.ExponentialBackoff, + description="The type of retry strategy to use. [exponential_backoff, immediate] (default: exponential_backoff).", + ) + + max_retries: int | None = Field( + default=None, + description="The maximum number of retry attempts.", + ) + + base_delay: float | None = Field( + default=None, + description="The base delay in seconds for exponential backoff.", + ) + + jitter: bool | None = Field( + default=None, + description="Whether to apply jitter to the delay intervals in exponential backoff.", + ) + + max_delay: float | None = Field( + default=None, + description="The maximum delay in seconds between retries.", + ) + + def _validate_exponential_backoff_config(self) -> None: + """Validate Exponential Backoff retry configuration.""" + if self.max_retries is not None and self.max_retries <= 1: + msg = "max_retries must be greater than 1 for Exponential Backoff retry." + raise ValueError(msg) + + if self.base_delay is not None and self.base_delay <= 1.0: + msg = "base_delay must be greater than 1.0 for Exponential Backoff retry." + raise ValueError(msg) + + if self.max_delay is not None and self.max_delay <= 1: + msg = "max_delay must be greater than 1 for Exponential Backoff retry." + raise ValueError(msg) + + def _validate_immediate_config(self) -> None: + """Validate Immediate retry configuration.""" + if self.max_retries is not None and self.max_retries <= 1: + msg = "max_retries must be greater than 1 for Immediate retry." + raise ValueError(msg) + + @model_validator(mode="after") + def _validate_model(self): + """Validate the retry configuration based on its type.""" + if self.type == RetryType.ExponentialBackoff: + self._validate_exponential_backoff_config() + elif self.type == RetryType.Immediate: + self._validate_immediate_config() + return self diff --git a/packages/graphrag-llm/graphrag_llm/config/template_engine_config.py b/packages/graphrag-llm/graphrag_llm/config/template_engine_config.py new file mode 100644 index 0000000000..02d20acd33 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/config/template_engine_config.py @@ -0,0 +1,69 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Template engine configuration.""" + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from graphrag_llm.config.types import ( + TemplateEngineType, + TemplateManagerType, +) + + +class TemplateEngineConfig(BaseModel): + """Configuration for the template engine.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom metrics implementations.""" + + type: str = Field( + default=TemplateEngineType.Jinja, + description="The template engine to use. [jinja]", + ) + + template_manager: str = Field( + default=TemplateManagerType.File, + description="The template manager to use. [file, memory] (default: file)", + ) + + base_dir: str | None = Field( + default=None, + description="The base directory for file-based template managers.", + ) + + template_extension: str | None = Field( + default=None, + description="The file extension for locating templates in file-based template managers.", + ) + + encoding: str | None = Field( + default=None, + description="The file encoding for reading templates in file-based template managers.", + ) + + def _validate_file_template_manager_config(self) -> None: + """Validate parameters for file-based template managers.""" + if self.base_dir is not None and self.base_dir.strip() == "": + msg = "base_dir must be specified for file-based template managers." + raise ValueError(msg) + + if ( + self.template_extension is not None + and self.template_extension.strip() == "" + ): + msg = "template_extension cannot be an empty string for file-based template managers." + raise ValueError(msg) + + if ( + self.template_extension is not None + and not self.template_extension.startswith(".") + ): + self.template_extension = f".{self.template_extension}" + + @model_validator(mode="after") + def _validate_model(self): + """Validate the template engine configuration based on its type.""" + if self.template_manager == TemplateManagerType.File: + self._validate_file_template_manager_config() + return self diff --git a/packages/graphrag-llm/graphrag_llm/config/tokenizer_config.py b/packages/graphrag-llm/graphrag_llm/config/tokenizer_config.py new file mode 100644 index 0000000000..b7e6545755 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/config/tokenizer_config.py @@ -0,0 +1,51 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Tokenizer model configuration.""" + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from graphrag_llm.config.types import TokenizerType + + +class TokenizerConfig(BaseModel): + """Configuration for a tokenizer.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom LLM provider implementations.""" + + type: str = Field( + default=TokenizerType.LiteLLM, + description="The type of tokenizer to use. [litellm] (default: litellm).", + ) + + model_id: str | None = Field( + default=None, + description="The identifier for the tokenizer model. Example: openai/gpt-4o. Used by the litellm tokenizer.", + ) + + encoding_name: str | None = Field( + default=None, + description="The encoding name for the tokenizer. Example: gpt-4o.", + ) + + def _validate_litellm_config(self) -> None: + """Validate LiteLLM tokenizer configuration.""" + if self.model_id is None or self.model_id.strip() == "": + msg = "model_id must be specified for LiteLLM tokenizer." + raise ValueError(msg) + + def _validate_tiktoken_config(self) -> None: + """Validate TikToken tokenizer configuration.""" + if self.encoding_name is None or self.encoding_name.strip() == "": + msg = "encoding_name must be specified for TikToken tokenizer." + raise ValueError(msg) + + @model_validator(mode="after") + def _validate_model(self): + """Validate the tokenizer configuration based on its type.""" + if self.type == TokenizerType.LiteLLM: + self._validate_litellm_config() + elif self.type == TokenizerType.Tiktoken: + self._validate_tiktoken_config() + return self diff --git a/packages/graphrag-llm/graphrag_llm/config/types.py b/packages/graphrag-llm/graphrag_llm/config/types.py new file mode 100644 index 0000000000..320e8765fb --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/config/types.py @@ -0,0 +1,72 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + + +"""GraphRAG LLM configuration types.""" + +from enum import StrEnum + + +class LLMProviderType(StrEnum): + """Enum for LLM provider types.""" + + LiteLLM = "litellm" + MockLLM = "mock" + + +class AuthMethod(StrEnum): + """Enum for authentication methods.""" + + ApiKey = "api_key" + AzureManagedIdentity = "azure_managed_identity" + + +class MetricsProcessorType(StrEnum): + """Enum for built-in MetricsProcessor types.""" + + Default = "default" + + +class MetricsWriterType(StrEnum): + """Enum for built-in MetricsWriter types.""" + + Log = "log" + File = "file" + + +class MetricsStoreType(StrEnum): + """Enum for built-in MetricsStore types.""" + + Memory = "memory" + + +class RateLimitType(StrEnum): + """Enum for built-in RateLimit types.""" + + SlidingWindow = "sliding_window" + + +class RetryType(StrEnum): + """Enum for built-in Retry types.""" + + ExponentialBackoff = "exponential_backoff" + Immediate = "immediate" + + +class TemplateEngineType(StrEnum): + """Enum for built-in TemplateEngine types.""" + + Jinja = "jinja" + + +class TemplateManagerType(StrEnum): + """Enum for built-in TemplateEngine types.""" + + File = "file" + + +class TokenizerType(StrEnum): + """Enum for tokenizer types.""" + + LiteLLM = "litellm" + Tiktoken = "tiktoken" diff --git a/packages/graphrag-llm/graphrag_llm/embedding/__init__.py b/packages/graphrag-llm/graphrag_llm/embedding/__init__.py new file mode 100644 index 0000000000..1fb7352ea1 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/embedding/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""LLMEmbedding module for graphrag_llm.""" + +from graphrag_llm.embedding.embedding import LLMEmbedding +from graphrag_llm.embedding.embedding_factory import ( + create_embedding, + register_embedding, +) + +__all__ = [ + "LLMEmbedding", + "create_embedding", + "register_embedding", +] diff --git a/packages/graphrag-llm/graphrag_llm/embedding/embedding.py b/packages/graphrag-llm/graphrag_llm/embedding/embedding.py new file mode 100644 index 0000000000..b97cc3e3cf --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/embedding/embedding.py @@ -0,0 +1,191 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Completion Abstract Base Class.""" + +from abc import ABC, abstractmethod +from contextlib import contextmanager +from typing import TYPE_CHECKING, Any, Unpack + +from graphrag_llm.threading.embedding_thread_runner import embedding_thread_runner + +if TYPE_CHECKING: + from collections.abc import Iterator + + from graphrag_cache import Cache, CacheKeyCreator + + from graphrag_llm.config import ModelConfig + from graphrag_llm.metrics import MetricsProcessor, MetricsStore + from graphrag_llm.rate_limit import RateLimiter + from graphrag_llm.retry import Retry + from graphrag_llm.threading.embedding_thread_runner import ( + ThreadedLLMEmbeddingFunction, + ThreadedLLMEmbeddingResponseHandler, + ) + from graphrag_llm.tokenizer import Tokenizer + from graphrag_llm.types import LLMEmbeddingArgs, LLMEmbeddingResponse + + +class LLMEmbedding(ABC): + """Abstract base class for language model embeddings.""" + + @abstractmethod + def __init__( + self, + *, + model_id: str, + model_config: "ModelConfig", + tokenizer: "Tokenizer", + metrics_store: "MetricsStore", + metrics_processor: "MetricsProcessor | None" = None, + rate_limiter: "RateLimiter | None" = None, + retrier: "Retry | None" = None, + cache: "Cache | None" = None, + cache_key_creator: "CacheKeyCreator", + **kwargs: Any, + ): + """Initialize the LLMEmbedding. + + Args + ---- + model_id: str + The model ID, e.g., "openai/gpt-4o". + model_config: ModelConfig + The configuration for the language model. + tokenizer: Tokenizer + The tokenizer to use. + metrics_store: MetricsStore | None (default=None) + The metrics store to use. + metrics_processor: MetricsProcessor | None (default: None) + The metrics processor to use. + rate_limiter: RateLimiter | None (default=None) + The rate limiter to use. + retrier: Retry | None (default=None) + The retry strategy to use. + cache: Cache | None (default=None) + Optional cache for embeddings. + cache_key_creator: CacheKeyCreator | None (default=None) + Optional cache key creator function. + (dict[str, Any]) -> str + **kwargs: Any + Additional keyword arguments. + """ + raise NotImplementedError + + @abstractmethod + def embedding( + self, /, **kwargs: Unpack["LLMEmbeddingArgs"] + ) -> "LLMEmbeddingResponse": + """Sync embedding method.""" + raise NotImplementedError + + @abstractmethod + async def embedding_async( + self, /, **kwargs: Unpack["LLMEmbeddingArgs"] + ) -> "LLMEmbeddingResponse": + """Async embedding method.""" + raise NotImplementedError + + @contextmanager + def embedding_thread_pool( + self, + *, + response_handler: "ThreadedLLMEmbeddingResponseHandler", + concurrency: int, + queue_limit: int = 0, + ) -> "Iterator[ThreadedLLMEmbeddingFunction]": + """Run an embedding thread pool. + + Args + ---- + response_handler: ThreadedLLMEmbeddingResponseHandler + The callback function to handle embedding responses. + (request_id, response|exception) -> Awaitable[None] | None + concurrency: int + The number of threads to spin up in a thread pool. + queue_limit: int (default=0) + The maximum number of items allowed in the input queue. + 0 means unlimited. + Set this to a value to create backpressure on the caller. + + Yields + ------ + ThreadedLLMEmbeddingFunction: + A function that can be used to submit embedding requests to the thread pool. + (input, request_id, **kwargs) -> None + + The thread pool will process the requests and invoke the provided callback + with the responses. + + same signature as LLMEmbeddingFunction but requires a `request_id` parameter + to identify the request and does not return anything. + + """ + with embedding_thread_runner( + embedding=self.embedding, + response_handler=response_handler, + concurrency=concurrency, + queue_limit=queue_limit, + metrics_store=self.metrics_store, + ) as embedding: + yield embedding + + def embedding_batch( + self, + embedding_requests: list["LLMEmbeddingArgs"], + *, + concurrency: int, + queue_limit: int = 0, + ) -> list["LLMEmbeddingResponse | Exception"]: + """Process a batch of embedding requests using a thread pool. + + Args + ---- + embedding_requests: list[LLMEmbeddingArgs] + A list of embedding request arguments to process in parallel. + batch_size: int + The number of inputs to process in each batch. + concurrency: int + The number of threads to spin up in a thread pool. + queue_limit: int (default=0) + The maximum number of items allowed in the input queue. + 0 means unlimited. + Set this to a value to create backpressure on the caller. + + Returns + ------- + list[LLMEmbeddingResponse | Exception] + A list of embedding responses or exceptions for each input. + """ + results: list[LLMEmbeddingResponse | Exception] = [None] * len( + embedding_requests + ) # type: ignore + + def handle_response( + request_id: str, + response: "LLMEmbeddingResponse | Exception", + ) -> None: + index = int(request_id) + results[index] = response + + with self.embedding_thread_pool( + response_handler=handle_response, + concurrency=concurrency, + queue_limit=queue_limit, + ) as embedding: + for idx, embedding_request in enumerate(embedding_requests): + embedding(request_id=str(idx), **embedding_request) + + return results + + @property + @abstractmethod + def metrics_store(self) -> "MetricsStore": + """Metrics store.""" + raise NotImplementedError + + @property + @abstractmethod + def tokenizer(self) -> "Tokenizer": + """Tokenizer.""" + raise NotImplementedError diff --git a/packages/graphrag-llm/graphrag_llm/embedding/embedding_factory.py b/packages/graphrag-llm/graphrag_llm/embedding/embedding_factory.py new file mode 100644 index 0000000000..44592991b3 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/embedding/embedding_factory.py @@ -0,0 +1,150 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Embedding factory.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +from graphrag_common.factory import Factory + +from graphrag_llm.cache import create_cache_key +from graphrag_llm.config.tokenizer_config import TokenizerConfig +from graphrag_llm.config.types import LLMProviderType +from graphrag_llm.metrics.noop_metrics_store import NoopMetricsStore +from graphrag_llm.tokenizer.tokenizer_factory import create_tokenizer + +if TYPE_CHECKING: + from graphrag_cache import Cache, CacheKeyCreator + from graphrag_common.factory import ServiceScope + + from graphrag_llm.config.model_config import ModelConfig + from graphrag_llm.embedding.embedding import LLMEmbedding + from graphrag_llm.metrics import MetricsProcessor, MetricsStore + from graphrag_llm.rate_limit import RateLimiter + from graphrag_llm.retry import Retry + from graphrag_llm.tokenizer import Tokenizer + + +class EmbeddingFactory(Factory["LLMEmbedding"]): + """Factory for creating Embedding instances.""" + + +embedding_factory = EmbeddingFactory() + + +def register_embedding( + embedding_type: str, + embedding_initializer: Callable[..., "LLMEmbedding"], + scope: "ServiceScope" = "transient", +) -> None: + """Register a custom completion implementation. + + Args + ---- + embedding_type: str + The embedding id to register. + embedding_initializer: Callable[..., LLMEmbedding] + The embedding initializer to register. + scope: ServiceScope (default: "transient") + The service scope for the embedding. + """ + embedding_factory.register(embedding_type, embedding_initializer, scope) + + +def create_embedding( + model_config: "ModelConfig", + *, + cache: "Cache | None" = None, + cache_key_creator: "CacheKeyCreator | None" = None, + tokenizer: "Tokenizer | None" = None, +) -> "LLMEmbedding": + """Create an Embedding instance based on the model configuration. + + Args + ---- + model_config: ModelConfig + The configuration for the model. + cache: Cache | None (default: None) + An optional cache instance. + cache_key_creator: CacheKeyCreator | None (default: create_cache_key) + An optional cache key creator function. + tokenizer: Tokenizer | None (default: litellm) + An optional tokenizer instance. + + Returns + ------- + LLMEmbedding: + An instance of an Embedding subclass. + """ + cache_key_creator = cache_key_creator or create_cache_key + model_id = f"{model_config.model_provider}/{model_config.model}" + strategy = model_config.type + extra: dict[str, Any] = model_config.model_extra or {} + + if strategy not in embedding_factory: + match strategy: + case LLMProviderType.LiteLLM: + from graphrag_llm.embedding.lite_llm_embedding import ( + LiteLLMEmbedding, + ) + + register_embedding( + embedding_type=LLMProviderType.LiteLLM, + embedding_initializer=LiteLLMEmbedding, + scope="singleton", + ) + case LLMProviderType.MockLLM: + from graphrag_llm.embedding.mock_llm_embedding import MockLLMEmbedding + + register_embedding( + embedding_type=LLMProviderType.MockLLM, + embedding_initializer=MockLLMEmbedding, + ) + case _: + msg = f"ModelConfig.type '{strategy}' is not registered in the CompletionFactory. Registered strategies: {', '.join(embedding_factory.keys())}" + raise ValueError(msg) + + tokenizer = tokenizer or create_tokenizer(TokenizerConfig(model_id=model_id)) + + rate_limiter: RateLimiter | None = None + if model_config.rate_limit: + from graphrag_llm.rate_limit.rate_limit_factory import create_rate_limiter + + rate_limiter = create_rate_limiter(rate_limit_config=model_config.rate_limit) + + retrier: Retry | None = None + if model_config.retry: + from graphrag_llm.retry.retry_factory import create_retry + + retrier = create_retry(retry_config=model_config.retry) + + metrics_store: MetricsStore = NoopMetricsStore() + metrics_processor: MetricsProcessor | None = None + if model_config.metrics: + from graphrag_llm.metrics import ( + create_metrics_processor, + create_metrics_store, + ) + + metrics_store = create_metrics_store( + config=model_config.metrics, + id=model_id, + ) + metrics_processor = create_metrics_processor(model_config.metrics) + + return embedding_factory.create( + strategy=strategy, + init_args={ + **extra, + "model_id": model_id, + "model_config": model_config, + "tokenizer": tokenizer, + "metrics_store": metrics_store, + "metrics_processor": metrics_processor, + "rate_limiter": rate_limiter, + "retrier": retrier, + "cache": cache, + "cache_key_creator": cache_key_creator, + }, + ) diff --git a/packages/graphrag-llm/graphrag_llm/embedding/lite_llm_embedding.py b/packages/graphrag-llm/graphrag_llm/embedding/lite_llm_embedding.py new file mode 100644 index 0000000000..18e39a4ea8 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/embedding/lite_llm_embedding.py @@ -0,0 +1,198 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""LLMEmbedding based on litellm.""" + +from typing import TYPE_CHECKING, Any, Unpack + +import litellm +from azure.identity import DefaultAzureCredential, get_bearer_token_provider + +from graphrag_llm.config.types import AuthMethod +from graphrag_llm.embedding.embedding import LLMEmbedding +from graphrag_llm.middleware import with_middleware_pipeline +from graphrag_llm.types import LLMEmbeddingResponse + +if TYPE_CHECKING: + from graphrag_cache import Cache, CacheKeyCreator + + from graphrag_llm.config import ModelConfig + from graphrag_llm.metrics import MetricsProcessor, MetricsStore + from graphrag_llm.rate_limit import RateLimiter + from graphrag_llm.retry import Retry + from graphrag_llm.tokenizer import Tokenizer + from graphrag_llm.types import ( + AsyncLLMEmbeddingFunction, + LLMEmbeddingArgs, + LLMEmbeddingFunction, + Metrics, + ) + +litellm.suppress_debug_info = True + + +class LiteLLMEmbedding(LLMEmbedding): + """LLMEmbedding based on litellm.""" + + _model_config: "ModelConfig" + _model_id: str + _track_metrics: bool = False + _metrics_store: "MetricsStore" + _metrics_processor: "MetricsProcessor | None" + _cache: "Cache | None" + _cache_key_creator: "CacheKeyCreator" + _tokenizer: "Tokenizer" + _rate_limiter: "RateLimiter | None" + _retrier: "Retry | None" + + def __init__( + self, + *, + model_id: str, + model_config: "ModelConfig", + tokenizer: "Tokenizer", + metrics_store: "MetricsStore", + metrics_processor: "MetricsProcessor | None" = None, + rate_limiter: "RateLimiter | None" = None, + retrier: "Retry | None" = None, + cache: "Cache | None" = None, + cache_key_creator: "CacheKeyCreator", + azure_cognitive_services_audience: str = "https://cognitiveservices.azure.com/.default", + drop_unsupported_params: bool = True, + **kwargs: Any, + ): + """Initialize LiteLLMEmbedding. + + Args + ---- + model_id: str + The LiteLLM model ID, e.g., "openai/gpt-4o" + model_config: ModelConfig + The configuration for the model. + tokenizer: Tokenizer + The tokenizer to use. + metrics_store: MetricsStore | None (default: None) + The metrics store to use. + metrics_processor: MetricsProcessor | None (default: None) + The metrics processor to use. + cache: Cache | None (default: None) + An optional cache instance. + cache_key_prefix: str | None (default: "chat") + The cache key prefix. Required if cache is provided. + rate_limiter: RateLimiter | None (default: None) + The rate limiter to use. + retrier: Retry | None (default: None) + The retry strategy to use. + azure_cognitive_services_audience: str (default: "https://cognitiveservices.azure.com/.default") + The audience for Azure Cognitive Services when using Managed Identity. + drop_unsupported_params: bool (default: True) + Whether to drop unsupported parameters for the model provider. + """ + self._model_id = model_id + self._model_config = model_config + self._tokenizer = tokenizer + self._metrics_store = metrics_store + self._metrics_processor = metrics_processor + self._track_metrics = metrics_processor is not None + self._cache = cache + self._cache_key_creator = cache_key_creator + self._rate_limiter = rate_limiter + self._retrier = retrier + + self._embedding, self._embedding_async = _create_base_embeddings( + model_config=model_config, + drop_unsupported_params=drop_unsupported_params, + azure_cognitive_services_audience=azure_cognitive_services_audience, + ) + + self._embedding, self._embedding_async = with_middleware_pipeline( + model_config=self._model_config, + model_fn=self._embedding, + async_model_fn=self._embedding_async, + request_type="embedding", + cache=self._cache, + cache_key_creator=self._cache_key_creator, + tokenizer=self._tokenizer, + metrics_processor=self._metrics_processor, + rate_limiter=self._rate_limiter, + retrier=self._retrier, + ) + + def embedding( + self, /, **kwargs: Unpack["LLMEmbeddingArgs"] + ) -> "LLMEmbeddingResponse": + """Sync embedding method.""" + request_metrics: Metrics | None = kwargs.pop("metrics", None) or {} + if not self._track_metrics: + request_metrics = None + + try: + return self._embedding(metrics=request_metrics, **kwargs) + finally: + if request_metrics: + self._metrics_store.update_metrics(metrics=request_metrics) + + async def embedding_async( + self, /, **kwargs: Unpack["LLMEmbeddingArgs"] + ) -> "LLMEmbeddingResponse": + """Async embedding method.""" + request_metrics: Metrics | None = kwargs.pop("metrics", None) or {} + if not self._track_metrics: + request_metrics = None + + try: + return await self._embedding_async(metrics=request_metrics, **kwargs) + finally: + if request_metrics: + self._metrics_store.update_metrics(metrics=request_metrics) + + @property + def metrics_store(self) -> "MetricsStore": + """Get metrics store.""" + return self._metrics_store + + @property + def tokenizer(self) -> "Tokenizer": + """Get tokenizer.""" + return self._tokenizer + + +def _create_base_embeddings( + *, + model_config: "ModelConfig", + drop_unsupported_params: bool, + azure_cognitive_services_audience: str, +) -> tuple["LLMEmbeddingFunction", "AsyncLLMEmbeddingFunction"]: + """Create base embedding functions.""" + model_provider = model_config.model_provider + model = model_config.azure_deployment_name or model_config.model + + base_args: dict[str, Any] = { + "drop_params": drop_unsupported_params, + "model": f"{model_provider}/{model}", + "api_key": model_config.api_key, + "api_base": model_config.api_base, + "api_version": model_config.api_version, + **model_config.call_args, + } + + if model_config.auth_method == AuthMethod.AzureManagedIdentity: + base_args["azure_ad_token_provider"] = get_bearer_token_provider( + DefaultAzureCredential(), azure_cognitive_services_audience + ) + + def _base_embedding(**kwargs: Any) -> LLMEmbeddingResponse: + kwargs.pop("metrics", None) # Remove metrics if present + new_args: dict[str, Any] = {**base_args, **kwargs} + + response = litellm.embedding(**new_args) + return LLMEmbeddingResponse(**response.model_dump()) + + async def _base_embedding_async(**kwargs: Any) -> LLMEmbeddingResponse: + kwargs.pop("metrics", None) # Remove metrics if present + new_args: dict[str, Any] = {**base_args, **kwargs} + + response = await litellm.aembedding(**new_args) + return LLMEmbeddingResponse(**response.model_dump()) + + return _base_embedding, _base_embedding_async diff --git a/packages/graphrag-llm/graphrag_llm/embedding/mock_llm_embedding.py b/packages/graphrag-llm/graphrag_llm/embedding/mock_llm_embedding.py new file mode 100644 index 0000000000..3b9649038c --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/embedding/mock_llm_embedding.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""MockLLMEmbedding.""" + +from typing import TYPE_CHECKING, Any, Unpack + +import litellm + +from graphrag_llm.embedding.embedding import LLMEmbedding +from graphrag_llm.utils import create_embedding_response + +if TYPE_CHECKING: + from graphrag_llm.config import ModelConfig + from graphrag_llm.metrics import MetricsStore + from graphrag_llm.tokenizer import Tokenizer + from graphrag_llm.types import ( + LLMEmbeddingArgs, + LLMEmbeddingResponse, + ) + +litellm.suppress_debug_info = True + + +class MockLLMEmbedding(LLMEmbedding): + """MockLLMEmbedding.""" + + _metrics_store: "MetricsStore" + _tokenizer: "Tokenizer" + _mock_responses: list[float] + _mock_index: int = 0 + + def __init__( + self, + *, + model_config: "ModelConfig", + tokenizer: "Tokenizer", + metrics_store: "MetricsStore", + **kwargs: Any, + ): + """Initialize MockLLMEmbedding.""" + self._tokenizer = tokenizer + self._metrics_store = metrics_store + + mock_responses = model_config.mock_responses + if not isinstance(mock_responses, list) or len(mock_responses) == 0: + msg = "ModelConfig.mock_responses must be a non-empty list of embedding responses." + raise ValueError(msg) + + if not all(isinstance(resp, float) for resp in mock_responses): + msg = "Each item in ModelConfig.mock_responses must be a float." + raise ValueError(msg) + + self._mock_responses = mock_responses # type: ignore + + def embedding( + self, /, **kwargs: Unpack["LLMEmbeddingArgs"] + ) -> "LLMEmbeddingResponse": + """Sync embedding method.""" + input = kwargs.get("input") + response = create_embedding_response( + self._mock_responses, batch_size=len(input) + ) + self._mock_index += 1 + return response + + async def embedding_async( + self, /, **kwargs: Unpack["LLMEmbeddingArgs"] + ) -> "LLMEmbeddingResponse": + """Async embedding method.""" + return self.embedding(**kwargs) + + @property + def metrics_store(self) -> "MetricsStore": + """Get metrics store.""" + return self._metrics_store + + @property + def tokenizer(self) -> "Tokenizer": + """Get tokenizer.""" + return self._tokenizer diff --git a/packages/graphrag-llm/graphrag_llm/metrics/__init__.py b/packages/graphrag-llm/graphrag_llm/metrics/__init__.py new file mode 100644 index 0000000000..e039bf8acf --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/__init__.py @@ -0,0 +1,34 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Metrics module for graphrag-llm.""" + +from graphrag_llm.metrics.metrics_aggregator import metrics_aggregator +from graphrag_llm.metrics.metrics_processor import MetricsProcessor +from graphrag_llm.metrics.metrics_processor_factory import ( + create_metrics_processor, + register_metrics_processor, +) +from graphrag_llm.metrics.metrics_store import MetricsStore +from graphrag_llm.metrics.metrics_store_factory import ( + create_metrics_store, + register_metrics_store, +) +from graphrag_llm.metrics.metrics_writer import MetricsWriter +from graphrag_llm.metrics.metrics_writer_factory import ( + create_metrics_writer, + register_metrics_writer, +) + +__all__ = [ + "MetricsProcessor", + "MetricsStore", + "MetricsWriter", + "create_metrics_processor", + "create_metrics_store", + "create_metrics_writer", + "metrics_aggregator", + "register_metrics_processor", + "register_metrics_store", + "register_metrics_writer", +] diff --git a/packages/graphrag-llm/graphrag_llm/metrics/default_metrics_processor.py b/packages/graphrag-llm/graphrag_llm/metrics/default_metrics_processor.py new file mode 100644 index 0000000000..7249e701a1 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/default_metrics_processor.py @@ -0,0 +1,130 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Default Metrics Processor.""" + +from typing import TYPE_CHECKING, Any + +from graphrag_llm.metrics.metrics_processor import MetricsProcessor +from graphrag_llm.model_cost_registry import model_cost_registry +from graphrag_llm.types import LLMCompletionResponse, LLMEmbeddingResponse + +if TYPE_CHECKING: + from collections.abc import AsyncIterator, Iterator + + from graphrag_llm.config import ModelConfig + from graphrag_llm.types import ( + LLMCompletionChunk, + Metrics, + ) + + +class DefaultMetricsProcessor(MetricsProcessor): + """Default metrics processor that does nothing.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize DefaultMetricsProcessor.""" + + def process_metrics( + self, + *, + model_config: "ModelConfig", + metrics: "Metrics", + input_args: dict[str, Any], + response: "LLMCompletionResponse \ + | Iterator[LLMCompletionChunk] \ + | AsyncIterator[LLMCompletionChunk] \ + | LLMEmbeddingResponse", + ) -> None: + """Process metrics.""" + self._process_metrics_common( + model_config=model_config, + metrics=metrics, + input_args=input_args, + response=response, + ) + + def _process_metrics_common( + self, + *, + model_config: "ModelConfig", + metrics: "Metrics", + input_args: dict[str, Any], + response: "LLMCompletionResponse \ + | Iterator[LLMCompletionChunk] \ + | AsyncIterator[LLMCompletionChunk] \ + | LLMEmbeddingResponse", + ) -> None: + if isinstance(response, LLMCompletionResponse): + self._process_lm_chat_completion( + model_config=model_config, + metrics=metrics, + input_args=input_args, + response=response, + ) + elif isinstance(response, LLMEmbeddingResponse): + self._process_lm_embedding_response( + model_config=model_config, + metrics=metrics, + input_args=input_args, + response=response, + ) + + def _process_lm_chat_completion( + self, + model_config: "ModelConfig", + metrics: "Metrics", + input_args: dict[str, Any], + response: "LLMCompletionResponse", + ) -> None: + """Process LMChatCompletion metrics.""" + prompt_tokens = response.usage.prompt_tokens if response.usage else 0 + completion_tokens = response.usage.completion_tokens if response.usage else 0 + total_tokens = prompt_tokens + completion_tokens + + if total_tokens > 0: + metrics["responses_with_tokens"] = 1 + metrics["prompt_tokens"] = prompt_tokens + metrics["completion_tokens"] = completion_tokens + metrics["total_tokens"] = total_tokens + + model_id = f"{model_config.model_provider}/{model_config.model}" + model_costs = model_cost_registry.get_model_costs(model_id) + + if not model_costs: + return + + input_cost = prompt_tokens * model_costs["input_cost_per_token"] + output_cost = completion_tokens * model_costs["output_cost_per_token"] + total_cost = input_cost + output_cost + + metrics["responses_with_cost"] = 1 + metrics["input_cost"] = input_cost + metrics["output_cost"] = output_cost + metrics["total_cost"] = total_cost + + def _process_lm_embedding_response( + self, + model_config: "ModelConfig", + metrics: "Metrics", + input_args: dict[str, Any], + response: "LLMEmbeddingResponse", + ) -> None: + """Process LLMEmbeddingResponse metrics.""" + prompt_tokens = response.usage.prompt_tokens if response.usage else 0 + + if prompt_tokens > 0: + metrics["responses_with_tokens"] = 1 + metrics["prompt_tokens"] = prompt_tokens + metrics["total_tokens"] = prompt_tokens + + model_id = f"{model_config.model_provider}/{model_config.model}" + model_costs = model_cost_registry.get_model_costs(model_id) + + if not model_costs: + return + + input_cost = prompt_tokens * model_costs["input_cost_per_token"] + metrics["responses_with_cost"] = 1 + metrics["input_cost"] = input_cost + metrics["total_cost"] = input_cost diff --git a/packages/graphrag-llm/graphrag_llm/metrics/file_metrics_writer.py b/packages/graphrag-llm/graphrag_llm/metrics/file_metrics_writer.py new file mode 100644 index 0000000000..c80f345279 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/file_metrics_writer.py @@ -0,0 +1,37 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""File metrics writer implementation.""" + +import json +from collections.abc import Callable +from datetime import datetime, timezone +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from graphrag_llm.metrics.metrics_writer import MetricsWriter + +if TYPE_CHECKING: + from graphrag_llm.types import Metrics + + +class FileMetricsWriter(MetricsWriter): + """File metrics writer implementation.""" + + _log_method: Callable[..., None] | None = None + _base_dir: Path + _file_path: Path + + def __init__(self, *, base_dir: str | None = None, **kwargs: Any) -> None: + """Initialize FileMetricsWriter.""" + self._base_dir = Path(base_dir or Path.cwd()).resolve() + now = datetime.now(timezone.utc).astimezone().strftime("%Y%m%d_%H%M%S") + self._file_path = self._base_dir / f"{now}.jsonl" + + self._base_dir.mkdir(parents=True, exist_ok=True) + + def write_metrics(self, *, id: str, metrics: "Metrics") -> None: + """Write the given metrics.""" + record = json.dumps({"id": id, "metrics": metrics}) + with self._file_path.open("a", encoding="utf-8") as f: + f.write(f"{record}\n") diff --git a/packages/graphrag-llm/graphrag_llm/metrics/log_metrics_writer.py b/packages/graphrag-llm/graphrag_llm/metrics/log_metrics_writer.py new file mode 100644 index 0000000000..f09b878a12 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/log_metrics_writer.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Log metrics writer implementation.""" + +import json +import logging +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +from graphrag_llm.metrics.metrics_writer import MetricsWriter + +if TYPE_CHECKING: + from graphrag_llm.types import Metrics + +logger = logging.getLogger(__name__) + +_log_methods = { + logging.DEBUG: logger.debug, + logging.INFO: logger.info, + logging.WARNING: logger.warning, + logging.ERROR: logger.error, + logging.CRITICAL: logger.critical, +} + + +class LogMetricsWriter(MetricsWriter): + """Log metrics writer implementation.""" + + _log_method: Callable[..., None] = _log_methods[logging.INFO] + + def __init__(self, *, log_level: int | None = None, **kwargs: Any) -> None: + """Initialize LogMetricsWriter.""" + if log_level and log_level in _log_methods: + self._log_method = _log_methods[log_level] + + def write_metrics(self, *, id: str, metrics: "Metrics") -> None: + """Write the given metrics.""" + self._log_method(f"Metrics for {id}: {json.dumps(metrics, indent=2)}") diff --git a/packages/graphrag-llm/graphrag_llm/metrics/memory_metrics_store.py b/packages/graphrag-llm/graphrag_llm/metrics/memory_metrics_store.py new file mode 100644 index 0000000000..c2456299e1 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/memory_metrics_store.py @@ -0,0 +1,112 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Default metrics store.""" + +import atexit +import threading +from typing import TYPE_CHECKING, Any + +from graphrag_llm.metrics.metrics_aggregator import metrics_aggregator +from graphrag_llm.metrics.metrics_store import MetricsStore + +if TYPE_CHECKING: + from graphrag_llm.metrics.metrics_writer import MetricsWriter + from graphrag_llm.types import Metrics + +_default_sort_order: list[str] = [ + "attempted_request_count", + "successful_response_count", + "failed_response_count", + "failure_rate", + "requests_with_retries", + "retries", + "retry_rate", + "compute_duration_seconds", + "compute_duration_per_response_seconds", + "runtime_duration_seconds", + "cached_responses", + "cache_hit_rate", + "streaming_responses", + "responses_with_tokens", + "prompt_tokens", + "completion_tokens", + "total_tokens", + "tokens_per_response", + "responses_with_cost", + "input_cost", + "output_cost", + "total_cost", + "cost_per_response", +] + + +class MemoryMetricsStore(MetricsStore): + """Store for metrics.""" + + _metrics_writer: "MetricsWriter | None" = None + _id: str + _sort_order: list[str] + _thread_lock: threading.Lock + _metrics: "Metrics" + + def __init__( + self, + *, + id: str, + metrics_writer: "MetricsWriter | None" = None, + sort_order: list[str] | None = None, + **kwargs: Any, + ) -> None: + """Initialize MemoryMetricsStore.""" + self._id = id + self._sort_order = sort_order or _default_sort_order + self._thread_lock = threading.Lock() + self._metrics = {} + + if metrics_writer: + self._metrics_writer = metrics_writer + atexit.register(self._on_exit_) + + def _on_exit_(self) -> None: + if self._metrics_writer: + self._metrics_writer.write_metrics(id=self._id, metrics=self.get_metrics()) + + @property + def id(self) -> str: + """Get the ID of the metrics store.""" + return self._id + + def update_metrics(self, *, metrics: "Metrics") -> None: + """Update the store with multiple metrics.""" + with self._thread_lock: + for name, value in metrics.items(): + if name in self._metrics: + self._metrics[name] += value + else: + self._metrics[name] = value + + def _sort_metrics(self) -> "Metrics": + """Sort metrics based on the predefined sort order.""" + sorted_metrics: Metrics = {} + for key in self._sort_order: + if key in self._metrics: + sorted_metrics[key] = self._metrics[key] + for key in self._metrics: + if key not in sorted_metrics: + sorted_metrics[key] = self._metrics[key] + return sorted_metrics + + def get_metrics(self) -> "Metrics": + """Get all metrics from the store.""" + metrics_aggregator.aggregate(self._metrics) + return self._sort_metrics() + + def clear_metrics(self) -> None: + """Clear all metrics from the store. + + Returns + ------- + None + """ + self._metrics = {} diff --git a/packages/graphrag-llm/graphrag_llm/metrics/metrics_aggregator.py b/packages/graphrag-llm/graphrag_llm/metrics/metrics_aggregator.py new file mode 100644 index 0000000000..53726e75c2 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/metrics_aggregator.py @@ -0,0 +1,142 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Metrics aggregator module.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, ClassVar + +from typing_extensions import Self + +if TYPE_CHECKING: + from graphrag_llm.types.types import Metrics + + +class MetricsAggregator: + """Metrics Aggregator.""" + + _instance: ClassVar["Self | None"] = None + _aggregate_functions: dict[str, Callable[["Metrics"], None]] + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """Create a new instance of MetricsAggregator if it does not exist.""" + if cls._instance is None: + cls._instance = super().__new__(cls, *args, **kwargs) + return cls._instance + + def __init__(self): + if not hasattr(self, "_initialized"): + self._initialized = True + self._aggregate_functions = {} + + def register(self, name: str, func: Callable[["Metrics"], None]) -> None: + """Register an aggregate function. + + Args + ---- + name: str + The name of the aggregate function. + func: Callable[[Metrics], None] + The aggregate function to register. It should take a Metrics + dictionary as input and return None, modifying the Metrics in place. + """ + self._aggregate_functions[name] = func + + def clear(self, name: str | None = None) -> None: + """Clear registered aggregate functions. + + Args + ---- + name: str | None + The name of the aggregate function to clear. If None, clears all + registered aggregate functions. + + """ + if name: + self._aggregate_functions.pop(name, None) + else: + self._aggregate_functions.clear() + + def aggregate(self, metrics: "Metrics") -> None: + """Aggregate metrics using registered aggregate functions. + + Args + ---- + metrics: Metrics + The metrics dictionary to aggregate. + """ + for func in self._aggregate_functions.values(): + func(metrics) + + +def _failure_rate(metrics: "Metrics") -> None: + """Calculate failure rate metric.""" + attempted = metrics.get("attempted_request_count", 0) + failed = metrics.get("failed_response_count", 0) + if attempted > 0: + metrics["failure_rate"] = failed / attempted + else: + metrics["failure_rate"] = 0.0 + + +def _retry_rate(metrics: "Metrics") -> None: + """Calculate failure rate metric.""" + attempted = metrics.get("attempted_request_count", 0) + retries = metrics.get("retries", 0) + if attempted > 0 and "retries" in metrics: + metrics["retry_rate"] = retries / (retries + attempted) + elif "retries" in metrics: + metrics["retry_rate"] = 0.0 + + +def _tokens_per_response(metrics: "Metrics") -> None: + """Calculate tokens per response metric.""" + responses = metrics.get("responses_with_tokens", 0) + total_tokens = metrics.get("total_tokens", 0) + if responses > 0: + metrics["tokens_per_response"] = total_tokens / responses + else: + metrics["tokens_per_response"] = 0.0 + + +def _cost_per_response(metrics: "Metrics") -> None: + """Calculate cost per response metric.""" + responses = metrics.get("responses_with_cost", 0) + total_cost = metrics.get("total_cost", 0) + if responses > 0: + metrics["cost_per_response"] = total_cost / responses + else: + metrics["cost_per_response"] = 0.0 + + +def _compute_duration_per_response(metrics: "Metrics") -> None: + """Calculate compute duration per response metric.""" + responses = metrics.get("successful_response_count", 0) + streaming_responses = metrics.get("streaming_responses", 0) + responses = responses - streaming_responses + compute_duration = metrics.get("compute_duration_seconds", 0) + if responses > 0: + metrics["compute_duration_per_response_seconds"] = compute_duration / responses + else: + metrics["compute_duration_per_response_seconds"] = 0.0 + + +def _cache_hit_rate(metrics: "Metrics") -> None: + """Calculate cache hit rate metric.""" + responses = metrics.get("successful_response_count", 0) + cached = metrics.get("cached_responses", 0) + if responses > 0: + metrics["cache_hit_rate"] = cached / responses + else: + metrics["cache_hit_rate"] = 0.0 + + +metrics_aggregator = MetricsAggregator() +metrics_aggregator.register("failure_rate", _failure_rate) +metrics_aggregator.register("retry_rate", _retry_rate) +metrics_aggregator.register("tokens_per_response", _tokens_per_response) +metrics_aggregator.register("cost_per_response", _cost_per_response) +metrics_aggregator.register( + "compute_duration_per_response", _compute_duration_per_response +) +metrics_aggregator.register("cache_hit_rate", _cache_hit_rate) diff --git a/packages/graphrag-llm/graphrag_llm/metrics/metrics_processor.py b/packages/graphrag-llm/graphrag_llm/metrics/metrics_processor.py new file mode 100644 index 0000000000..61893742db --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/metrics_processor.py @@ -0,0 +1,59 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Metrics processor abstract base class.""" + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import AsyncIterator, Iterator + + from graphrag_llm.config import ModelConfig + from graphrag_llm.types import ( + LLMCompletionChunk, + LLMCompletionResponse, + LLMEmbeddingResponse, + Metrics, + ) + + +class MetricsProcessor(ABC): + """Abstract base class for metrics processors.""" + + @abstractmethod + def __init__(self, **kwargs: Any): + """Initialize MetricsProcessor.""" + raise NotImplementedError + + @abstractmethod + def process_metrics( + self, + *, + model_config: "ModelConfig", + metrics: "Metrics", + input_args: dict[str, Any], + response: "LLMCompletionResponse \ + | Iterator[LLMCompletionChunk] \ + | AsyncIterator[LLMCompletionChunk] \ + | LLMEmbeddingResponse", + ) -> None: + """Process metrics. + + Update the metrics dictionary in place. + + Args + ---- + metrics: Metrics + The metrics to process. + input_args: dict[str, Any] + The input arguments passed to completion or embedding + used to generate the response. + response: LLMCompletionResponse | Iterator[LLMCompletionChunk] | LLMEmbeddingResponse + Either a completion or embedding response from the LLM. + + Returns + ------- + None + """ + raise NotImplementedError diff --git a/packages/graphrag-llm/graphrag_llm/metrics/metrics_processor_factory.py b/packages/graphrag-llm/graphrag_llm/metrics/metrics_processor_factory.py new file mode 100644 index 0000000000..74f0111a9c --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/metrics_processor_factory.py @@ -0,0 +1,79 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Metrics processor factory.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from graphrag_common.factory import Factory + +from graphrag_llm.config.types import MetricsProcessorType +from graphrag_llm.metrics.metrics_processor import MetricsProcessor + +if TYPE_CHECKING: + from graphrag_llm.config import MetricsConfig + + +class MetricsProcessorFactory(Factory[MetricsProcessor]): + """Factory for creating MetricsProcessor instances.""" + + +metrics_processor_factory = MetricsProcessorFactory() + + +def register_metrics_processor( + processor_type: str, + processor_initializer: Callable[..., MetricsProcessor], +) -> None: + """Register a custom metrics processor implementation. + + Args + ---- + processor_type: str + The metrics processor id to register. + processor_initializer: Callable[..., MetricsProcessor] + The metrics processor initializer to register. + """ + metrics_processor_factory.register(processor_type, processor_initializer) + + +def create_metrics_processor(metrics_config: "MetricsConfig") -> MetricsProcessor: + """Create a MetricsProcessor instance based on the configuration. + + Args + ---- + metrics_config: MetricsConfig + The configuration for the metrics processor. + + Returns + ------- + MetricsProcessor: + An instance of a MetricsProcessor subclass. + """ + strategy = metrics_config.type + init_args = metrics_config.model_dump() + + if strategy not in metrics_processor_factory: + match strategy: + case MetricsProcessorType.Default: + from graphrag_llm.metrics.default_metrics_processor import ( + DefaultMetricsProcessor, + ) + + metrics_processor_factory.register( + strategy=MetricsProcessorType.Default, + initializer=DefaultMetricsProcessor, + scope="singleton", + ) + case _: + msg = f"MetricsConfig.processor '{strategy}' is not registered in the MetricsProcessorFactory. Registered strategies: {', '.join(metrics_processor_factory.keys())}" + raise ValueError(msg) + + return metrics_processor_factory.create( + strategy=strategy, + init_args={ + **init_args, + "metrics_config": metrics_config, + }, + ) diff --git a/packages/graphrag-llm/graphrag_llm/metrics/metrics_store.py b/packages/graphrag-llm/graphrag_llm/metrics/metrics_store.py new file mode 100644 index 0000000000..a9933f203d --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/metrics_store.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Metrics Store.""" + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphrag_llm.metrics.metrics_writer import MetricsWriter + from graphrag_llm.types import Metrics + + +class MetricsStore(ABC): + """Abstract base class for metrics stores.""" + + @abstractmethod + def __init__( + self, + *, + id: str, + metrics_writer: "MetricsWriter | None" = None, + **kwargs: Any, + ) -> None: + """Initialize MetricsStore. + + Args + ---- + id: str + The ID of the metrics store. + One metric store is created per ID so a good + candidate is the model id (e.g., openai/gpt-4o). + That way one store tracks and aggregates the metrics + per model. + metrics_writer: MetricsWriter + The metrics writer to use for writing metrics. + + """ + raise NotImplementedError + + @property + @abstractmethod + def id(self) -> str: + """Get the ID of the metrics store.""" + raise NotImplementedError + + @abstractmethod + def update_metrics(self, *, metrics: "Metrics") -> None: + """Update the store with multiple metrics. + + Args + ---- + metrics: Metrics + The metrics to merge into the store. + + Returns + ------- + None + """ + raise NotImplementedError + + @abstractmethod + def get_metrics(self) -> "Metrics": + """Get all metrics from the store. + + Returns + ------- + Metrics: + All metrics stored in the store. + """ + raise NotImplementedError + + @abstractmethod + def clear_metrics(self) -> None: + """Clear all metrics from the store. + + Returns + ------- + None + """ + raise NotImplementedError diff --git a/packages/graphrag-llm/graphrag_llm/metrics/metrics_store_factory.py b/packages/graphrag-llm/graphrag_llm/metrics/metrics_store_factory.py new file mode 100644 index 0000000000..51e06fc0dc --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/metrics_store_factory.py @@ -0,0 +1,91 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Metrics store factory.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +from graphrag_common.factory import Factory + +from graphrag_llm.config.types import MetricsStoreType +from graphrag_llm.metrics.metrics_store import MetricsStore + +if TYPE_CHECKING: + from graphrag_common.factory import ServiceScope + + from graphrag_llm.config import MetricsConfig + from graphrag_llm.metrics.metrics_writer import MetricsWriter + + +class MetricsStoreFactory(Factory[MetricsStore]): + """Factory for creating MetricsProcessor instances.""" + + +metrics_store_factory = MetricsStoreFactory() + + +def register_metrics_store( + store_type: str, + store_initializer: Callable[..., MetricsStore], + scope: "ServiceScope" = "transient", +) -> None: + """Register a custom metrics store implementation. + + Args + ---- + store_type: str + The metrics store id to register. + store_initializer: Callable[..., MetricsStore] + The metrics store initializer to register. + """ + metrics_store_factory.register(store_type, store_initializer, scope) + + +def create_metrics_store(config: "MetricsConfig", id: str) -> MetricsStore: + """Create a MetricsStore instance based on the configuration. + + Args + ---- + config: MetricsConfig + The configuration for the metrics store. + id: str + The identifier for the metrics store. + Example: openai/gpt-4o + + Returns + ------- + MetricsStore: + An instance of a MetricsStore subclass. + """ + strategy = config.store + metrics_writer: MetricsWriter | None = None + if config.writer: + from graphrag_llm.metrics.metrics_writer_factory import create_metrics_writer + + metrics_writer = create_metrics_writer(config) + init_args: dict[str, Any] = config.model_dump() + + if strategy not in metrics_store_factory: + match strategy: + case MetricsStoreType.Memory: + from graphrag_llm.metrics.memory_metrics_store import MemoryMetricsStore + + register_metrics_store( + store_type=strategy, + store_initializer=MemoryMetricsStore, + scope="singleton", + ) + case _: + msg = f"MetricsConfig.store '{strategy}' is not registered in the MetricsStoreFactory. Registered strategies: {', '.join(metrics_store_factory.keys())}" + raise ValueError(msg) + + return metrics_store_factory.create( + strategy=strategy, + init_args={ + **init_args, + "id": id, + "metrics_config": config, + "metrics_writer": metrics_writer, + }, + ) diff --git a/packages/graphrag-llm/graphrag_llm/metrics/metrics_writer.py b/packages/graphrag-llm/graphrag_llm/metrics/metrics_writer.py new file mode 100644 index 0000000000..4e5df0a980 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/metrics_writer.py @@ -0,0 +1,32 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Metrics writer abstract base class.""" + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphrag_llm.types import Metrics + + +class MetricsWriter(ABC): + """Abstract base class for metrics writers.""" + + @abstractmethod + def __init__(self, **kwargs: Any) -> None: + """Initialize MetricsWriter.""" + raise NotImplementedError + + @abstractmethod + def write_metrics(self, *, id: str, metrics: "Metrics") -> None: + """Write the given metrics. + + Args + ---- + id : str + The identifier for the metrics. + metrics : Metrics + The metrics data to write. + """ + raise NotImplementedError diff --git a/packages/graphrag-llm/graphrag_llm/metrics/metrics_writer_factory.py b/packages/graphrag-llm/graphrag_llm/metrics/metrics_writer_factory.py new file mode 100644 index 0000000000..fac8c5957d --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/metrics_writer_factory.py @@ -0,0 +1,91 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + + +"""Metrics writer factory.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from graphrag_common.factory import Factory + +from graphrag_llm.config.types import MetricsWriterType +from graphrag_llm.metrics.metrics_writer import MetricsWriter + +if TYPE_CHECKING: + from graphrag_common.factory import ServiceScope + + from graphrag_llm.config import MetricsConfig + + +class MetricsWriterFactory(Factory[MetricsWriter]): + """Metrics writer factory.""" + + +metrics_writer_factory = MetricsWriterFactory() + + +def register_metrics_writer( + metrics_writer_type: str, + metrics_writer_initializer: Callable[..., MetricsWriter], + scope: "ServiceScope" = "transient", +) -> None: + """Register a custom metrics writer implementation. + + Args + ---- + metrics_writer_type: str + The metrics writer id to register. + metrics_writer_initializer: Callable[..., MetricsWriter] + The metrics writer initializer to register. + scope: ServiceScope (default: "transient") + The service scope for the metrics writer. + """ + metrics_writer_factory.register( + metrics_writer_type, metrics_writer_initializer, scope + ) + + +def create_metrics_writer(metrics_config: "MetricsConfig") -> MetricsWriter: + """Create a MetricsWriter instance based on the configuration. + + Args + ---- + metrics_config: MetricsConfig + The configuration for the metrics writer. + + Returns + ------- + MetricsWriter: + An instance of a MetricsWriter subclass. + """ + strategy = metrics_config.writer + if not strategy: + msg = "MetricsConfig.writer needs to be set to create a MetricsWriter." + raise ValueError(msg) + + init_args = metrics_config.model_dump() + + if strategy not in metrics_writer_factory: + match strategy: + case MetricsWriterType.Log: + from graphrag_llm.metrics.log_metrics_writer import LogMetricsWriter + + metrics_writer_factory.register( + strategy=MetricsWriterType.Log, + initializer=LogMetricsWriter, + scope="singleton", + ) + case MetricsWriterType.File: + from graphrag_llm.metrics.file_metrics_writer import FileMetricsWriter + + metrics_writer_factory.register( + strategy=MetricsWriterType.File, + initializer=FileMetricsWriter, + scope="singleton", + ) + case _: + msg = f"MetricsConfig.writer '{strategy}' is not registered in the MetricsWriterFactory. Registered strategies: {', '.join(metrics_writer_factory.keys())}" + raise ValueError(msg) + + return metrics_writer_factory.create(strategy=strategy, init_args=init_args) diff --git a/packages/graphrag-llm/graphrag_llm/metrics/noop_metrics_store.py b/packages/graphrag-llm/graphrag_llm/metrics/noop_metrics_store.py new file mode 100644 index 0000000000..dbd41e13f7 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/metrics/noop_metrics_store.py @@ -0,0 +1,41 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Noop metrics store.""" + +from typing import Any + +from graphrag_llm.metrics.metrics_store import MetricsStore +from graphrag_llm.types import Metrics + + +class NoopMetricsStore(MetricsStore): + """Noop store for metrics.""" + + def __init__( + self, + **kwargs: Any, + ) -> None: + """Initialize NoopMetricsStore.""" + + @property + def id(self) -> str: + """Get the ID of the metrics store.""" + return "" + + def update_metrics(self, *, metrics: Metrics) -> None: + """Noop update.""" + return + + def get_metrics(self) -> Metrics: + """Noop get all metrics from the store.""" + return {} + + def clear_metrics(self) -> None: + """Clear all metrics from the store. + + Returns + ------- + None + """ + return diff --git a/packages/graphrag-llm/graphrag_llm/middleware/__init__.py b/packages/graphrag-llm/graphrag_llm/middleware/__init__.py new file mode 100644 index 0000000000..d038b28594 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/middleware/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Middleware.""" + +from graphrag_llm.middleware.with_cache import with_cache +from graphrag_llm.middleware.with_errors_for_testing import with_errors_for_testing +from graphrag_llm.middleware.with_logging import with_logging +from graphrag_llm.middleware.with_metrics import with_metrics +from graphrag_llm.middleware.with_middleware_pipeline import with_middleware_pipeline +from graphrag_llm.middleware.with_rate_limiting import with_rate_limiting +from graphrag_llm.middleware.with_request_count import with_request_count +from graphrag_llm.middleware.with_retries import with_retries + +__all__ = [ + "with_cache", + "with_errors_for_testing", + "with_logging", + "with_metrics", + "with_middleware_pipeline", + "with_rate_limiting", + "with_request_count", + "with_retries", +] diff --git a/packages/graphrag-llm/graphrag_llm/middleware/with_cache.py b/packages/graphrag-llm/graphrag_llm/middleware/with_cache.py new file mode 100644 index 0000000000..2809538073 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/middleware/with_cache.py @@ -0,0 +1,153 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Cache middleware.""" + +import asyncio +from typing import TYPE_CHECKING, Any, Literal + +from graphrag_llm.types import LLMCompletionResponse, LLMEmbeddingResponse + +if TYPE_CHECKING: + from graphrag_cache import Cache, CacheKeyCreator + + from graphrag_llm.types import ( + AsyncLLMFunction, + LLMFunction, + Metrics, + ) + + +def with_cache( + *, + sync_middleware: "LLMFunction", + async_middleware: "AsyncLLMFunction", + request_type: Literal["chat", "embedding"], + cache: "Cache", + cache_key_creator: "CacheKeyCreator", +) -> tuple[ + "LLMFunction", + "AsyncLLMFunction", +]: + """Wrap model functions with cache middleware. + + Args + ---- + sync_middleware: LLMFunction + The synchronous model function to wrap. + Either a completion function or an embedding function. + async_middleware: AsyncLLMFunction + The asynchronous model function to wrap. + Either a completion function or an embedding function. + cache: Cache + The cache instance to use. + request_type: Literal["chat", "embedding"] + The type of request, either "chat" or "embedding". + cache_key_creator: CacheKeyCreator + The cache key creator to use. + + Returns + ------- + tuple[LLMFunction, AsyncLLMFunction] + The synchronous and asynchronous model functions with caching. + + """ + + def _cache_middleware( + **kwargs: Any, + ): + is_streaming = kwargs.get("stream") or False + is_mocked = kwargs.get("mock_response") or False + metrics: Metrics | None = kwargs.get("metrics") + + if is_streaming or is_mocked: + # don't cache streaming or mocked responses + return sync_middleware(**kwargs) + + cache_key = cache_key_creator(kwargs) + + event_loop = asyncio.new_event_loop() + asyncio.set_event_loop(event_loop) + cached_response = event_loop.run_until_complete(cache.get(cache_key)) + if ( + cached_response is not None + and isinstance(cached_response, dict) + and "response" in cached_response + and cached_response["response"] is not None + and isinstance(cached_response["response"], dict) + ): + try: + if ( + metrics is not None + and "metrics" in cached_response + and cached_response["metrics"] is not None + and isinstance(cached_response["metrics"], dict) + ): + metrics.update(cached_response["metrics"]) + metrics["cached_responses"] = 1 + + if request_type == "chat": + return LLMCompletionResponse(**cached_response["response"]) + return LLMEmbeddingResponse(**cached_response["response"]) + except Exception: # noqa: BLE001 + # Try to retrieve value from cache but if it fails, continue + # to make the request. + ... + + response = sync_middleware(**kwargs) + cache_value = { + "response": response.model_dump(), # type: ignore + "metrics": metrics if metrics is not None else {}, + } + event_loop.run_until_complete(cache.set(cache_key, cache_value)) + event_loop.close() + return response + + async def _cache_middleware_async( + **kwargs: Any, + ): + is_streaming = kwargs.get("stream") or False + is_mocked = kwargs.get("mock_response") or False + metrics: Metrics | None = kwargs.get("metrics") + + if is_streaming or is_mocked: + # don't cache streaming or mocked responses + return await async_middleware(**kwargs) + + cache_key = cache_key_creator(kwargs) + + cached_response = await cache.get(cache_key) + if ( + cached_response is not None + and isinstance(cached_response, dict) + and "response" in cached_response + and cached_response["response"] is not None + and isinstance(cached_response["response"], dict) + ): + try: + if ( + metrics is not None + and "metrics" in cached_response + and cached_response["metrics"] is not None + and isinstance(cached_response["metrics"], dict) + ): + metrics.update(cached_response["metrics"]) + metrics["cached_responses"] = 1 + + if request_type == "chat": + return LLMCompletionResponse(**cached_response["response"]) + return LLMEmbeddingResponse(**cached_response["response"]) + except Exception: # noqa: BLE001 + # Try to retrieve value from cache but if it fails, continue + # to make the request. + ... + + response = await async_middleware(**kwargs) + cache_value = { + "response": response.model_dump(), # type: ignore + "metrics": metrics if metrics is not None else {}, + } + await cache.set(cache_key, cache_value) + return response + + return (_cache_middleware, _cache_middleware_async) # type: ignore diff --git a/packages/graphrag-llm/graphrag_llm/middleware/with_errors_for_testing.py b/packages/graphrag-llm/graphrag_llm/middleware/with_errors_for_testing.py new file mode 100644 index 0000000000..cabdb1eaa4 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/middleware/with_errors_for_testing.py @@ -0,0 +1,83 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Error testing middleware.""" + +import asyncio +import random +import time +from typing import TYPE_CHECKING, Any + +import litellm.exceptions as exceptions + +if TYPE_CHECKING: + from graphrag_llm.types import ( + AsyncLLMFunction, + LLMFunction, + ) + + +def with_errors_for_testing( + *, + sync_middleware: "LLMFunction", + async_middleware: "AsyncLLMFunction", + failure_rate: float = 0.0, + exception_type: str = "ValueError", + exception_args: list[Any] | None = None, +) -> tuple[ + "LLMFunction", + "AsyncLLMFunction", +]: + """Wrap model functions with error testing middleware. + + Args + ---- + sync_middleware: LLMFunction + The synchronous model function to wrap. + Either a completion function or an embedding function. + async_middleware: AsyncLLMFunction + The asynchronous model function to wrap. + Either a completion function or an embedding function. + failure_rate: float + The failure rate for testing, between 0.0 and 1.0. + Defaults to 0.0 (no failures). + exception_type: str + The name of the exceptions class from litellm.exceptions to raise. + Defaults to "ValueError". + exception_args: list[Any] | None + The arguments to pass to the exception when raising it. Defaults to None, + which results in a default message. + + Returns + ------- + tuple[LLMFunction, AsyncLLMFunction] + The synchronous and asynchronous model functions wrapped with error testing middleware. + """ + + def _errors_middleware( + **kwargs: Any, + ): + if failure_rate > 0.0 and random.random() <= failure_rate: # noqa: S311 + time.sleep(0.5) + + exception_cls = exceptions.__dict__.get(exception_type, ValueError) + raise exception_cls( + *(exception_args or ["Simulated failure for debugging purposes."]) + ) + + return sync_middleware(**kwargs) + + async def _errors_middleware_async( + **kwargs: Any, + ): + if failure_rate > 0.0 and random.random() <= failure_rate: # noqa: S311 + await asyncio.sleep(0.5) + + exception_cls = exceptions.__dict__.get(exception_type, ValueError) + raise exception_cls( + *(exception_args or ["Simulated failure for debugging purposes."]) + ) + + return await async_middleware(**kwargs) + + return (_errors_middleware, _errors_middleware_async) # type: ignore diff --git a/packages/graphrag-llm/graphrag_llm/middleware/with_logging.py b/packages/graphrag-llm/graphrag_llm/middleware/with_logging.py new file mode 100644 index 0000000000..121ffbdbb6 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/middleware/with_logging.py @@ -0,0 +1,73 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Request count middleware.""" + +import logging +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphrag_llm.types import ( + AsyncLLMFunction, + LLMFunction, + Metrics, + ) + +logger = logging.getLogger(__name__) + + +def with_logging( + *, + sync_middleware: "LLMFunction", + async_middleware: "AsyncLLMFunction", +) -> tuple[ + "LLMFunction", + "AsyncLLMFunction", +]: + """Wrap model functions with logging middleware. + + Args + ---- + sync_middleware: LLMFunction + The synchronous model function to wrap. + Either a completion function or an embedding function. + async_middleware: AsyncLLMFunction + The asynchronous model function to wrap. + Either a completion function or an embedding function. + + Returns + ------- + tuple[LLMFunction, AsyncLLMFunction] + The synchronous and asynchronous model functions wrapped with request count middleware. + """ + + def _request_count_middleware( + **kwargs: Any, + ): + metrics: Metrics | None = kwargs.get("metrics") + try: + return sync_middleware(**kwargs) + except Exception as e: + retries = metrics.get("retries", None) if metrics else None + retry_str = f" after {retries} retries" if retries else "" + logger.exception( + f"Request failed{retry_str} with exception={e}", # noqa: G004, TRY401 + ) + raise + + async def _request_count_middleware_async( + **kwargs: Any, + ): + metrics: Metrics | None = kwargs.get("metrics") + + try: + return await async_middleware(**kwargs) + except Exception as e: + retries = metrics.get("retries", None) if metrics else None + retry_str = f" after {retries} retries" if retries else "" + logger.exception( + f"Async request failed{retry_str} with exception={e}", # noqa: G004, TRY401 + ) + raise + + return (_request_count_middleware, _request_count_middleware_async) # type: ignore diff --git a/packages/graphrag-llm/graphrag_llm/middleware/with_metrics.py b/packages/graphrag-llm/graphrag_llm/middleware/with_metrics.py new file mode 100644 index 0000000000..64ff7a41f7 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/middleware/with_metrics.py @@ -0,0 +1,98 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Metrics middleware to process metrics using a MetricsProcessor.""" + +import time +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphrag_llm.config import ModelConfig + from graphrag_llm.metrics import MetricsProcessor + from graphrag_llm.types import ( + AsyncLLMFunction, + LLMFunction, + Metrics, + ) + + +def with_metrics( + *, + model_config: "ModelConfig", + sync_middleware: "LLMFunction", + async_middleware: "AsyncLLMFunction", + metrics_processor: "MetricsProcessor", +) -> tuple[ + "LLMFunction", + "AsyncLLMFunction", +]: + """Wrap model functions with metrics middleware. + + Args + ---- + model_config: ModelConfig + The model configuration. + sync_middleware: LLMFunction + The synchronous model function to wrap. + Either a completion function or an embedding function. + async_middleware: AsyncLLMFunction + The asynchronous model function to wrap. + Either a completion function or an embedding function. + metrics_processor: MetricsProcessor + The metrics processor to use. + + Returns + ------- + tuple[LLMFunction, AsyncLLMFunction] + The synchronous and asynchronous model functions wrapped with metrics middleware. + + """ + + def _metrics_middleware( + **kwargs: Any, + ): + metrics: Metrics | None = kwargs.get("metrics") + start_time = time.time() + response = sync_middleware(**kwargs) + end_time = time.time() + + if metrics is not None: + metrics_processor.process_metrics( + model_config=model_config, + metrics=metrics, + input_args=kwargs, + response=response, + ) + if kwargs.get("stream"): + metrics["compute_duration_seconds"] = 0 + metrics["streaming_responses"] = 1 + else: + metrics["compute_duration_seconds"] = end_time - start_time + metrics["streaming_responses"] = 0 + return response + + async def _metrics_middleware_async( + **kwargs: Any, + ): + metrics: Metrics | None = kwargs.get("metrics") + + start_time = time.time() + response = await async_middleware(**kwargs) + end_time = time.time() + + if metrics is not None: + metrics_processor.process_metrics( + model_config=model_config, + metrics=metrics, + input_args=kwargs, + response=response, + ) + if kwargs.get("stream"): + metrics["compute_duration_seconds"] = 0 + metrics["streaming_responses"] = 1 + else: + metrics["compute_duration_seconds"] = end_time - start_time + metrics["streaming_responses"] = 0 + return response + + return (_metrics_middleware, _metrics_middleware_async) # type: ignore diff --git a/packages/graphrag-llm/graphrag_llm/middleware/with_middleware_pipeline.py b/packages/graphrag-llm/graphrag_llm/middleware/with_middleware_pipeline.py new file mode 100644 index 0000000000..41860acfaf --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/middleware/with_middleware_pipeline.py @@ -0,0 +1,154 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Wraps model functions in middleware pipeline.""" + +from typing import TYPE_CHECKING, Literal + +from graphrag_llm.middleware.with_cache import with_cache +from graphrag_llm.middleware.with_errors_for_testing import with_errors_for_testing +from graphrag_llm.middleware.with_logging import with_logging +from graphrag_llm.middleware.with_metrics import with_metrics +from graphrag_llm.middleware.with_rate_limiting import with_rate_limiting +from graphrag_llm.middleware.with_request_count import with_request_count +from graphrag_llm.middleware.with_retries import with_retries + +if TYPE_CHECKING: + from graphrag_cache import Cache, CacheKeyCreator + + from graphrag_llm.config import ModelConfig + from graphrag_llm.metrics import MetricsProcessor + from graphrag_llm.rate_limit import RateLimiter + from graphrag_llm.retry import Retry + from graphrag_llm.tokenizer import Tokenizer + from graphrag_llm.types import ( + AsyncLLMFunction, + LLMFunction, + ) + + +def with_middleware_pipeline( + *, + model_config: "ModelConfig", + model_fn: "LLMFunction", + async_model_fn: "AsyncLLMFunction", + metrics_processor: "MetricsProcessor | None", + cache: "Cache | None", + cache_key_creator: "CacheKeyCreator", + request_type: Literal["chat", "embedding"], + tokenizer: "Tokenizer", + rate_limiter: "RateLimiter | None", + retrier: "Retry | None", +) -> tuple[ + "LLMFunction", + "AsyncLLMFunction", +]: + """Wrap model functions in middleware pipeline. + + Full Pipeline Order: + - with_requests_counts: Counts incoming requests and + successes, and failures that bubble back up. + - with_cache: Returns cached responses when available + and caches new successful responses that bubble back up. + - with_retries: Retries failed requests. + Since the retry middleware occurs prior to rate limiting, + all retries get back in line for rate limiting. This is + to avoid threads that retry rapidly against an endpoint, + thus increasing the required cooldown. + - with_rate_limiting: Rate limits requests. + - with_metrics: Collects metrics about the request and responses. + - with_errors_for_testing: Raises errors for testing purposes. + Relies on ModelConfig.failure_rate_for_testing to determine + the failure rate. 'failure_rate_for_testing' is not an exposed + configuration option and is only intended for internal testing. + + Args + ---- + model_config: ModelConfig + The model configuration. + model_fn: LLMFunction + The synchronous model function to wrap. + Either a completion function or an embedding function. + async_model_fn: AsyncLLMFunction + The asynchronous model function to wrap. + Either a completion function or an embedding function. + metrics_processor: MetricsProcessor | None + The metrics processor to use. If None, metrics middleware is skipped. + cache: Cache | None + The cache instance to use. If None, caching middleware is skipped. + cache_key_creator: CacheKeyCreator + The cache key creator to use. + request_type: Literal["chat", "embedding"] + The type of request, either "chat" or "embedding". + The middleware pipeline is used for both completions and embeddings + and some of the steps need to know which type of request it is. + tokenizer: Tokenizer + The tokenizer to use for rate limiting. + rate_limiter: RateLimiter | None + The rate limiter to use. If None, rate limiting middleware is skipped. + retrier: Retry | None + The retrier to use. If None, retry middleware is skipped. + + Returns + ------- + tuple[LLMFunction, AsyncLLMFunction] + The synchronous and asynchronous model functions wrapped in the middleware pipeline. + """ + extra_config = model_config.model_extra or {} + failure_rate_for_testing = extra_config.get("failure_rate_for_testing", 0.0) + + if failure_rate_for_testing > 0.0: + model_fn, async_model_fn = with_errors_for_testing( + sync_middleware=model_fn, + async_middleware=async_model_fn, + failure_rate=failure_rate_for_testing, + exception_type=extra_config.get( + "failure_rate_for_testing_exception_type", "ValueError" + ), + exception_args=extra_config.get("failure_rate_for_testing_exception_args"), + ) + + if metrics_processor: + model_fn, async_model_fn = with_metrics( + model_config=model_config, + sync_middleware=model_fn, + async_middleware=async_model_fn, + metrics_processor=metrics_processor, + ) + + if rate_limiter: + model_fn, async_model_fn = with_rate_limiting( + sync_middleware=model_fn, + async_middleware=async_model_fn, + tokenizer=tokenizer, + rate_limiter=rate_limiter, + ) + + if retrier: + model_fn, async_model_fn = with_retries( + sync_middleware=model_fn, + async_middleware=async_model_fn, + retrier=retrier, + ) + + if cache: + model_fn, async_model_fn = with_cache( + sync_middleware=model_fn, + async_middleware=async_model_fn, + request_type=request_type, + cache=cache, + cache_key_creator=cache_key_creator, + ) + + if metrics_processor: + model_fn, async_model_fn = with_request_count( + sync_middleware=model_fn, + async_middleware=async_model_fn, + ) + + model_fn, async_model_fn = with_logging( + sync_middleware=model_fn, + async_middleware=async_model_fn, + ) + + return (model_fn, async_model_fn) diff --git a/packages/graphrag-llm/graphrag_llm/middleware/with_rate_limiting.py b/packages/graphrag-llm/graphrag_llm/middleware/with_rate_limiting.py new file mode 100644 index 0000000000..8d1b09a393 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/middleware/with_rate_limiting.py @@ -0,0 +1,79 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Rate limit middleware.""" + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphrag_llm.rate_limit import RateLimiter + from graphrag_llm.tokenizer import Tokenizer + from graphrag_llm.types import ( + AsyncLLMFunction, + LLMFunction, + ) + + +def with_rate_limiting( + *, + sync_middleware: "LLMFunction", + async_middleware: "AsyncLLMFunction", + rate_limiter: "RateLimiter", + tokenizer: "Tokenizer", +) -> tuple[ + "LLMFunction", + "AsyncLLMFunction", +]: + """Wrap model functions with rate limit middleware. + + Args + ---- + sync_middleware: LLMFunction + The synchronous model function to wrap. + Either a completion function or an embedding function. + async_middleware: AsyncLLMFunction + The asynchronous model function to wrap. + Either a completion function or an embedding function. + rate_limiter: RateLimiter + The rate limiter to use. + tokenizer: Tokenizer + The tokenizer to use for counting tokens. + + Returns + ------- + tuple[LLMFunction, AsyncLLMFunction] + The synchronous and asynchronous model functions wrapped with rate limit middleware. + """ + + def _rate_limit_middleware( + **kwargs: Any, + ): + token_count = int( + kwargs.get("max_tokens") or kwargs.get("max_completion_tokens") or 0 + ) + messages = kwargs.get("messages") # completion call + input: list[str] | None = kwargs.get("input") # embedding call + if messages: + token_count += tokenizer.num_prompt_tokens(messages=messages) + elif input: + token_count += sum(tokenizer.num_tokens(text) for text in input) + + with rate_limiter.acquire(token_count): + return sync_middleware(**kwargs) + + async def _rate_limit_middleware_async( + **kwargs: Any, + ): + token_count = int( + kwargs.get("max_tokens") or kwargs.get("max_completion_tokens") or 0 + ) + messages = kwargs.get("messages") # completion call + input = kwargs.get("input") # embedding call + if messages: + token_count += tokenizer.num_prompt_tokens(messages=messages) + elif input: + token_count += sum(tokenizer.num_tokens(text) for text in input) + with rate_limiter.acquire(token_count): + return await async_middleware(**kwargs) + + return (_rate_limit_middleware, _rate_limit_middleware_async) # type: ignore diff --git a/packages/graphrag-llm/graphrag_llm/middleware/with_request_count.py b/packages/graphrag-llm/graphrag_llm/middleware/with_request_count.py new file mode 100644 index 0000000000..24f61d8f47 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/middleware/with_request_count.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Request count middleware.""" + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphrag_llm.types import ( + AsyncLLMFunction, + LLMFunction, + Metrics, + ) + + +def with_request_count( + *, + sync_middleware: "LLMFunction", + async_middleware: "AsyncLLMFunction", +) -> tuple[ + "LLMFunction", + "AsyncLLMFunction", +]: + """Wrap model functions with request count middleware. + + This is the first step in the middleware pipeline. + It counts how many requests were made, how many succeeded, and how many failed + + Args + ---- + sync_middleware: LLMFunction + The synchronous model function to wrap. + Either a completion function or an embedding function. + async_middleware: AsyncLLMFunction + The asynchronous model function to wrap. + Either a completion function or an embedding function. + + Returns + ------- + tuple[LLMFunction, AsyncLLMFunction] + The synchronous and asynchronous model functions wrapped with request count middleware. + """ + + def _request_count_middleware( + **kwargs: Any, + ): + metrics: Metrics | None = kwargs.get("metrics") + if metrics is not None: + metrics["attempted_request_count"] = 1 + metrics["successful_response_count"] = 0 + metrics["failed_response_count"] = 0 + try: + result = sync_middleware(**kwargs) + if metrics is not None: + metrics["successful_response_count"] = 1 + return result # noqa: TRY300 + except Exception: + if metrics is not None: + metrics["failed_response_count"] = 1 + raise + + async def _request_count_middleware_async( + **kwargs: Any, + ): + metrics: Metrics | None = kwargs.get("metrics") + + if metrics is not None: + metrics["attempted_request_count"] = 1 + metrics["successful_response_count"] = 0 + metrics["failed_response_count"] = 0 + try: + result = await async_middleware(**kwargs) + if metrics is not None: + metrics["successful_response_count"] = 1 + return result # noqa: TRY300 + except Exception: + if metrics is not None: + metrics["failed_response_count"] = 1 + raise + + return (_request_count_middleware, _request_count_middleware_async) # type: ignore diff --git a/packages/graphrag-llm/graphrag_llm/middleware/with_retries.py b/packages/graphrag-llm/graphrag_llm/middleware/with_retries.py new file mode 100644 index 0000000000..1e7e17b208 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/middleware/with_retries.py @@ -0,0 +1,60 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Retry middleware.""" + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphrag_llm.retry import Retry + from graphrag_llm.types import ( + AsyncLLMFunction, + LLMFunction, + ) + + +def with_retries( + *, + sync_middleware: "LLMFunction", + async_middleware: "AsyncLLMFunction", + retrier: "Retry", +) -> tuple[ + "LLMFunction", + "AsyncLLMFunction", +]: + """Wrap model functions with retry middleware. + + Args + ---- + sync_middleware: LLMFunction + The synchronous model function to wrap. + Either a completion function or an embedding function. + async_middleware: AsyncLLMFunction + The asynchronous model function to wrap. + Either a completion function or an embedding function. + retrier: Retry + The retrier instance to use for retrying failed requests. + + Returns + ------- + tuple[LLMFunction, AsyncLLMFunction] + The synchronous and asynchronous model functions wrapped with retry middleware. + """ + + def _retry_middleware( + **kwargs: Any, + ): + return retrier.retry( + func=sync_middleware, + input_args=kwargs, + ) + + async def _retry_middleware_async( + **kwargs: Any, + ): + return await retrier.retry_async( + func=async_middleware, + input_args=kwargs, + ) + + return (_retry_middleware, _retry_middleware_async) # type: ignore diff --git a/packages/graphrag-llm/graphrag_llm/model_cost_registry/__init__.py b/packages/graphrag-llm/graphrag_llm/model_cost_registry/__init__.py new file mode 100644 index 0000000000..2742335b5d --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/model_cost_registry/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Model cost registry module.""" + +from graphrag_llm.model_cost_registry.model_cost_registry import ( + ModelCosts, + model_cost_registry, +) + +__all__ = ["ModelCosts", "model_cost_registry"] diff --git a/packages/graphrag-llm/graphrag_llm/model_cost_registry/model_cost_registry.py b/packages/graphrag-llm/graphrag_llm/model_cost_registry/model_cost_registry.py new file mode 100644 index 0000000000..09a45bc750 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/model_cost_registry/model_cost_registry.py @@ -0,0 +1,65 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Model cost registry module.""" + +from typing import Any, ClassVar, TypedDict + +from litellm import model_cost +from typing_extensions import Self + + +class ModelCosts(TypedDict): + """Model costs.""" + + input_cost_per_token: float + output_cost_per_token: float + + +class ModelCostRegistry: + """Registry for model costs.""" + + _instance: ClassVar["Self | None"] = None + _model_costs: dict[str, ModelCosts] + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """Create a new instance of ModelCostRegistry if it does not exist.""" + if cls._instance is None: + cls._instance = super().__new__(cls, *args, **kwargs) + return cls._instance + + def __init__(self): + if not hasattr(self, "_initialized"): + self._model_costs = model_cost + self._initialized = True + + def register_model_costs(self, model: str, costs: ModelCosts) -> None: + """Register the cost per unit for a given model. + + Args + ---- + model: str + The model id, e.g., "openai/gpt-4o". + costs: ModelCosts + The costs associated with the model. + """ + self._model_costs[model] = costs + + def get_model_costs(self, model: str) -> ModelCosts | None: + """Retrieve the cost per unit for a given model. + + Args + ---- + model: str + The model id, e.g., "openai/gpt-4o". + + Returns + ------- + ModelCosts | None + The costs associated with the model, or None if not found. + + """ + return self._model_costs.get(model) + + +model_cost_registry = ModelCostRegistry() diff --git a/packages/graphrag-llm/graphrag_llm/py.typed b/packages/graphrag-llm/graphrag_llm/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/graphrag-llm/graphrag_llm/rate_limit/__init__.py b/packages/graphrag-llm/graphrag_llm/rate_limit/__init__.py new file mode 100644 index 0000000000..4c3316102d --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/rate_limit/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Rate limit module for graphrag-llm.""" + +from graphrag_llm.rate_limit.rate_limit_factory import ( + create_rate_limiter, + register_rate_limiter, +) +from graphrag_llm.rate_limit.rate_limiter import RateLimiter + +__all__ = [ + "RateLimiter", + "create_rate_limiter", + "register_rate_limiter", +] diff --git a/packages/graphrag-llm/graphrag_llm/rate_limit/rate_limit_factory.py b/packages/graphrag-llm/graphrag_llm/rate_limit/rate_limit_factory.py new file mode 100644 index 0000000000..c15cd65c67 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/rate_limit/rate_limit_factory.py @@ -0,0 +1,84 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Rate limit factory.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from graphrag_common.factory import Factory + +from graphrag_llm.config import RateLimitType +from graphrag_llm.rate_limit.rate_limiter import RateLimiter + +if TYPE_CHECKING: + from graphrag_common.factory import ServiceScope + + from graphrag_llm.config import RateLimitConfig + + +class RateLimitFactory(Factory[RateLimiter]): + """Factory to create RateLimiter instances.""" + + +rate_limit_factory = RateLimitFactory() + + +def register_rate_limiter( + rate_limit_type: str, + rate_limiter_initializer: Callable[..., RateLimiter], + scope: "ServiceScope" = "transient", +) -> None: + """Register a custom RateLimiter implementation. + + Args + ---- + rate_limit_type: str + The rate limit id to register. + rate_limiter_initializer: Callable[..., RateLimiter] + The rate limiter initializer to register. + scope: ServiceScope (default: "transient") + The service scope for the rate limiter instance. + """ + rate_limit_factory.register( + strategy=rate_limit_type, + initializer=rate_limiter_initializer, + scope=scope, + ) + + +def create_rate_limiter( + rate_limit_config: "RateLimitConfig", +) -> RateLimiter: + """Create a RateLimiter instance. + + Args + ---- + rate_limit_config: RateLimitConfig + The configuration for the rate limit strategy. + + Returns + ------- + RateLimiter: + An instance of a RateLimiter subclass. + """ + strategy = rate_limit_config.type + init_args = rate_limit_config.model_dump() + + if strategy not in rate_limit_factory: + match strategy: + case RateLimitType.SlidingWindow: + from graphrag_llm.rate_limit.sliding_window_rate_limiter import ( + SlidingWindowRateLimiter, + ) + + register_rate_limiter( + rate_limit_type=RateLimitType.SlidingWindow, + rate_limiter_initializer=SlidingWindowRateLimiter, + ) + + case _: + msg = f"RateLimitConfig.type '{strategy}' is not registered in the RateLimitFactory. Registered strategies: {', '.join(rate_limit_factory.keys())}" + raise ValueError(msg) + + return rate_limit_factory.create(strategy=strategy, init_args=init_args) diff --git a/graphrag/language_model/providers/litellm/services/rate_limiter/rate_limiter.py b/packages/graphrag-llm/graphrag_llm/rate_limit/rate_limiter.py similarity index 66% rename from graphrag/language_model/providers/litellm/services/rate_limiter/rate_limiter.py rename to packages/graphrag-llm/graphrag_llm/rate_limit/rate_limiter.py index 24a01a42a0..14c32402cf 100644 --- a/graphrag/language_model/providers/litellm/services/rate_limiter/rate_limiter.py +++ b/packages/graphrag-llm/graphrag_llm/rate_limit/rate_limiter.py @@ -15,23 +15,24 @@ class RateLimiter(ABC): @abstractmethod def __init__( self, - /, **kwargs: Any, - ) -> None: ... + ) -> None: + """Initialize the Rate Limiter.""" + raise NotImplementedError @abstractmethod @contextmanager - def acquire(self, *, token_count: int) -> Iterator[None]: + def acquire(self, token_count: int) -> Iterator[None]: """ Acquire Rate Limiter. Args ---- - token_count: The estimated number of tokens for the current request. + token_count: int + The estimated number of prompt and response tokens for the current request. Yields ------ None: This context manager does not return any value. """ - msg = "RateLimiter subclasses must implement the acquire method." - raise NotImplementedError(msg) + raise NotImplementedError diff --git a/packages/graphrag-llm/graphrag_llm/rate_limit/sliding_window_rate_limiter.py b/packages/graphrag-llm/graphrag_llm/rate_limit/sliding_window_rate_limiter.py new file mode 100644 index 0000000000..c8eb89e5a7 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/rate_limit/sliding_window_rate_limiter.py @@ -0,0 +1,143 @@ +# Copyright (c) 2025 Microsoft Corporation. +# Licensed under the MIT License + +"""LiteLLM Static Rate Limiter.""" + +import threading +import time +from collections import deque +from collections.abc import Iterator +from contextlib import contextmanager +from typing import Any + +from graphrag_llm.rate_limit.rate_limiter import RateLimiter + + +class SlidingWindowRateLimiter(RateLimiter): + """Sliding Window Rate Limiter implementation.""" + + _rpp: int | None = None + _tpp: int | None = None + _lock: threading.Lock + _rate_queue: deque[float] + _token_queue: deque[int] + _period_in_seconds: int + _last_time: float | None = None + _stagger: float = 0.0 + + def __init__( + self, + *, + period_in_seconds: int = 60, + requests_per_period: int | None = None, + tokens_per_period: int | None = None, + **kwargs: Any, + ): + """Initialize the Sliding Window Rate Limiter. + + Args + ---- + period_in_seconds: int + The time period in seconds for rate limiting. + requests_per_period: int | None + The maximum number of requests allowed per time period. If None, request limiting is disabled. + tokens_per_period: int | None + The maximum number of tokens allowed per time period. If None, token limiting is disabled. + + Raises + ------ + ValueError + If period_in_seconds is not a positive integer. + If requests_per_period or tokens_per_period are not positive integers. + """ + self._rpp = requests_per_period + self._tpp = tokens_per_period + self._lock = threading.Lock() + self._rate_queue: deque[float] = deque() + self._token_queue: deque[int] = deque() + self._period_in_seconds = period_in_seconds + self._last_time: float | None = None + + if self._rpp is not None and self._rpp > 0: + self._stagger = self._period_in_seconds / self._rpp + + @contextmanager + def acquire(self, token_count: int) -> Iterator[None]: + """ + Acquire Rate Limiter. + + Args + ---- + token_count: The estimated number of tokens for the current request. + + Yields + ------ + None: This context manager does not return any value. + """ + while True: + with self._lock: + current_time = time.time() + + # Use two sliding windows to keep track of requests and tokens per period + # Drop old requests and tokens out of the sliding windows + while ( + len(self._rate_queue) > 0 + and self._rate_queue[0] < current_time - self._period_in_seconds + ): + self._rate_queue.popleft() + self._token_queue.popleft() + + # If sliding window still exceed request limit, wait again + # Waiting requires reacquiring the lock, allowing other threads + # to see if their request fits within the rate limiting windows + # Makes more sense for token limit than request limit + if ( + self._rpp is not None + and self._rpp > 0 + and len(self._rate_queue) >= self._rpp + ): + continue + + # Check if current token window exceeds token limit + # If it does, wait again + # This does not account for the tokens from the current request + # This is intentional, as we want to allow the current request + # to be processed if it is larger than the tpm but smaller than context window. + # tpm is a rate/soft limit and not the hard limit of context window limits. + if ( + self._tpp is not None + and self._tpp > 0 + and sum(self._token_queue) >= self._tpp + ): + continue + + # This check accounts for the current request token usage + # is within the token limits bound. + # If the current requests tokens exceeds the token limit, + # Then let it be processed. + if ( + self._tpp is not None + and self._tpp > 0 + and token_count <= self._tpp + and sum(self._token_queue) + token_count > self._tpp + ): + continue + + # If there was a previous call, check if we need to stagger + if ( + self._stagger > 0 + and ( + self._last_time # is None if this is the first hit to the rate limiter + and current_time - self._last_time + < self._stagger # If more time has passed than the stagger time, we can proceed + ) + ): + time.sleep(self._stagger - (current_time - self._last_time)) + current_time = time.time() + + # Add the current request to the sliding window + self._rate_queue.append(current_time) + self._token_queue.append(token_count) + self._last_time = current_time + break + yield diff --git a/packages/graphrag-llm/graphrag_llm/retry/__init__.py b/packages/graphrag-llm/graphrag_llm/retry/__init__.py new file mode 100644 index 0000000000..638c958e37 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/retry/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Retry module for graphrag-llm.""" + +from graphrag_llm.retry.retry import Retry +from graphrag_llm.retry.retry_factory import create_retry, register_retry + +__all__ = [ + "Retry", + "create_retry", + "register_retry", +] diff --git a/packages/graphrag-llm/graphrag_llm/retry/exceptions_to_skip.py b/packages/graphrag-llm/graphrag_llm/retry/exceptions_to_skip.py new file mode 100644 index 0000000000..f751b2ad28 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/retry/exceptions_to_skip.py @@ -0,0 +1,22 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""List of exception names to skip for retries.""" + +_default_exceptions_to_skip = [ + "BadRequestError", + "UnsupportedParamsError", + "ContextWindowExceededError", + "ContentPolicyViolationError", + "ImageFetchError", + "InvalidRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "UnprocessableEntityError", + "APIConnectionError", + "APIError", + "ServiceUnavailableError", + "APIResponseValidationError", + "BudgetExceededError", +] diff --git a/packages/graphrag-llm/graphrag_llm/retry/exponential_retry.py b/packages/graphrag-llm/graphrag_llm/retry/exponential_retry.py new file mode 100644 index 0000000000..f7abc9b0a8 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/retry/exponential_retry.py @@ -0,0 +1,119 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Exponential backoff retry implementation.""" + +import asyncio +import random +import time +from collections.abc import Awaitable, Callable +from typing import TYPE_CHECKING, Any + +from graphrag_llm.retry.exceptions_to_skip import _default_exceptions_to_skip +from graphrag_llm.retry.retry import Retry + +if TYPE_CHECKING: + from graphrag_llm.types import Metrics + + +class ExponentialRetry(Retry): + """Exponential backoff retry implementation.""" + + _base_delay: float + _jitter: bool + _max_retries: int + _max_delay: float + _exceptions_to_skip: list[str] + + def __init__( + self, + *, + max_retries: int = 7, # 2^7 = 128 second max delay with default settings + base_delay: float = 2.0, + jitter: bool = True, + max_delay: float | None = None, + exceptions_to_skip: list[str] | None = None, + **kwargs: dict, + ) -> None: + """Initialize ExponentialRetry. + + Args + ---- + max_retries: int (default=7, 2^7 = 128 second max delay with default settings) + The maximum number of retries to attempt. + base_delay: float + The base delay multiplier for exponential backoff. + jitter: bool + Whether to apply jitter to the delay intervals. + max_delay: float | None + The maximum delay between retries. If None, there is no limit. + + Raises + ------ + ValueError + If max_retries is less than or equal to 0. + If base_delay is less than or equal to 1.0. + """ + self._base_delay = base_delay + self._jitter = jitter + self._max_retries = max_retries + self._max_delay = max_delay or float("inf") + self._exceptions_to_skip = exceptions_to_skip or _default_exceptions_to_skip + + def retry(self, *, func: Callable[..., Any], input_args: dict[str, Any]) -> Any: + """Retry a synchronous function.""" + retries: int = 0 + delay = 1.0 + metrics: Metrics | None = input_args.get("metrics") + while True: + try: + return func(**input_args) + except Exception as e: + if e.__class__.__name__ in self._exceptions_to_skip: + raise + + if retries >= self._max_retries: + raise + retries += 1 + delay *= self._base_delay + sleep_delay = min( + self._max_delay, + delay + (self._jitter * random.uniform(0, 1)), # noqa: S311 + ) + + time.sleep(sleep_delay) + finally: + if metrics is not None: + metrics["retries"] = retries + metrics["requests_with_retries"] = 1 if retries > 0 else 0 + + async def retry_async( + self, + *, + func: Callable[..., Awaitable[Any]], + input_args: dict[str, Any], + ) -> Any: + """Retry an asynchronous function.""" + retries: int = 0 + delay = 1.0 + metrics: Metrics | None = input_args.get("metrics") + while True: + try: + return await func(**input_args) + except Exception as e: + if e.__class__.__name__ in self._exceptions_to_skip: + raise + if retries >= self._max_retries: + raise + retries += 1 + delay *= self._base_delay + sleep_delay = min( + self._max_delay, + delay + (self._jitter * random.uniform(0, 1)), # noqa: S311 + ) + + await asyncio.sleep(sleep_delay) + finally: + if metrics is not None: + metrics["retries"] = retries + metrics["requests_with_retries"] = 1 if retries > 0 else 0 diff --git a/packages/graphrag-llm/graphrag_llm/retry/immediate_retry.py b/packages/graphrag-llm/graphrag_llm/retry/immediate_retry.py new file mode 100644 index 0000000000..cb8150c089 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/retry/immediate_retry.py @@ -0,0 +1,85 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Native (immediate) retry implementation.""" + +from collections.abc import Awaitable, Callable +from typing import TYPE_CHECKING, Any + +from graphrag_llm.retry.exceptions_to_skip import _default_exceptions_to_skip +from graphrag_llm.retry.retry import Retry + +if TYPE_CHECKING: + from graphrag_llm.types import Metrics + + +class ImmediateRetry(Retry): + """Immediate retry implementation.""" + + _max_retries: int + _exceptions_to_skip: list[str] + + def __init__( + self, + *, + max_retries: int = 7, + exceptions_to_skip: list[str] | None = None, + **kwargs: dict, + ) -> None: + """Initialize ImmediateRetry. + + Args + ---- + max_retries: int (default=7) + The maximum number of retries to attempt. + + Raises + ------ + ValueError + If max_retries is less than or equal to 0. + """ + self._max_retries = max_retries + self._exceptions_to_skip = exceptions_to_skip or _default_exceptions_to_skip + + def retry(self, *, func: Callable[..., Any], input_args: dict[str, Any]) -> Any: + """Retry a synchronous function.""" + retries: int = 0 + metrics: Metrics | None = input_args.get("metrics") + while True: + try: + return func(**input_args) + except Exception as e: + if e.__class__.__name__ in self._exceptions_to_skip: + raise + + if retries >= self._max_retries: + raise + retries += 1 + finally: + if metrics is not None: + metrics["retries"] = retries + metrics["requests_with_retries"] = 1 if retries > 0 else 0 + + async def retry_async( + self, + *, + func: Callable[..., Awaitable[Any]], + input_args: dict[str, Any], + ) -> Any: + """Retry an asynchronous function.""" + retries: int = 0 + metrics: Metrics | None = input_args.get("metrics") + while True: + try: + return await func(**input_args) + except Exception as e: + if e.__class__.__name__ in self._exceptions_to_skip: + raise + + if retries >= self._max_retries: + raise + retries += 1 + finally: + if metrics is not None: + metrics["retries"] = retries + metrics["requests_with_retries"] = 1 if retries > 0 else 0 diff --git a/packages/graphrag-llm/graphrag_llm/retry/retry.py b/packages/graphrag-llm/graphrag_llm/retry/retry.py new file mode 100644 index 0000000000..5ed07070e8 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/retry/retry.py @@ -0,0 +1,32 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Retry abstract base class.""" + +from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable +from typing import Any + + +class Retry(ABC): + """Retry Abstract Base Class.""" + + @abstractmethod + def __init__(self, /, **kwargs: Any): + """Initialize Retry.""" + raise NotImplementedError + + @abstractmethod + def retry(self, *, func: Callable[..., Any], input_args: dict[str, Any]) -> Any: + """Retry a synchronous function.""" + raise NotImplementedError + + @abstractmethod + async def retry_async( + self, + *, + func: Callable[..., Awaitable[Any]], + input_args: dict[str, Any], + ) -> Any: + """Retry an asynchronous function.""" + raise NotImplementedError diff --git a/packages/graphrag-llm/graphrag_llm/retry/retry_factory.py b/packages/graphrag-llm/graphrag_llm/retry/retry_factory.py new file mode 100644 index 0000000000..e0d9cc8489 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/retry/retry_factory.py @@ -0,0 +1,86 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Retry factory.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from graphrag_common.factory import Factory + +from graphrag_llm.config.types import RetryType +from graphrag_llm.retry.retry import Retry + +if TYPE_CHECKING: + from graphrag_common.factory import ServiceScope + + from graphrag_llm.config.retry_config import RetryConfig + + +class RetryFactory(Factory[Retry]): + """Factory to create Retry instances.""" + + +retry_factory = RetryFactory() + + +def register_retry( + retry_type: str, + retry_initializer: Callable[..., Retry], + scope: "ServiceScope" = "transient", +) -> None: + """Register a custom Retry implementation. + + Args + ---- + retry_type: str + The retry id to register. + retry_initializer: Callable[..., Retry] + The retry initializer to register. + """ + retry_factory.register( + strategy=retry_type, + initializer=retry_initializer, + scope=scope, + ) + + +def create_retry( + retry_config: "RetryConfig", +) -> Retry: + """Create a Retry instance. + + Args + ---- + retry_config: RetryConfig + The configuration for the retry strategy. + + Returns + ------- + Retry: + An instance of a Retry subclass. + """ + strategy = retry_config.type + init_args = retry_config.model_dump() + + if strategy not in retry_factory: + match strategy: + case RetryType.ExponentialBackoff: + from graphrag_llm.retry.exponential_retry import ExponentialRetry + + retry_factory.register( + strategy=RetryType.ExponentialBackoff, + initializer=ExponentialRetry, + ) + case RetryType.Immediate: + from graphrag_llm.retry.immediate_retry import ImmediateRetry + + retry_factory.register( + strategy=RetryType.Immediate, + initializer=ImmediateRetry, + ) + case _: + msg = f"RetryConfig.type '{strategy}' is not registered in the RetryFactory. Registered strategies: {', '.join(retry_factory.keys())}" + raise ValueError(msg) + + return retry_factory.create(strategy=strategy, init_args=init_args) diff --git a/packages/graphrag-llm/graphrag_llm/templating/__init__.py b/packages/graphrag-llm/graphrag_llm/templating/__init__.py new file mode 100644 index 0000000000..3dd39619ac --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/templating/__init__.py @@ -0,0 +1,24 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Templates module.""" + +from graphrag_llm.templating.template_engine import TemplateEngine +from graphrag_llm.templating.template_engine_factory import ( + create_template_engine, + register_template_engine, +) +from graphrag_llm.templating.template_manager import TemplateManager +from graphrag_llm.templating.template_manager_factory import ( + create_template_manager, + register_template_manager, +) + +__all__ = [ + "TemplateEngine", + "TemplateManager", + "create_template_engine", + "create_template_manager", + "register_template_engine", + "register_template_manager", +] diff --git a/packages/graphrag-llm/graphrag_llm/templating/file_template_manager.py b/packages/graphrag-llm/graphrag_llm/templating/file_template_manager.py new file mode 100644 index 0000000000..a4f9a679fc --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/templating/file_template_manager.py @@ -0,0 +1,76 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""In-memory template manager implementation.""" + +from pathlib import Path +from typing import Any + +from graphrag_llm.templating.template_manager import TemplateManager + + +class FileTemplateManager(TemplateManager): + """Abstract base class for template managers.""" + + _encoding: str + _templates_extension: str + _templates_dir: Path + + def __init__( + self, + base_dir: str = "templates", + template_extension: str = ".jinja", + encoding: str = "utf-8", + **kwargs: Any, + ) -> None: + """Initialize the template manager. + + Args + ---- + base_dir: str (default="./templates") + The base directory where templates are stored. + template_extension: str (default=".jinja") + The file extension for template files. + encoding: str (default="utf-8") + The encoding used to read template files. + + Raises + ------ + ValueError + If the base directory does not exist or is not a directory. + If the template_extension is an empty string. + """ + self._templates = {} + self._encoding = encoding + + self._templates_extension = template_extension + + self._templates_dir = Path(base_dir).resolve() + if not self._templates_dir.exists() or not self._templates_dir.is_dir(): + msg = f"Templates directory '{base_dir}' does not exist or is not a directory." + raise ValueError(msg) + + def get(self, template_name: str) -> str | None: + """Retrieve a template by its name.""" + template_file = ( + self._templates_dir / f"{template_name}{self._templates_extension}" + ) + if template_file.exists() and template_file.is_file(): + return template_file.read_text(encoding=self._encoding) + return None + + def register(self, template_name: str, template: str) -> None: + """Register a new template.""" + self._templates[template_name] = template + template_path = ( + self._templates_dir / f"{template_name}{self._templates_extension}" + ) + template_path.write_text(template, encoding=self._encoding) + + def keys(self) -> list[str]: + """List all registered template names.""" + return list(self._templates.keys()) + + def __contains__(self, template_name: str) -> bool: + """Check if a template is registered.""" + return template_name in self._templates diff --git a/packages/graphrag-llm/graphrag_llm/templating/jinja_template_engine.py b/packages/graphrag-llm/graphrag_llm/templating/jinja_template_engine.py new file mode 100644 index 0000000000..e520e4b0cc --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/templating/jinja_template_engine.py @@ -0,0 +1,55 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Jinja template engine.""" + +from typing import TYPE_CHECKING, Any + +from jinja2 import StrictUndefined, Template, UndefinedError + +from graphrag_llm.templating.template_engine import TemplateEngine + +if TYPE_CHECKING: + from graphrag_llm.templating.template_manager import TemplateManager + + +class JinjaTemplateEngine(TemplateEngine): + """Jinja template engine.""" + + _templates: dict[str, Template] + _template_manager: "TemplateManager" + + def __init__(self, *, template_manager: "TemplateManager", **kwargs: Any) -> None: + """Initialize the template engine. + + Args + ---- + template_manager: TemplateManager + The template manager to use for loading templates. + """ + self._templates = {} + self._template_manager = template_manager + + def render(self, template_name: str, context: dict[str, Any]) -> str: + """Render a template with the given context.""" + jinja_template = self._templates.get(template_name) + if jinja_template is None: + template_contents = self._template_manager.get(template_name) + if template_contents is None: + msg = f"Template '{template_name}' not found." + raise KeyError(msg) + jinja_template = Template(template_contents, undefined=StrictUndefined) + self._templates[template_name] = jinja_template + try: + return jinja_template.render(**context) + except UndefinedError as e: + msg = f"Missing key in context for template '{template_name}': {e.message}" + raise KeyError(msg) from e + except Exception as e: + msg = f"Error rendering template '{template_name}': {e!s}" + raise RuntimeError(msg) from e + + @property + def template_manager(self) -> "TemplateManager": + """Template manager associated with this engine.""" + return self._template_manager diff --git a/packages/graphrag-llm/graphrag_llm/templating/template_engine.py b/packages/graphrag-llm/graphrag_llm/templating/template_engine.py new file mode 100644 index 0000000000..29b3250145 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/templating/template_engine.py @@ -0,0 +1,53 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Abstract base class for template engines.""" + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphrag_llm.templating.template_manager import TemplateManager + + +class TemplateEngine(ABC): + """Abstract base class for template engines.""" + + @abstractmethod + def __init__(self, *, template_manager: "TemplateManager", **kwargs: Any) -> None: + """Initialize the template engine. + + Args + ---- + template_manager: TemplateManager + The template manager to use for loading templates. + + """ + raise NotImplementedError + + @abstractmethod + def render(self, template_name: str, context: dict[str, Any]) -> str: + """Render a template with the given context. + + Args + ---- + template_name: str + The name of the template to render. + context: dict[str, str] + The context to use for rendering the template. + + Returns + ------- + str: The rendered template. + + Raises + ------ + KeyError: If the template is not found or a required key is missing in the context. + """ + raise NotImplementedError + + @property + @abstractmethod + def template_manager(self) -> "TemplateManager": + """Template manager associated with this engine.""" + raise NotImplementedError diff --git a/packages/graphrag-llm/graphrag_llm/templating/template_engine_factory.py b/packages/graphrag-llm/graphrag_llm/templating/template_engine_factory.py new file mode 100644 index 0000000000..fb7fbbbaf8 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/templating/template_engine_factory.py @@ -0,0 +1,95 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Template engine factory implementation.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from graphrag_common.factory import Factory + +from graphrag_llm.config.template_engine_config import TemplateEngineConfig +from graphrag_llm.config.types import TemplateEngineType +from graphrag_llm.templating.template_engine import TemplateEngine +from graphrag_llm.templating.template_manager_factory import create_template_manager + +if TYPE_CHECKING: + from graphrag_common.factory import ServiceScope + + +class TemplateEngineFactory(Factory[TemplateEngine]): + """Factory for creating template engine instances.""" + + +template_engine_factory = TemplateEngineFactory() + + +def register_template_engine( + template_engine_type: str, + template_engine_initializer: Callable[..., TemplateEngine], + scope: "ServiceScope" = "transient", +) -> None: + """Register a custom template engine implementation. + + Args + ---- + template_engine_type: str + The template engine id to register. + template_engine_initializer: Callable[..., TemplateEngine] + The template engine initializer to register. + scope: ServiceScope (default: "transient") + The service scope for the template engine instance. + """ + template_engine_factory.register( + strategy=template_engine_type, + initializer=template_engine_initializer, + scope=scope, + ) + + +def create_template_engine( + template_engine_config: TemplateEngineConfig | None = None, +) -> TemplateEngine: + """Create a TemplateEngine instance. + + Args + ---- + template_engine_config: TemplateEngineConfig | None + The configuration for the template engine. If None, defaults will be used. + + Returns + ------- + TemplateEngine: + An instance of a TemplateEngine subclass. + """ + template_engine_config = template_engine_config or TemplateEngineConfig() + + strategy = template_engine_config.type + template_manager = create_template_manager( + template_engine_config=template_engine_config + ) + init_args = template_engine_config.model_dump() + + if strategy not in template_engine_factory: + match strategy: + case TemplateEngineType.Jinja: + from graphrag_llm.templating.jinja_template_engine import ( + JinjaTemplateEngine, + ) + + template_engine_factory.register( + strategy=TemplateEngineType.Jinja, + initializer=JinjaTemplateEngine, + scope="singleton", + ) + case _: + msg = f"TemplateEngineConfig.type '{strategy}' is not registered in the TemplateEngineFactory. Registered strategies: {', '.join(template_engine_factory.keys())}" + raise ValueError(msg) + + return template_engine_factory.create( + strategy=strategy, + init_args={ + **init_args, + "template_manager": template_manager, + }, + ) diff --git a/packages/graphrag-llm/graphrag_llm/templating/template_manager.py b/packages/graphrag-llm/graphrag_llm/templating/template_manager.py new file mode 100644 index 0000000000..6731176d96 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/templating/template_manager.py @@ -0,0 +1,65 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Abstract base class for template managers.""" + +from abc import ABC, abstractmethod +from typing import Any + + +class TemplateManager(ABC): + """Abstract base class for template managers.""" + + @abstractmethod + def __init__(self, **kwargs: Any) -> None: + """Initialize the template manager.""" + raise NotImplementedError + + @abstractmethod + def get(self, template_name: str) -> str | None: + """Retrieve a template by its name. + + Args + ---- + template_name: str + The name of the template to retrieve. + + Returns + ------- + str | None: The content of the template, if found. + """ + raise NotImplementedError + + @abstractmethod + def register(self, template_name: str, template: str) -> None: + """Register a new template. + + Args + ---- + template_name: str + The name of the template. + template: str + The content of the template. + """ + raise NotImplementedError + + @abstractmethod + def keys(self) -> list[str]: + """List all registered template names. + + Returns + ------- + list[str]: A list of registered template names. + """ + raise NotImplementedError + + @abstractmethod + def __contains__(self, template_name: str) -> bool: + """Check if a template is registered. + + Args + ---- + template_name: str + The name of the template to check. + """ + raise NotImplementedError diff --git a/packages/graphrag-llm/graphrag_llm/templating/template_manager_factory.py b/packages/graphrag-llm/graphrag_llm/templating/template_manager_factory.py new file mode 100644 index 0000000000..9de35a3693 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/templating/template_manager_factory.py @@ -0,0 +1,82 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Template manager factory implementation.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from graphrag_common.factory import Factory + +from graphrag_llm.config.template_engine_config import TemplateEngineConfig +from graphrag_llm.config.types import TemplateManagerType +from graphrag_llm.templating.template_manager import TemplateManager + +if TYPE_CHECKING: + from graphrag_common.factory import ServiceScope + + +class TemplateManagerFactory(Factory[TemplateManager]): + """Factory for creating template manager instances.""" + + +template_manager_factory = TemplateManagerFactory() + + +def register_template_manager( + template_manager_type: str, + template_manager_initializer: Callable[..., TemplateManager], + scope: "ServiceScope" = "transient", +) -> None: + """Register a custom template manager implementation. + + Args + ---- + - template_manager_type: str + The template manager id to register. + - template_manager_initializer: Callable[..., TemplateManager] + The template manager initializer to register. + """ + template_manager_factory.register( + strategy=template_manager_type, + initializer=template_manager_initializer, + scope=scope, + ) + + +def create_template_manager( + template_engine_config: TemplateEngineConfig | None = None, +) -> TemplateManager: + """Create a TemplateManager instance. + + Args + ---- + template_engine_config: TemplateEngineConfig + The configuration for the template engine. + + Returns + ------- + TemplateManager: + An instance of a TemplateManager subclass. + """ + template_engine_config = template_engine_config or TemplateEngineConfig() + strategy = template_engine_config.template_manager + init_args = template_engine_config.model_dump() + + if strategy not in template_manager_factory: + match strategy: + case TemplateManagerType.File: + from graphrag_llm.templating.file_template_manager import ( + FileTemplateManager, + ) + + template_manager_factory.register( + strategy=TemplateManagerType.File, + initializer=FileTemplateManager, + scope="singleton", + ) + case _: + msg = f"TemplateEngineConfig.template_manager '{strategy}' is not registered in the TemplateManagerFactory. Registered strategies: {', '.join(template_manager_factory.keys())}" + raise ValueError(msg) + + return template_manager_factory.create(strategy=strategy, init_args=init_args) diff --git a/packages/graphrag-llm/graphrag_llm/threading/__init__.py b/packages/graphrag-llm/graphrag_llm/threading/__init__.py new file mode 100644 index 0000000000..cc797aceed --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/threading/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Threading module.""" + +from graphrag_llm.threading.completion_thread_runner import completion_thread_runner + +__all__ = [ + "completion_thread_runner", +] diff --git a/packages/graphrag-llm/graphrag_llm/threading/completion_thread.py b/packages/graphrag-llm/graphrag_llm/threading/completion_thread.py new file mode 100644 index 0000000000..4b0dbd4f4b --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/threading/completion_thread.py @@ -0,0 +1,91 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Completion Thread.""" + +import threading +from queue import Empty, Queue +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterator + + from graphrag_llm.types import ( + LLMCompletionArgs, + LLMCompletionChunk, + LLMCompletionFunction, + LLMCompletionResponse, + ) + + +LLMCompletionRequestQueue = Queue[tuple[str, "LLMCompletionArgs"] | None] +"""Input queue for LLM completions. + +A queue for tracking requests to be made to a completion endpoint. +Each item in the queue is a tuple containing a request ID and a dictionary of +completion arguments. A `None` value indicates that the thread should terminate. + +Queue Item Type: + tuple[request_id, completion_args_dict] | None + +Items in the queue are processed by a thread pool in which the results are placed +into an output queue to be handled by a response handler. +""" + + +LLMCompletionResponseQueue = Queue[ + tuple[ + str, + "LLMCompletionResponse | Iterator[LLMCompletionChunk] | Exception", + ] + | None +] +"""Output queue for LLM completion responses. + +A queue for tracking responses from completion requests. Each item in the queue is a tuple +containing the request ID and the corresponding response, which can be a full response, +a stream of chunks, or an exception if the request failed. A `None` value indicates that the +thread should terminate. + +Queue Item Type: + tuple[request_id, response | exception] | None + +Items in the queue are produced by a thread pool that processes completion requests +from an input queue. This output queue is then consumed by a response handler provided +by the user. +""" + + +class CompletionThread(threading.Thread): + """Thread for handling LLM completions.""" + + def __init__( + self, + *, + quit_process_event: threading.Event, + input_queue: LLMCompletionRequestQueue, + output_queue: LLMCompletionResponseQueue, + completion: "LLMCompletionFunction", + ) -> None: + super().__init__() + self._quit_process_event = quit_process_event + self._input_queue = input_queue + self._output_queue = output_queue + self._completion = completion + + def run(self): + """Run the completion thread.""" + while True and not self._quit_process_event.is_set(): + try: + input_data = self._input_queue.get(timeout=1) + except Empty: + continue + if input_data is None: + break + request_id, data = input_data + try: + response = self._completion(**data) + + self._output_queue.put((request_id, response)) + except Exception as e: # noqa: BLE001 + self._output_queue.put((request_id, e)) diff --git a/packages/graphrag-llm/graphrag_llm/threading/completion_thread_runner.py b/packages/graphrag-llm/graphrag_llm/threading/completion_thread_runner.py new file mode 100644 index 0000000000..bb34cf4c54 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/threading/completion_thread_runner.py @@ -0,0 +1,243 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Completion Thread Runner.""" + +import asyncio +import sys +import threading +import time +from collections.abc import Awaitable, Iterator +from contextlib import contextmanager +from queue import Empty, Queue +from typing import TYPE_CHECKING, Protocol, Unpack, runtime_checkable + +from graphrag_llm.threading.completion_thread import CompletionThread + +if TYPE_CHECKING: + from graphrag_llm.metrics import MetricsStore + from graphrag_llm.threading.completion_thread import ( + LLMCompletionRequestQueue, + LLMCompletionResponseQueue, + ) + from graphrag_llm.types import ( + LLMCompletionArgs, + LLMCompletionChunk, + LLMCompletionFunction, + LLMCompletionResponse, + ) + + +@runtime_checkable +class ThreadedLLMCompletionResponseHandler(Protocol): + """Threaded completion response handler. + + This function is used to handle responses from the threaded completion runner. + + Args + ---- + request_id: str + The request ID associated with the completion request. + resp: LLMCompletionResponse | Iterator[LLMCompletionChunk] | Exception + The completion response, which can be a full response, a stream of chunks, + or an exception if the request failed. + + Returns + ------- + Awaitable[None] | None + The callback can be asynchronous or synchronous. + """ + + def __call__( + self, + request_id: str, + response: "LLMCompletionResponse | Iterator[LLMCompletionChunk] | Exception", + /, + ) -> Awaitable[None] | None: + """Threaded completion response handler.""" + ... + + +@runtime_checkable +class ThreadedLLMCompletionFunction(Protocol): + """Threaded completion function. + + This function is used to submit requests to a thread pool for processing. + The thread pool will process the requests and invoke the provided callback + with the responses. + + same signature as LLMCompletionFunction but requires a `request_id` parameter + to identify the request and does not return anything. + + Args + ---- + messages: LLMCompletionMessagesParam + The messages to send to the LLM. + Can be str | list[dict[str, str]] | list[ChatCompletionMessageParam]. + request_id: str + The request ID to associate with the completion request. + response_format: BaseModel | None (default=None) + The structured response format. + Must extend pydantic BaseModel. + stream: bool (default=False) + Whether to stream the response. + streaming is not supported when using response_format. + max_completion_tokens: int | None (default=None) + The maximum number of tokens to generate in the completion. + temperature: float | None (default=None) + The temperature to control how deterministic vs. creative the responses are. + top_p: float | None (default=None) + top_p for nucleus sampling, where the model considers tokens with + cumulative probabilities up to top_p. Values range from 0 to 1. + n: int | None (default=None) + The number of completions to generate for each prompt. + tools: list[Tool] | None (default=None) + Optional tools to use during completion. + https://docs.litellm.ai/docs/completion/function_call + **kwargs: Any + Additional keyword arguments. + + Returns + ------- + None + """ + + def __call__( + self, + /, + request_id: str, + **kwargs: Unpack["LLMCompletionArgs"], + ) -> None: + """Threaded Chat completion function.""" + ... + + +def _start_completion_thread_pool( + *, + completion: "LLMCompletionFunction", + quit_process_event: threading.Event, + concurrency: int, + queue_limit: int, +) -> tuple[ + list[CompletionThread], + "LLMCompletionRequestQueue", + "LLMCompletionResponseQueue", +]: + threads: list[CompletionThread] = [] + input_queue: LLMCompletionRequestQueue = Queue(queue_limit) + output_queue: LLMCompletionResponseQueue = Queue() + for _ in range(concurrency): + thread = CompletionThread( + quit_process_event=quit_process_event, + input_queue=input_queue, + output_queue=output_queue, + completion=completion, + ) + thread.start() + threads.append(thread) + + return threads, input_queue, output_queue + + +@contextmanager +def completion_thread_runner( + *, + completion: "LLMCompletionFunction", + response_handler: ThreadedLLMCompletionResponseHandler, + concurrency: int, + queue_limit: int = 0, + metrics_store: "MetricsStore | None" = None, +) -> Iterator[ThreadedLLMCompletionFunction]: + """Run a completion thread pool. + + Args + ---- + completion: LLMCompletion + The LLMCompletion instance to use for processing requests. + response_handler: ThreadedLLMCompletionResponseHandler + The callback function to handle completion responses. + (request_id, response|exception) -> Awaitable[None] | None + concurrency: int + The number of threads to spin up in a thread pool. + queue_limit: int (default=0) + The maximum number of items allowed in the input queue. + 0 means unlimited. + Set this to a value to create backpressure on the caller. + metrics_store: MetricsStore | None (default=None) + Optional metrics store to record runtime duration. + + Yields + ------ + ThreadedLLMCompletionFunction: + A function that can be used to submit completion requests to the thread pool. + (messages, request_id, **kwargs) -> None + + The thread pool will process the requests and invoke the provided callback + with the responses. + + same signature as LLMCompletionFunction but requires a `request_id` parameter + to identify the request and does not return anything. + """ + quit_process_event = threading.Event() + threads, input_queue, output_queue = _start_completion_thread_pool( + completion=completion, + quit_process_event=quit_process_event, + concurrency=concurrency, + queue_limit=queue_limit, + ) + + def _process_output( + quit_process_event: threading.Event, + output_queue: "LLMCompletionResponseQueue", + callback: ThreadedLLMCompletionResponseHandler, + ): + while True and not quit_process_event.is_set(): + try: + data = output_queue.get(timeout=1) + except Empty: + continue + if data is None: + break + request_id, response = data + response = callback(request_id, response) + + if asyncio.iscoroutine(response): + response = asyncio.run(response) + + def _process_input(request_id: str, **kwargs: Unpack["LLMCompletionArgs"]): + if not request_id: + msg = "request_id needs to be passed as a keyword argument" + raise ValueError(msg) + input_queue.put((request_id, kwargs)) + + handle_response_thread = threading.Thread( + target=_process_output, + args=(quit_process_event, output_queue, response_handler), + ) + handle_response_thread.start() + + def _cleanup(): + for _ in threads: + input_queue.put(None) + + for thread in threads: + while thread.is_alive(): + thread.join(timeout=1) + + output_queue.put(None) + + while handle_response_thread.is_alive(): + handle_response_thread.join(timeout=1) + + start_time = time.time() + try: + yield _process_input + _cleanup() + except KeyboardInterrupt: + quit_process_event.set() + sys.exit(1) + finally: + end_time = time.time() + runtime = end_time - start_time + if metrics_store: + metrics_store.update_metrics(metrics={"runtime_duration_seconds": runtime}) diff --git a/packages/graphrag-llm/graphrag_llm/threading/embedding_thread.py b/packages/graphrag-llm/graphrag_llm/threading/embedding_thread.py new file mode 100644 index 0000000000..7980d08fc1 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/threading/embedding_thread.py @@ -0,0 +1,88 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Embedding Thread.""" + +import threading +from queue import Empty, Queue +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from graphrag_llm.types import ( + LLMEmbeddingArgs, + LLMEmbeddingFunction, + LLMEmbeddingResponse, + ) + + +LLMEmbeddingRequestQueue = Queue[tuple[str, "LLMEmbeddingArgs"] | None] +"""Input queue for LLM embeddings. + +A queue for tracking requests to be made to an embedding endpoint. +Each item in the queue is a tuple containing a request ID and a dictionary of +embedding arguments. A `None` value indicates that the thread should terminate. + +Queue Item Type: + tuple[request_id, embedding_args_dict] | None + +Items in the queue are processed by a thread pool in which the results are placed +into an output queue to be handled by a response handler. +""" + +LLMEmbeddingResponseQueue = Queue[ + tuple[ + str, + "LLMEmbeddingResponse | Exception", + ] + | None +] +"""Output queue for LLM embedding responses. + +A queue for tracking responses from embedding requests. Each item in the queue is a tuple +containing the request ID and the corresponding response, which can be a full response +or an exception if the request failed. A `None` value indicates that the +thread should terminate. + +Queue Item Type: + tuple[request_id, response | exception] | None + +Items in the queue are produced by a thread pool that processes embedding requests +from an input queue. This output queue is then consumed by a response handler provided +by the user. +""" + + +class EmbeddingThread(threading.Thread): + """Thread for handling LLM embeddings.""" + + def __init__( + self, + *, + quit_process_event: threading.Event, + input_queue: LLMEmbeddingRequestQueue, + output_queue: LLMEmbeddingResponseQueue, + embedding: "LLMEmbeddingFunction", + ) -> None: + super().__init__() + self._quit_process_event = quit_process_event + self._input_queue = input_queue + self._output_queue = output_queue + self._embedding = embedding + + def run(self) -> None: + """Run the embedding thread.""" + while not self._quit_process_event.is_set(): + try: + input_data = self._input_queue.get(timeout=0.1) + except Empty: + continue + + if input_data is None: + break + request_id, data = input_data + try: + response = self._embedding(**data) + + self._output_queue.put((request_id, response)) + except Exception as e: # noqa: BLE001 + self._output_queue.put((request_id, e)) diff --git a/packages/graphrag-llm/graphrag_llm/threading/embedding_thread_runner.py b/packages/graphrag-llm/graphrag_llm/threading/embedding_thread_runner.py new file mode 100644 index 0000000000..995e69ffda --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/threading/embedding_thread_runner.py @@ -0,0 +1,216 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Embedding Thread Runner.""" + +import asyncio +import sys +import threading +import time +from collections.abc import Awaitable, Iterator +from contextlib import contextmanager +from queue import Empty, Queue +from typing import TYPE_CHECKING, Protocol, Unpack, runtime_checkable + +from graphrag_llm.threading.embedding_thread import EmbeddingThread + +if TYPE_CHECKING: + from graphrag_llm.metrics import MetricsStore + from graphrag_llm.threading.embedding_thread import ( + LLMEmbeddingRequestQueue, + LLMEmbeddingResponseQueue, + ) + from graphrag_llm.types import ( + LLMEmbeddingArgs, + LLMEmbeddingFunction, + LLMEmbeddingResponse, + ) + + +@runtime_checkable +class ThreadedLLMEmbeddingResponseHandler(Protocol): + """Threaded embedding response handler. + + This function is used to handle responses from the threaded embedding runner. + + Args + ---- + request_id: str + The request ID associated with the embedding request. + resp: LLMEmbeddingResponse | Exception + The embedding response, which can be a full response or + an exception if the request failed. + + Returns + ------- + Awaitable[None] | None + The callback can be asynchronous or synchronous. + """ + + def __call__( + self, + request_id: str, + response: "LLMEmbeddingResponse | Exception", + /, + ) -> Awaitable[None] | None: + """Threaded embedding response handler.""" + ... + + +@runtime_checkable +class ThreadedLLMEmbeddingFunction(Protocol): + """Threaded embedding function. + + This function is used to make embedding requests in a threaded context. + + Args + ---- + request_id: str + The request ID associated with the embedding request. + input: list[str] + The input texts to be embedded. + **kwargs: Any + Additional keyword arguments. + + Returns + ------- + LLMEmbeddingResponse + The embedding response. + """ + + def __call__( + self, /, request_id: str, **kwargs: Unpack["LLMEmbeddingArgs"] + ) -> None: + """Threaded embedding function.""" + ... + + +def _start_embedding_thread_pool( + *, + embedding: "LLMEmbeddingFunction", + quit_process_event: threading.Event, + concurrency: int, + queue_limit: int, +) -> tuple[ + list["EmbeddingThread"], + "LLMEmbeddingRequestQueue", + "LLMEmbeddingResponseQueue", +]: + threads: list[EmbeddingThread] = [] + input_queue: LLMEmbeddingRequestQueue = Queue(queue_limit) + output_queue: LLMEmbeddingResponseQueue = Queue() + for _ in range(concurrency): + thread = EmbeddingThread( + quit_process_event=quit_process_event, + input_queue=input_queue, + output_queue=output_queue, + embedding=embedding, + ) + thread.start() + threads.append(thread) + + return threads, input_queue, output_queue + + +@contextmanager +def embedding_thread_runner( + *, + embedding: "LLMEmbeddingFunction", + response_handler: ThreadedLLMEmbeddingResponseHandler, + concurrency: int, + queue_limit: int = 0, + metrics_store: "MetricsStore | None" = None, +) -> Iterator[ThreadedLLMEmbeddingFunction]: + """Run an embedding thread pool. + + Args + ---- + embedding: LLMEmbeddingFunction + The LLMEmbeddingFunction instance to use for processing requests. + response_handler: ThreadedLLMEmbeddingResponseHandler + The callback function to handle embedding responses. + (request_id, response|exception) -> Awaitable[None] | None + concurrency: int + The number of threads to spin up in a thread pool. + queue_limit: int (default=0) + The maximum number of items allowed in the input queue. + 0 means unlimited. + Set this to a value to create backpressure on the caller. + metrics_store: MetricsStore | None (default=None) + Optional metrics store to record runtime duration. + + Yields + ------ + ThreadedLLMEmbeddingFunction: + A function that can be used to submit embedding requests to the thread pool. + (input, request_id, **kwargs) -> None + + The thread pool will process the requests and invoke the provided callback + with the responses. + + same signature as LLMEmbeddingFunction but requires a `request_id` parameter + to identify the request and does not return anything. + """ + quit_process_event = threading.Event() + threads, input_queue, output_queue = _start_embedding_thread_pool( + embedding=embedding, + quit_process_event=quit_process_event, + concurrency=concurrency, + queue_limit=queue_limit, + ) + + def _process_output( + quit_process_event: threading.Event, + output_queue: "LLMEmbeddingResponseQueue", + callback: ThreadedLLMEmbeddingResponseHandler, + ): + while True and not quit_process_event.is_set(): + try: + data = output_queue.get(timeout=1) + except Empty: + continue + if data is None: + break + request_id, response = data + response = callback(request_id, response) + + if asyncio.iscoroutine(response): + response = asyncio.run(response) + + def _process_input(request_id: str, **kwargs: Unpack["LLMEmbeddingArgs"]): + if not request_id: + msg = "request_id needs to be passed as a keyword argument" + raise ValueError(msg) + input_queue.put((request_id, kwargs)) + + handle_response_thread = threading.Thread( + target=_process_output, + args=(quit_process_event, output_queue, response_handler), + ) + handle_response_thread.start() + + def _cleanup(): + for _ in threads: + input_queue.put(None) + + for thread in threads: + while thread.is_alive(): + thread.join(timeout=1) + + output_queue.put(None) + + while handle_response_thread.is_alive(): + handle_response_thread.join(timeout=1) + + start_time = time.time() + try: + yield _process_input + _cleanup() + except KeyboardInterrupt: + quit_process_event.set() + sys.exit(1) + finally: + end_time = time.time() + runtime = end_time - start_time + if metrics_store: + metrics_store.update_metrics(metrics={"runtime_duration_seconds": runtime}) diff --git a/packages/graphrag-llm/graphrag_llm/tokenizer/__init__.py b/packages/graphrag-llm/graphrag_llm/tokenizer/__init__.py new file mode 100644 index 0000000000..0010fd2b5f --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/tokenizer/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Tokenizer module.""" + +from graphrag_llm.tokenizer.tokenizer import Tokenizer +from graphrag_llm.tokenizer.tokenizer_factory import ( + create_tokenizer, + register_tokenizer, +) + +__all__ = [ + "Tokenizer", + "create_tokenizer", + "register_tokenizer", +] diff --git a/graphrag/tokenizer/litellm_tokenizer.py b/packages/graphrag-llm/graphrag_llm/tokenizer/lite_llm_tokenizer.py similarity index 56% rename from graphrag/tokenizer/litellm_tokenizer.py rename to packages/graphrag-llm/graphrag_llm/tokenizer/lite_llm_tokenizer.py index 1a85f56086..392f113eba 100644 --- a/graphrag/tokenizer/litellm_tokenizer.py +++ b/packages/graphrag-llm/graphrag_llm/tokenizer/lite_llm_tokenizer.py @@ -3,45 +3,52 @@ """LiteLLM Tokenizer.""" +from typing import Any + from litellm import decode, encode # type: ignore -from graphrag.tokenizer.tokenizer import Tokenizer +from graphrag_llm.tokenizer.tokenizer import Tokenizer -class LitellmTokenizer(Tokenizer): +class LiteLLMTokenizer(Tokenizer): """LiteLLM Tokenizer.""" - def __init__(self, model_name: str) -> None: + _model_id: str + + def __init__(self, *, model_id: str, **kwargs: Any) -> None: """Initialize the LiteLLM Tokenizer. Args ---- - model_name (str): The name of the LiteLLM model to use for tokenization. + model_id: str + The LiteLLM model ID, e.g., "openai/gpt-4o". """ - self.model_name = model_name + self._model_id = model_id def encode(self, text: str) -> list[int]: """Encode the given text into a list of tokens. Args ---- - text (str): The input text to encode. + text: str + The input text to encode. Returns ------- list[int]: A list of tokens representing the encoded text. """ - return encode(model=self.model_name, text=text) + return encode(model=self._model_id, text=text) def decode(self, tokens: list[int]) -> str: """Decode a list of tokens back into a string. Args ---- - tokens (list[int]): A list of tokens to decode. + tokens: list[int] + A list of tokens to decode. Returns ------- str: The decoded string from the list of tokens. """ - return decode(model=self.model_name, tokens=tokens) + return decode(model=self._model_id, tokens=tokens) diff --git a/graphrag/tokenizer/tiktoken_tokenizer.py b/packages/graphrag-llm/graphrag_llm/tokenizer/tiktoken_tokenizer.py similarity index 52% rename from graphrag/tokenizer/tiktoken_tokenizer.py rename to packages/graphrag-llm/graphrag_llm/tokenizer/tiktoken_tokenizer.py index fa6c6e9b43..9388c13b51 100644 --- a/graphrag/tokenizer/tiktoken_tokenizer.py +++ b/packages/graphrag-llm/graphrag_llm/tokenizer/tiktoken_tokenizer.py @@ -1,47 +1,55 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""Tiktoken Tokenizer.""" +"""LiteLLM Tokenizer.""" + +from typing import Any import tiktoken -from graphrag.tokenizer.tokenizer import Tokenizer +from graphrag_llm.tokenizer.tokenizer import Tokenizer class TiktokenTokenizer(Tokenizer): - """Tiktoken Tokenizer.""" + """LiteLLM Tokenizer.""" + + _encoding_name: str - def __init__(self, encoding_name: str) -> None: + def __init__(self, *, encoding_name: str, **kwargs: Any) -> None: """Initialize the Tiktoken Tokenizer. Args ---- - encoding_name (str): The name of the Tiktoken encoding to use for tokenization. + encoding_name: str + The encoding name, e.g., "gpt-4o". """ - self.encoding = tiktoken.get_encoding(encoding_name) + self._encoding_name = encoding_name + self._encoding = tiktoken.get_encoding(encoding_name) def encode(self, text: str) -> list[int]: """Encode the given text into a list of tokens. Args ---- - text (str): The input text to encode. + text: str + The input text to encode. Returns ------- list[int]: A list of tokens representing the encoded text. """ - return self.encoding.encode(text) + return self._encoding.encode(text) def decode(self, tokens: list[int]) -> str: """Decode a list of tokens back into a string. Args ---- - tokens (list[int]): A list of tokens to decode. + tokens: list[int] + A list of tokens to decode. Returns ------- str: The decoded string from the list of tokens. """ - return self.encoding.decode(tokens) + return self._encoding.decode(tokens) diff --git a/packages/graphrag-llm/graphrag_llm/tokenizer/tokenizer.py b/packages/graphrag-llm/graphrag_llm/tokenizer/tokenizer.py new file mode 100644 index 0000000000..37bc3de721 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/tokenizer/tokenizer.py @@ -0,0 +1,111 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Tokenizer Abstract Base Class.""" + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from graphrag_llm.types import LLMCompletionMessagesParam + + +class Tokenizer(ABC): + """Tokenizer Abstract Base Class.""" + + @abstractmethod + def __init__(self, **kwargs: Any) -> None: + """Initialize the LiteLLM Tokenizer.""" + + @abstractmethod + def encode(self, text: str) -> list[int]: + """Encode the given text into a list of tokens. + + Args + ---- + text: str + The input text to encode. + + Returns + ------- + list[int]: A list of tokens representing the encoded text. + """ + raise NotImplementedError + + @abstractmethod + def decode(self, tokens: list[int]) -> str: + """Decode a list of tokens back into a string. + + Args + ---- + tokens: list[int] + A list of tokens to decode. + + Returns + ------- + str: The decoded string from the list of tokens. + """ + raise NotImplementedError + + def num_prompt_tokens( + self, + messages: "LLMCompletionMessagesParam", + ) -> int: + """Count the number of tokens in a prompt for a given model. + + Counts the number of tokens used for roles, names, and content in the messages. + + modeled after: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + + Args + ---- + messages: LLMCompletionMessagesParam + The messages comprising the prompt. Can either be a string or a list of message dicts. + + Returns + ------- + int: The number of tokens in the prompt. + """ + total_tokens = 3 # overhead for reply + tokens_per_message = 3 # fixed overhead per message + tokens_per_name = 1 # fixed overhead per name field + + if isinstance(messages, str): + return ( + self.num_tokens(messages) + + total_tokens + + tokens_per_message + + tokens_per_name + ) + + for message in messages: + total_tokens += tokens_per_message + if not isinstance(message, dict): + message = message.model_dump() + for key, value in message.items(): + if key == "content": + if isinstance(value, str): + total_tokens += self.num_tokens(value) + elif isinstance(value, list): + for part in value: + if isinstance(part, dict) and "text" in part: + total_tokens += self.num_tokens(part["text"]) + elif key == "role": + total_tokens += self.num_tokens(str(value)) + elif key == "name": + total_tokens += self.num_tokens(str(value)) + tokens_per_name + return total_tokens + + def num_tokens(self, text: str) -> int: + """Return the number of tokens in the given text. + + Args + ---- + text: str + The input text to analyze. + + Returns + ------- + int: The number of tokens in the input text. + """ + return len(self.encode(text)) diff --git a/packages/graphrag-llm/graphrag_llm/tokenizer/tokenizer_factory.py b/packages/graphrag-llm/graphrag_llm/tokenizer/tokenizer_factory.py new file mode 100644 index 0000000000..929991c616 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/tokenizer/tokenizer_factory.py @@ -0,0 +1,89 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Tokenizer factory.""" + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from graphrag_common.factory import Factory + +from graphrag_llm.config.types import TokenizerType +from graphrag_llm.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_common.factory import ServiceScope + + from graphrag_llm.config.tokenizer_config import TokenizerConfig + + +class TokenizerFactory(Factory[Tokenizer]): + """Factory for creating Tokenizer instances.""" + + +tokenizer_factory = TokenizerFactory() + + +def register_tokenizer( + tokenizer_type: str, + tokenizer_initializer: Callable[..., Tokenizer], + scope: "ServiceScope" = "transient", +) -> None: + """Register a custom tokenizer implementation. + + Args + ---- + tokenizer_type: str + The tokenizer id to register. + tokenizer_initializer: Callable[..., Tokenizer] + The tokenizer initializer to register. + """ + tokenizer_factory.register(tokenizer_type, tokenizer_initializer, scope) + + +def create_tokenizer(tokenizer_config: "TokenizerConfig") -> Tokenizer: + """Create a Tokenizer instance based on the configuration. + + Args + ---- + tokenizer_config: TokenizerConfig + The configuration for the tokenizer. + + Returns + ------- + Tokenizer: + An instance of a Tokenizer subclass. + """ + strategy = tokenizer_config.type + init_args = tokenizer_config.model_dump() + + if strategy not in tokenizer_factory: + match strategy: + case TokenizerType.LiteLLM: + from graphrag_llm.tokenizer.lite_llm_tokenizer import ( + LiteLLMTokenizer, + ) + + register_tokenizer( + TokenizerType.LiteLLM, + LiteLLMTokenizer, + scope="singleton", + ) + case TokenizerType.Tiktoken: + from graphrag_llm.tokenizer.tiktoken_tokenizer import ( + TiktokenTokenizer, + ) + + register_tokenizer( + TokenizerType.Tiktoken, + TiktokenTokenizer, + scope="singleton", + ) + case _: + msg = f"TokenizerConfig.type '{strategy}' is not registered in the TokenizerFactory. Registered strategies: {', '.join(tokenizer_factory.keys())}" + raise ValueError(msg) + + return tokenizer_factory.create( + strategy=strategy, + init_args=init_args, + ) diff --git a/packages/graphrag-llm/graphrag_llm/types/__init__.py b/packages/graphrag-llm/graphrag_llm/types/__init__.py new file mode 100644 index 0000000000..6453ff6163 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/types/__init__.py @@ -0,0 +1,58 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Types module for graphrag-llm.""" + +from graphrag_llm.types.types import ( + AsyncLLMCompletionFunction, + AsyncLLMEmbeddingFunction, + AsyncLLMFunction, + LLMChoice, + LLMChoiceChunk, + LLMChoiceDelta, + LLMCompletionArgs, + LLMCompletionChunk, + LLMCompletionFunction, + LLMCompletionFunctionToolParam, + LLMCompletionMessage, + LLMCompletionMessagesParam, + LLMCompletionResponse, + LLMCompletionTokensDetails, + LLMCompletionUsage, + LLMEmbedding, + LLMEmbeddingArgs, + LLMEmbeddingFunction, + LLMEmbeddingResponse, + LLMEmbeddingUsage, + LLMFunction, + LLMPromptTokensDetails, + Metrics, + ResponseFormat, +) + +__all__ = [ + "AsyncLLMCompletionFunction", + "AsyncLLMEmbeddingFunction", + "AsyncLLMFunction", + "LLMChoice", + "LLMChoiceChunk", + "LLMChoiceDelta", + "LLMCompletionArgs", + "LLMCompletionChunk", + "LLMCompletionFunction", + "LLMCompletionFunctionToolParam", + "LLMCompletionMessage", + "LLMCompletionMessagesParam", + "LLMCompletionResponse", + "LLMCompletionTokensDetails", + "LLMCompletionUsage", + "LLMEmbedding", + "LLMEmbeddingArgs", + "LLMEmbeddingFunction", + "LLMEmbeddingResponse", + "LLMEmbeddingUsage", + "LLMFunction", + "LLMPromptTokensDetails", + "Metrics", + "ResponseFormat", +] diff --git a/packages/graphrag-llm/graphrag_llm/types/types.py b/packages/graphrag-llm/graphrag_llm/types/types.py new file mode 100644 index 0000000000..0980cba3aa --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/types/types.py @@ -0,0 +1,265 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Types for graphrag-llm.""" + +from collections.abc import AsyncIterator, Awaitable, Iterator, Sequence +from typing import ( + Any, + Generic, + Literal, + Protocol, + Required, + TypeVar, + Unpack, + runtime_checkable, +) + +from litellm import ( + AnthropicThinkingParam, + ChatCompletionAudioParam, + ChatCompletionModality, + ChatCompletionPredictionContentParam, + OpenAIWebSearchOptions, +) +from openai.types.chat.chat_completion import ( + ChatCompletion, + Choice, +) +from openai.types.chat.chat_completion_chunk import ChatCompletionChunk, ChoiceDelta +from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice +from openai.types.chat.chat_completion_function_tool_param import ( + ChatCompletionFunctionToolParam, +) +from openai.types.chat.chat_completion_message import ChatCompletionMessage +from openai.types.chat.chat_completion_message_param import ChatCompletionMessageParam +from openai.types.completion_usage import ( + CompletionTokensDetails, + CompletionUsage, + PromptTokensDetails, +) +from openai.types.create_embedding_response import CreateEmbeddingResponse, Usage +from openai.types.embedding import Embedding +from pydantic import BaseModel, computed_field +from typing_extensions import TypedDict + +LLMCompletionMessagesParam = str | Sequence[ChatCompletionMessageParam | dict[str, Any]] + +LLMChoice = Choice +LLMCompletionMessage = ChatCompletionMessage + +LLMCompletionChunk = ChatCompletionChunk +LLMChoiceChunk = ChunkChoice +LLMChoiceDelta = ChoiceDelta + +LLMCompletionUsage = CompletionUsage +LLMPromptTokensDetails = PromptTokensDetails +LLMCompletionTokensDetails = CompletionTokensDetails + + +LLMEmbedding = Embedding +LLMEmbeddingUsage = Usage + +LLMCompletionFunctionToolParam = ChatCompletionFunctionToolParam + + +Metrics = dict[str, float] +"""Represents single request metrics and aggregated metrics for an entire model. + +example: { + "duration_ms": 123.45, + "successful_requests": 1, +} + +On the individual request level, successful_requests will be either 0 or 1. +On the aggregated model level, successful_requests will be the sum of all +successful requests. +""" + +ResponseFormat = TypeVar( + "ResponseFormat", + bound=BaseModel, +) +"""Generic type variable for structured response format.""" + + +class LLMCompletionResponse(ChatCompletion, Generic[ResponseFormat]): + """LLM Completion Response extending OpenAI ChatCompletion. + + The response type returned by graphrag-llm LLMCompletionFunction. + graphrag-llm automatically handles structured response parsing based on the + provided ResponseFormat model. + """ + + formatted_response: ResponseFormat | None = None # type: ignore + """Formatted response according to the specified response_format json schema.""" + + @computed_field + @property + def content(self) -> str: + """Get the content of the first choice message.""" + return self.choices[0].message.content or "" + + +class LLMCompletionArgs( + TypedDict, Generic[ResponseFormat], total=False, extra_items=Any +): + """Arguments for LLMCompletionFunction. + + Same signature as litellm.completion but without the `model` parameter + as this is already set in the model configuration. + """ + + messages: Required[LLMCompletionMessagesParam] + response_format: type[ResponseFormat] | None + timeout: float | None + temperature: float | None + top_p: float | None + n: int | None + stream: bool | None + stream_options: dict | None + stop: None + max_completion_tokens: int | None + max_tokens: int | None + modalities: list[ChatCompletionModality] | None + prediction: ChatCompletionPredictionContentParam | None + audio: ChatCompletionAudioParam | None + presence_penalty: float | None + frequency_penalty: float | None + logit_bias: dict | None + user: str | None + reasoning_effort: ( + Literal["none", "minimal", "low", "medium", "high", "default"] | None + ) + seed: int | None + tools: list | None + tool_choice: str | dict | None + logprobs: bool | None + top_logprobs: int | None + parallel_tool_calls: bool | None + web_search_options: OpenAIWebSearchOptions | None + deployment_id: Any + extra_headers: dict | None + safety_identifier: str | None + functions: list | None + function_call: str | None + thinking: AnthropicThinkingParam | None + + +@runtime_checkable +class LLMCompletionFunction(Protocol): + """Synchronous completion function. + + Same signature as litellm.completion but without the `model` parameter + as this is already set in the model configuration. + """ + + def __call__( + self, /, **kwargs: Unpack[LLMCompletionArgs[ResponseFormat]] + ) -> LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk]: + """Completion function.""" + ... + + +@runtime_checkable +class AsyncLLMCompletionFunction(Protocol): + """Asynchronous completion function. + + Same signature as litellm.completion but without the `model` parameter + as this is already set in the model configuration. + """ + + def __call__( + self, /, **kwargs: Unpack[LLMCompletionArgs[ResponseFormat]] + ) -> Awaitable[ + LLMCompletionResponse[ResponseFormat] | AsyncIterator[LLMCompletionChunk] + ]: + """Completion function.""" + ... + + +class LLMEmbeddingResponse(CreateEmbeddingResponse): + """LLM Embedding Response extending OpenAI CreateEmbeddingResponse. + + The response type returned by graphrag-llm LLMEmbeddingFunction. + Adds utilities for accessing embeddings. + """ + + @computed_field + @property + def embeddings(self) -> list[list[float]]: + """Get the embeddings as a list of lists of floats.""" + return [data.embedding for data in self.data] + + @computed_field + @property + def first_embedding(self) -> list[float]: + """Get the first embedding.""" + return self.embeddings[0] if self.embeddings else [] + + +class LLMEmbeddingArgs(TypedDict, total=False, extra_items=Any): + """Arguments for embedding functions. + + Same signature as litellm.embedding but without the `model` parameter + as this is already set in the model configuration. + """ + + input: Required[list[str]] + dimensions: int | None + encoding_format: str | None + timeout: int + user: str | None + + +@runtime_checkable +class LLMEmbeddingFunction(Protocol): + """Synchronous embedding function. + + Same signature as litellm.embedding but without the `model` parameter + as this is already set in the model configuration. + """ + + def __call__( + self, + /, + **kwargs: Unpack[LLMEmbeddingArgs], + ) -> LLMEmbeddingResponse: + """Embedding function.""" + ... + + +@runtime_checkable +class AsyncLLMEmbeddingFunction(Protocol): + """Asynchronous embedding function. + + Same signature as litellm.aembedding but without the `model` parameter + as this is already set in the model configuration. + """ + + async def __call__( + self, + /, + **kwargs: Unpack[LLMEmbeddingArgs], + ) -> LLMEmbeddingResponse: + """Embedding function.""" + ... + + +LLMFunction = TypeVar("LLMFunction", LLMCompletionFunction, LLMEmbeddingFunction) +"""Generic representation of completion and embedding functions. + +This type is used in the middleware pipeline as the pipeline can handle both +completion and embedding functions. That way services such as retries, caching, +and rate limiting can be reused for both completions and embeddings. +""" + +AsyncLLMFunction = TypeVar( + "AsyncLLMFunction", AsyncLLMCompletionFunction, AsyncLLMEmbeddingFunction +) +"""Generic representation of asynchronous completion and embedding functions. + +This type is used in the middleware pipeline as the pipeline can handle both +completion and embedding functions. That way services such as retries, caching, +and rate limiting can be reused for both completions and embeddings. +""" diff --git a/packages/graphrag-llm/graphrag_llm/utils/__init__.py b/packages/graphrag-llm/graphrag_llm/utils/__init__.py new file mode 100644 index 0000000000..8ae722bfbc --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/utils/__init__.py @@ -0,0 +1,40 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Utils module.""" + +from graphrag_llm.utils.completion_messages_builder import ( + CompletionContentPartBuilder, + CompletionMessagesBuilder, +) +from graphrag_llm.utils.create_completion_response import ( + create_completion_response, +) +from graphrag_llm.utils.create_embedding_response import create_embedding_response +from graphrag_llm.utils.function_tool_manager import ( + FunctionArgumentModel, + FunctionDefinition, + FunctionToolManager, + ToolMessage, +) +from graphrag_llm.utils.gather_completion_response import ( + gather_completion_response, + gather_completion_response_async, +) +from graphrag_llm.utils.structure_response import ( + structure_completion_response, +) + +__all__ = [ + "CompletionContentPartBuilder", + "CompletionMessagesBuilder", + "FunctionArgumentModel", + "FunctionDefinition", + "FunctionToolManager", + "ToolMessage", + "create_completion_response", + "create_embedding_response", + "gather_completion_response", + "gather_completion_response_async", + "structure_completion_response", +] diff --git a/packages/graphrag-llm/graphrag_llm/utils/completion_messages_builder.py b/packages/graphrag-llm/graphrag_llm/utils/completion_messages_builder.py new file mode 100644 index 0000000000..f61e60e6c4 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/utils/completion_messages_builder.py @@ -0,0 +1,328 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""ChatCompletionMessageParamBuilder class.""" + +from collections.abc import Iterable +from typing import TYPE_CHECKING, Literal + +from openai.types.chat.chat_completion_assistant_message_param import ( + ChatCompletionAssistantMessageParam, +) +from openai.types.chat.chat_completion_content_part_image_param import ( + ChatCompletionContentPartImageParam, + ImageURL, +) +from openai.types.chat.chat_completion_content_part_input_audio_param import ( + ChatCompletionContentPartInputAudioParam, + InputAudio, +) +from openai.types.chat.chat_completion_content_part_param import ( + ChatCompletionContentPartParam, +) +from openai.types.chat.chat_completion_content_part_text_param import ( + ChatCompletionContentPartTextParam, +) +from openai.types.chat.chat_completion_developer_message_param import ( + ChatCompletionDeveloperMessageParam, +) +from openai.types.chat.chat_completion_function_message_param import ( + ChatCompletionFunctionMessageParam, +) +from openai.types.chat.chat_completion_message import ChatCompletionMessage +from openai.types.chat.chat_completion_system_message_param import ( + ChatCompletionSystemMessageParam, +) +from openai.types.chat.chat_completion_tool_message_param import ( + ChatCompletionToolMessageParam, +) +from openai.types.chat.chat_completion_user_message_param import ( + ChatCompletionUserMessageParam, +) + +if TYPE_CHECKING: + from openai.types.chat.chat_completion_message_param import ( + ChatCompletionMessageParam, + ) + + from graphrag_llm.types import LLMCompletionMessagesParam + + +class CompletionMessagesBuilder: + """CompletionMessagesBuilder class.""" + + def __init__(self) -> None: + """Initialize CompletionMessagesBuilder.""" + self._messages: list[ChatCompletionMessageParam] = [] + + def add_system_message( + self, + content: str | Iterable[ChatCompletionContentPartTextParam], + name: str | None = None, + ) -> "CompletionMessagesBuilder": + """Add system message. + + Parameters + ---------- + content : str | Iterable[ChatCompletionContentPartTextParam] + Content of the system message. + If passing in Iterable[ChatCompletionContentPartTextParam], may use + `CompletionContentPartBuilder` to build the content. + name : str | None + Optional name for the participant. + + Returns + ------- + None + """ + if name: + self._messages.append( + ChatCompletionSystemMessageParam( + role="system", content=content, name=name + ) + ) + else: + self._messages.append( + ChatCompletionSystemMessageParam(role="system", content=content) + ) + return self + + def add_developer_message( + self, + content: str | Iterable[ChatCompletionContentPartTextParam], + name: str | None = None, + ) -> "CompletionMessagesBuilder": + """Add developer message. + + Parameters + ---------- + content : str | Iterable[ChatCompletionContentPartTextParam] + Content of the developer message. + If passing in Iterable[ChatCompletionContentPartTextParam], may use + `CompletionContentPartBuilder` to build the content. + name : str | None + Optional name for the participant. + + Returns + ------- + None + """ + if name: + self._messages.append( + ChatCompletionDeveloperMessageParam( + role="developer", content=content, name=name + ) + ) + else: + self._messages.append( + ChatCompletionDeveloperMessageParam(role="developer", content=content) + ) + + return self + + def add_tool_message( + self, + content: str | Iterable[ChatCompletionContentPartTextParam], + tool_call_id: str, + ) -> "CompletionMessagesBuilder": + """Add developer message. + + Parameters + ---------- + content : str | Iterable[ChatCompletionContentPartTextParam] + Content of the developer message. + If passing in Iterable[ChatCompletionContentPartTextParam], may use + `CompletionContentPartBuilder` to build the content. + tool_call_id : str + ID of the tool call that this message is responding to. + + Returns + ------- + None + """ + self._messages.append( + ChatCompletionToolMessageParam( + role="tool", content=content, tool_call_id=tool_call_id + ) + ) + + return self + + def add_function_message( + self, + function_name: str, + content: str | None = None, + ) -> "CompletionMessagesBuilder": + """Add function message. + + Parameters + ---------- + function_name : str + Name of the function to call. + content : str | None + Content of the function message. + + Returns + ------- + None + """ + self._messages.append( + ChatCompletionFunctionMessageParam( + role="function", content=content, name=function_name + ) + ) + + return self + + def add_user_message( + self, + content: str | Iterable[ChatCompletionContentPartParam], + name: str | None = None, + ) -> "CompletionMessagesBuilder": + """Add user message. + + Parameters + ---------- + content : str | Iterable[ChatCompletionContentPartParam] + Content of the user message. + If passing in Iterable[ChatCompletionContentPartParam], may use + `CompletionContentPartBuilder` to build the content. + name : str | None + Optional name for the participant. + + Returns + ------- + None + """ + if name: + self._messages.append( + ChatCompletionUserMessageParam(role="user", content=content, name=name) + ) + else: + self._messages.append( + ChatCompletionUserMessageParam(role="user", content=content) + ) + + return self + + def add_assistant_message( + self, + message: str | ChatCompletionMessage, + name: str | None = None, + ) -> "CompletionMessagesBuilder": + """Add assistant message. + + Parameters + ---------- + message : ChatCompletionMessage + Previous response message. + name : str | None + Optional name for the participant. + + Returns + ------- + None + """ + args = { + "role": "assistant", + "content": message if isinstance(message, str) else message.content, + "refusal": None if isinstance(message, str) else message.refusal, + } + if name: + args["name"] = name + if not isinstance(message, str): + if message.function_call: + args["function_call"] = message.function_call + if message.tool_calls: + args["tool_calls"] = message.tool_calls + if message.audio: + args["audio"] = message.audio + + self._messages.append(ChatCompletionAssistantMessageParam(**args)) + + return self + + def build(self) -> "LLMCompletionMessagesParam": + """Get messages.""" + return self._messages + + +class CompletionContentPartBuilder: + """CompletionContentPartBuilder class.""" + + def __init__(self) -> None: + """Initialize CompletionContentPartBuilder.""" + self._content_parts: list[ChatCompletionContentPartParam] = [] + + def add_text_part(self, text: str) -> "CompletionContentPartBuilder": + """Add text part. + + Parameters + ---------- + text : str + Text content. + + Returns + ------- + None + """ + self._content_parts.append( + ChatCompletionContentPartTextParam(text=text, type="text") + ) + return self + + def add_image_part( + self, url: str, detail: Literal["auto", "low", "high"] + ) -> "CompletionContentPartBuilder": + """Add image part. + + Parameters + ---------- + url : str + Either an URL of the image or the base64 encoded image data. + detail : Literal["auto", "low", "high"] + Specifies the detail level of the image. + + Returns + ------- + None + """ + self._content_parts.append( + ChatCompletionContentPartImageParam( + image_url=ImageURL(url=url, detail=detail), type="image_url" + ) + ) + return self + + def add_audio_part( + self, data: str, _format: Literal["wav", "mp3"] + ) -> "CompletionContentPartBuilder": + """Add audio part. + + Parameters + ---------- + data : str + Base64 encoded audio data. + _format : Literal["wav", "mp3"] + The format of the encoded audio data. Currently supports "wav" and "mp3". + + Returns + ------- + None + """ + self._content_parts.append( + ChatCompletionContentPartInputAudioParam( + input_audio=InputAudio(data=data, format=_format), type="input_audio" + ) + ) + return self + + def build(self) -> list[ChatCompletionContentPartParam]: + """Get content parts. + + Returns + ------- + list[ChatCompletionContentPartParam] + List of content parts. + """ + return self._content_parts diff --git a/packages/graphrag-llm/graphrag_llm/utils/create_completion_response.py b/packages/graphrag-llm/graphrag_llm/utils/create_completion_response.py new file mode 100644 index 0000000000..7f7ffd9091 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/utils/create_completion_response.py @@ -0,0 +1,45 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Create completion response.""" + +from graphrag_llm.types import ( + LLMChoice, + LLMCompletionMessage, + LLMCompletionResponse, + LLMCompletionUsage, +) + + +def create_completion_response(response: str) -> LLMCompletionResponse: + """Create a completion response object. + + Args: + response: The completion response string. + + Returns + ------- + LLMCompletionResponse: The completion response object. + """ + return LLMCompletionResponse( + id="completion-id", + object="chat.completion", + created=0, + model="mock-model", + choices=[ + LLMChoice( + index=0, + message=LLMCompletionMessage( + role="assistant", + content=response, + ), + finish_reason="stop", + ) + ], + usage=LLMCompletionUsage( + prompt_tokens=0, + completion_tokens=0, + total_tokens=0, + ), + formatted_response=None, + ) diff --git a/packages/graphrag-llm/graphrag_llm/utils/create_embedding_response.py b/packages/graphrag-llm/graphrag_llm/utils/create_embedding_response.py new file mode 100644 index 0000000000..b651a32816 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/utils/create_embedding_response.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Create embedding response utilities.""" + +from graphrag_llm.types import LLMEmbedding, LLMEmbeddingResponse, LLMEmbeddingUsage + + +def create_embedding_response( + embeddings: list[float], batch_size: int = 1 +) -> LLMEmbeddingResponse: + """Create a CreateEmbeddingResponse object. + + Args: + embeddings: List of embedding vectors. + model: The model used to create the embeddings. + + Returns + ------- + An LLMEmbeddingResponse object. + """ + embeddings_objects = [ + LLMEmbedding( + object="embedding", + embedding=embeddings, + index=index, + ) + for index in range(batch_size) + ] + + return LLMEmbeddingResponse( + object="list", + data=embeddings_objects, + model="mock-model", + usage=LLMEmbeddingUsage( + prompt_tokens=0, + total_tokens=0, + ), + ) diff --git a/packages/graphrag-llm/graphrag_llm/utils/function_tool_manager.py b/packages/graphrag-llm/graphrag_llm/utils/function_tool_manager.py new file mode 100644 index 0000000000..029008c6b0 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/utils/function_tool_manager.py @@ -0,0 +1,138 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Function tool manager.""" + +import json +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from openai import pydantic_function_tool +from pydantic import BaseModel +from typing_extensions import TypedDict + +if TYPE_CHECKING: + from graphrag_llm.types import LLMCompletionFunctionToolParam, LLMCompletionResponse + +FunctionArgumentModel = TypeVar( + "FunctionArgumentModel", bound=BaseModel, covariant=True +) + + +class FunctionDefinition(TypedDict, Generic[FunctionArgumentModel]): + """Function definition.""" + + name: str + description: str + input_model: type[FunctionArgumentModel] + function: Callable[[FunctionArgumentModel], str] + + +class ToolMessage(TypedDict): + """Function tool response message to be added to message history.""" + + content: str + tool_call_id: str + + +class FunctionToolManager: + """Function tool manager.""" + + _tools: dict[str, FunctionDefinition[Any]] + + def __init__(self) -> None: + """Initialize FunctionToolManager.""" + self._tools = {} + + def register_function_tool( + self, + *, + name: str, + description: str, + input_model: type[FunctionArgumentModel], + function: Callable[[FunctionArgumentModel], str], + ) -> None: + """Register function tool. + + Args + ---- + name: str + The name of the function tool. + description: str + The description of the function tool. + input_model: type[T] + The pydantic model type for the function tool input. + function: Callable[[T], str] + The function to call for the function tool. + """ + self._tools[name] = { + "name": name, + "description": description, + "input_model": input_model, + "function": function, + } + + def definitions(self) -> list["LLMCompletionFunctionToolParam"]: + """Get function tool definitions. + + Returns + ------- + list[LLMCompletionFunctionToolParam] + List of function tool definitions. + """ + return [ + pydantic_function_tool( + tool_def["input_model"], + name=tool_def["name"], + description=tool_def["description"], + ) + for tool_def in self._tools.values() + ] + + def call_functions(self, response: "LLMCompletionResponse") -> list[ToolMessage]: + """Call functions based on the response. + + Args + ---- + response: LLMCompletionResponse + The LLM completion response. + + Returns + ------- + list[ToolMessage] + The list of tool response messages to be added to the message history. + """ + if not response.choices[0].message.tool_calls: + return [] + + tool_messages: list[ToolMessage] = [] + + for tool_call in response.choices[0].message.tool_calls: + if tool_call.type != "function": + continue + tool_id = tool_call.id + function_name = tool_call.function.name + function_args = tool_call.function.arguments + + if function_name not in self._tools: + msg = f"Function '{function_name}' not registered." + raise ValueError(msg) + + tool_def = self._tools[function_name] + input_model = tool_def["input_model"] + function = tool_def["function"] + + try: + parsed_args_dict = json.loads(function_args) + input_model_instance = input_model(**parsed_args_dict) + except Exception as e: + msg = f"Failed to parse arguments for function '{function_name}': {e}" + raise ValueError(msg) from e + + result = function(input_model_instance) + tool_messages.append({ + "content": result, + "tool_call_id": tool_id, + }) + + return tool_messages diff --git a/packages/graphrag-llm/graphrag_llm/utils/gather_completion_response.py b/packages/graphrag-llm/graphrag_llm/utils/gather_completion_response.py new file mode 100644 index 0000000000..0722e95efd --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/utils/gather_completion_response.py @@ -0,0 +1,57 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Gather Completion Response Utility.""" + +from collections.abc import AsyncIterator, Iterator +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from graphrag_llm.types import ( + LLMCompletionChunk, + LLMCompletionResponse, + ) + + +def gather_completion_response( + response: "LLMCompletionResponse | Iterator[LLMCompletionChunk]", +) -> str: + """Gather completion response from an iterator of response chunks. + + Args + ---- + response: LMChatCompletion | Iterator[LLMChatCompletionChunk] + The completion response or an iterator of response chunks. + + Returns + ------- + The gathered response as a single string. + """ + if isinstance(response, Iterator): + return "".join(chunk.choices[0].delta.content or "" for chunk in response) + + return response.choices[0].message.content or "" + + +async def gather_completion_response_async( + response: "LLMCompletionResponse | AsyncIterator[LLMCompletionChunk]", +) -> str: + """Gather completion response from an iterator of response chunks. + + Args + ---- + response: LMChatCompletion | AsyncIterator[LLMChatCompletionChunk] + The completion response or an iterator of response chunks. + + Returns + ------- + The gathered response as a single string. + """ + if isinstance(response, AsyncIterator): + gathered_content = "" + async for chunk in response: + gathered_content += chunk.choices[0].delta.content or "" + + return gathered_content + + return response.choices[0].message.content or "" diff --git a/packages/graphrag-llm/graphrag_llm/utils/structure_response.py b/packages/graphrag-llm/graphrag_llm/utils/structure_response.py new file mode 100644 index 0000000000..dfc261bc87 --- /dev/null +++ b/packages/graphrag-llm/graphrag_llm/utils/structure_response.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Structure response as pydantic base model.""" + +import json +from typing import Any, TypeVar + +from pydantic import BaseModel + +T = TypeVar("T", bound=BaseModel, covariant=True) + + +def structure_completion_response(response: str, model: type[T]) -> T: + """Structure completion response as pydantic base model. + + Args + ---- + response: str + The completion response as a JSON string. + model: type[T] + The pydantic base model type to structure the response into. + + Returns + ------- + The structured response as a pydantic base model. + """ + parsed_dict: dict[str, Any] = json.loads(response) + return model(**parsed_dict) diff --git a/packages/graphrag-llm/notebooks/01_basic.ipynb b/packages/graphrag-llm/notebooks/01_basic.ipynb new file mode 100644 index 0000000000..d015231e35 --- /dev/null +++ b/packages/graphrag-llm/notebooks/01_basic.ipynb @@ -0,0 +1,415 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6e35563a", + "metadata": {}, + "source": [ + "# Basic Completion and Embedding Examples\n", + "\n", + "## Completion\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "aa03e40d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The capital of France is Paris.\n", + "The capital of France is Paris.\n", + "Full Response:\n", + "{\n", + " \"id\": \"chatcmpl-CyPuxOjKPmvuCvJwTJiLRH1lwO77J\",\n", + " \"choices\": [\n", + " {\n", + " \"finish_reason\": \"stop\",\n", + " \"index\": 0,\n", + " \"logprobs\": null,\n", + " \"message\": {\n", + " \"content\": \"The capital of France is Paris.\",\n", + " \"refusal\": null,\n", + " \"role\": \"assistant\",\n", + " \"annotations\": [],\n", + " \"audio\": null,\n", + " \"function_call\": null,\n", + " \"tool_calls\": null\n", + " },\n", + " \"provider_specific_fields\": {}\n", + " }\n", + " ],\n", + " \"created\": 1768515343,\n", + " \"model\": \"gpt-4o-2024-05-13\",\n", + " \"object\": \"chat.completion\",\n", + " \"service_tier\": null,\n", + " \"system_fingerprint\": \"fp_3eed281ddb\",\n", + " \"usage\": {\n", + " \"completion_tokens\": 8,\n", + " \"prompt_tokens\": 14,\n", + " \"total_tokens\": 22,\n", + " \"completion_tokens_details\": {\n", + " \"accepted_prediction_tokens\": 0,\n", + " \"audio_tokens\": 0,\n", + " \"reasoning_tokens\": 0,\n", + " \"rejected_prediction_tokens\": 0,\n", + " \"text_tokens\": null\n", + " },\n", + " \"prompt_tokens_details\": {\n", + " \"audio_tokens\": 0,\n", + " \"cached_tokens\": 0,\n", + " \"text_tokens\": null,\n", + " \"image_tokens\": null\n", + " }\n", + " },\n", + " \"formatted_response\": null,\n", + " \"content\": \"The capital of France is Paris.\"\n", + "}\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import os\n", + "from collections.abc import AsyncIterator, Iterator\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig\n", + "from graphrag_llm.types import LLMCompletionChunk, LLMCompletionResponse\n", + "\n", + "load_dotenv()\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "response: LLMCompletionResponse | Iterator[LLMCompletionChunk] = (\n", + " llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + " )\n", + ")\n", + "\n", + "if isinstance(response, Iterator):\n", + " # Streaming response\n", + " for chunk in response:\n", + " print(chunk.choices[0].delta.content or \"\", end=\"\", flush=True)\n", + "else:\n", + " # Non-streaming response\n", + " print(response.choices[0].message.content)\n", + " # Or alternatively, access via the content property\n", + " # This is equivalent to the above line, getting the content of the first choice\n", + " print(response.content)\n", + "\n", + "print(\"Full Response:\")\n", + "print(response.model_dump_json(indent=2)) # type: ignore" + ] + }, + { + "cell_type": "markdown", + "id": "558392ce", + "metadata": {}, + "source": [ + "## Async Completion\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8405fcb7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The capital of France is Paris.\n" + ] + } + ], + "source": [ + "response: LLMCompletionResponse = await llm_completion.completion_async(\n", + " messages=\"What is the capital of France?\",\n", + ") # type: ignore\n", + "print(response.content)" + ] + }, + { + "cell_type": "markdown", + "id": "e70fc49a", + "metadata": {}, + "source": [ + "## Streaming Completion\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9f60c4e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The capital of France is Paris." + ] + } + ], + "source": [ + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + " stream=True,\n", + ")\n", + "\n", + "if isinstance(response, Iterator):\n", + " # Streaming response\n", + " for chunk in response:\n", + " print(chunk.choices[0].delta.content or \"\", end=\"\", flush=True)" + ] + }, + { + "cell_type": "markdown", + "id": "fe8c2e35", + "metadata": {}, + "source": [ + "## Async Streaming Completion\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0be849ce", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The capital of France is Paris." + ] + } + ], + "source": [ + "response = await llm_completion.completion_async(\n", + " messages=\"What is the capital of France?\",\n", + " stream=True,\n", + ")\n", + "\n", + "if isinstance(response, AsyncIterator):\n", + " # Streaming response\n", + " async for chunk in response:\n", + " print(chunk.choices[0].delta.content or \"\", end=\"\", flush=True)" + ] + }, + { + "cell_type": "markdown", + "id": "c32070ad", + "metadata": {}, + "source": [ + "## Completion Arguments\n", + "\n", + "The completion API adheres to litellm completion API and thus the OpanAI SDK API. The `messages` parameter can be one of the following:\n", + "\n", + "- `str`: Raw string for the prompt.\n", + "- `list[dict[str, Any]]`: A list of dicts in the form `{\"role\": \"user|system|...\", \"content\": \"...\"}`\n", + "- `list[ChatCompletionMessageParam]`: A list of OpenAI `ChatCompletionMessageParam`. `graphrag_llm.utils` provides a `ChatCompletionMessageParamBuilder` to help construct these objects. See the message builder notebook for more details on using `ChatCompletionMessageParamBuilder`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "8fe480cb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The capital of France is Paris.\n", + "The capital of France is Paris.\n", + "Arrr, ye got me there, matey! Truth be, back in 2006, them fancy scallywags at the International Astronomical Union be sayin' Pluto ain't a full-fledged planet no more. They be callin' it a \"dwarf planet\" now. So, officially, she be a dwarf planet, savvy?\n" + ] + } + ], + "source": [ + "from graphrag_llm.utils import (\n", + " CompletionMessagesBuilder,\n", + ")\n", + "\n", + "# raw string input\n", + "response1: LLMCompletionResponse = llm_completion.completion(\n", + " messages=\"What is the capital of France?\"\n", + ") # type: ignore\n", + "print(response1.content)\n", + "\n", + "# list of message dicts input\n", + "response2: LLMCompletionResponse = llm_completion.completion(\n", + " messages=[{\"role\": \"user\", \"content\": \"What is the capital of France?\"}]\n", + ") # type: ignore\n", + "print(response2.content)\n", + "\n", + "# using the builder to create complex message\n", + "messages = (\n", + " CompletionMessagesBuilder()\n", + " .add_system_message(\n", + " \"You are a helpful assistant that likes to talk like a pirate. Respond as if you are a pirate using pirate speak.\"\n", + " )\n", + " .add_user_message(\"Is pluto a planet? Respond with a yes or no.\")\n", + " .add_assistant_message(\"Aye, matey! Pluto be a planet in me book.\")\n", + " .add_user_message(\"Are you sure? I want the truth. Can you elaborate?\")\n", + " .build()\n", + ")\n", + "\n", + "response3: LLMCompletionResponse = llm_completion.completion(messages=messages) # type: ignore\n", + "print(response3.content)" + ] + }, + { + "cell_type": "markdown", + "id": "dda66594", + "metadata": {}, + "source": [ + "## Embedding\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "51fe336b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[-0.002078542485833168, -0.04908587411046028, 0.020946789532899857]\n", + "[0.027567066252231598, -0.026544300839304924, -0.027091361582279205]\n" + ] + } + ], + "source": [ + "from graphrag_llm.embedding import LLMEmbedding, create_embedding\n", + "from graphrag_llm.types import LLMEmbeddingResponse\n", + "\n", + "embedding_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_EMBEDDING_MODEL\", \"text-embedding-3-small\"),\n", + " azure_deployment_name=os.getenv(\n", + " \"GRAPHRAG_LLM_EMBEDDING_MODEL\", \"text-embedding-3-small\"\n", + " ),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "\n", + "llm_embedding: LLMEmbedding = create_embedding(embedding_config)\n", + "\n", + "embeddings_batch: LLMEmbeddingResponse = llm_embedding.embedding(\n", + " input=[\"Hello world\", \"How are you?\"]\n", + ")\n", + "for embedding in embeddings_batch.embeddings:\n", + " print(embedding[0:3])" + ] + }, + { + "cell_type": "markdown", + "id": "e3b7bedf", + "metadata": {}, + "source": [ + "### First Embedding\n", + "\n", + "`.embedding` batches by default, it takes a list of strings to embed. If embedding a single string then you can use `.first_embedding` on the response to obtain the first embedding.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e428c64a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0.05073608458042145, 0.003799507161602378, 0.019212841987609863]\n" + ] + } + ], + "source": [ + "embedding_response = llm_embedding.embedding(\n", + " input=[\"This is a single input string for embedding.\"]\n", + ")\n", + "\n", + "print(embedding_response.first_embedding[0:3])" + ] + }, + { + "cell_type": "markdown", + "id": "6b4cf0fa", + "metadata": {}, + "source": [ + "## Async Embedding\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c9519657", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[-0.002078542485833168, -0.04908587411046028, 0.020946789532899857]\n", + "[0.027567066252231598, -0.026544300839304924, -0.027091361582279205]\n" + ] + } + ], + "source": [ + "embeddings_batch = await llm_embedding.embedding_async(\n", + " input=[\"Hello world\", \"How are you?\"]\n", + ")\n", + "\n", + "for embedding in embeddings_batch.embeddings:\n", + " print(embedding[0:3])" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/02_encoding_decoding.ipynb b/packages/graphrag-llm/notebooks/02_encoding_decoding.ipynb new file mode 100644 index 0000000000..3a43503fdd --- /dev/null +++ b/packages/graphrag-llm/notebooks/02_encoding_decoding.ipynb @@ -0,0 +1,169 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "578551ee", + "metadata": {}, + "source": [ + "# Encoding/Decoding\n", + "\n", + "`LLMCompletion` and `LLMEmbedding` expose a `Tokenizer` property corresponding to the underlying model.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "986a0bad", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Encoded tokens: [9906, 11, 1917, 0]\n", + "Number of tokens: 4\n", + "Number of tokens: 4\n", + "Decoded text: Hello, world!\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig\n", + "\n", + "load_dotenv()\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "encoded = llm_completion.tokenizer.encode(\"Hello, world!\")\n", + "print(f\"Encoded tokens: {encoded}\")\n", + "print(f\"Number of tokens: {len(encoded)}\")\n", + "# OR\n", + "print(f\"Number of tokens: {llm_completion.tokenizer.num_tokens('Hello, world!')}\")\n", + "decoded = llm_completion.tokenizer.decode(encoded)\n", + "print(f\"Decoded text: {decoded}\")" + ] + }, + { + "cell_type": "markdown", + "id": "4e4a7515", + "metadata": {}, + "source": [ + "## Standalone Tokenizer\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5920cf74", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Encoded tokens: [9906, 11, 1917, 0]\n", + "Number of tokens: 4\n", + "Decoded text: Hello, world!\n" + ] + } + ], + "source": [ + "from graphrag_llm.config import TokenizerConfig, TokenizerType\n", + "from graphrag_llm.tokenizer import create_tokenizer\n", + "\n", + "tokenizer = create_tokenizer(\n", + " TokenizerConfig(\n", + " type=TokenizerType.LiteLLM,\n", + " model_id=\"openai/text-embedding-3-small\",\n", + " )\n", + ")\n", + "\n", + "encoded = tokenizer.encode(\"Hello, world!\")\n", + "print(f\"Encoded tokens: {encoded}\")\n", + "print(f\"Number of tokens: {len(encoded)}\")\n", + "decoded = tokenizer.decode(encoded)\n", + "print(f\"Decoded text: {decoded}\")" + ] + }, + { + "cell_type": "markdown", + "id": "115f63b9", + "metadata": {}, + "source": [ + "## Tiktoken\n", + "\n", + "By default, `LLMCompletion` and `LLMEmbedding` use a litellm based tokenizer that supports the 100+ models that litellm supports but you may use a tiktoken based tokenizer by specifying a tokenizer type of `TokenizerType.Tiktoken` and providing an `encoding_name` to the config.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "abeb9753", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Encoded tokens: [13225, 11, 2375, 0]\n", + "Encoded tokens: [13225, 11, 2375, 0]\n" + ] + } + ], + "source": [ + "tokenizer = create_tokenizer(\n", + " TokenizerConfig(\n", + " type=TokenizerType.Tiktoken,\n", + " encoding_name=\"o200k_base\",\n", + " )\n", + ")\n", + "encoded = tokenizer.encode(\"Hello, world!\")\n", + "print(f\"Encoded tokens: {encoded}\")\n", + "\n", + "# Using with LLMCompletion\n", + "llm_completion: LLMCompletion = create_completion(model_config, tokenizer=tokenizer)\n", + "\n", + "encoded = llm_completion.tokenizer.encode(\"Hello, world!\")\n", + "print(f\"Encoded tokens: {encoded}\")" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/03_structured_responses.ipynb b/packages/graphrag-llm/notebooks/03_structured_responses.ipynb new file mode 100644 index 0000000000..f01499d2d2 --- /dev/null +++ b/packages/graphrag-llm/notebooks/03_structured_responses.ipynb @@ -0,0 +1,170 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5b094500", + "metadata": {}, + "source": [ + "# Structured Response\n", + "\n", + "`LLMCompletion.completion` accepts a `response_format` parameter that is a pydantic model for parsing and returning structured responses.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a79c242b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "City: Seattle\n", + " Temperature: 11.1 °C\n", + " Condition: sunny\n", + "City: San Francisco\n", + " Temperature: 23.9 °C\n", + " Condition: cloudy\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig\n", + "from graphrag_llm.types import LLMCompletionResponse\n", + "from pydantic import BaseModel, Field\n", + "\n", + "load_dotenv()\n", + "\n", + "\n", + "class LocalWeather(BaseModel):\n", + " \"\"\"City weather information model.\"\"\"\n", + "\n", + " city: str = Field(description=\"The name of the city\")\n", + " temperature: float = Field(description=\"The temperature in Celsius\")\n", + " condition: str = Field(description=\"The weather condition description\")\n", + "\n", + "\n", + "class WeatherReports(BaseModel):\n", + " \"\"\"Weather information model.\"\"\"\n", + "\n", + " reports: list[LocalWeather] = Field(\n", + " description=\"The weather reports for multiple cities\"\n", + " )\n", + "\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "response: LLMCompletionResponse[WeatherReports] = llm_completion.completion(\n", + " messages=\"It is sunny and 52 degrees fahrenheit in Seattle. It is cloudy and 75 degrees fahrenheit in San Francisco.\",\n", + " response_format=WeatherReports,\n", + ") # type: ignore\n", + "\n", + "local_weather_reports: WeatherReports = response.formatted_response # type: ignore\n", + "for report in local_weather_reports.reports:\n", + " print(f\"City: {report.city}\")\n", + " print(f\" Temperature: {report.temperature} °C\")\n", + " print(f\" Condition: {report.condition}\")" + ] + }, + { + "cell_type": "markdown", + "id": "6dcfa20c", + "metadata": {}, + "source": [ + "## Checking for support\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "aa1edadb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Supports structured responses: True\n" + ] + } + ], + "source": [ + "print(f\"Supports structured responses: {llm_completion.supports_structured_response()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "6360f512", + "metadata": {}, + "source": [ + "## Streaming\n", + "\n", + "Streaming is not supported when using `response_format`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "e08b9ba6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Error during streaming completion: response_format is not supported for streaming completions.\n" + ] + } + ], + "source": [ + "try:\n", + " response = llm_completion.completion(\n", + " messages=\"It is sunny and 52 degrees fahrenheit in Seattle. It is cloudy and 75 degrees fahrenheit in San Francisco.\",\n", + " response_format=WeatherReports,\n", + " stream=True,\n", + " )\n", + "except Exception as e: # noqa: BLE001\n", + " print(f\"Error during streaming completion: {e}\")" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/04_metrics.ipynb b/packages/graphrag-llm/notebooks/04_metrics.ipynb new file mode 100644 index 0000000000..e8649f0a93 --- /dev/null +++ b/packages/graphrag-llm/notebooks/04_metrics.ipynb @@ -0,0 +1,595 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4dc68732", + "metadata": {}, + "source": [ + "# Metrics\n", + "\n", + "Metrics are automatically tracked for completion and embedding calls.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "868deb65", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Metrics for: azure/gpt-4o\n", + "{\n", + " \"attempted_request_count\": 1,\n", + " \"successful_response_count\": 1,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 3.4281113147735596,\n", + " \"compute_duration_per_response_seconds\": 3.4281113147735596,\n", + " \"cache_hit_rate\": 0.0,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 1,\n", + " \"prompt_tokens\": 14,\n", + " \"completion_tokens\": 8,\n", + " \"total_tokens\": 22,\n", + " \"tokens_per_response\": 22.0,\n", + " \"responses_with_cost\": 1,\n", + " \"input_cost\": 3.5000000000000004e-05,\n", + " \"output_cost\": 8e-05,\n", + " \"total_cost\": 0.000115,\n", + " \"cost_per_response\": 0.000115\n", + "}\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import json\n", + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig\n", + "\n", + "load_dotenv()\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "\n", + "print(f\"Metrics for: {llm_completion.metrics_store.id}\")\n", + "print(json.dumps(llm_completion.metrics_store.get_metrics(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "dd9e7e19", + "metadata": {}, + "source": [ + "## Disable Metrics\n", + "\n", + "Set `metrics` to `None` in the `ModelConfig` to disable metrics.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "44ab5fcd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Metrics for: \n", + "{}\n" + ] + } + ], + "source": [ + "model_config.metrics = None\n", + "llm_completion_no_metrics: LLMCompletion = create_completion(model_config)\n", + "\n", + "response = llm_completion_no_metrics.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "\n", + "# Now .metrics_store should be a NoOpMetricsStore\n", + "print(f\"Metrics for: {llm_completion_no_metrics.metrics_store.id}\")\n", + "print(json.dumps(llm_completion_no_metrics.metrics_store.get_metrics(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "f38a5a44", + "metadata": {}, + "source": [ + "## Automatic Metrics Logging\n", + "\n", + "Metrics foreach instantiated model are automatically logged on process exit. To see this, update the log level to info.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "16b71da8", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m22:45:27 - LiteLLM:INFO\u001b[0m: utils.py:3373 - \n", + "LiteLLM completion() model= gpt-4o; provider = azure\n", + "INFO:LiteLLM:\n", + "LiteLLM completion() model= gpt-4o; provider = azure\n", + "\u001b[92m22:45:27 - LiteLLM:INFO\u001b[0m: utils.py:1286 - Wrapper: Completed Call, calling success_handler\n", + "INFO:LiteLLM:Wrapper: Completed Call, calling success_handler\n", + "INFO:graphrag_llm.metrics.log_metrics_writer:Metrics for azure/gpt-4o: {\n", + " \"attempted_request_count\": 1,\n", + " \"successful_response_count\": 1,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 0.3004579544067383,\n", + " \"compute_duration_per_response_seconds\": 0.3004579544067383,\n", + " \"cache_hit_rate\": 0.0,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 1,\n", + " \"prompt_tokens\": 14,\n", + " \"completion_tokens\": 8,\n", + " \"total_tokens\": 22,\n", + " \"tokens_per_response\": 22.0,\n", + " \"responses_with_cost\": 1,\n", + " \"input_cost\": 3.5000000000000004e-05,\n", + " \"output_cost\": 8e-05,\n", + " \"total_cost\": 0.000115,\n", + " \"cost_per_response\": 0.000115\n", + "}\n" + ] + } + ], + "source": [ + "import logging\n", + "\n", + "logging.basicConfig(level=logging.INFO)\n", + "\n", + "llm_completion.metrics_store.clear_metrics()\n", + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "\n", + "# NOTE: Call _on_exit_ to simulate application exit since\n", + "# the notebook process does not exit and the llm_completion\n", + "# object is not garbage collected.\n", + "# This should not be called in normal python scripts.\n", + "llm_completion.metrics_store._on_exit_() # type: ignore" + ] + }, + { + "cell_type": "markdown", + "id": "7d97bd8c", + "metadata": {}, + "source": [ + "## Save Metrics to a File\n", + "\n", + "Instead of logging on exit, metrics can automatically be saved to a file on exit by using a `MetricsWriter.File` metrics writer.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4c16806a", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:azure.identity._credentials.environment:No environment configuration found.\n", + "INFO:azure.identity._credentials.managed_identity:ManagedIdentityCredential will use IMDS\n", + "\u001b[92m22:45:27 - LiteLLM:INFO\u001b[0m: utils.py:3373 - \n", + "LiteLLM completion() model= gpt-4o; provider = azure\n", + "INFO:LiteLLM:\n", + "LiteLLM completion() model= gpt-4o; provider = azure\n", + "\u001b[92m22:45:28 - LiteLLM:INFO\u001b[0m: utils.py:1286 - Wrapper: Completed Call, calling success_handler\n", + "INFO:LiteLLM:Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Contents of metrics\\20260111_211007.jsonl:\n", + "{\"id\": \"azure/gpt-4o\", \"metrics\": {\"attempted_request_count\": 1, \"successful_response_count\": 1, \"failed_response_count\": 0, \"failure_rate\": 0.0, \"requests_with_retries\": 0, \"retries\": 0, \"retry_rate\": 0.0, \"compute_duration_seconds\": 0.6868698596954346, \"compute_duration_per_response_seconds\": 0.6868698596954346, \"streaming_responses\": 0, \"responses_with_tokens\": 1, \"prompt_tokens\": 14, \"completion_tokens\": 8, \"total_tokens\": 22, \"tokens_per_response\": 22.0, \"responses_with_cost\": 1, \"input_cost\": 3.5000000000000004e-05, \"output_cost\": 8e-05, \"total_cost\": 0.000115, \"cost_per_response\": 0.000115}}\n", + "\n" + ] + } + ], + "source": [ + "from pathlib import Path\n", + "\n", + "from graphrag_llm.config import MetricsConfig, MetricsWriterType\n", + "\n", + "model_config.metrics = MetricsConfig(\n", + " writer=MetricsWriterType.File,\n", + " base_dir=\"./metrics\", # Default\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "\n", + "# NOTE: Call _on_exit_ to simulate application exit since\n", + "# the notebook process does not exit and the llm_completion\n", + "# object is not garbage collected.\n", + "# This should not be called in normal python scripts.\n", + "llm_completion.metrics_store._on_exit_() # type: ignore\n", + "\n", + "metrics_dir = Path(\"./metrics\")\n", + "for metric_file in metrics_dir.glob(\"*.jsonl\"):\n", + " print(f\"Contents of {metric_file}:\")\n", + " print(metric_file.read_text())\n", + " break # Just print one file for brevity" + ] + }, + { + "cell_type": "markdown", + "id": "9076af04", + "metadata": {}, + "source": [ + "## Default Metrics\n", + "\n", + "- `attempted_request_count`: Number of network requests made, not including retries.\n", + "- `successful_response_count`: Number of successful responses.\n", + "- `failed_response_count`: Number of network requests that threw errors and could not be resolved even after retries. `successful_response_count + failed_response_count` should equal `attempted_request_count` unless the job or process was killed early.\n", + "- `failure_rate`: `failed_response_count / attempted_request_count`.\n", + "- `requests_with_retries`: Number of original requests that had to go through a retry loop.\n", + "- `retries`: Number of network requests that were retries.\n", + "- `retry_rate`: `retries / (retries + attempted_request_count)`\n", + "- `compute_duration_seconds`: Total number of seconds to complete all non-streaming network requests.\n", + "- `compute_duration_per_response_seconds`: `compute_duration_seconds / successful non-streaming responses`\n", + "- `runtime_duration_seconds`: Only present if using the batching utilities. The batching utilities run multiple completions/embedding in parallel so `runtime_duration_seconds` is the actual runtime duration. Comparing this with `compute_duration_seconds` indicates how much time was saved using the batching utilities vs if all network requests ran in series.\n", + "- `cached_responses`: Number of cached responses. Only present if using a cache. When a response is cached so are the corresponding metrics. When a response is retrieved from the cache the corresponding metrics are also retrieved from the cache and provided in the overall metrics so metrics like `compute_duration_seconds`, `input_cost`, `output_cost`, etc include cached rsponses metrics. This is helpful when having to resume stopped jobs or rerunning failed jobs. At the end of the job the metrics indicate how long and costly the job would have been when running off a fresh cache/no cache. The `cached_responses` only indicates how many network requests were skipped and retrieved from cache.\n", + "- `streaming_responses`: Number of requests using the `stream=True` parameter. Many metrics such as token counts and costs are not tracked for streaming requests as that would require analyzing the stream to completion within the middleware stack and preventing the ability to build true streaming interfaces with `graphrag-llm`\n", + "- `responses_with_tokens`: Number of responses in which token counts were obtained. Typically this should equal `successful_response_count - streaming_responses`.\n", + "- `prompt_tokens`: Total number of prompt tokens used accross all successful non-streaming network requests.\n", + "- `completion_tokens`: Total number of completion tokens accress all succesful non-streaming network requests.\n", + "- `total_tokens`: `prompt_tokens + completion_tokens`\n", + "- `tokens_per_response`: `total_tokens / responses_with_tokens`\n", + "- `responses_with_cost`: Number of responses in which costs were calculated. typically this should equal `successful_response_count - streaming_responses`.\n", + "- `input_cost`: Cost of the input tokens accross all successful non-streaming network requests.\n", + "- `output_cost`: Cost of the output tokens accross all successful non-streaming network requests.\n", + "- `total_cost`: `input_cost + output_cost`\n", + "- `cost_per_response`: `total_cost / responses_with_cost`.\n" + ] + }, + { + "cell_type": "markdown", + "id": "2749473e", + "metadata": {}, + "source": [ + "## Custom Model Costs\n", + "\n", + "The default metrics include costs for prompt tokens and completion tokens. These are calculated using a registry of known models and associated costs managed by litellm: https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json\n", + "\n", + "One can register custom model costs if using a custom model that is not in the registry or one that differs from the known/default cost.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a47f496", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[92m22:45:28 - LiteLLM:INFO\u001b[0m: utils.py:3373 - \n", + "LiteLLM completion() model= gpt-4o; provider = azure\n", + "INFO:LiteLLM:\n", + "LiteLLM completion() model= gpt-4o; provider = azure\n", + "\u001b[92m22:45:28 - LiteLLM:INFO\u001b[0m: utils.py:1286 - Wrapper: Completed Call, calling success_handler\n", + "INFO:LiteLLM:Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"attempted_request_count\": 1,\n", + " \"successful_response_count\": 1,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 0.3090023994445801,\n", + " \"compute_duration_per_response_seconds\": 0.3090023994445801,\n", + " \"cache_hit_rate\": 0.0,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 1,\n", + " \"prompt_tokens\": 14,\n", + " \"completion_tokens\": 8,\n", + " \"total_tokens\": 22,\n", + " \"tokens_per_response\": 22.0,\n", + " \"responses_with_cost\": 1,\n", + " \"input_cost\": 14000,\n", + " \"output_cost\": 40000,\n", + " \"total_cost\": 54000,\n", + " \"cost_per_response\": 54000.0\n", + "}\n" + ] + } + ], + "source": [ + "from graphrag_llm.model_cost_registry import model_cost_registry\n", + "\n", + "model_cost_registry.register_model_costs(\n", + " model=\"azure/gpt-4o\", # This should use format \"{model_provider}/{model_name}\" and not the azure deployment name\n", + " costs={\n", + " # Expensive model\n", + " \"input_cost_per_token\": 1000,\n", + " \"output_cost_per_token\": 5000,\n", + " },\n", + ")\n", + "\n", + "llm_completion.metrics_store.clear_metrics()\n", + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "\n", + "print(json.dumps(llm_completion.metrics_store.get_metrics(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "4132a718", + "metadata": {}, + "source": [ + "## Custom Metrics Processor\n", + "\n", + "It is possible to register a custom metrics processor if one needs to track metrics not already tracked.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f68ed4bb", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:azure.identity._credentials.environment:No environment configuration found.\n", + "INFO:azure.identity._credentials.managed_identity:ManagedIdentityCredential will use IMDS\n", + "\u001b[92m22:45:28 - LiteLLM:INFO\u001b[0m: utils.py:3373 - \n", + "LiteLLM completion() model= gpt-4o; provider = azure\n", + "INFO:LiteLLM:\n", + "LiteLLM completion() model= gpt-4o; provider = azure\n", + "\u001b[92m22:45:28 - LiteLLM:INFO\u001b[0m: utils.py:1286 - Wrapper: Completed Call, calling success_handler\n", + "INFO:LiteLLM:Wrapper: Completed Call, calling success_handler\n", + "\u001b[92m22:45:28 - LiteLLM:INFO\u001b[0m: utils.py:3373 - \n", + "LiteLLM completion() model= gpt-4o; provider = azure\n", + "INFO:LiteLLM:\n", + "LiteLLM completion() model= gpt-4o; provider = azure\n", + "\u001b[92m22:45:29 - LiteLLM:INFO\u001b[0m: utils.py:1286 - Wrapper: Completed Call, calling success_handler\n", + "INFO:LiteLLM:Wrapper: Completed Call, calling success_handler\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Metrics for: azure/gpt-4o\n", + "{\n", + " \"attempted_request_count\": 2,\n", + " \"successful_response_count\": 2,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 0.6117346286773682,\n", + " \"compute_duration_per_response_seconds\": 0.3058673143386841,\n", + " \"cache_hit_rate\": 0.0,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 2,\n", + " \"prompt_tokens\": 28,\n", + " \"completion_tokens\": 16,\n", + " \"total_tokens\": 44,\n", + " \"tokens_per_response\": 22.0,\n", + " \"responses_with_cost\": 2,\n", + " \"input_cost\": 28000,\n", + " \"output_cost\": 80000,\n", + " \"total_cost\": 108000,\n", + " \"cost_per_response\": 54000.0,\n", + " \"responses_with_temperature\": 1,\n", + " \"temperature_rate\": 0.5\n", + "}\n" + ] + } + ], + "source": [ + "import json\n", + "import os\n", + "from collections.abc import AsyncIterator, Iterator\n", + "from typing import Any\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import MetricsConfig, MetricsWriterType, ModelConfig\n", + "from graphrag_llm.metrics import metrics_aggregator, register_metrics_processor\n", + "from graphrag_llm.metrics.default_metrics_processor import DefaultMetricsProcessor\n", + "from graphrag_llm.types import (\n", + " LLMCompletionChunk,\n", + " LLMCompletionResponse,\n", + " LLMEmbeddingResponse,\n", + " Metrics,\n", + ")\n", + "\n", + "load_dotenv()\n", + "\n", + "\n", + "class MyCustomMetricsProcessor(DefaultMetricsProcessor):\n", + " \"\"\"Custom metrics processor.\n", + "\n", + " Inheriting from DefaultMetricsProcessor to add to the default metrics being\n", + " tracked instead of implementing the interface from scratch.\n", + "\n", + " Metrics = dict[str, float]. The metrics passed to process_metrics method\n", + " represent the metrics for a single request. Typically, you will count/flag\n", + " metrics of interest per request and then aggregate them in the metrics_aggregator.\n", + " \"\"\"\n", + "\n", + " def __init__(self, some_custom_option: str, **kwargs: Any) -> None:\n", + " \"\"\"Initialize the custom metrics processor.\"\"\"\n", + " super().__init__(**kwargs)\n", + " self._some_custom_option = some_custom_option # Not actually used\n", + "\n", + " def process_metrics(\n", + " self,\n", + " *,\n", + " model_config: ModelConfig,\n", + " metrics: Metrics,\n", + " input_args: dict[str, Any],\n", + " response: LLMCompletionResponse\n", + " | Iterator[LLMCompletionChunk]\n", + " | AsyncIterator[LLMCompletionChunk]\n", + " | LLMEmbeddingResponse,\n", + " ) -> None:\n", + " \"\"\"On top of the default metrics, track if temperature argument was used.\n", + "\n", + " Expected to mutate the metrics dict in place with metrics you want to track.\n", + "\n", + " process_metrics is only called for successful requests and will be passed in the response\n", + " from either a completion or embedding call.\n", + "\n", + " Args\n", + " ----\n", + " model_config: ModelConfig\n", + " The model config used for the request.\n", + " metrics: Metrics\n", + " The metrics dict to be mutated in place.\n", + " input_args: dict[str, Any]\n", + " The input arguments passed to completion or embedding.\n", + " response: LLMChatCompletion | Iterator[LLMChatCompletionChunk] | LLMEmbeddingResponse\n", + " Either a completion or embedding response from the LLM.\n", + " \"\"\"\n", + " # Track default metrics first\n", + " super().process_metrics(\n", + " model_config=model_config,\n", + " metrics=metrics,\n", + " input_args=input_args,\n", + " response=response,\n", + " )\n", + "\n", + " metrics[\"responses_with_temperature\"] = 1 if \"temperature\" in input_args else 0\n", + "\n", + "\n", + "# Register custom metrics processor\n", + "register_metrics_processor(\n", + " processor_type=\"custom_with_temperature\",\n", + " processor_initializer=MyCustomMetricsProcessor,\n", + ")\n", + "\n", + "\n", + "# Custom aggregator to calculate temperature usage rate\n", + "def _temperature_rate(metrics: \"Metrics\") -> None:\n", + " \"\"\"Calculate temperature usage rate.\n", + "\n", + " Custom aggregate function to track the usage rate of temperature parameter.\n", + "\n", + " Here, metrics represents the aggregated metrics for the current model.\n", + " \"\"\"\n", + " responses = metrics.get(\"successful_response_count\", 0)\n", + " temperature_responses = metrics.get(\"responses_with_temperature\", 0)\n", + " if responses > 0:\n", + " metrics[\"temperature_rate\"] = temperature_responses / responses\n", + " else:\n", + " metrics[\"temperature_rate\"] = 0.0\n", + "\n", + "\n", + "# Register custom aggregator\n", + "metrics_aggregator.register(\"temperature_rate\", _temperature_rate)\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + " metrics=MetricsConfig(\n", + " # Use the custom metrics processor registered above\n", + " type=\"custom_with_temperature\",\n", + " some_custom_option=\"example_option_value\", # type: ignore\n", + " writer=MetricsWriterType.File,\n", + " base_dir=\"./metrics\", # Default\n", + " ),\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "\n", + "response_with_temperature = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + " temperature=0.7,\n", + ")\n", + "\n", + "print(f\"Metrics for: {llm_completion.metrics_store.id}\")\n", + "print(json.dumps(llm_completion.metrics_store.get_metrics(), indent=2))" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/05_caching.ipynb b/packages/graphrag-llm/notebooks/05_caching.ipynb new file mode 100644 index 0000000000..75067a54e6 --- /dev/null +++ b/packages/graphrag-llm/notebooks/05_caching.ipynb @@ -0,0 +1,264 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "347b0fc9", + "metadata": {}, + "source": [ + "# Caching\n", + "\n", + "To enabling caching, pass in a `Cache` instance to the `create_completion` or `create_embedding` functions.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96b0c42f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Metrics for: azure/gpt-4o\n", + "{\n", + " \"attempted_request_count\": 3,\n", + " \"successful_response_count\": 3,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 2.8864328861236572,\n", + " \"compute_duration_per_response_seconds\": 0.9621442953745524,\n", + " \"cached_responses\": 1,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 3,\n", + " \"prompt_tokens\": 191,\n", + " \"completion_tokens\": 59,\n", + " \"total_tokens\": 250,\n", + " \"tokens_per_response\": 83.33333333333333,\n", + " \"responses_with_cost\": 3,\n", + " \"input_cost\": 0.0004775,\n", + " \"output_cost\": 0.00059,\n", + " \"total_cost\": 0.0010675,\n", + " \"cost_per_response\": 0.0003558333333333334\n", + "}\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import json\n", + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_cache import CacheConfig, CacheType, create_cache\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig\n", + "from graphrag_storage import StorageConfig, StorageType\n", + "\n", + "load_dotenv()\n", + "\n", + "cache = create_cache()\n", + "# The above default is equivalent to:\n", + "cache = create_cache(\n", + " CacheConfig(\n", + " type=CacheType.Json,\n", + " storage=StorageConfig(type=StorageType.File, base_dir=\"cache\"),\n", + " )\n", + ")\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config, cache=cache)\n", + "\n", + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "\n", + "print(f\"Metrics for: {llm_completion.metrics_store.id}\")\n", + "metrics = llm_completion.metrics_store.get_metrics()\n", + "print(json.dumps(metrics, indent=2))\n", + "assert metrics[\"cached_responses\"] == 1" + ] + }, + { + "cell_type": "markdown", + "id": "c70a72fc", + "metadata": {}, + "source": [ + "## Note on the above metrics\n", + "\n", + "`cached_responses == 1` since the request was cached by the time the second call was made.\n", + "\n", + "The `cached_responses` indicates how many cache hits occurred but the rest of the metrics exist as if a cache was not used. For example, `compute_duration_seconds` and all the token counts and cost counts are as if cache was not used. This is because both the response and metrics are cached and retrieved from the cache when a cache hit occurs. Metrics were designed to give an idea of how long and costly a job would be if there were no cache.\n" + ] + }, + { + "cell_type": "markdown", + "id": "27b026d7", + "metadata": {}, + "source": [ + "## Tests\n", + "\n", + "This is in here because notebooks are being used as integration tests. This ensures objects are being loaded and deserialized from cache properly and the cache is bypassing the rate limiting.\n" + ] + }, + { + "cell_type": "markdown", + "id": "22cc179e", + "metadata": {}, + "source": [ + "### Test Timing\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "efb228ce", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total time for 100 requests: 0.3867683410644531 seconds\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "from graphrag_llm.config import RateLimitConfig, RateLimitType\n", + "\n", + "model_config.rate_limit = RateLimitConfig(\n", + " type=RateLimitType.SlidingWindow,\n", + " period_in_seconds=60, # limit requests per minute\n", + " requests_per_period=1, # max 1 request per minute. Without cache this would take forever\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config, cache=cache)\n", + "\n", + "start_time = time.time()\n", + "for _ in range(100):\n", + " response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + " )\n", + "end_time = time.time()\n", + "total_time = end_time - start_time\n", + "print(f\"Total time for 100 requests: {total_time} seconds\")\n", + "assert total_time < 5.0 # Ensure that caching is effective" + ] + }, + { + "cell_type": "markdown", + "id": "dcf4bf16", + "metadata": {}, + "source": [ + "### Test Structured Responses\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "21e0e1e4", + "metadata": {}, + "outputs": [], + "source": [ + "from graphrag_llm.types import LLMCompletionResponse\n", + "from pydantic import BaseModel, Field\n", + "\n", + "\n", + "class LocalWeather(BaseModel):\n", + " \"\"\"City weather information model.\"\"\"\n", + "\n", + " city: str = Field(description=\"The name of the city\")\n", + " temperature: float = Field(description=\"The temperature in Celsius\")\n", + " condition: str = Field(description=\"The weather condition description\")\n", + "\n", + "\n", + "class WeatherReports(BaseModel):\n", + " \"\"\"Weather information model.\"\"\"\n", + "\n", + " reports: list[LocalWeather] = Field(\n", + " description=\"The weather reports for multiple cities\"\n", + " )\n", + "\n", + "\n", + "llm_completion.metrics_store.clear_metrics()\n", + "response: LLMCompletionResponse[WeatherReports] = llm_completion.completion( # type: ignore\n", + " messages=\"It is sunny and 52 degrees fahrenheit in Seattle. It is cloudy and 75 degrees fahrenheit in San Francisco.\",\n", + " response_format=WeatherReports,\n", + ") # type: ignore\n", + "response: LLMCompletionResponse[WeatherReports] = llm_completion.completion( # type: ignore\n", + " messages=\"It is sunny and 52 degrees fahrenheit in Seattle. It is cloudy and 75 degrees fahrenheit in San Francisco.\",\n", + " response_format=WeatherReports,\n", + ") # type: ignore\n", + "\n", + "metrics = llm_completion.metrics_store.get_metrics()\n", + "assert metrics[\"cached_responses\"] == 1, (\n", + " f\"Expected 1 cached response, got {metrics['cached_responses']}\"\n", + ")\n", + "\n", + "\n", + "# Changing the response format should not hit the cache and\n", + "# instead be a new request and store a new response in the cache.\n", + "\n", + "\n", + "class WeatherReports2(BaseModel):\n", + " \"\"\"Weather information model.\"\"\"\n", + "\n", + " local_reports: list[LocalWeather] = Field(\n", + " description=\"The weather reports for multiple cities\"\n", + " )\n", + "\n", + "\n", + "llm_completion.metrics_store.clear_metrics()\n", + "# Same request but different response format. Should not hit cache.\n", + "response: LLMCompletionResponse[WeatherReports2] = llm_completion.completion(\n", + " messages=\"It is sunny and 52 degrees fahrenheit in Seattle. It is cloudy and 75 degrees fahrenheit in San Francisco.\",\n", + " response_format=WeatherReports2,\n", + ") # type: ignore\n", + "\n", + "metrics = llm_completion.metrics_store.get_metrics()\n", + "assert metrics.get(\"cached_responses\", 0) == 0, (\n", + " f\"Expected 0 cached responses, got {metrics['cached_responses']}\"\n", + ")" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/06_retries.ipynb b/packages/graphrag-llm/notebooks/06_retries.ipynb new file mode 100644 index 0000000000..a49dd1d2d5 --- /dev/null +++ b/packages/graphrag-llm/notebooks/06_retries.ipynb @@ -0,0 +1,115 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d75247ed", + "metadata": {}, + "source": [ + "# Retries\n", + "\n", + "Retries are disabled by default. Retries can be enabled with the following example.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "299065b7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Metrics for: azure/gpt-4o\n", + "{\n", + " \"attempted_request_count\": 1,\n", + " \"successful_response_count\": 1,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 1,\n", + " \"retries\": 2,\n", + " \"retry_rate\": 0.6666666666666666,\n", + " \"compute_duration_seconds\": 2.6571085453033447,\n", + " \"compute_duration_per_response_seconds\": 2.6571085453033447,\n", + " \"cache_hit_rate\": 0.0,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 1,\n", + " \"prompt_tokens\": 14,\n", + " \"completion_tokens\": 8,\n", + " \"total_tokens\": 22,\n", + " \"tokens_per_response\": 22.0,\n", + " \"responses_with_cost\": 1,\n", + " \"input_cost\": 3.5000000000000004e-05,\n", + " \"output_cost\": 8e-05,\n", + " \"total_cost\": 0.000115,\n", + " \"cost_per_response\": 0.000115\n", + "}\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import json\n", + "import logging\n", + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig, RetryConfig, RetryType\n", + "\n", + "load_dotenv()\n", + "\n", + "logging.basicConfig(level=logging.CRITICAL)\n", + "\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + " retry=RetryConfig(\n", + " type=RetryType.ExponentialBackoff, max_retries=7, base_delay=2.0, jitter=True\n", + " ),\n", + " # Internal option to test error handling and retries\n", + " failure_rate_for_testing=0.5, # type: ignore\n", + ")\n", + "\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "\n", + "print(f\"Metrics for: {llm_completion.metrics_store.id}\")\n", + "print(json.dumps(llm_completion.metrics_store.get_metrics(), indent=2))" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/07_rate_limiting.ipynb b/packages/graphrag-llm/notebooks/07_rate_limiting.ipynb new file mode 100644 index 0000000000..081382eac6 --- /dev/null +++ b/packages/graphrag-llm/notebooks/07_rate_limiting.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0a68b531", + "metadata": {}, + "source": [ + "# Rate Limiting\n", + "\n", + "Rate limiting is disabled by default. Requests can be limited by either requests per period or tokens per period or both.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "df4fa775", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time taken for two requests: 20.87 seconds\n", + "Metrics for: azure/gpt-4o\n", + "{\n", + " \"attempted_request_count\": 2,\n", + " \"successful_response_count\": 2,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 3.534508228302002,\n", + " \"compute_duration_per_response_seconds\": 1.767254114151001,\n", + " \"cache_hit_rate\": 0.0,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 2,\n", + " \"prompt_tokens\": 28,\n", + " \"completion_tokens\": 16,\n", + " \"total_tokens\": 44,\n", + " \"tokens_per_response\": 22.0,\n", + " \"responses_with_cost\": 2,\n", + " \"input_cost\": 7.000000000000001e-05,\n", + " \"output_cost\": 0.00016,\n", + " \"total_cost\": 0.00023,\n", + " \"cost_per_response\": 0.000115\n", + "}\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import json\n", + "import os\n", + "import time\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig, RateLimitConfig, RateLimitType\n", + "\n", + "load_dotenv()\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + " rate_limit=RateLimitConfig(\n", + " type=RateLimitType.SlidingWindow,\n", + " period_in_seconds=60, # limit requests per minute\n", + " requests_per_period=3, # max 3 requests per minute. Fire one off every 20 seconds\n", + " ),\n", + ")\n", + "\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "start_time = time.time()\n", + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "response = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ")\n", + "end_time = time.time()\n", + "total_time = end_time - start_time\n", + "assert total_time >= 20, \"Rate limiting did not work as expected.\"\n", + "\n", + "print(f\"Time taken for two requests: {total_time:.2f} seconds\")\n", + "print(f\"Metrics for: {llm_completion.metrics_store.id}\")\n", + "print(json.dumps(llm_completion.metrics_store.get_metrics(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "59f92d3f", + "metadata": {}, + "source": [ + "Notice that the `compute_duration_seconds` in the metrics only tracks how long a network request actually takes and does track paused periods that occur due to rate limits.\n" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/08_batching.ipynb b/packages/graphrag-llm/notebooks/08_batching.ipynb new file mode 100644 index 0000000000..fccd984ddd --- /dev/null +++ b/packages/graphrag-llm/notebooks/08_batching.ipynb @@ -0,0 +1,536 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "91a0ee2b", + "metadata": {}, + "source": [ + "# Batching\n" + ] + }, + { + "cell_type": "markdown", + "id": "422fcc73", + "metadata": {}, + "source": [ + "## Completion Batching\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88e715fe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "In the velvet silence of the night,\n", + "A canvas vast and infinite unfolds,\n", + "Where stories of the cosmos,\n", + "Metrics for: azure/gpt-4o\n", + "{\n", + " \"attempted_request_count\": 10,\n", + " \"successful_response_count\": 10,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 157.70280289649963,\n", + " \"compute_duration_per_response_seconds\": 15.770280289649964,\n", + " \"runtime_duration_seconds\": 18.660003900527954,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 10,\n", + " \"prompt_tokens\": 280,\n", + " \"completion_tokens\": 9234,\n", + " \"total_tokens\": 9514,\n", + " \"tokens_per_response\": 951.4,\n", + " \"responses_with_cost\": 10,\n", + " \"input_cost\": 0.0007,\n", + " \"output_cost\": 0.09234,\n", + " \"total_cost\": 0.09304000000000001,\n", + " \"cost_per_response\": 0.009304000000000002\n", + "}\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import json\n", + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig\n", + "from graphrag_llm.types import LLMCompletionArgs\n", + "\n", + "load_dotenv()\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "\n", + "completion_requests: list[LLMCompletionArgs] = [\n", + " {\n", + " \"messages\": \"Write a 1000 word poem about the night sky and all the wonders and mysteries of the universe.\"\n", + " },\n", + "] * 10\n", + "\n", + "# Spins up to 25 concurrent requests\n", + "# Which is more than the number of requests being made\n", + "# and since rate limiting is not enabled, all the requests fire off immediately\n", + "# and complete as fast as the LLM provider allows\n", + "responses = llm_completion.completion_batch(completion_requests, concurrency=25)\n", + "for response in responses:\n", + " if isinstance(response, Exception):\n", + " print(f\"Error: {response}\")\n", + " else:\n", + " # Print the first 100 characters of the first successful response\n", + " print(response.content[0:100]) # type: ignore\n", + " break\n", + "\n", + "print(f\"Metrics for: {llm_completion.metrics_store.id}\")\n", + "print(json.dumps(llm_completion.metrics_store.get_metrics(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "668e1f94", + "metadata": {}, + "source": [ + "Notice the difference between `compute_duration_seconds` and `runtime_duration_seconds`. The former indicates how long all the network requests took to complete and would be how long the whole process took to complete if running the requests in series. The latter indicates how long the batch as a whole took to complete when running with concurrency.\n" + ] + }, + { + "cell_type": "markdown", + "id": "49ec7716", + "metadata": {}, + "source": [ + "### With Rate Limiting\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eb73f940", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Metrics for: azure/gpt-4o\n", + "{\n", + " \"attempted_request_count\": 10,\n", + " \"successful_response_count\": 10,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 108.16670417785645,\n", + " \"compute_duration_per_response_seconds\": 10.816670417785645,\n", + " \"runtime_duration_seconds\": 38.489975929260254,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 10,\n", + " \"prompt_tokens\": 280,\n", + " \"completion_tokens\": 8965,\n", + " \"total_tokens\": 9245,\n", + " \"tokens_per_response\": 924.5,\n", + " \"responses_with_cost\": 10,\n", + " \"input_cost\": 0.0007,\n", + " \"output_cost\": 0.08965000000000002,\n", + " \"total_cost\": 0.09035000000000001,\n", + " \"cost_per_response\": 0.009035000000000001\n", + "}\n" + ] + } + ], + "source": [ + "from graphrag_llm.config import RateLimitConfig, RateLimitType\n", + "\n", + "model_config.rate_limit = RateLimitConfig(\n", + " type=RateLimitType.SlidingWindow,\n", + " period_in_seconds=60, # limit requests per minute\n", + " requests_per_period=20, # max 20 requests per minute. Fire one off every 3 seconds\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "llm_completion.metrics_store.clear_metrics()\n", + "\n", + "responses = llm_completion.completion_batch(completion_requests, concurrency=25)\n", + "\n", + "print(f\"Metrics for: {llm_completion.metrics_store.id}\")\n", + "print(json.dumps(llm_completion.metrics_store.get_metrics(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "ceb93f24", + "metadata": {}, + "source": [ + "Notice the `runtime_duration_seconds` is now much slower as the requests are being throttled by the rate limit.\n" + ] + }, + { + "cell_type": "markdown", + "id": "05bd00e6", + "metadata": {}, + "source": [ + "### With Cache\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3cb345ec", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Metrics for: azure/gpt-4o\n", + "{\n", + " \"attempted_request_count\": 10,\n", + " \"successful_response_count\": 10,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 93.54697012901306,\n", + " \"compute_duration_per_response_seconds\": 9.354697012901307,\n", + " \"runtime_duration_seconds\": 10.748144149780273,\n", + " \"cached_responses\": 6,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 10,\n", + " \"prompt_tokens\": 280,\n", + " \"completion_tokens\": 7869,\n", + " \"total_tokens\": 8149,\n", + " \"tokens_per_response\": 814.9,\n", + " \"responses_with_cost\": 10,\n", + " \"input_cost\": 0.0007,\n", + " \"output_cost\": 0.07869000000000001,\n", + " \"total_cost\": 0.07939000000000002,\n", + " \"cost_per_response\": 0.007939000000000002\n", + "}\n" + ] + } + ], + "source": [ + "from graphrag_cache import create_cache\n", + "\n", + "cache = create_cache()\n", + "\n", + "# Redisable rate limiting\n", + "model_config.rate_limit = None\n", + "\n", + "llm_completion: LLMCompletion = create_completion(model_config, cache=cache)\n", + "llm_completion.metrics_store.clear_metrics()\n", + "\n", + "responses = llm_completion.completion_batch(completion_requests, concurrency=4)\n", + "\n", + "print(f\"Metrics for: {llm_completion.metrics_store.id}\")\n", + "print(json.dumps(llm_completion.metrics_store.get_metrics(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "6de2c4cf", + "metadata": {}, + "source": [ + "Notice the `cached_responses == 6` since we are spinning up `4` threads. The first 4 requests are fired off immediately prior to any data in the cache. This means when identical requests are fired in the same thread cycle they will all hit the model since the cache is not yet populated.\n", + "\n", + "The `cached_responses` indicates how many cache hits occurred but the rest of the metrics exist as if a cache was not used. For example, `compute_duration_seconds` and all the tokens and cost counts are as if cache was not used so `compute_duration_seconds` includes network timings for the cached responses. This is because both the response and metrics are cached and retrieved from the cache when a cache hit occurs. This means the above metrics should closely match the metrics from the first example in this notebook other than the `runtime_duration_seconds` which gives the true idea of how long a job takes to run. Rerunning a job with a fully hydrated cache should result in a quick `runtime_duration_seconds`. Metrics were designed to give an idea of how long and costly a job would be if there were no cache.\n" + ] + }, + { + "cell_type": "markdown", + "id": "3e6d20d6", + "metadata": {}, + "source": [ + "## Embedding Batching\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e95c4e6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Embedding vector length: 1536\n", + "[0.012382184155285358, -0.0487498939037323, 0.02962493523955345, 0.0321056991815567, -0.030259549617767334]\n", + "Embedding vector length: 1536\n", + "[-0.01842353865504265, -0.00725775770843029, 0.0036669441033154726, -0.0542047917842865, -0.022724902257323265]\n", + "Embedding vector length: 1536\n", + "[-0.055969491600990295, 0.023217301815748215, -0.007630861829966307, 0.002210293198004365, 0.01284848153591156]\n", + "Metrics for: azure/text-embedding-3-small\n", + "{\n", + " \"attempted_request_count\": 2,\n", + " \"successful_response_count\": 2,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 2.0372798442840576,\n", + " \"compute_duration_per_response_seconds\": 1.0186399221420288,\n", + " \"runtime_duration_seconds\": 1.02105712890625,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 2,\n", + " \"prompt_tokens\": 23,\n", + " \"total_tokens\": 23,\n", + " \"tokens_per_response\": 11.5,\n", + " \"responses_with_cost\": 2,\n", + " \"input_cost\": 4.6e-07,\n", + " \"total_cost\": 4.6e-07,\n", + " \"cost_per_response\": 2.3e-07\n", + "}\n" + ] + } + ], + "source": [ + "from graphrag_llm.embedding import LLMEmbedding, create_embedding\n", + "from graphrag_llm.types import LLMEmbeddingArgs\n", + "\n", + "embedding_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_EMBEDDING_MODEL\", \"text-embedding-3-small\"),\n", + " azure_deployment_name=os.getenv(\n", + " \"GRAPHRAG_LLM_EMBEDDING_MODEL\", \"text-embedding-3-small\"\n", + " ),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "\n", + "llm_embedding: LLMEmbedding = create_embedding(embedding_config)\n", + "\n", + "# A single embedding request already accepts a list of inputs to embed\n", + "# Here we demonstrate batching multiple embedding requests concurrently\n", + "# The first request has two inputs to embed and the second has one input\n", + "embedding_requests: list[LLMEmbeddingArgs] = [\n", + " {\"input\": [\"Hello World.\", \"The quick brown fox jumps over the lazy dog.\"]},\n", + " {\"input\": [\"GraphRag is an amazing LLM framework.\"]},\n", + "]\n", + "\n", + "responses = llm_embedding.embedding_batch(embedding_requests, concurrency=4)\n", + "for response in responses:\n", + " if isinstance(response, Exception):\n", + " print(f\"Error: {response}\")\n", + " else:\n", + " for embedding in response.embeddings:\n", + " print(f\"Embedding vector length: {len(embedding)}\")\n", + " print(embedding[0:5]) # Print first 5 dimensions of the embedding vector\n", + "\n", + "print(f\"Metrics for: {llm_embedding.metrics_store.id}\")\n", + "print(json.dumps(llm_embedding.metrics_store.get_metrics(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "0ab62eca", + "metadata": {}, + "source": [ + "## Details\n", + "\n", + "The batch utils start up `concurrency` number of threads in a thread pool and then push all requests into an input queue where free threads pick up the next request to process. The threads will process requests within any defined rate limits and retry any failed request according to the retry settings. If a request fails after all the retries the thread will capture the exception and return it. Thus the batch result may contain exceptions.\n" + ] + }, + { + "cell_type": "markdown", + "id": "005ee408", + "metadata": {}, + "source": [ + "## Thread Pool\n", + "\n", + "The batch utils are convenient if all your requests are loaded in memory. If you wish to stream over an input source then you can use the lower level thread pool utils.\n" + ] + }, + { + "cell_type": "markdown", + "id": "b4a6553c", + "metadata": {}, + "source": [ + "### Completion Thread Pool\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "05643c93", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "request_number_1: Succeeded\n", + "request_number_2: Succeeded\n", + "request_number_6: Succeeded\n", + "request_number_9: Succeeded\n", + "request_number_0: Succeeded\n", + "request_number_5: Succeeded\n", + "request_number_7: Succeeded\n", + "request_number_4: Succeeded\n", + "request_number_3: Succeeded\n", + "request_number_8: Succeeded\n", + "Metrics for: azure/gpt-4o\n", + "{\n", + " \"attempted_request_count\": 10,\n", + " \"successful_response_count\": 10,\n", + " \"failed_response_count\": 0,\n", + " \"failure_rate\": 0.0,\n", + " \"requests_with_retries\": 0,\n", + " \"retries\": 0,\n", + " \"retry_rate\": 0.0,\n", + " \"compute_duration_seconds\": 107.33663082122803,\n", + " \"compute_duration_per_response_seconds\": 10.733663082122803,\n", + " \"runtime_duration_seconds\": 0.04277801513671875,\n", + " \"cached_responses\": 10,\n", + " \"streaming_responses\": 0,\n", + " \"responses_with_tokens\": 10,\n", + " \"prompt_tokens\": 280,\n", + " \"completion_tokens\": 9240,\n", + " \"total_tokens\": 9520,\n", + " \"tokens_per_response\": 952.0,\n", + " \"responses_with_cost\": 10,\n", + " \"input_cost\": 0.0007,\n", + " \"output_cost\": 0.0924,\n", + " \"total_cost\": 0.0931,\n", + " \"cost_per_response\": 0.00931\n", + "}\n" + ] + } + ], + "source": [ + "from collections.abc import Iterator\n", + "\n", + "from graphrag_llm.types import LLMCompletionChunk, LLMCompletionResponse\n", + "\n", + "llm_completion.metrics_store.clear_metrics()\n", + "\n", + "\n", + "# The response handler may also be asynchronous if needed\n", + "def _handle_response(\n", + " request_id: str,\n", + " resp: LLMCompletionResponse | Iterator[LLMCompletionChunk] | Exception,\n", + "):\n", + " # Imagine streaming responses to disk or elsewhere\n", + " if isinstance(resp, Exception):\n", + " print(f\"{request_id}: Failed\")\n", + " else:\n", + " print(f\"{request_id}: Succeeded\")\n", + "\n", + "\n", + "with llm_completion.completion_thread_pool(\n", + " response_handler=_handle_response,\n", + " concurrency=25,\n", + " # set queue_limit to create backpressure on reading the requests\n", + " queue_limit=10,\n", + ") as completion:\n", + " # Iterating over a list of completion requests already in memory\n", + " # but can imagine reading them from disk or another source\n", + " # The completion function returned from the context manager\n", + " # will block if the queue_limit is reached until some requests complete\n", + " # and also requires a request_id for tracking the requests\n", + " # and allowing you to identify them in the response handler\n", + " for index, request in enumerate(completion_requests):\n", + " completion(request_id=f\"request_number_{index}\", **request)\n", + "\n", + "# Using the same request that was used in the caching example so\n", + "# this should complete instantly from cache\n", + "print(f\"Metrics for: {llm_completion.metrics_store.id}\")\n", + "print(json.dumps(llm_completion.metrics_store.get_metrics(), indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "6e254d56", + "metadata": {}, + "source": [ + "### Embedding Thread Pool\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "7eed1a15", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "embedding_request_number_1: Succeeded\n", + "embedding_request_number_0: Succeeded\n" + ] + } + ], + "source": [ + "from graphrag_llm.types import LLMEmbeddingResponse\n", + "\n", + "llm_embedding.metrics_store.clear_metrics()\n", + "\n", + "\n", + "# The response handler may also be asynchronous if needed\n", + "def _handle_response(\n", + " request_id: str,\n", + " resp: LLMEmbeddingResponse | Exception,\n", + "):\n", + " if isinstance(resp, Exception):\n", + " print(f\"{request_id}: Failed\")\n", + " else:\n", + " print(f\"{request_id}: Succeeded\")\n", + "\n", + "\n", + "with llm_embedding.embedding_thread_pool(\n", + " response_handler=_handle_response,\n", + " concurrency=25,\n", + " queue_limit=10,\n", + ") as embedding:\n", + " for index, request in enumerate(embedding_requests):\n", + " embedding(request_id=f\"embedding_request_number_{index}\", **request)" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/09_message_builder_and_history.ipynb b/packages/graphrag-llm/notebooks/09_message_builder_and_history.ipynb new file mode 100644 index 0000000000..acae5c7f2d --- /dev/null +++ b/packages/graphrag-llm/notebooks/09_message_builder_and_history.ipynb @@ -0,0 +1,199 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b22254c3", + "metadata": {}, + "source": [ + "# Message Builder\n", + "\n", + "The completion API adheres to litellm completion API and thus the OpanAI SDK API. The `messages` parameter can be one of the following:\n", + "\n", + "- `str`: Raw string for the prompt.\n", + "- `list[dict[str, Any]]`: A list of dicts in the form `{\"role\": \"user|system|...\", \"content\": \"...\"}`\n", + "- `list[ChatCompletionMessageParam]`: A list of OpenAI `ChatCompletionMessageParam`.\n", + "\n", + "`graphrag_llm.utils` provides a `ChatCompletionMessageParamBuilder` to help construct these objects. Below are examples of using the builder.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "553f83d9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arrr, ye got me there, matey. Truth be, Pluto ain't considered a full-fledged planet no more. Back in 2006, them scallywags at the International Astronomical Union demoted it to a “dwarf planet.” So in the eyes of modern astronomers, 'tis a no.\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig\n", + "from graphrag_llm.types import LLMCompletionResponse\n", + "from graphrag_llm.utils import (\n", + " CompletionMessagesBuilder,\n", + ")\n", + "\n", + "load_dotenv()\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "\n", + "messages = (\n", + " CompletionMessagesBuilder()\n", + " .add_system_message(\n", + " \"You are a helpful assistant that likes to talk like a pirate. Respond as if you are a pirate using pirate speak.\"\n", + " )\n", + " .add_user_message(\"Is pluto a planet? Respond with a yes or no.\")\n", + " .add_assistant_message(\"Aye, matey! Pluto be a planet in me book.\")\n", + " .add_user_message(\"Are you sure? I want the truth. Can you elaborate?\")\n", + " .build()\n", + ")\n", + "\n", + "response: LLMCompletionResponse = llm_completion.completion(messages=messages) # type: ignore\n", + "\n", + "print(response.content)" + ] + }, + { + "cell_type": "markdown", + "id": "acb265fe", + "metadata": {}, + "source": [ + "## Other Message Types\n", + "\n", + "Can use the `ChatCompletionMessageParamBuilder` along with `ChatCompletionContentPartParamBuilder` to build more complicated messages such as those using images.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c7b094a6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The image features a capybara floating in space. The backdrop displays a colorful and vibrant interstellar scene filled with nebulae and stars, showcasing various shades of blue, purple, pink, and green. The capybara is slightly tilted with its face foregrounded, giving a whimsical and surreal feel as if it is soaring through the cosmos.\n" + ] + } + ], + "source": [ + "from graphrag_llm.utils import CompletionContentPartBuilder\n", + "\n", + "messages = (\n", + " CompletionMessagesBuilder()\n", + " .add_user_message(\n", + " # Instead of providing a string we are providing content parts\n", + " # By using the CompletionContentPartBuilder\n", + " CompletionContentPartBuilder()\n", + " .add_text_part(\"Describe this image\")\n", + " .add_image_part(\n", + " # Can also be a base64 encoded image string\n", + " url=\"https://th.bing.com/th/id/OUG.0A10DBFCEB3A9A7C6707FCF6F0D96BFD?cb=ucfimg2&ucfimg=1&rs=1&pid=ImgDetMain&o=7&rm=3\",\n", + " detail=\"high\",\n", + " )\n", + " .build()\n", + " )\n", + " .build()\n", + ")\n", + "\n", + "response: LLMCompletionResponse = llm_completion.completion(messages=messages) # type: ignore\n", + "print(response.content)" + ] + }, + { + "cell_type": "markdown", + "id": "e8b9be3e", + "metadata": {}, + "source": [ + "## History\n", + "\n", + "The first example eluded to how the `ChatCompletionMessageParamBuilder` can be used to track history.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92abc427", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "User: Is Pluto a planet? Answer with a yes or no.\n", + "Assistant: No.\n", + "User: Can you elaborate?\n", + "Assistant: In 2006, the International Astronomical Union (IAU) redefined the criteria for classifying planets. According to this new definition, for an object to be considered a planet, it must:\n", + "\n", + "1. Orbit the Sun.\n", + "2. Be spherical in shape (have sufficient mass for its gravity to overcome rigid body forces so that it assumes a nearly round shape).\n", + "3. Have cleared its orbit of other debris.\n", + "\n", + "Pluto meets the first two criteria but does not meet the third criterion because it shares its orbit with other objects in the Kuiper Belt. Therefore, Pluto was reclassified as a \"dwarf planet.\"\n" + ] + } + ], + "source": [ + "user_messages = [\"Is Pluto a planet? Answer with a yes or no.\", \"Can you elaborate?\"]\n", + "\n", + "messages_builder = CompletionMessagesBuilder()\n", + "\n", + "for msg in user_messages:\n", + " print(f\"User: {msg}\")\n", + "\n", + " messages_builder.add_user_message(msg)\n", + "\n", + " response: LLMCompletionResponse = llm_completion.completion(\n", + " messages=messages_builder.build()\n", + " ) # type: ignore\n", + " print(f\"Assistant: {response.content}\")\n", + "\n", + " messages_builder.add_assistant_message(response.content)" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/10_tool_calling.ipynb b/packages/graphrag-llm/notebooks/10_tool_calling.ipynb new file mode 100644 index 0000000000..8ebec94eb1 --- /dev/null +++ b/packages/graphrag-llm/notebooks/10_tool_calling.ipynb @@ -0,0 +1,387 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a56845b0", + "metadata": {}, + "source": [ + "# Function Tool Calling\n", + "\n", + "In order to use function tools, the completion endpoint needs a json schema of the function(s). This notebook uses `pydantic` to describe a function and its parameters and the `OpenAI` built-in `pydantic_function_tool` to create the necessary json schema. Other techniques may be used to create a definition for your functions.\n" + ] + }, + { + "cell_type": "markdown", + "id": "daf62482", + "metadata": {}, + "source": [ + "## Manual Function Tool Calling\n", + "\n", + "This example demonstrates function tool calling by manually using `pydantic` and `pydantic_function_tool`. See the next example for a simplified approach.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53437ac4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Adding 5 and 7 gives you 12.\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import json\n", + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig\n", + "from graphrag_llm.types import LLMCompletionResponse\n", + "from graphrag_llm.utils import (\n", + " CompletionMessagesBuilder,\n", + ")\n", + "from openai import pydantic_function_tool\n", + "from pydantic import BaseModel, ConfigDict, Field\n", + "\n", + "load_dotenv()\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "\n", + "class AddTwoNumbers(BaseModel):\n", + " \"\"\"Input Argument for add two numbers.\"\"\"\n", + "\n", + " model_config = ConfigDict(\n", + " extra=\"forbid\",\n", + " )\n", + "\n", + " a: int = Field(description=\"The first number to add.\")\n", + " b: int = Field(description=\"The second number to add.\")\n", + "\n", + "\n", + "# The actual function\n", + "def add_two_numbers(options: AddTwoNumbers) -> int:\n", + " \"\"\"Add two numbers.\"\"\"\n", + " return options.a + options.b\n", + "\n", + "\n", + "add_definition = pydantic_function_tool(\n", + " AddTwoNumbers,\n", + " # Function name and description\n", + " name=\"my_add_two_numbers_function\",\n", + " description=\"Add two numbers.\",\n", + ")\n", + "\n", + "# Mapping of available functions\n", + "available_functions = {\n", + " \"my_add_two_numbers_function\": {\n", + " \"function\": add_two_numbers,\n", + " \"input_model\": AddTwoNumbers,\n", + " },\n", + "}\n", + "\n", + "messages_builder = CompletionMessagesBuilder().add_user_message(\n", + " \"Add 5 and 7 using a function call.\"\n", + ")\n", + "\n", + "response: LLMCompletionResponse = llm_completion.completion(\n", + " messages=messages_builder.build(),\n", + " tools=[add_definition],\n", + ") # type: ignore\n", + "\n", + "if not response.choices[0].message.tool_calls:\n", + " msg = \"No function call found in response.\"\n", + " raise ValueError(msg)\n", + "\n", + "# Add the assistant message with the function call to the message history\n", + "messages_builder.add_assistant_message(\n", + " message=response.choices[0].message,\n", + ")\n", + "\n", + "for tool_call in response.choices[0].message.tool_calls:\n", + " tool_id = tool_call.id\n", + " if tool_call.type != \"function\":\n", + " continue\n", + " function_name = tool_call.function.name\n", + " function_args = tool_call.function.arguments\n", + "\n", + " args_dict = json.loads(function_args)\n", + "\n", + " InputModel = available_functions[function_name][\"input_model\"]\n", + " function = available_functions[function_name][\"function\"]\n", + " input_options = InputModel(**args_dict)\n", + "\n", + " result = function(input_options)\n", + "\n", + " messages_builder.add_tool_message(\n", + " content=str(result),\n", + " tool_call_id=tool_id,\n", + " )\n", + "\n", + "final_response: LLMCompletionResponse = llm_completion.completion(\n", + " messages=messages_builder.build(),\n", + ") # type: ignore\n", + "print(final_response.content)" + ] + }, + { + "cell_type": "markdown", + "id": "b31c7a9c", + "metadata": {}, + "source": [ + "### Function Tool Definition\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eb6950e8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"my_add_two_numbers_function\",\n", + " \"strict\": true,\n", + " \"parameters\": {\n", + " \"additionalProperties\": false,\n", + " \"description\": \"Input Argument for add two numbers.\",\n", + " \"properties\": {\n", + " \"a\": {\n", + " \"description\": \"The first number to add.\",\n", + " \"title\": \"A\",\n", + " \"type\": \"integer\"\n", + " },\n", + " \"b\": {\n", + " \"description\": \"The second number to add.\",\n", + " \"title\": \"B\",\n", + " \"type\": \"integer\"\n", + " }\n", + " },\n", + " \"required\": [\n", + " \"a\",\n", + " \"b\"\n", + " ],\n", + " \"title\": \"AddTwoNumbers\",\n", + " \"type\": \"object\"\n", + " },\n", + " \"description\": \"Add two numbers.\"\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "# View the output schema\n", + "# This is what is passed to the completion tools param\n", + "# Created using pydantic and pydantic_function_tool\n", + "# but may be created manually as well\n", + "print(json.dumps(add_definition, indent=2))" + ] + }, + { + "cell_type": "markdown", + "id": "660de4c9", + "metadata": {}, + "source": [ + "## Tool Calling with FunctionToolManager\n", + "\n", + "If using `pydantic` to describe function arguments, you can use the `FunctionToolManager` to register functions, produce defintions, and call functions in response to the LLM. This helps automate some of the above work.\n", + "\n", + "The following example demonstrates calling multiple functions in one LLM call.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4fae701e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Adding numbers: 3 8\n", + "Multiplying numbers: 9 5\n", + "Reversing text: GraphRAG\n", + "3 + 8 is 11, 9 * 5 is 45, and the reversed string 'GraphRAG' is 'GARhparG'.\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import AuthMethod, ModelConfig\n", + "from graphrag_llm.types import LLMCompletionResponse\n", + "from graphrag_llm.utils import (\n", + " CompletionMessagesBuilder,\n", + " FunctionToolManager,\n", + ")\n", + "from pydantic import BaseModel, ConfigDict, Field\n", + "\n", + "load_dotenv()\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "\n", + "class NumbersInput(BaseModel):\n", + " \"\"\"Numbers input.\"\"\"\n", + "\n", + " model_config = ConfigDict(\n", + " extra=\"forbid\",\n", + " )\n", + "\n", + " a: int = Field(description=\"The first number.\")\n", + " b: int = Field(description=\"The second number.\")\n", + "\n", + "\n", + "def add(options: NumbersInput) -> str:\n", + " \"\"\"Add two numbers.\"\"\"\n", + " # Print something to ensure function is called for verification\n", + " print(\"Adding numbers:\", options.a, options.b)\n", + " return str(options.a + options.b)\n", + "\n", + "\n", + "def multiply(options: NumbersInput) -> str:\n", + " \"\"\"Multiply two numbers.\"\"\"\n", + " # Print something to ensure function is called for verification\n", + " print(\"Multiplying numbers:\", options.a, options.b)\n", + " return str(options.a * options.b)\n", + "\n", + "\n", + "class TextInput(BaseModel):\n", + " \"\"\"Text input.\"\"\"\n", + "\n", + " model_config = ConfigDict(\n", + " extra=\"forbid\",\n", + " )\n", + "\n", + " test: str = Field(description=\"The string to reverse.\")\n", + "\n", + "\n", + "def reverse_text(options: TextInput) -> str:\n", + " \"\"\"Reverse a string.\"\"\"\n", + " # Print something to ensure function is called for verification\n", + " print(\"Reversing text:\", options.test)\n", + " return options.test[::-1]\n", + "\n", + "\n", + "function_tool_manager = FunctionToolManager()\n", + "\n", + "function_tool_manager.register_function_tool(\n", + " name=\"add\",\n", + " description=\"Add two numbers.\",\n", + " function=add,\n", + " input_model=NumbersInput,\n", + ")\n", + "function_tool_manager.register_function_tool(\n", + " name=\"multiply\",\n", + " description=\"Multiply two numbers.\",\n", + " function=multiply,\n", + " input_model=NumbersInput,\n", + ")\n", + "function_tool_manager.register_function_tool(\n", + " name=\"reverse_text\",\n", + " description=\"Reverse a string.\",\n", + " function=reverse_text,\n", + " input_model=TextInput,\n", + ")\n", + "\n", + "\n", + "messages_builder = CompletionMessagesBuilder().add_user_message(\n", + " \"What is 3 + 8 and 9 * 5? Also, reverse the string 'GraphRAG'.\"\n", + ")\n", + "\n", + "# Multiple tool calls in parallel\n", + "response: LLMCompletionResponse = llm_completion.completion(\n", + " messages=messages_builder.build(),\n", + " tools=function_tool_manager.definitions(),\n", + " parallel_tool_calls=True,\n", + ") # type: ignore\n", + "\n", + "# Add the assistant message with the function call to the message history\n", + "messages_builder.add_assistant_message(\n", + " message=response.choices[0].message,\n", + ")\n", + "\n", + "tool_results = function_tool_manager.call_functions(response)\n", + "\n", + "for tool_message in tool_results:\n", + " messages_builder.add_tool_message(**tool_message)\n", + "\n", + "final_response: LLMCompletionResponse = llm_completion.completion(\n", + " messages=messages_builder.build(),\n", + ") # type: ignore\n", + "print(final_response.content)" + ] + }, + { + "cell_type": "markdown", + "id": "b2d36f7a", + "metadata": {}, + "source": [ + "## MCP Tools\n", + "\n", + "**Not currently supported**. `graphrag_llm` currently only implements the `completion` endpoints which do not support MCP tools.\n" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/11_templating.ipynb b/packages/graphrag-llm/notebooks/11_templating.ipynb new file mode 100644 index 0000000000..bfee940c53 --- /dev/null +++ b/packages/graphrag-llm/notebooks/11_templating.ipynb @@ -0,0 +1,149 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6945138b", + "metadata": {}, + "source": [ + "# Templating\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3322e6a7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The rendered message to parse: \n", + "It is sunny and 52 degrees fahrenheit in Seattle.\n", + "\n", + "It is cloudy and 75 degrees fahrenheit in San Francisco.\n", + "\n", + "City: Seattle\n", + " Temperature: 11.1 °C\n", + " Condition: Sunny\n", + "City: San Francisco\n", + " Temperature: 23.9 °C\n", + " Condition: Cloudy\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import os\n", + "\n", + "from dotenv import load_dotenv\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import (\n", + " AuthMethod,\n", + " ModelConfig,\n", + " TemplateEngineConfig,\n", + " TemplateEngineType,\n", + " TemplateManagerType,\n", + ")\n", + "from graphrag_llm.templating import create_template_engine\n", + "from graphrag_llm.types import LLMCompletionResponse\n", + "from pydantic import BaseModel, Field\n", + "\n", + "load_dotenv()\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " model_provider=\"azure\",\n", + " model=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " azure_deployment_name=os.getenv(\"GRAPHRAG_MODEL\", \"gpt-4o\"),\n", + " api_base=os.getenv(\"GRAPHRAG_API_BASE\"),\n", + " api_version=os.getenv(\"GRAPHRAG_API_VERSION\", \"2025-04-01-preview\"),\n", + " api_key=api_key,\n", + " auth_method=AuthMethod.AzureManagedIdentity if not api_key else AuthMethod.ApiKey,\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "\n", + "template_engine = create_template_engine()\n", + "\n", + "# The above default is the same as the following configuration:\n", + "template_engine = create_template_engine(\n", + " TemplateEngineConfig(\n", + " type=TemplateEngineType.Jinja,\n", + " template_manager=TemplateManagerType.File,\n", + " base_dir=\"templates\",\n", + " template_extension=\".jinja\",\n", + " encoding=\"utf-8\",\n", + " )\n", + ")\n", + "\n", + "msg = template_engine.render(\n", + " # Name of the template file without extension\n", + " template_name=\"weather_listings\",\n", + " # Values to fill in the template\n", + " context={\n", + " \"weather_reports\": [\n", + " {\"city\": \"Seattle\", \"temperature_f\": 52, \"condition\": \"sunny\"},\n", + " {\"city\": \"San Francisco\", \"temperature_f\": 75, \"condition\": \"cloudy\"},\n", + " ]\n", + " },\n", + ")\n", + "\n", + "\n", + "print(f\"The rendered message to parse: {msg}\")\n", + "\n", + "\n", + "# Structured response parsing using pydantic\n", + "class LocalWeather(BaseModel):\n", + " \"\"\"City weather information model.\"\"\"\n", + "\n", + " city: str = Field(description=\"The name of the city\")\n", + " temperature: float = Field(description=\"The temperature in Celsius\")\n", + " condition: str = Field(description=\"The weather condition description\")\n", + "\n", + "\n", + "class WeatherReports(BaseModel):\n", + " \"\"\"Weather information model.\"\"\"\n", + "\n", + " reports: list[LocalWeather] = Field(\n", + " description=\"The weather reports for multiple cities\"\n", + " )\n", + "\n", + "\n", + "response: LLMCompletionResponse[WeatherReports] = llm_completion.completion(\n", + " messages=msg,\n", + " response_format=WeatherReports,\n", + ") # type: ignore\n", + "\n", + "local_weather_reports: WeatherReports = response.formatted_response # type: ignore\n", + "for report in local_weather_reports.reports:\n", + " print(f\"City: {report.city}\")\n", + " print(f\" Temperature: {report.temperature} °C\")\n", + " print(f\" Condition: {report.condition}\")" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/12_mocking.ipynb b/packages/graphrag-llm/notebooks/12_mocking.ipynb new file mode 100644 index 0000000000..6cee72f293 --- /dev/null +++ b/packages/graphrag-llm/notebooks/12_mocking.ipynb @@ -0,0 +1,164 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9418b981", + "metadata": {}, + "source": [ + "# Mocking\n" + ] + }, + { + "cell_type": "markdown", + "id": "1d000d70", + "metadata": {}, + "source": [ + "## Completions\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "792c4fa3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Who cares?\n", + "You tell me!\n", + "{\"reports\":[{\"city\":\"New York\",\"temperature\":22.5,\"condition\":\"Sunny\"}]}\n", + "Who cares?\n" + ] + } + ], + "source": [ + "# Copyright (c) 2024 Microsoft Corporation.\n", + "# Licensed under the MIT License\n", + "\n", + "import os\n", + "\n", + "from graphrag_llm.completion import LLMCompletion, create_completion\n", + "from graphrag_llm.config import LLMProviderType, ModelConfig\n", + "from graphrag_llm.types import LLMCompletionResponse\n", + "from pydantic import BaseModel, Field\n", + "\n", + "\n", + "class LocalWeather(BaseModel):\n", + " \"\"\"City weather information model.\"\"\"\n", + "\n", + " city: str = Field(description=\"The name of the city\")\n", + " temperature: float = Field(description=\"The temperature in Celsius\")\n", + " condition: str = Field(description=\"The weather condition description\")\n", + "\n", + "\n", + "class WeatherReports(BaseModel):\n", + " \"\"\"Weather information model.\"\"\"\n", + "\n", + " reports: list[LocalWeather] = Field(\n", + " description=\"The weather reports for multiple cities\"\n", + " )\n", + "\n", + "\n", + "weather_reports = WeatherReports(\n", + " reports=[\n", + " LocalWeather(city=\"New York\", temperature=22.5, condition=\"Sunny\"),\n", + " ]\n", + ")\n", + "\n", + "api_key = os.getenv(\"GRAPHRAG_API_KEY\")\n", + "model_config = ModelConfig(\n", + " type=LLMProviderType.MockLLM,\n", + " model_provider=\"openai\",\n", + " model=\"gpt-4o\",\n", + " mock_responses=[\"Who cares?\", \"You tell me!\", weather_reports.model_dump_json()],\n", + ")\n", + "llm_completion: LLMCompletion = create_completion(model_config)\n", + "\n", + "response: LLMCompletionResponse = llm_completion.completion(\n", + " messages=\"What is the capital of France?\",\n", + ") # type: ignore\n", + "\n", + "print(response.content)\n", + "\n", + "response: LLMCompletionResponse = llm_completion.completion(\n", + " messages=\"Should be second response\",\n", + ") # type: ignore\n", + "print(response.content)\n", + "\n", + "response_formatted: LLMCompletionResponse[WeatherReports] = llm_completion.completion(\n", + " messages=\"Structured response.\",\n", + " response_format=WeatherReports,\n", + ") # type: ignore\n", + "print(response_formatted.formatted_response.model_dump_json()) # type: ignore\n", + "\n", + "response: LLMCompletionResponse = llm_completion.completion(\n", + " messages=\"Should cycle back to first response\",\n", + ") # type: ignore\n", + "print(response.content)" + ] + }, + { + "cell_type": "markdown", + "id": "2c8f1b7a", + "metadata": {}, + "source": [ + "## Embeddings\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6eec6dc3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1.0, 2.0, 3.0]\n", + "[1.0, 2.0, 3.0]\n" + ] + } + ], + "source": [ + "from graphrag_llm.embedding import LLMEmbedding, create_embedding\n", + "\n", + "embedding_config = ModelConfig(\n", + " type=LLMProviderType.MockLLM,\n", + " model_provider=\"openai\",\n", + " model=\"text-embedding-3-small\",\n", + " mock_responses=[1.0, 2.0, 3.0],\n", + ")\n", + "\n", + "llm_embedding: LLMEmbedding = create_embedding(embedding_config)\n", + "\n", + "embeddings_response = llm_embedding.embedding(input=[\"Hello world\", \"How are you?\"])\n", + "for embedding in embeddings_response.embeddings:\n", + " print(embedding[0:3])" + ] + } + ], + "metadata": { + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/graphrag-llm/notebooks/README.md b/packages/graphrag-llm/notebooks/README.md new file mode 100644 index 0000000000..0496ddc6aa --- /dev/null +++ b/packages/graphrag-llm/notebooks/README.md @@ -0,0 +1,11 @@ +To run the notebooks you need to add a `.env` file to the `notebooks` directory with the following information + +``` +GRAPHRAG_MODEL="..." +GRAPHRAG_EMBEDDING_MODEL="..." +GRAPHRAG_API_BASE="..." +# API Key and version are optional +# If not provided, Azure managed identity will be used +GRAPHRAG_API_KEY="..." +GRAPHRAG_API_VERSION="..." +``` \ No newline at end of file diff --git a/packages/graphrag-llm/notebooks/templates/weather_listings.jinja b/packages/graphrag-llm/notebooks/templates/weather_listings.jinja new file mode 100644 index 0000000000..b3f083dea8 --- /dev/null +++ b/packages/graphrag-llm/notebooks/templates/weather_listings.jinja @@ -0,0 +1,3 @@ +{% for report in weather_reports %} +It is {{report.condition}} and {{report.temperature_f}} degrees fahrenheit in {{report.city}}. +{% endfor %} \ No newline at end of file diff --git a/packages/graphrag-llm/pyproject.toml b/packages/graphrag-llm/pyproject.toml new file mode 100644 index 0000000000..b10639a702 --- /dev/null +++ b/packages/graphrag-llm/pyproject.toml @@ -0,0 +1,46 @@ +[project] +name = "graphrag-llm" +version = "2.7.1" +description = "GraphRAG LLM package." +authors = [ + {name = "Alonso Guevara Fernández", email = "alonsog@microsoft.com"}, + {name = "Andrés Morales Esquivel", email = "andresmor@microsoft.com"}, + {name = "Chris Trevino", email = "chtrevin@microsoft.com"}, + {name = "David Tittsworth", email = "datittsw@microsoft.com"}, + {name = "Dayenne de Souza", email = "ddesouza@microsoft.com"}, + {name = "Derek Worthen", email = "deworthe@microsoft.com"}, + {name = "Gaudy Blanco Meneses", email = "gaudyb@microsoft.com"}, + {name = "Ha Trinh", email = "trinhha@microsoft.com"}, + {name = "Jonathan Larson", email = "jolarso@microsoft.com"}, + {name = "Josh Bradley", email = "joshbradley@microsoft.com"}, + {name = "Kate Lytvynets", email = "kalytv@microsoft.com"}, + {name = "Kenny Zhang", email = "zhangken@microsoft.com"}, + {name = "Mónica Carvajal"}, + {name = "Nathan Evans", email = "naevans@microsoft.com"}, + {name = "Rodrigo Racanicci", email = "rracanicci@microsoft.com"}, + {name = "Sarah Smith", email = "smithsarah@microsoft.com"}, +] +license = "MIT" +readme = "README.md" +license-files = ["LICENSE"] +requires-python = ">=3.10,<3.13" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "azure-identity~=1.19.0", + "graphrag-cache==2.7.1", + "graphrag-common==2.7.1", + "jinja2~=3.1", + "litellm~=1.80", + "nest-asyncio2~=1.7", + "pydantic~=2.10", + "typing-extensions~=4.12" +] + +[build-system] +requires = ["hatchling>=1.27.0,<2.0.0"] +build-backend = "hatchling.build" diff --git a/packages/graphrag-storage/README.md b/packages/graphrag-storage/README.md new file mode 100644 index 0000000000..d190130f19 --- /dev/null +++ b/packages/graphrag-storage/README.md @@ -0,0 +1,92 @@ +# GraphRAG Storage + +This package provides a unified storage abstraction layer with support for multiple backends including file system, Azure Blob, Azure Cosmos, and memory storage. It features a factory-based creation system with configuration-driven setup and extensible architecture for implementing custom storage providers. + +## Basic + +This example creates a file storage system using the GraphRAG storage package's configuration system. The example shows setting up file storage in a specified directory and demonstrates basic storage operations like setting and getting key-value pairs. + +```python +import asyncio +from graphrag_storage import StorageConfig, create_storage, StorageType + +async def run(): + storage = create_storage( + StorageConfig( + type=StorageType.File + base_dir="output" + ) + ) + + await storage.set("my_key", "value") + print(await storage.get("my_key")) + +if __name__ == "__main__": + asyncio.run(run()) +``` + +## Custom Storage + +Here we create a custom storage implementation by extending the base Storage class and registering it with the GraphRAG storage system. Once registered, the custom storage can be instantiated through the factory pattern using either StorageConfig or directly via storage_factory, enabling extensible storage solutions for different backends. + +```python +import asyncio +from typing import Any +from graphrag_storage import Storage, StorageConfig, create_storage, register_storage + +class MyStorage(Storage): + def __init__(self, some_setting: str, optional_setting: str = "default setting", **kwargs: Any): + # Validate settings and initialize + ... + + #Implement rest of interface + ... + +register_storage("MyStorage", MyStorage) + +async def run(): + storage = create_storage( + StorageConfig( + type="MyStorage" + some_setting="My Setting" + ) + ) + # Or use the factory directly to instantiate with a dict instead of using + # StorageConfig + create_factory + # from graphrag_storage.storage_factory import storage_factory + # storage = storage_factory.create(strategy="MyStorage", init_args={"some_setting": "My Setting"}) + + await storage.set("my_key", "value") + print(await storage.get("my_key")) + +if __name__ == "__main__": + asyncio.run(run()) +``` + +## Details + +By default, the `create_storage` comes with the following storage providers registered that correspond to the entries in the `StorageType` enum. + +- `FileStorage` +- `AzureBlobStorage` +- `AzureCosmosStorage` +- `MemoryStorage` + +The preregistration happens dynamically, e.g., `FileStorage` is only imported and registered if you request a `FileStorage` with `create_storage(StorageType.File, ...)`. There is no need to manually import and register builtin storage providers when using `create_storage`. + +If you want a clean factory with no preregistered storage providers then directly import `storage_factory` and bypass using `create_storage`. The downside is that `storage_factory.create` uses a dict for init args instead of the strongly typed `StorageConfig` used with `create_storage`. + +```python +from graphrag_storage.storage_factory import storage_factory +from graphrag_storage.file_storage import FileStorage + +# storage_factory has no preregistered providers so you must register any +# providers you plan on using. +# May also register a custom implementation, see above for example. +storage_factory.register("my_storage_key", FileStorage) + +storage = storage_factory.create(strategy="my_storage_key", init_args={"base_dir": "...", "other_settings": "..."}) + +... + +``` \ No newline at end of file diff --git a/packages/graphrag-storage/graphrag_storage/__init__.py b/packages/graphrag-storage/graphrag_storage/__init__.py new file mode 100644 index 0000000000..2ae67be741 --- /dev/null +++ b/packages/graphrag-storage/graphrag_storage/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""The GraphRAG Storage package.""" + +from graphrag_storage.storage import Storage +from graphrag_storage.storage_config import StorageConfig +from graphrag_storage.storage_factory import ( + create_storage, + register_storage, +) +from graphrag_storage.storage_type import StorageType + +__all__ = [ + "Storage", + "StorageConfig", + "StorageType", + "create_storage", + "register_storage", +] diff --git a/graphrag/storage/blob_pipeline_storage.py b/packages/graphrag-storage/graphrag_storage/azure_blob_storage.py similarity index 51% rename from graphrag/storage/blob_pipeline_storage.py rename to packages/graphrag-storage/graphrag_storage/azure_blob_storage.py index 2a5ba8c41f..68cca2c017 100644 --- a/graphrag/storage/blob_pipeline_storage.py +++ b/packages/graphrag-storage/graphrag_storage/azure_blob_storage.py @@ -1,7 +1,7 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""Azure Blob Storage implementation of PipelineStorage.""" +"""Azure Blob Storage implementation of Storage.""" import logging import re @@ -12,64 +12,68 @@ from azure.identity import DefaultAzureCredential from azure.storage.blob import BlobServiceClient -from graphrag.storage.pipeline_storage import ( - PipelineStorage, +from graphrag_storage.storage import ( + Storage, get_timestamp_formatted_with_local_tz, ) logger = logging.getLogger(__name__) -class BlobPipelineStorage(PipelineStorage): +class AzureBlobStorage(Storage): """The Blob-Storage implementation.""" _connection_string: str | None _container_name: str - _path_prefix: str + _base_dir: str | None _encoding: str - _storage_account_blob_url: str | None + _account_url: str | None + _blob_service_client: BlobServiceClient + _storage_account_name: str | None - def __init__(self, **kwargs: Any) -> None: + def __init__( + self, + container_name: str, + account_url: str | None = None, + connection_string: str | None = None, + base_dir: str | None = None, + encoding: str = "utf-8", + **kwargs: Any, + ) -> None: """Create a new BlobStorage instance.""" - connection_string = kwargs.get("connection_string") - storage_account_blob_url = kwargs.get("storage_account_blob_url") - path_prefix = kwargs.get("base_dir") - container_name = kwargs["container_name"] - if container_name is None: - msg = "No container name provided for blob storage." - raise ValueError(msg) - if connection_string is None and storage_account_blob_url is None: - msg = "No storage account blob url provided for blob storage." + if connection_string is not None and account_url is not None: + msg = "AzureBlobStorage requires only one of connection_string or account_url to be specified, not both." + logger.error(msg) raise ValueError(msg) - logger.info("Creating blob storage at %s", container_name) + _validate_blob_container_name(container_name) + + logger.info( + "Creating blob storage at [%s] and base_dir [%s]", + container_name, + base_dir, + ) if connection_string: self._blob_service_client = BlobServiceClient.from_connection_string( connection_string ) - else: - if storage_account_blob_url is None: - msg = "Either connection_string or storage_account_blob_url must be provided." - raise ValueError(msg) - + elif account_url: self._blob_service_client = BlobServiceClient( - account_url=storage_account_blob_url, + account_url=account_url, credential=DefaultAzureCredential(), ) - self._encoding = kwargs.get("encoding", "utf-8") + else: + msg = "AzureBlobStorage requires either a connection_string or account_url to be specified." + logger.error(msg) + raise ValueError(msg) + + self._encoding = encoding self._container_name = container_name self._connection_string = connection_string - self._path_prefix = path_prefix or "" - self._storage_account_blob_url = storage_account_blob_url + self._base_dir = base_dir + self._account_url = account_url self._storage_account_name = ( - storage_account_blob_url.split("//")[1].split(".")[0] - if storage_account_blob_url - else None - ) - logger.debug( - "creating blob storage at container=%s, path=%s", - self._container_name, - self._path_prefix, + account_url.split("//")[1].split(".")[0] if account_url else None ) self._create_container() @@ -82,6 +86,7 @@ def _create_container(self) -> None: for container in self._blob_service_client.list_containers() ] if container_name not in container_names: + logger.debug("Creating new container [%s]", container_name) self._blob_service_client.create_container(container_name) def _delete_container(self) -> None: @@ -100,85 +105,63 @@ def _container_exists(self) -> bool: def find( self, file_pattern: re.Pattern[str], - base_dir: str | None = None, - file_filter: dict[str, Any] | None = None, - max_count=-1, - ) -> Iterator[tuple[str, dict[str, Any]]]: - """Find blobs in a container using a file pattern, as well as a custom filter function. + ) -> Iterator[str]: + """Find blobs in a container using a file pattern. Params: - base_dir: The name of the base container. file_pattern: The file pattern to use. - file_filter: A dictionary of key-value pairs to filter the blobs. - max_count: The maximum number of blobs to return. If -1, all blobs are returned. Returns ------- An iterator of blob names and their corresponding regex matches. """ - base_dir = base_dir or "" - logger.info( - "search container %s for files matching %s", + "Search container [%s] in base_dir [%s] for files matching [%s]", self._container_name, + self._base_dir, file_pattern.pattern, ) def _blobname(blob_name: str) -> str: - if blob_name.startswith(self._path_prefix): - blob_name = blob_name.replace(self._path_prefix, "", 1) + if self._base_dir and blob_name.startswith(self._base_dir): + blob_name = blob_name.replace(self._base_dir, "", 1) if blob_name.startswith("/"): blob_name = blob_name[1:] return blob_name - def item_filter(item: dict[str, Any]) -> bool: - if file_filter is None: - return True - - return all( - re.search(value, item[key]) for key, value in file_filter.items() - ) - try: container_client = self._blob_service_client.get_container_client( self._container_name ) - all_blobs = list(container_client.list_blobs()) - + all_blobs = list(container_client.list_blobs(self._base_dir)) + logger.debug("All blobs: %s", [blob.name for blob in all_blobs]) num_loaded = 0 num_total = len(list(all_blobs)) num_filtered = 0 for blob in all_blobs: match = file_pattern.search(blob.name) - if match and blob.name.startswith(base_dir): - group = match.groupdict() - if item_filter(group): - yield (_blobname(blob.name), group) - num_loaded += 1 - if max_count > 0 and num_loaded >= max_count: - break - else: - num_filtered += 1 + if match: + yield _blobname(blob.name) + num_loaded += 1 else: num_filtered += 1 - logger.debug( - "Blobs loaded: %d, filtered: %d, total: %d", - num_loaded, - num_filtered, - num_total, - ) + logger.debug( + "Blobs loaded: %d, filtered: %d, total: %d", + num_loaded, + num_filtered, + num_total, + ) except Exception: # noqa: BLE001 logger.warning( - "Error finding blobs: base_dir=%s, file_pattern=%s, file_filter=%s", - base_dir, + "Error finding blobs: base_dir=%s, file_pattern=%s", + self._base_dir, file_pattern, - file_filter, ) async def get( self, key: str, as_bytes: bool | None = False, encoding: str | None = None ) -> Any: - """Get a value from the cache.""" + """Get a value from the blob.""" try: key = self._keyname(key) container_client = self._blob_service_client.get_container_client( @@ -196,7 +179,7 @@ async def get( return blob_data async def set(self, key: str, value: Any, encoding: str | None = None) -> None: - """Set a value in the cache.""" + """Set a value in the blob.""" try: key = self._keyname(key) container_client = self._blob_service_client.get_container_client( @@ -211,46 +194,8 @@ async def set(self, key: str, value: Any, encoding: str | None = None) -> None: except Exception: logger.exception("Error setting key %s: %s", key) - def _set_df_json(self, key: str, dataframe: Any) -> None: - """Set a json dataframe.""" - if self._connection_string is None and self._storage_account_name: - dataframe.to_json( - self._abfs_url(key), - storage_options={ - "account_name": self._storage_account_name, - "credential": DefaultAzureCredential(), - }, - orient="records", - lines=True, - force_ascii=False, - ) - else: - dataframe.to_json( - self._abfs_url(key), - storage_options={"connection_string": self._connection_string}, - orient="records", - lines=True, - force_ascii=False, - ) - - def _set_df_parquet(self, key: str, dataframe: Any) -> None: - """Set a parquet dataframe.""" - if self._connection_string is None and self._storage_account_name: - dataframe.to_parquet( - self._abfs_url(key), - storage_options={ - "account_name": self._storage_account_name, - "credential": DefaultAzureCredential(), - }, - ) - else: - dataframe.to_parquet( - self._abfs_url(key), - storage_options={"connection_string": self._connection_string}, - ) - async def has(self, key: str) -> bool: - """Check if a key exists in the cache.""" + """Check if a key exists in the blob.""" key = self._keyname(key) container_client = self._blob_service_client.get_container_client( self._container_name @@ -259,7 +204,7 @@ async def has(self, key: str) -> bool: return blob_client.exists() async def delete(self, key: str) -> None: - """Delete a key from the cache.""" + """Delete a key from the blob.""" key = self._keyname(key) container_client = self._blob_service_client.get_container_client( self._container_name @@ -270,17 +215,17 @@ async def delete(self, key: str) -> None: async def clear(self) -> None: """Clear the cache.""" - def child(self, name: str | None) -> "PipelineStorage": + def child(self, name: str | None) -> "Storage": """Create a child storage instance.""" if name is None: return self - path = str(Path(self._path_prefix) / name) - return BlobPipelineStorage( + path = str(Path(self._base_dir) / name) if self._base_dir else name + return AzureBlobStorage( connection_string=self._connection_string, container_name=self._container_name, encoding=self._encoding, base_dir=path, - storage_account_blob_url=self._storage_account_blob_url, + account_url=self._account_url, ) def keys(self) -> list[str]: @@ -290,15 +235,10 @@ def keys(self) -> list[str]: def _keyname(self, key: str) -> str: """Get the key name.""" - return str(Path(self._path_prefix) / key) - - def _abfs_url(self, key: str) -> str: - """Get the ABFS URL.""" - path = str(Path(self._container_name) / self._path_prefix / key) - return f"abfs://{path}" + return str(Path(self._base_dir) / key) if self._base_dir else key async def get_creation_date(self, key: str) -> str: - """Get a value from the cache.""" + """Get creation date for the blob.""" try: key = self._keyname(key) container_client = self._blob_service_client.get_container_client( @@ -312,7 +252,7 @@ async def get_creation_date(self, key: str) -> str: return "" -def validate_blob_container_name(container_name: str): +def _validate_blob_container_name(container_name: str) -> None: """ Check if the provided blob container name is valid based on Azure rules. @@ -332,34 +272,7 @@ def validate_blob_container_name(container_name: str): ------- bool: True if valid, False otherwise. """ - # Check the length of the name - if len(container_name) < 3 or len(container_name) > 63: - return ValueError( - f"Container name must be between 3 and 63 characters in length. Name provided was {len(container_name)} characters long." - ) - - # Check if the name starts with a letter or number - if not container_name[0].isalnum(): - return ValueError( - f"Container name must start with a letter or number. Starting character was {container_name[0]}." - ) - - # Check for valid characters (letters, numbers, hyphen) and lowercase letters - if not re.match(r"^[a-z0-9-]+$", container_name): - return ValueError( - f"Container name must only contain:\n- lowercase letters\n- numbers\n- or hyphens\nName provided was {container_name}." - ) - - # Check for consecutive hyphens - if "--" in container_name: - return ValueError( - f"Container name cannot contain consecutive hyphens. Name provided was {container_name}." - ) - - # Check for hyphens at the end of the name - if container_name[-1] == "-": - return ValueError( - f"Container name cannot end with a hyphen. Name provided was {container_name}." - ) - - return True + # Match alphanumeric or single hyphen not at the start or end, repeated 3-63 times. + if not re.match(r"^(?:[0-9a-z]|(? None: + def __init__( + self, + database_name: str, + container_name: str, + connection_string: str | None = None, + account_url: str | None = None, + encoding: str = "utf-8", + **kwargs: Any, + ) -> None: """Create a CosmosDB storage instance.""" logger.info("Creating cosmosdb storage") - cosmosdb_account_url = kwargs.get("cosmosdb_account_url") - connection_string = kwargs.get("connection_string") - database_name = kwargs["base_dir"] - container_name = kwargs["container_name"] - if not database_name: - msg = "No base_dir provided for database name" + database_name = database_name + if database_name is None: + msg = "CosmosDB Storage requires a base_dir to be specified. This is used as the database name." + logger.error(msg) raise ValueError(msg) - if connection_string is None and cosmosdb_account_url is None: - msg = "connection_string or cosmosdb_account_url is required." + + if connection_string is not None and account_url is not None: + msg = "CosmosDB Storage requires either a connection_string or cosmosdb_account_url to be specified, not both." + logger.error(msg) raise ValueError(msg) if connection_string: self._cosmos_client = CosmosClient.from_connection_string(connection_string) - else: - if cosmosdb_account_url is None: - msg = ( - "Either connection_string or cosmosdb_account_url must be provided." - ) - raise ValueError(msg) + elif account_url: self._cosmos_client = CosmosClient( - url=cosmosdb_account_url, + url=account_url, credential=DefaultAzureCredential(), ) - self._encoding = kwargs.get("encoding", "utf-8") + else: + msg = "CosmosDB Storage requires either a connection_string or cosmosdb_account_url to be specified." + logger.error(msg) + raise ValueError(msg) + + self._encoding = encoding self._database_name = database_name self._connection_string = connection_string - self._cosmosdb_account_url = cosmosdb_account_url + self._cosmosdb_account_url = account_url self._container_name = container_name self._cosmosdb_account_name = ( - cosmosdb_account_url.split("//")[1].split(".")[0] - if cosmosdb_account_url - else None + account_url.split("//")[1].split(".")[0] if account_url else None ) self._no_id_prefixes = [] logger.debug( - "creating cosmosdb storage with account: %s and database: %s and container: %s", + "Creating cosmosdb storage with account [%s] and database [%s] and container [%s]", self._cosmosdb_account_name, self._database_name, self._container_name, @@ -120,48 +126,30 @@ def _delete_container(self) -> None: def find( self, file_pattern: re.Pattern[str], - base_dir: str | None = None, - file_filter: dict[str, Any] | None = None, - max_count=-1, - ) -> Iterator[tuple[str, dict[str, Any]]]: - """Find documents in a Cosmos DB container using a file pattern regex and custom file filter (optional). + ) -> Iterator[str]: + """Find documents in a Cosmos DB container using a file pattern regex. Params: - base_dir: The name of the base directory (not used in Cosmos DB context). file_pattern: The file pattern to use. - file_filter: A dictionary of key-value pairs to filter the documents. - max_count: The maximum number of documents to return. If -1, all documents are returned. Returns ------- An iterator of document IDs and their corresponding regex matches. """ - base_dir = base_dir or "" logger.info( - "search container %s for documents matching %s", + "Search container [%s] for documents matching [%s]", self._container_name, file_pattern.pattern, ) if not self._database_client or not self._container_client: return - def item_filter(item: dict[str, Any]) -> bool: - if file_filter is None: - return True - return all( - re.search(value, item.get(key, "")) - for key, value in file_filter.items() - ) - try: query = "SELECT * FROM c WHERE RegexMatch(c.id, @pattern)" parameters: list[dict[str, Any]] = [ {"name": "@pattern", "value": file_pattern.pattern} ] - if file_filter: - for key, value in file_filter.items(): - query += f" AND c.{key} = @{key}" - parameters.append({"name": f"@{key}", "value": value}) + items = list( self._container_client.query_items( query=query, @@ -169,6 +157,7 @@ def item_filter(item: dict[str, Any]) -> bool: enable_cross_partition_query=True, ) ) + logger.debug("All items: %s", [item["id"] for item in items]) num_loaded = 0 num_total = len(items) if num_total == 0: @@ -177,26 +166,20 @@ def item_filter(item: dict[str, Any]) -> bool: for item in items: match = file_pattern.search(item["id"]) if match: - group = match.groupdict() - if item_filter(group): - yield (item["id"], group) - num_loaded += 1 - if max_count > 0 and num_loaded >= max_count: - break - else: - num_filtered += 1 + yield item["id"] + num_loaded += 1 else: num_filtered += 1 - progress_status = _create_progress_status( - num_loaded, num_filtered, num_total - ) - logger.debug( - "Progress: %s (%d/%d completed)", - progress_status.description, - progress_status.completed_items, - progress_status.total_items, - ) + progress_status = _create_progress_status( + num_loaded, num_filtered, num_total + ) + logger.debug( + "Progress: %s (%d/%d completed)", + progress_status.description, + progress_status.completed_items, + progress_status.total_items, + ) except Exception: # noqa: BLE001 logger.warning( "An error occurred while searching for documents in Cosmos DB." @@ -330,7 +313,7 @@ def keys(self) -> list[str]: msg = "CosmosDB storage does yet not support listing keys." raise NotImplementedError(msg) - def child(self, name: str | None) -> PipelineStorage: + def child(self, name: str | None) -> "Storage": """Create a child storage instance.""" return self diff --git a/graphrag/storage/file_pipeline_storage.py b/packages/graphrag-storage/graphrag_storage/file_storage.py similarity index 51% rename from graphrag/storage/file_pipeline_storage.py rename to packages/graphrag-storage/graphrag_storage/file_storage.py index a52a92d36d..547659abcd 100644 --- a/graphrag/storage/file_pipeline_storage.py +++ b/packages/graphrag-storage/graphrag_storage/file_storage.py @@ -1,7 +1,7 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""File-based Storage implementation of PipelineStorage.""" +"""File-based Storage implementation of Storage.""" import logging import os @@ -16,86 +16,65 @@ from aiofiles.os import remove from aiofiles.ospath import exists -from graphrag.storage.pipeline_storage import ( - PipelineStorage, +from graphrag_storage.storage import ( + Storage, get_timestamp_formatted_with_local_tz, ) logger = logging.getLogger(__name__) -class FilePipelineStorage(PipelineStorage): +class FileStorage(Storage): """File storage class definition.""" - _root_dir: str + _base_dir: Path _encoding: str - def __init__(self, **kwargs: Any) -> None: + def __init__(self, base_dir: str, encoding: str = "utf-8", **kwargs: Any) -> None: """Create a file based storage.""" - self._root_dir = kwargs.get("base_dir", "") - self._encoding = kwargs.get("encoding", "utf-8") - logger.info("Creating file storage at %s", self._root_dir) - Path(self._root_dir).mkdir(parents=True, exist_ok=True) + self._base_dir = Path(base_dir).resolve() + self._encoding = encoding + logger.info("Creating file storage at [%s]", self._base_dir) + self._base_dir.mkdir(parents=True, exist_ok=True) def find( self, file_pattern: re.Pattern[str], - base_dir: str | None = None, - file_filter: dict[str, Any] | None = None, - max_count=-1, - ) -> Iterator[tuple[str, dict[str, Any]]]: - """Find files in the storage using a file pattern, as well as a custom filter function.""" - - def item_filter(item: dict[str, Any]) -> bool: - if file_filter is None: - return True - return all( - re.search(value, item[key]) for key, value in file_filter.items() - ) - - search_path = Path(self._root_dir) / (base_dir or "") + ) -> Iterator[str]: + """Find files in the storage using a file pattern.""" logger.info( - "search %s for files matching %s", search_path, file_pattern.pattern + "Search [%s] for files matching [%s]", self._base_dir, file_pattern.pattern ) - all_files = list(search_path.rglob("**/*")) + all_files = list(self._base_dir.rglob("**/*")) + logger.debug("All files and folders: %s", [file.name for file in all_files]) num_loaded = 0 num_total = len(all_files) num_filtered = 0 for file in all_files: match = file_pattern.search(f"{file}") if match: - group = match.groupdict() - if item_filter(group): - filename = f"{file}".replace(self._root_dir, "") - if filename.startswith(os.sep): - filename = filename[1:] - yield (filename, group) - num_loaded += 1 - if max_count > 0 and num_loaded >= max_count: - break - else: - num_filtered += 1 + filename = f"{file}".replace(str(self._base_dir), "", 1) + if filename.startswith(os.sep): + filename = filename[1:] + yield filename + num_loaded += 1 else: num_filtered += 1 - logger.debug( - "Files loaded: %d, filtered: %d, total: %d", - num_loaded, - num_filtered, - num_total, - ) + logger.debug( + "Files loaded: %d, filtered: %d, total: %d", + num_loaded, + num_filtered, + num_total, + ) async def get( self, key: str, as_bytes: bool | None = False, encoding: str | None = None ) -> Any: """Get method definition.""" - file_path = join_path(self._root_dir, key) + file_path = _join_path(self._base_dir, key) if await self.has(key): return await self._read_file(file_path, as_bytes, encoding) - if await exists(key): - # Lookup for key, as it is pressumably a new file loaded from inputs - # and not yet written to storage - return await self._read_file(key, as_bytes, encoding) return None @@ -122,7 +101,7 @@ async def set(self, key: str, value: Any, encoding: str | None = None) -> None: write_type = "wb" if is_bytes else "w" encoding = None if is_bytes else encoding or self._encoding async with aiofiles.open( - join_path(self._root_dir, key), + _join_path(self._base_dir, key), cast("Any", write_type), encoding=encoding, ) as f: @@ -130,35 +109,35 @@ async def set(self, key: str, value: Any, encoding: str | None = None) -> None: async def has(self, key: str) -> bool: """Has method definition.""" - return await exists(join_path(self._root_dir, key)) + return await exists(_join_path(self._base_dir, key)) async def delete(self, key: str) -> None: """Delete method definition.""" if await self.has(key): - await remove(join_path(self._root_dir, key)) + await remove(_join_path(self._base_dir, key)) async def clear(self) -> None: """Clear method definition.""" - for file in Path(self._root_dir).glob("*"): + for file in self._base_dir.glob("*"): if file.is_dir(): shutil.rmtree(file) else: file.unlink() - def child(self, name: str | None) -> "PipelineStorage": + def child(self, name: str | None) -> "Storage": """Create a child storage instance.""" if name is None: return self - child_path = str(Path(self._root_dir) / Path(name)) - return FilePipelineStorage(base_dir=child_path, encoding=self._encoding) + child_path = str(self._base_dir / name) + return FileStorage(base_dir=child_path, encoding=self._encoding) def keys(self) -> list[str]: """Return the keys in the storage.""" - return [item.name for item in Path(self._root_dir).iterdir() if item.is_file()] + return [item.name for item in self._base_dir.iterdir() if item.is_file()] async def get_creation_date(self, key: str) -> str: """Get the creation date of a file.""" - file_path = Path(join_path(self._root_dir, key)) + file_path = _join_path(self._base_dir, key) creation_timestamp = file_path.stat().st_ctime creation_time_utc = datetime.fromtimestamp(creation_timestamp, tz=timezone.utc) @@ -166,6 +145,6 @@ async def get_creation_date(self, key: str) -> str: return get_timestamp_formatted_with_local_tz(creation_time_utc) -def join_path(file_path: str, file_name: str) -> Path: +def _join_path(file_path: Path, file_name: str) -> Path: """Join a path and a file. Independent of the OS.""" - return Path(file_path) / Path(file_name).parent / Path(file_name).name + return (file_path / Path(file_name).parent / Path(file_name).name).resolve() diff --git a/graphrag/storage/memory_pipeline_storage.py b/packages/graphrag-storage/graphrag_storage/memory_storage.py similarity index 79% rename from graphrag/storage/memory_pipeline_storage.py rename to packages/graphrag-storage/graphrag_storage/memory_storage.py index 3567e3d1e3..f92a52a204 100644 --- a/graphrag/storage/memory_pipeline_storage.py +++ b/packages/graphrag-storage/graphrag_storage/memory_storage.py @@ -1,24 +1,29 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing 'InMemoryStorage' model.""" +"""In-memory storage implementation.""" from typing import TYPE_CHECKING, Any -from graphrag.storage.file_pipeline_storage import FilePipelineStorage +from graphrag_storage.file_storage import FileStorage if TYPE_CHECKING: - from graphrag.storage.pipeline_storage import PipelineStorage + from graphrag_storage.storage import Storage -class MemoryPipelineStorage(FilePipelineStorage): +class MemoryStorage(FileStorage): """In memory storage class definition.""" _storage: dict[str, Any] - def __init__(self): + def __init__(self, **kwargs: Any) -> None: """Init method definition.""" - super().__init__() + kwargs = { + "base_dir": "", + **kwargs, + } + kwargs.pop("type", None) + super().__init__(**kwargs) self._storage = {} async def get( @@ -69,9 +74,9 @@ async def clear(self) -> None: """Clear the storage.""" self._storage.clear() - def child(self, name: str | None) -> "PipelineStorage": + def child(self, name: str | None) -> "Storage": """Create a child storage instance.""" - return MemoryPipelineStorage() + return MemoryStorage() def keys(self) -> list[str]: """Return the keys in the storage.""" diff --git a/packages/graphrag-storage/graphrag_storage/py.typed b/packages/graphrag-storage/graphrag_storage/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/graphrag-storage/graphrag_storage/storage.py b/packages/graphrag-storage/graphrag_storage/storage.py new file mode 100644 index 0000000000..d8016d4ae7 --- /dev/null +++ b/packages/graphrag-storage/graphrag_storage/storage.py @@ -0,0 +1,141 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Abstract base class for storage.""" + +import re +from abc import ABC, abstractmethod +from collections.abc import Iterator +from datetime import datetime +from typing import Any + + +class Storage(ABC): + """Provide a storage interface.""" + + @abstractmethod + def __init__(self, **kwargs: Any) -> None: + """Create a storage instance.""" + + @abstractmethod + def find( + self, + file_pattern: re.Pattern[str], + ) -> Iterator[str]: + """Find files in the storage using a file pattern. + + Args + ---- + - file_pattern: re.Pattern[str] + The file pattern to use for finding files. + + Returns + ------- + Iterator[str]: + An iterator over the found file keys. + + """ + + @abstractmethod + async def get( + self, key: str, as_bytes: bool | None = None, encoding: str | None = None + ) -> Any: + """Get the value for the given key. + + Args + ---- + - key: str + The key to get the value for. + - as_bytes: bool | None, optional (default=None) + Whether or not to return the value as bytes. + - encoding: str | None, optional (default=None) + The encoding to use when decoding the value. + + Returns + ------- + Any: + The value for the given key. + """ + + @abstractmethod + async def set(self, key: str, value: Any, encoding: str | None = None) -> None: + """Set the value for the given key. + + Args + ---- + - key: str + The key to set the value for. + - value: Any + The value to set. + """ + + @abstractmethod + async def has(self, key: str) -> bool: + """Return True if the given key exists in the storage. + + Args + ---- + - key: str + The key to check for. + + Returns + ------- + bool: + True if the key exists in the storage, False otherwise. + """ + + @abstractmethod + async def delete(self, key: str) -> None: + """Delete the given key from the storage. + + Args + ---- + - key: str + The key to delete. + """ + + @abstractmethod + async def clear(self) -> None: + """Clear the storage.""" + + @abstractmethod + def child(self, name: str | None) -> "Storage": + """Create a child storage instance. + + Args + ---- + - name: str | None + The name of the child storage. + + Returns + ------- + Storage + The child storage instance. + + """ + + @abstractmethod + def keys(self) -> list[str]: + """List all keys in the storage.""" + + @abstractmethod + async def get_creation_date(self, key: str) -> str: + """Get the creation date for the given key. + + Args + ---- + - key: str + The key to get the creation date for. + + Returns + ------- + str: + The creation date for the given key. + """ + + +def get_timestamp_formatted_with_local_tz(timestamp: datetime) -> str: + """Get the formatted timestamp with the local time zone.""" + creation_time_local = timestamp.astimezone() + + return creation_time_local.strftime("%Y-%m-%d %H:%M:%S %z") diff --git a/packages/graphrag-storage/graphrag_storage/storage_config.py b/packages/graphrag-storage/graphrag_storage/storage_config.py new file mode 100644 index 0000000000..45cd63a734 --- /dev/null +++ b/packages/graphrag-storage/graphrag_storage/storage_config.py @@ -0,0 +1,48 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Storage configuration model.""" + +from pydantic import BaseModel, ConfigDict, Field + +from graphrag_storage.storage_type import StorageType + + +class StorageConfig(BaseModel): + """The default configuration section for storage.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom storage implementations.""" + + type: str = Field( + description="The storage type to use. Builtin types include 'File', 'AzureBlob', and 'AzureCosmos'.", + default=StorageType.File, + ) + + encoding: str | None = Field( + description="The encoding to use for file storage.", + default=None, + ) + + base_dir: str | None = Field( + description="The base directory for the output when using file or AzureBlob storage.", + default=None, + ) + + connection_string: str | None = Field( + description="The connection string for remote services.", + default=None, + ) + + container_name: str | None = Field( + description="The Azure Blob Storage container name or CosmosDB container name to use.", + default=None, + ) + account_url: str | None = Field( + description="The account url for Azure services.", + default=None, + ) + database_name: str | None = Field( + description="The database name to use.", + default=None, + ) diff --git a/packages/graphrag-storage/graphrag_storage/storage_factory.py b/packages/graphrag-storage/graphrag_storage/storage_factory.py new file mode 100644 index 0000000000..0b525fec5f --- /dev/null +++ b/packages/graphrag-storage/graphrag_storage/storage_factory.py @@ -0,0 +1,78 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + + +"""Storage factory implementation.""" + +from collections.abc import Callable + +from graphrag_common.factory import Factory, ServiceScope + +from graphrag_storage.storage import Storage +from graphrag_storage.storage_config import StorageConfig +from graphrag_storage.storage_type import StorageType + + +class StorageFactory(Factory[Storage]): + """A factory class for storage implementations.""" + + +storage_factory = StorageFactory() + + +def register_storage( + storage_type: str, + storage_initializer: Callable[..., Storage], + scope: ServiceScope = "transient", +) -> None: + """Register a custom storage implementation. + + Args + ---- + - storage_type: str + The storage id to register. + - storage_initializer: Callable[..., Storage] + The storage initializer to register. + """ + storage_factory.register(storage_type, storage_initializer, scope) + + +def create_storage(config: StorageConfig) -> Storage: + """Create a storage implementation based on the given configuration. + + Args + ---- + - config: StorageConfig + The storage configuration to use. + + Returns + ------- + Storage + The created storage implementation. + """ + config_model = config.model_dump() + storage_strategy = config.type + + if storage_strategy not in storage_factory: + match storage_strategy: + case StorageType.File: + from graphrag_storage.file_storage import FileStorage + + register_storage(StorageType.File, FileStorage) + case StorageType.Memory: + from graphrag_storage.memory_storage import MemoryStorage + + register_storage(StorageType.Memory, MemoryStorage) + case StorageType.AzureBlob: + from graphrag_storage.azure_blob_storage import AzureBlobStorage + + register_storage(StorageType.AzureBlob, AzureBlobStorage) + case StorageType.AzureCosmos: + from graphrag_storage.azure_cosmos_storage import AzureCosmosStorage + + register_storage(StorageType.AzureCosmos, AzureCosmosStorage) + case _: + msg = f"StorageConfig.type '{storage_strategy}' is not registered in the StorageFactory. Registered types: {', '.join(storage_factory.keys())}." + raise ValueError(msg) + + return storage_factory.create(storage_strategy, config_model) diff --git a/packages/graphrag-storage/graphrag_storage/storage_type.py b/packages/graphrag-storage/graphrag_storage/storage_type.py new file mode 100644 index 0000000000..dd2b1376fd --- /dev/null +++ b/packages/graphrag-storage/graphrag_storage/storage_type.py @@ -0,0 +1,16 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + + +"""Builtin storage implementation types.""" + +from enum import StrEnum + + +class StorageType(StrEnum): + """Enum for storage types.""" + + File = "file" + Memory = "memory" + AzureBlob = "blob" + AzureCosmos = "cosmosdb" diff --git a/packages/graphrag-storage/pyproject.toml b/packages/graphrag-storage/pyproject.toml new file mode 100644 index 0000000000..e21020061f --- /dev/null +++ b/packages/graphrag-storage/pyproject.toml @@ -0,0 +1,48 @@ +[project] +name = "graphrag-storage" +version = "2.7.1" +description = "GraphRAG storage package." +authors = [ + {name = "Alonso Guevara Fernández", email = "alonsog@microsoft.com"}, + {name = "Andrés Morales Esquivel", email = "andresmor@microsoft.com"}, + {name = "Chris Trevino", email = "chtrevin@microsoft.com"}, + {name = "David Tittsworth", email = "datittsw@microsoft.com"}, + {name = "Dayenne de Souza", email = "ddesouza@microsoft.com"}, + {name = "Derek Worthen", email = "deworthe@microsoft.com"}, + {name = "Gaudy Blanco Meneses", email = "gaudyb@microsoft.com"}, + {name = "Ha Trinh", email = "trinhha@microsoft.com"}, + {name = "Jonathan Larson", email = "jolarso@microsoft.com"}, + {name = "Josh Bradley", email = "joshbradley@microsoft.com"}, + {name = "Kate Lytvynets", email = "kalytv@microsoft.com"}, + {name = "Kenny Zhang", email = "zhangken@microsoft.com"}, + {name = "Mónica Carvajal"}, + {name = "Nathan Evans", email = "naevans@microsoft.com"}, + {name = "Rodrigo Racanicci", email = "rracanicci@microsoft.com"}, + {name = "Sarah Smith", email = "smithsarah@microsoft.com"}, +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.11,<3.14" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "aiofiles~=24.1", + "azure-cosmos~=4.9", + "azure-identity~=1.19", + "azure-storage-blob~=12.24", + "graphrag-common==2.7.1", + "pandas~=2.3", + "pydantic~=2.10", +] + +[project.urls] +Source = "https://github.com/microsoft/graphrag" + +[build-system] +requires = ["hatchling>=1.27.0,<2.0.0"] +build-backend = "hatchling.build" + diff --git a/packages/graphrag-vectors/README.md b/packages/graphrag-vectors/README.md new file mode 100644 index 0000000000..fb97f08a86 --- /dev/null +++ b/packages/graphrag-vectors/README.md @@ -0,0 +1,111 @@ +# GraphRAG Vectors + +This package provides vector store implementations for GraphRAG with support for multiple backends including LanceDB, Azure AI Search, and Azure Cosmos DB. It offers both a convenient configuration-driven API and direct factory access for creating and managing vector stores with flexible index schema definitions. + +## Basic usage with the utility function (recommended) + +This demonstrates the recommended approach to create a vector store using the create_vector_store convenience function with configuration objects that specify the store type and index schema. The example shows setting up a LanceDB vector store with a defined index configuration, then connecting to it and creating the index for vector operations. + +```python +from graphrag_vectors import ( + create_vector_store, + VectorStoreType, + IndexSchema, +) + +# Create a vector store using the convenience function +store_config = VectorStoreConfig( + type="lancedb", + db_uri="lance" +) + +schema_config = IndexSchema( + index_name="my_index", + vector_size=1536, +) + +vector_store = create_vector_store( + config=store_config + index_schema=schema_config, +) + +vector_store.connect() +vector_store.create_index() +``` + +## Basic usage implementing the factory directly + +This example shows a different approach to create vector stores by directly using the vector_store_factory with enum types and dictionary-based initialization arguments. This method provides more direct control over the factory creation process while bypassing the convenience function layer. + +```python +from graphrag_vectors import ( + VectorStoreFactory, + vector_store_factory, + VectorStoreType, + IndexSchema, +) + +# Create a vector store using the factory +schema_config = IndexSchema( + index_name="my_index", + vector_size=1536, +) + +vector_store = vector_store_factory.create( + VectorStoreType.LanceDB, + { + "index_schema": schema_config, + "db_uri": "./lancedb" + } +) + +vector_store.connect() +vector_store.create_index() +``` + +## Supported Vector Stores + +- **LanceDB**: Local vector database +- **Azure AI Search**: Azure's managed search service with vector capabilities +- **Azure Cosmos DB**: Azure's NoSQL database with vector search support + +## Custom Vector Store + +You can register custom vector store implementations: + +```python +from graphrag_vectors import VectorStore, register_vector_store, create_vector_store + +class MyCustomVectorStore(VectorStore): + def __init__(self, my_param): + self.my_param = my_param + + def connect(self): + # Implementation + pass + + def create_index(self): + # Implementation + pass + + # ... implement other required methods + +# Register your custom implementation +register_vector_store("my_custom_store", MyCustomVectorStore) + +# Use your custom vector store +config = VectorStoreConfig( + type="my_custom_store", + my_param="something" +) +custom_store = create_vector_store( + config=config, + index_schema=schema_config, +) +``` + +## Configuration + +Vector stores are configured using: +- `VectorStoreConfig`: baseline parameters for the store +- `IndexSchema`: Schema configuration for the specific index to create/connect to (index name, field names, vector size) diff --git a/packages/graphrag-vectors/graphrag_vectors/__init__.py b/packages/graphrag-vectors/graphrag_vectors/__init__.py new file mode 100644 index 0000000000..915d1f0cd1 --- /dev/null +++ b/packages/graphrag-vectors/graphrag_vectors/__init__.py @@ -0,0 +1,34 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""GraphRAG vector store implementations.""" + +from graphrag_vectors.index_schema import IndexSchema +from graphrag_vectors.types import TextEmbedder +from graphrag_vectors.vector_store import ( + VectorStore, + VectorStoreDocument, + VectorStoreSearchResult, +) +from graphrag_vectors.vector_store_config import VectorStoreConfig +from graphrag_vectors.vector_store_factory import ( + VectorStoreFactory, + create_vector_store, + register_vector_store, + vector_store_factory, +) +from graphrag_vectors.vector_store_type import VectorStoreType + +__all__ = [ + "IndexSchema", + "TextEmbedder", + "VectorStore", + "VectorStoreConfig", + "VectorStoreDocument", + "VectorStoreFactory", + "VectorStoreSearchResult", + "VectorStoreType", + "create_vector_store", + "register_vector_store", + "vector_store_factory", +] diff --git a/packages/graphrag-vectors/graphrag_vectors/azure_ai_search.py b/packages/graphrag-vectors/graphrag_vectors/azure_ai_search.py new file mode 100644 index 0000000000..fcbb5e48ae --- /dev/null +++ b/packages/graphrag-vectors/graphrag_vectors/azure_ai_search.py @@ -0,0 +1,173 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A package containing the Azure AI Search vector store implementation.""" + +from typing import Any + +from azure.core.credentials import AzureKeyCredential +from azure.identity import DefaultAzureCredential +from azure.search.documents import SearchClient +from azure.search.documents.indexes import SearchIndexClient +from azure.search.documents.indexes.models import ( + HnswAlgorithmConfiguration, + HnswParameters, + SearchField, + SearchFieldDataType, + SearchIndex, + SimpleField, + VectorSearch, + VectorSearchAlgorithmMetric, + VectorSearchProfile, +) +from azure.search.documents.models import VectorizedQuery + +from graphrag_vectors.vector_store import ( + VectorStore, + VectorStoreDocument, + VectorStoreSearchResult, +) + + +class AzureAISearchVectorStore(VectorStore): + """Azure AI Search vector storage implementation.""" + + index_client: SearchIndexClient + + def __init__( + self, + url: str, + api_key: str | None = None, + audience: str | None = None, + vector_search_profile_name: str = "vectorSearchProfile", + **kwargs: Any, + ): + super().__init__(**kwargs) + if not url: + msg = "url must be provided for Azure AI Search." + raise ValueError(msg) + self.url = url + self.api_key = api_key + self.audience = audience + self.vector_search_profile_name = vector_search_profile_name + + def connect(self) -> Any: + """Connect to AI search vector storage.""" + audience_arg = ( + {"audience": self.audience} if self.audience and not self.api_key else {} + ) + self.db_connection = SearchClient( + endpoint=self.url, + index_name=self.index_name, + credential=( + AzureKeyCredential(self.api_key) + if self.api_key + else DefaultAzureCredential() + ), + **audience_arg, + ) + self.index_client = SearchIndexClient( + endpoint=self.url, + credential=( + AzureKeyCredential(self.api_key) + if self.api_key + else DefaultAzureCredential() + ), + **audience_arg, + ) + + def create_index(self) -> None: + """Load documents into an Azure AI Search index.""" + if ( + self.index_name is not None + and self.index_name in self.index_client.list_index_names() + ): + self.index_client.delete_index(self.index_name) + + # Configure vector search profile + vector_search = VectorSearch( + algorithms=[ + HnswAlgorithmConfiguration( + name="HnswAlg", + parameters=HnswParameters( + metric=VectorSearchAlgorithmMetric.COSINE + ), + ) + ], + profiles=[ + VectorSearchProfile( + name=self.vector_search_profile_name, + algorithm_configuration_name="HnswAlg", + ) + ], + ) + # Configure the index + index = SearchIndex( + name=self.index_name, + fields=[ + SimpleField( + name=self.id_field, + type=SearchFieldDataType.String, + key=True, + ), + SearchField( + name=self.vector_field, + type=SearchFieldDataType.Collection(SearchFieldDataType.Single), + searchable=True, + hidden=False, # DRIFT needs to return the vector for client-side similarity + vector_search_dimensions=self.vector_size, + vector_search_profile_name=self.vector_search_profile_name, + ), + ], + vector_search=vector_search, + ) + self.index_client.create_or_update_index( + index, + ) + + def load_documents(self, documents: list[VectorStoreDocument]) -> None: + """Load documents into an Azure AI Search index.""" + batch = [ + { + self.id_field: doc.id, + self.vector_field: doc.vector, + } + for doc in documents + if doc.vector is not None + ] + + if len(batch) > 0: + self.db_connection.upload_documents(batch) + + def similarity_search_by_vector( + self, query_embedding: list[float], k: int = 10 + ) -> list[VectorStoreSearchResult]: + """Perform a vector-based similarity search.""" + vectorized_query = VectorizedQuery( + vector=query_embedding, k_nearest_neighbors=k, fields=self.vector_field + ) + + response = self.db_connection.search( + vector_queries=[vectorized_query], + ) + + return [ + VectorStoreSearchResult( + document=VectorStoreDocument( + id=doc.get(self.id_field, ""), + vector=doc.get(self.vector_field, []), + ), + # Cosine similarity between 0.333 and 1.000 + # https://learn.microsoft.com/en-us/azure/search/hybrid-search-ranking#scores-in-a-hybrid-search-results + score=doc["@search.score"], + ) + for doc in response + ] + + def search_by_id(self, id: str) -> VectorStoreDocument: + """Search for a document by id.""" + response = self.db_connection.get_document(id) + return VectorStoreDocument( + id=response.get(self.id_field, ""), + vector=response.get(self.vector_field, []), + ) diff --git a/graphrag/vector_stores/cosmosdb.py b/packages/graphrag-vectors/graphrag_vectors/cosmosdb.py similarity index 65% rename from graphrag/vector_stores/cosmosdb.py rename to packages/graphrag-vectors/graphrag_vectors/cosmosdb.py index 6703c0ff08..fcff47e782 100644 --- a/graphrag/vector_stores/cosmosdb.py +++ b/packages/graphrag-vectors/graphrag_vectors/cosmosdb.py @@ -3,7 +3,6 @@ """A package containing the CosmosDB vector store implementation.""" -import json from typing import Any from azure.cosmos import ContainerProxy, CosmosClient, DatabaseProxy @@ -11,16 +10,14 @@ from azure.cosmos.partition_key import PartitionKey from azure.identity import DefaultAzureCredential -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig -from graphrag.data_model.types import TextEmbedder -from graphrag.vector_stores.base import ( - BaseVectorStore, +from graphrag_vectors.vector_store import ( + VectorStore, VectorStoreDocument, VectorStoreSearchResult, ) -class CosmosDBVectorStore(BaseVectorStore): +class CosmosDBVectorStore(VectorStore): """Azure CosmosDB vector storage implementation.""" _cosmos_client: CosmosClient @@ -28,58 +25,56 @@ class CosmosDBVectorStore(BaseVectorStore): _container_client: ContainerProxy def __init__( - self, vector_store_schema_config: VectorStoreSchemaConfig, **kwargs: Any - ) -> None: - super().__init__( - vector_store_schema_config=vector_store_schema_config, **kwargs - ) + self, + database_name: str, + connection_string: str | None = None, + url: str | None = None, + **kwargs, + ): + super().__init__(**kwargs) + if self.id_field != "id": + msg = "CosmosDB requires the id_field to be 'id'." + raise ValueError(msg) + if not connection_string and not url: + msg = "Either connection_string or url must be provided for CosmosDB." + raise ValueError(msg) - def connect(self, **kwargs: Any) -> Any: + self.database_name = database_name + self.connection_string = connection_string + self.url = url + + def connect(self) -> Any: """Connect to CosmosDB vector storage.""" - connection_string = kwargs.get("connection_string") - if connection_string: - self._cosmos_client = CosmosClient.from_connection_string(connection_string) + if self.connection_string: + self._cosmos_client = CosmosClient.from_connection_string( + self.connection_string + ) else: - url = kwargs.get("url") - if not url: - msg = "Either connection_string or url must be provided." - raise ValueError(msg) self._cosmos_client = CosmosClient( - url=url, credential=DefaultAzureCredential() + url=self.url, credential=DefaultAzureCredential() ) - database_name = kwargs.get("database_name") - if database_name is None: - msg = "Database name must be provided." - raise ValueError(msg) - self._database_name = database_name - if self.index_name is None: - msg = "Index name is empty or not provided." - raise ValueError(msg) - self._container_name = self.index_name - - self.vector_size = self.vector_size self._create_database() self._create_container() def _create_database(self) -> None: """Create the database if it doesn't exist.""" - self._cosmos_client.create_database_if_not_exists(id=self._database_name) + self._cosmos_client.create_database_if_not_exists(id=self.database_name) self._database_client = self._cosmos_client.get_database_client( - self._database_name + self.database_name ) def _delete_database(self) -> None: """Delete the database if it exists.""" if self._database_exists(): - self._cosmos_client.delete_database(self._database_name) + self._cosmos_client.delete_database(self.database_name) def _database_exists(self) -> bool: """Check if the database exists.""" existing_database_names = [ database["id"] for database in self._cosmos_client.list_databases() ] - return self._database_name in existing_database_names + return self.database_name in existing_database_names def _create_container(self) -> None: """Create the container if it doesn't exist.""" @@ -117,7 +112,7 @@ def _create_container(self) -> None: # Create the container and container client self._database_client.create_container_if_not_exists( - id=self._container_name, + id=self.index_name, partition_key=partition_key, indexing_policy=indexing_policy, vector_embedding_policy=vector_embedding_policy, @@ -128,58 +123,51 @@ def _create_container(self) -> None: # Create the container with compatible indexing policy self._database_client.create_container_if_not_exists( - id=self._container_name, + id=self.index_name, partition_key=partition_key, indexing_policy=indexing_policy, vector_embedding_policy=vector_embedding_policy, ) self._container_client = self._database_client.get_container_client( - self._container_name + self.index_name ) def _delete_container(self) -> None: """Delete the vector store container in the database if it exists.""" if self._container_exists(): - self._database_client.delete_container(self._container_name) + self._database_client.delete_container(self.index_name) def _container_exists(self) -> bool: """Check if the container name exists in the database.""" existing_container_names = [ container["id"] for container in self._database_client.list_containers() ] - return self._container_name in existing_container_names + return self.index_name in existing_container_names - def load_documents( - self, documents: list[VectorStoreDocument], overwrite: bool = True - ) -> None: + def create_index(self) -> None: """Load documents into CosmosDB.""" # Create a CosmosDB container on overwrite - if overwrite: - self._delete_container() - self._create_container() + self._delete_container() + self._create_container() if self._container_client is None: msg = "Container client is not initialized." raise ValueError(msg) + def load_documents(self, documents: list[VectorStoreDocument]) -> None: + """Load documents into CosmosDB.""" # Upload documents to CosmosDB for doc in documents: if doc.vector is not None: - print("Document to store:") # noqa: T201 - print(doc) # noqa: T201 doc_json = { self.id_field: doc.id, self.vector_field: doc.vector, - self.text_field: doc.text, - self.attributes_field: json.dumps(doc.attributes), } - print("Storing document in CosmosDB:") # noqa: T201 - print(doc_json) # noqa: T201 self._container_client.upsert_item(doc_json) def similarity_search_by_vector( - self, query_embedding: list[float], k: int = 10, **kwargs: Any + self, query_embedding: list[float], k: int = 10 ) -> list[VectorStoreSearchResult]: """Perform a vector-based similarity search.""" if self._container_client is None: @@ -187,7 +175,7 @@ def similarity_search_by_vector( raise ValueError(msg) try: - query = f"SELECT TOP {k} c.{self.id_field}, c.{self.text_field}, c.{self.vector_field}, c.{self.attributes_field}, VectorDistance(c.{self.vector_field}, @embedding) AS SimilarityScore FROM c ORDER BY VectorDistance(c.{self.vector_field}, @embedding)" # noqa: S608 + query = f"SELECT TOP {k} c.{self.id_field}, c.{self.vector_field}, VectorDistance(c.{self.vector_field}, @embedding) AS SimilarityScore FROM c ORDER BY VectorDistance(c.{self.vector_field}, @embedding)" # noqa: S608 query_params = [{"name": "@embedding", "value": query_embedding}] items = list( self._container_client.query_items( @@ -199,7 +187,7 @@ def similarity_search_by_vector( except (CosmosHttpResponseError, ValueError): # Currently, the CosmosDB emulator does not support the VectorDistance function. # For emulator or test environments - fetch all items and calculate distance locally - query = f"SELECT c.{self.id_field}, c.{self.text_field}, c.{self.vector_field}, c.{self.attributes_field} FROM c" # noqa: S608 + query = f"SELECT c.{self.id_field}, c.{self.vector_field} FROM c" # noqa: S608 items = list( self._container_client.query_items( query=query, @@ -231,40 +219,13 @@ def cosine_similarity(a, b): VectorStoreSearchResult( document=VectorStoreDocument( id=item.get(self.id_field, ""), - text=item.get(self.text_field, ""), vector=item.get(self.vector_field, []), - attributes=(json.loads(item.get(self.attributes_field, "{}"))), ), score=item.get("SimilarityScore", 0.0), ) for item in items ] - def similarity_search_by_text( - self, text: str, text_embedder: TextEmbedder, k: int = 10, **kwargs: Any - ) -> list[VectorStoreSearchResult]: - """Perform a text-based similarity search.""" - query_embedding = text_embedder(text) - if query_embedding: - return self.similarity_search_by_vector( - query_embedding=query_embedding, k=k - ) - return [] - - def filter_by_id(self, include_ids: list[str] | list[int]) -> Any: - """Build a query filter to filter documents by a list of ids.""" - if include_ids is None or len(include_ids) == 0: - self.query_filter = None - else: - if isinstance(include_ids[0], str): - id_filter = ", ".join([f"'{id}'" for id in include_ids]) - else: - id_filter = ", ".join([str(id) for id in include_ids]) - self.query_filter = ( - f"SELECT * FROM c WHERE c.{self.id_field} IN ({id_filter})" # noqa: S608 - ) - return self.query_filter - def search_by_id(self, id: str) -> VectorStoreDocument: """Search for a document by id.""" if self._container_client is None: @@ -275,8 +236,6 @@ def search_by_id(self, id: str) -> VectorStoreDocument: return VectorStoreDocument( id=item.get(self.id_field, ""), vector=item.get(self.vector_field, []), - text=item.get(self.text_field, ""), - attributes=(json.loads(item.get(self.attributes_field, "{}"))), ) def clear(self) -> None: diff --git a/graphrag/config/models/vector_store_schema_config.py b/packages/graphrag-vectors/graphrag_vectors/index_schema.py similarity index 74% rename from graphrag/config/models/vector_store_schema_config.py rename to packages/graphrag-vectors/graphrag_vectors/index_schema.py index e149192515..2eacc3f0c0 100644 --- a/graphrag/config/models/vector_store_schema_config.py +++ b/packages/graphrag-vectors/graphrag_vectors/index_schema.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, Field, model_validator -DEFAULT_VECTOR_SIZE: int = 1536 +DEFAULT_VECTOR_SIZE: int = 3072 VALID_IDENTIFIER_REGEX = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") @@ -17,9 +17,13 @@ def is_valid_field_name(field: str) -> bool: return bool(VALID_IDENTIFIER_REGEX.match(field)) -class VectorStoreSchemaConfig(BaseModel): +class IndexSchema(BaseModel): """The default configuration section for Vector Store Schema.""" + index_name: str = Field( + description="The index name to use.", default="vector_index" + ) + id_field: str = Field( description="The ID field to use.", default="id", @@ -30,30 +34,16 @@ class VectorStoreSchemaConfig(BaseModel): default="vector", ) - text_field: str = Field( - description="The text field to use.", - default="text", - ) - - attributes_field: str = Field( - description="The attributes field to use.", - default="attributes", - ) - vector_size: int = Field( description="The vector size to use.", default=DEFAULT_VECTOR_SIZE, ) - index_name: str | None = Field(description="The index name to use.", default=None) - def _validate_schema(self) -> None: """Validate the schema.""" for field in [ self.id_field, self.vector_field, - self.text_field, - self.attributes_field, ]: if not is_valid_field_name(field): msg = f"Unsafe or invalid field name: {field}" diff --git a/packages/graphrag-vectors/graphrag_vectors/lancedb.py b/packages/graphrag-vectors/graphrag_vectors/lancedb.py new file mode 100644 index 0000000000..a133a268ee --- /dev/null +++ b/packages/graphrag-vectors/graphrag_vectors/lancedb.py @@ -0,0 +1,128 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""The LanceDB vector storage implementation package.""" + +from typing import Any + +import lancedb +import numpy as np +import pyarrow as pa + +from graphrag_vectors.vector_store import ( + VectorStore, + VectorStoreDocument, + VectorStoreSearchResult, +) + + +class LanceDBVectorStore(VectorStore): + """LanceDB vector storage implementation.""" + + def __init__(self, db_uri: str = "lancedb", **kwargs: Any): + super().__init__(**kwargs) + self.db_uri = db_uri + + def connect(self) -> Any: + """Connect to the vector storage.""" + self.db_connection = lancedb.connect(self.db_uri) + + if self.index_name and self.index_name in self.db_connection.table_names(): + self.document_collection = self.db_connection.open_table(self.index_name) + + def create_index(self) -> None: + """Create index.""" + dummy_vector = np.zeros(self.vector_size, dtype=np.float32) + flat_array = pa.array(dummy_vector, type=pa.float32()) + vector_column = pa.FixedSizeListArray.from_arrays(flat_array, self.vector_size) + + data = pa.table({ + self.id_field: pa.array(["__DUMMY__"], type=pa.string()), + self.vector_field: vector_column, + }) + + self.document_collection = self.db_connection.create_table( + self.index_name if self.index_name else "", + data=data, + mode="overwrite", + schema=data.schema, + ) + + # Step 5: Create index now that schema exists + self.document_collection.create_index( + vector_column_name=self.vector_field, index_type="IVF_FLAT" + ) + + def load_documents(self, documents: list[VectorStoreDocument]) -> None: + """Load documents into vector storage.""" + self.document_collection.delete(f"{self.id_field} = '__DUMMY__'") + + # Step 1: Prepare data columns manually + ids = [] + vectors = [] + + for document in documents: + self.vector_size = ( + len(document.vector) if document.vector else self.vector_size + ) + if document.vector is not None and len(document.vector) == self.vector_size: + ids.append(document.id) + vectors.append(np.array(document.vector, dtype=np.float32)) + + # Step 2: Handle empty case + if len(ids) == 0: + data = None + else: + # Step 3: Flatten the vectors and build FixedSizeListArray manually + flat_vector = np.concatenate(vectors).astype(np.float32) + flat_array = pa.array(flat_vector, type=pa.float32()) + vector_column = pa.FixedSizeListArray.from_arrays( + flat_array, self.vector_size + ) + + # Step 4: Create PyArrow table (let schema be inferred) + data = pa.table({ + self.id_field: pa.array(ids, type=pa.string()), + self.vector_field: vector_column, + }) + + if data: + self.document_collection.add(data) + + def similarity_search_by_vector( + self, query_embedding: list[float] | np.ndarray, k: int = 10 + ) -> list[VectorStoreSearchResult]: + """Perform a vector-based similarity search.""" + query_embedding = np.array(query_embedding, dtype=np.float32) + + docs = ( + self.document_collection + .search(query=query_embedding, vector_column_name=self.vector_field) + .limit(k) + .to_list() + ) + return [ + VectorStoreSearchResult( + document=VectorStoreDocument( + id=doc[self.id_field], + vector=doc[self.vector_field], + ), + score=1 - abs(float(doc["_distance"])), + ) + for doc in docs + ] + + def search_by_id(self, id: str) -> VectorStoreDocument: + """Search for a document by id.""" + doc = ( + self.document_collection + .search() + .where(f"{self.id_field} == '{id}'", prefilter=True) + .to_list() + ) + if doc: + return VectorStoreDocument( + id=doc[0][self.id_field], + vector=doc[0][self.vector_field], + ) + return VectorStoreDocument(id=id, vector=None) diff --git a/packages/graphrag-vectors/graphrag_vectors/types.py b/packages/graphrag-vectors/graphrag_vectors/types.py new file mode 100644 index 0000000000..63b032486c --- /dev/null +++ b/packages/graphrag-vectors/graphrag_vectors/types.py @@ -0,0 +1,8 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Common types for vector stores.""" + +from collections.abc import Callable + +TextEmbedder = Callable[[str], list[float]] diff --git a/packages/graphrag-vectors/graphrag_vectors/vector_store.py b/packages/graphrag-vectors/graphrag_vectors/vector_store.py new file mode 100644 index 0000000000..de19bb5a4c --- /dev/null +++ b/packages/graphrag-vectors/graphrag_vectors/vector_store.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Base classes for vector stores.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any + +from graphrag_vectors.types import TextEmbedder + + +@dataclass +class VectorStoreDocument: + """A document that is stored in vector storage.""" + + id: str | int + """unique id for the document""" + + vector: list[float] | None + + +@dataclass +class VectorStoreSearchResult: + """A vector storage search result.""" + + document: VectorStoreDocument + """Document that was found.""" + + score: float + """Similarity score between -1 and 1. Higher is more similar.""" + + +class VectorStore(ABC): + """The base class for vector storage data-access classes.""" + + def __init__( + self, + index_name: str = "vector_index", + id_field: str = "id", + vector_field: str = "vector", + vector_size: int = 3072, + **kwargs: Any, + ): + self.index_name = index_name + self.id_field = id_field + self.vector_field = vector_field + self.vector_size = vector_size + + @abstractmethod + def connect(self) -> None: + """Connect to vector storage.""" + + @abstractmethod + def create_index(self) -> None: + """Create index.""" + + @abstractmethod + def load_documents(self, documents: list[VectorStoreDocument]) -> None: + """Load documents into the vector-store.""" + + @abstractmethod + def similarity_search_by_vector( + self, query_embedding: list[float], k: int = 10 + ) -> list[VectorStoreSearchResult]: + """Perform ANN search by vector.""" + + def similarity_search_by_text( + self, text: str, text_embedder: TextEmbedder, k: int = 10 + ) -> list[VectorStoreSearchResult]: + """Perform a text-based similarity search.""" + query_embedding = text_embedder(text) + if query_embedding: + return self.similarity_search_by_vector( + query_embedding=query_embedding, k=k + ) + return [] + + @abstractmethod + def search_by_id(self, id: str) -> VectorStoreDocument: + """Search for a document by id.""" diff --git a/packages/graphrag-vectors/graphrag_vectors/vector_store_config.py b/packages/graphrag-vectors/graphrag_vectors/vector_store_config.py new file mode 100644 index 0000000000..17f70af86d --- /dev/null +++ b/packages/graphrag-vectors/graphrag_vectors/vector_store_config.py @@ -0,0 +1,53 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Parameterization settings for the default configuration.""" + +from pydantic import BaseModel, ConfigDict, Field + +from graphrag_vectors.index_schema import IndexSchema +from graphrag_vectors.vector_store_type import VectorStoreType + + +class VectorStoreConfig(BaseModel): + """The default configuration section for Vector Store.""" + + model_config = ConfigDict(extra="allow") + """Allow extra fields to support custom vector implementations.""" + + type: str = Field( + description="The vector store type to use.", + default=VectorStoreType.LanceDB, + ) + + db_uri: str | None = Field( + description="The database URI to use (only used by lancedb for built-in stores).", + default=None, + ) + + url: str | None = Field( + description="The database URL when type == azure_ai_search or cosmosdb.", + default=None, + ) + + api_key: str | None = Field( + description="The database API key when type == azure_ai_search.", + default=None, + ) + + audience: str | None = Field( + description="The database audience when type == azure_ai_search.", + default=None, + ) + + connection_string: str | None = Field( + description="The connection string when type == cosmosdb.", + default=None, + ) + + database_name: str | None = Field( + description="The database name to use when type == cosmosdb.", + default=None, + ) + + index_schema: dict[str, IndexSchema] = {} diff --git a/packages/graphrag-vectors/graphrag_vectors/vector_store_factory.py b/packages/graphrag-vectors/graphrag_vectors/vector_store_factory.py new file mode 100644 index 0000000000..6d94fa63a9 --- /dev/null +++ b/packages/graphrag-vectors/graphrag_vectors/vector_store_factory.py @@ -0,0 +1,99 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Factory functions for creating a vector store.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from graphrag_common.factory import Factory, ServiceScope + +from graphrag_vectors.vector_store import VectorStore +from graphrag_vectors.vector_store_type import VectorStoreType + +if TYPE_CHECKING: + from collections.abc import Callable + + from graphrag_vectors.index_schema import IndexSchema + from graphrag_vectors.vector_store_config import VectorStoreConfig + + +class VectorStoreFactory(Factory[VectorStore]): + """A factory for vector stores. + + Includes a method for users to register a custom vector store implementation. + + Configuration arguments are passed to each vector store implementation as kwargs + for individual enforcement of required/optional arguments. + """ + + +vector_store_factory = VectorStoreFactory() + + +def register_vector_store( + vector_store_type: str, + vector_store_initializer: Callable[..., VectorStore], + scope: ServiceScope = "transient", +) -> None: + """Register a custom vector store implementation. + + Args + ---- + - vector_store_type: str + The vector store id to register. + - vector_store_initializer: Callable[..., VectorStore] + The vector store initializer to register. + - scope: ServiceScope + The service scope for the vector store (default: "transient"). + """ + vector_store_factory.register(vector_store_type, vector_store_initializer, scope) + + +def create_vector_store( + config: VectorStoreConfig, index_schema: IndexSchema +) -> VectorStore: + """Create a vector store implementation based on the given type and configuration. + + Args + ---- + - config: VectorStoreConfig + The base vector store configuration. + - index_schema: IndexSchema + The index schema configuration for the vector store instance - i.e., for the specific table we are reading/writing. + + Returns + ------- + VectorStore + The created vector store implementation. + """ + strategy = config.type + + # Lazy load built-in implementations + if strategy not in vector_store_factory: + match strategy: + case VectorStoreType.LanceDB: + from graphrag_vectors.lancedb import LanceDBVectorStore + + register_vector_store(VectorStoreType.LanceDB, LanceDBVectorStore) + case VectorStoreType.AzureAISearch: + from graphrag_vectors.azure_ai_search import AzureAISearchVectorStore + + register_vector_store( + VectorStoreType.AzureAISearch, AzureAISearchVectorStore + ) + case VectorStoreType.CosmosDB: + from graphrag_vectors.cosmosdb import CosmosDBVectorStore + + register_vector_store(VectorStoreType.CosmosDB, CosmosDBVectorStore) + case _: + msg = f"Vector store type '{strategy}' is not registered in the VectorStoreFactory. Registered types: {', '.join(vector_store_factory.keys())}." + raise ValueError(msg) + + # collapse the base config and specific index config into a single dict for the initializer + config_model = config.model_dump() + index_model = index_schema.model_dump() + return vector_store_factory.create( + strategy, init_args={**config_model, **index_model} + ) diff --git a/packages/graphrag-vectors/graphrag_vectors/vector_store_type.py b/packages/graphrag-vectors/graphrag_vectors/vector_store_type.py new file mode 100644 index 0000000000..86b60bf2fc --- /dev/null +++ b/packages/graphrag-vectors/graphrag_vectors/vector_store_type.py @@ -0,0 +1,14 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Vector store type enum.""" + +from enum import StrEnum + + +class VectorStoreType(StrEnum): + """The supported vector store types.""" + + LanceDB = "lancedb" + AzureAISearch = "azure_ai_search" + CosmosDB = "cosmosdb" diff --git a/packages/graphrag-vectors/pyproject.toml b/packages/graphrag-vectors/pyproject.toml new file mode 100644 index 0000000000..8dedf25f99 --- /dev/null +++ b/packages/graphrag-vectors/pyproject.toml @@ -0,0 +1,49 @@ +[project] +name = "graphrag-vectors" +version = "2.7.1" +description = "GraphRAG vector store package." +authors = [ + {name = "Alonso Guevara Fernández", email = "alonsog@microsoft.com"}, + {name = "Andrés Morales Esquivel", email = "andresmor@microsoft.com"}, + {name = "Chris Trevino", email = "chtrevin@microsoft.com"}, + {name = "David Tittsworth", email = "datittsw@microsoft.com"}, + {name = "Dayenne de Souza", email = "ddesouza@microsoft.com"}, + {name = "Derek Worthen", email = "deworthe@microsoft.com"}, + {name = "Gaudy Blanco Meneses", email = "gaudyb@microsoft.com"}, + {name = "Ha Trinh", email = "trinhha@microsoft.com"}, + {name = "Jonathan Larson", email = "jolarso@microsoft.com"}, + {name = "Josh Bradley", email = "joshbradley@microsoft.com"}, + {name = "Kate Lytvynets", email = "kalytv@microsoft.com"}, + {name = "Kenny Zhang", email = "zhangken@microsoft.com"}, + {name = "Mónica Carvajal"}, + {name = "Nathan Evans", email = "naevans@microsoft.com"}, + {name = "Rodrigo Racanicci", email = "rracanicci@microsoft.com"}, + {name = "Sarah Smith", email = "smithsarah@microsoft.com"}, +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.11,<3.14" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "azure-core~=1.32", + "azure-cosmos~=4.9", + "azure-identity~=1.19", + "azure-search-documents~=11.6", + "graphrag-common==2.7.1", + "lancedb~=0.24.1", + "numpy~=2.1", + "pyarrow~=22.0", + "pydantic~=2.10", +] + +[project.urls] +Source = "https://github.com/microsoft/graphrag" + +[build-system] +requires = ["hatchling>=1.27.0,<2.0.0"] +build-backend = "hatchling.build" diff --git a/packages/graphrag/README.md b/packages/graphrag/README.md new file mode 100644 index 0000000000..2de7d4d0f0 --- /dev/null +++ b/packages/graphrag/README.md @@ -0,0 +1,76 @@ +# GraphRAG + +👉 [Microsoft Research Blog Post](https://www.microsoft.com/en-us/research/blog/graphrag-unlocking-llm-discovery-on-narrative-private-data/)
+👉 [Read the docs](https://microsoft.github.io/graphrag)
+👉 [GraphRAG Arxiv](https://arxiv.org/pdf/2404.16130) + + + +## Overview + +The GraphRAG project is a data pipeline and transformation suite that is designed to extract meaningful, structured data from unstructured text using the power of LLMs. + +To learn more about GraphRAG and how it can be used to enhance your LLM's ability to reason about your private data, please visit the Microsoft Research Blog Post. + +## Quickstart + +To get started with the GraphRAG system we recommend trying the [command line quickstart](https://microsoft.github.io/graphrag/get_started/). + +## Repository Guidance + +This repository presents a methodology for using knowledge graph memory structures to enhance LLM outputs. Please note that the provided code serves as a demonstration and is not an officially supported Microsoft offering. + +⚠️ *Warning: GraphRAG indexing can be an expensive operation, please read all of the documentation to understand the process and costs involved, and start small.* + +## Diving Deeper + +- To learn about our contribution guidelines, see [CONTRIBUTING.md](./CONTRIBUTING.md) +- To start developing _GraphRAG_, see [DEVELOPING.md](./DEVELOPING.md) +- Join the conversation and provide feedback in the [GitHub Discussions tab!](https://github.com/microsoft/graphrag/discussions) + +## Prompt Tuning + +Using _GraphRAG_ with your data out of the box may not yield the best possible results. +We strongly recommend to fine-tune your prompts following the [Prompt Tuning Guide](https://microsoft.github.io/graphrag/prompt_tuning/overview/) in our documentation. + +## Versioning + +Please see the [breaking changes](./breaking-changes.md) document for notes on our approach to versioning the project. + +*Always run `uv run poe init --root [path] --force` between minor version bumps to ensure you have the latest config format. Run the provided migration notebook between major version bumps if you want to avoid re-indexing prior datasets. Note that this will overwrite your configuration and prompts, so backup if necessary.* + +## Responsible AI FAQ + +See [RAI_TRANSPARENCY.md](./RAI_TRANSPARENCY.md) + +- [What is GraphRAG?](./RAI_TRANSPARENCY.md#what-is-graphrag) +- [What can GraphRAG do?](./RAI_TRANSPARENCY.md#what-can-graphrag-do) +- [What are GraphRAG’s intended use(s)?](./RAI_TRANSPARENCY.md#what-are-graphrags-intended-uses) +- [How was GraphRAG evaluated? What metrics are used to measure performance?](./RAI_TRANSPARENCY.md#how-was-graphrag-evaluated-what-metrics-are-used-to-measure-performance) +- [What are the limitations of GraphRAG? How can users minimize the impact of GraphRAG’s limitations when using the system?](./RAI_TRANSPARENCY.md#what-are-the-limitations-of-graphrag-how-can-users-minimize-the-impact-of-graphrags-limitations-when-using-the-system) +- [What operational factors and settings allow for effective and responsible use of GraphRAG?](./RAI_TRANSPARENCY.md#what-operational-factors-and-settings-allow-for-effective-and-responsible-use-of-graphrag) + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft +trademarks or logos is subject to and must follow +[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. +Any use of third-party trademarks or logos are subject to those third-party's policies. + +## Privacy + +[Microsoft Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) diff --git a/graphrag/__init__.py b/packages/graphrag/graphrag/__init__.py similarity index 100% rename from graphrag/__init__.py rename to packages/graphrag/graphrag/__init__.py diff --git a/graphrag/__main__.py b/packages/graphrag/graphrag/__main__.py similarity index 100% rename from graphrag/__main__.py rename to packages/graphrag/graphrag/__main__.py diff --git a/graphrag/api/__init__.py b/packages/graphrag/graphrag/api/__init__.py similarity index 79% rename from graphrag/api/__init__.py rename to packages/graphrag/graphrag/api/__init__.py index a3a06033bc..05692c4182 100644 --- a/graphrag/api/__init__.py +++ b/packages/graphrag/graphrag/api/__init__.py @@ -18,10 +18,6 @@ global_search_streaming, local_search, local_search_streaming, - multi_index_basic_search, - multi_index_drift_search, - multi_index_global_search, - multi_index_local_search, ) from graphrag.prompt_tune.types import DocSelectionType @@ -37,10 +33,6 @@ "drift_search_streaming", "basic_search", "basic_search_streaming", - "multi_index_basic_search", - "multi_index_drift_search", - "multi_index_global_search", - "multi_index_local_search", # prompt tuning API "DocSelectionType", "generate_indexing_prompts", diff --git a/graphrag/api/index.py b/packages/graphrag/graphrag/api/index.py similarity index 94% rename from graphrag/api/index.py rename to packages/graphrag/graphrag/api/index.py index 7265e46187..cc9d0956d5 100644 --- a/graphrag/api/index.py +++ b/packages/graphrag/graphrag/api/index.py @@ -30,7 +30,6 @@ async def build_index( config: GraphRagConfig, method: IndexingMethod | str = IndexingMethod.Standard, is_update_run: bool = False, - memory_profile: bool = False, callbacks: list[WorkflowCallbacks] | None = None, additional_context: dict[str, Any] | None = None, verbose: bool = False, @@ -67,9 +66,6 @@ async def build_index( outputs: list[PipelineRunResult] = [] - if memory_profile: - logger.warning("New pipeline does not yet support memory profiling.") - logger.info("Initializing indexing pipeline...") # todo: this could propagate out to the cli for better clarity, but will be a breaking api change method = _get_method(method, is_update_run) @@ -86,8 +82,9 @@ async def build_index( input_documents=input_documents, ): outputs.append(output) - if output.errors and len(output.errors) > 0: + if output.error is not None: logger.error("Workflow %s completed with errors", output.workflow) + workflow_callbacks.pipeline_error(output.error) else: logger.info("Workflow %s completed successfully", output.workflow) logger.debug(str(output.result)) diff --git a/graphrag/api/prompt_tune.py b/packages/graphrag/graphrag/api/prompt_tune.py similarity index 87% rename from graphrag/api/prompt_tune.py rename to packages/graphrag/graphrag/api/prompt_tune.py index 43fb0a5590..e1741f014b 100644 --- a/graphrag/api/prompt_tune.py +++ b/packages/graphrag/graphrag/api/prompt_tune.py @@ -12,15 +12,11 @@ """ import logging -from typing import Annotated -import annotated_types +from graphrag_llm.completion import create_completion from pydantic import PositiveInt, validate_call -from graphrag.callbacks.noop_workflow_callbacks import NoopWorkflowCallbacks -from graphrag.config.defaults import graphrag_config_defaults from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.language_model.manager import ModelManager from graphrag.logger.standard_logging import init_loggers from graphrag.prompt_tune.defaults import MAX_TOKEN_COUNT, PROMPT_TUNING_MODEL_ID from graphrag.prompt_tune.generator.community_report_rating import ( @@ -55,10 +51,6 @@ @validate_call(config={"arbitrary_types_allowed": True}) async def generate_indexing_prompts( config: GraphRagConfig, - chunk_size: PositiveInt = graphrag_config_defaults.chunks.size, - overlap: Annotated[ - int, annotated_types.Gt(-1) - ] = graphrag_config_defaults.chunks.overlap, limit: PositiveInt = 15, selection_method: DocSelectionType = DocSelectionType.RANDOM, domain: str | None = None, @@ -100,8 +92,6 @@ async def generate_indexing_prompts( limit=limit, select_method=selection_method, logger=logger, - chunk_size=chunk_size, - overlap=overlap, n_subset_max=n_subset_max, k=k, ) @@ -109,16 +99,10 @@ async def generate_indexing_prompts( # Create LLM from config # TODO: Expose a way to specify Prompt Tuning model ID through config logger.info("Retrieving language model configuration...") - default_llm_settings = config.get_language_model_config(PROMPT_TUNING_MODEL_ID) + default_llm_settings = config.get_completion_model_config(PROMPT_TUNING_MODEL_ID) logger.info("Creating language model...") - llm = ModelManager().register_chat( - name="prompt_tuning", - model_type=default_llm_settings.type, - config=default_llm_settings, - callbacks=NoopWorkflowCallbacks(), - cache=None, - ) + llm = create_completion(default_llm_settings) if not domain: logger.info("Generating domain...") @@ -137,8 +121,8 @@ async def generate_indexing_prompts( ) entity_types = None - extract_graph_llm_settings = config.get_language_model_config( - config.extract_graph.model_id + extract_graph_llm_settings = config.get_completion_model_config( + config.extract_graph.completion_model_id ) if discover_entity_types: logger.info("Generating entity types...") @@ -147,7 +131,7 @@ async def generate_indexing_prompts( domain=domain, persona=persona, docs=doc_list, - json_mode=extract_graph_llm_settings.model_supports_json or False, + json_mode=True, ) logger.info("Generating entity relationship examples...") diff --git a/packages/graphrag/graphrag/api/query.py b/packages/graphrag/graphrag/api/query.py new file mode 100644 index 0000000000..fe50d56a45 --- /dev/null +++ b/packages/graphrag/graphrag/api/query.py @@ -0,0 +1,546 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +""" +Query Engine API. + +This API provides access to the query engine of graphrag, allowing external applications +to hook into graphrag and run queries over a knowledge graph generated by graphrag. + +Contains the following functions: + - global_search: Perform a global search. + - global_search_streaming: Perform a global search and stream results back. + - local_search: Perform a local search. + - local_search_streaming: Perform a local search and stream results back. + +WARNING: This API is under development and may undergo changes in future releases. +Backwards compatibility is not guaranteed at this time. +""" + +import logging +from collections.abc import AsyncGenerator +from typing import Any + +import pandas as pd +from pydantic import validate_call + +from graphrag.callbacks.noop_query_callbacks import NoopQueryCallbacks +from graphrag.callbacks.query_callbacks import QueryCallbacks +from graphrag.config.embeddings import ( + community_full_content_embedding, + entity_description_embedding, + text_unit_text_embedding, +) +from graphrag.config.models.graph_rag_config import GraphRagConfig +from graphrag.logger.standard_logging import init_loggers +from graphrag.query.factory import ( + get_basic_search_engine, + get_drift_search_engine, + get_global_search_engine, + get_local_search_engine, +) +from graphrag.query.indexer_adapters import ( + read_indexer_communities, + read_indexer_covariates, + read_indexer_entities, + read_indexer_relationships, + read_indexer_report_embeddings, + read_indexer_reports, + read_indexer_text_units, +) +from graphrag.utils.api import ( + get_embedding_store, + load_search_prompt, + truncate, +) +from graphrag.utils.cli import redact + +# Initialize standard logger +logger = logging.getLogger(__name__) + + +@validate_call(config={"arbitrary_types_allowed": True}) +async def global_search( + config: GraphRagConfig, + entities: pd.DataFrame, + communities: pd.DataFrame, + community_reports: pd.DataFrame, + community_level: int | None, + dynamic_community_selection: bool, + response_type: str, + query: str, + callbacks: list[QueryCallbacks] | None = None, + verbose: bool = False, +) -> tuple[ + str | dict[str, Any] | list[dict[str, Any]], + str | list[pd.DataFrame] | dict[str, pd.DataFrame], +]: + """Perform a global search and return the context data and response. + + Parameters + ---------- + - config (GraphRagConfig): A graphrag configuration (from settings.yaml) + - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) + - communities (pd.DataFrame): A DataFrame containing the final communities (from communities.parquet) + - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) + - community_level (int): The community level to search at. + - dynamic_community_selection (bool): Enable dynamic community selection instead of using all community reports at a fixed level. Note that you can still provide community_level cap the maximum level to search. + - response_type (str): The type of response to return. + - query (str): The user query to search for. + + Returns + ------- + TODO: Document the search response type and format. + """ + init_loggers(config=config, verbose=verbose, filename="query.log") + + callbacks = callbacks or [] + full_response = "" + context_data = {} + + def on_context(context: Any) -> None: + nonlocal context_data + context_data = context + + local_callbacks = NoopQueryCallbacks() + local_callbacks.on_context = on_context + callbacks.append(local_callbacks) + + logger.debug("Executing global search query: %s", query) + async for chunk in global_search_streaming( + config=config, + entities=entities, + communities=communities, + community_reports=community_reports, + community_level=community_level, + dynamic_community_selection=dynamic_community_selection, + response_type=response_type, + query=query, + callbacks=callbacks, + ): + full_response += chunk + logger.debug("Query response: %s", truncate(full_response, 400)) + return full_response, context_data + + +@validate_call(config={"arbitrary_types_allowed": True}) +def global_search_streaming( + config: GraphRagConfig, + entities: pd.DataFrame, + communities: pd.DataFrame, + community_reports: pd.DataFrame, + community_level: int | None, + dynamic_community_selection: bool, + response_type: str, + query: str, + callbacks: list[QueryCallbacks] | None = None, + verbose: bool = False, +) -> AsyncGenerator: + """Perform a global search and return the context data and response via a generator. + + Context data is returned as a dictionary of lists, with one list entry for each record. + + Parameters + ---------- + - config (GraphRagConfig): A graphrag configuration (from settings.yaml) + - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) + - communities (pd.DataFrame): A DataFrame containing the final communities (from communities.parquet) + - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) + - community_level (int): The community level to search at. + - dynamic_community_selection (bool): Enable dynamic community selection instead of using all community reports at a fixed level. Note that you can still provide community_level cap the maximum level to search. + - response_type (str): The type of response to return. + - query (str): The user query to search for. + + Returns + ------- + TODO: Document the search response type and format. + """ + init_loggers(config=config, verbose=verbose, filename="query.log") + + communities_ = read_indexer_communities(communities, community_reports) + reports = read_indexer_reports( + community_reports, + communities, + community_level=community_level, + dynamic_community_selection=dynamic_community_selection, + ) + entities_ = read_indexer_entities( + entities, communities, community_level=community_level + ) + map_prompt = load_search_prompt(config.global_search.map_prompt) + reduce_prompt = load_search_prompt(config.global_search.reduce_prompt) + knowledge_prompt = load_search_prompt(config.global_search.knowledge_prompt) + + logger.debug("Executing streaming global search query: %s", query) + search_engine = get_global_search_engine( + config, + reports=reports, + entities=entities_, + communities=communities_, + response_type=response_type, + dynamic_community_selection=dynamic_community_selection, + map_system_prompt=map_prompt, + reduce_system_prompt=reduce_prompt, + general_knowledge_inclusion_prompt=knowledge_prompt, + callbacks=callbacks, + ) + return search_engine.stream_search(query=query) + + +@validate_call(config={"arbitrary_types_allowed": True}) +async def local_search( + config: GraphRagConfig, + entities: pd.DataFrame, + communities: pd.DataFrame, + community_reports: pd.DataFrame, + text_units: pd.DataFrame, + relationships: pd.DataFrame, + covariates: pd.DataFrame | None, + community_level: int, + response_type: str, + query: str, + callbacks: list[QueryCallbacks] | None = None, + verbose: bool = False, +) -> tuple[ + str | dict[str, Any] | list[dict[str, Any]], + str | list[pd.DataFrame] | dict[str, pd.DataFrame], +]: + """Perform a local search and return the context data and response. + + ---------- + - config (GraphRagConfig): A graphrag configuration (from settings.yaml) + - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) + - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) + - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) + - relationships (pd.DataFrame): A DataFrame containing the final relationships (from relationships.parquet) + - covariates (pd.DataFrame): A DataFrame containing the final covariates (from covariates.parquet) + - community_level (int): The community level to search at. + - response_type (str): The response type to return. + - query (str): The user query to search for. + + Returns + ------- + TODO: Document the search response type and format. + """ + init_loggers(config=config, verbose=verbose, filename="query.log") + + callbacks = callbacks or [] + full_response = "" + context_data = {} + + def on_context(context: Any) -> None: + nonlocal context_data + context_data = context + + local_callbacks = NoopQueryCallbacks() + local_callbacks.on_context = on_context + callbacks.append(local_callbacks) + + logger.debug("Executing local search query: %s", query) + async for chunk in local_search_streaming( + config=config, + entities=entities, + communities=communities, + community_reports=community_reports, + text_units=text_units, + relationships=relationships, + covariates=covariates, + community_level=community_level, + response_type=response_type, + query=query, + callbacks=callbacks, + ): + full_response += chunk + logger.debug("Query response: %s", truncate(full_response, 400)) + return full_response, context_data + + +@validate_call(config={"arbitrary_types_allowed": True}) +def local_search_streaming( + config: GraphRagConfig, + entities: pd.DataFrame, + communities: pd.DataFrame, + community_reports: pd.DataFrame, + text_units: pd.DataFrame, + relationships: pd.DataFrame, + covariates: pd.DataFrame | None, + community_level: int, + response_type: str, + query: str, + callbacks: list[QueryCallbacks] | None = None, + verbose: bool = False, +) -> AsyncGenerator: + """Perform a local search and return the context data and response via a generator. + + Parameters + ---------- + - config (GraphRagConfig): A graphrag configuration (from settings.yaml) + - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) + - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) + - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) + - relationships (pd.DataFrame): A DataFrame containing the final relationships (from relationships.parquet) + - covariates (pd.DataFrame): A DataFrame containing the final covariates (from covariates.parquet) + - community_level (int): The community level to search at. + - response_type (str): The response type to return. + - query (str): The user query to search for. + + Returns + ------- + TODO: Document the search response type and format. + """ + init_loggers(config=config, verbose=verbose, filename="query.log") + + msg = f"Vector Store Args: {redact(config.vector_store.model_dump())}" + logger.debug(msg) + + description_embedding_store = get_embedding_store( + config=config.vector_store, + embedding_name=entity_description_embedding, + ) + + entities_ = read_indexer_entities(entities, communities, community_level) + covariates_ = read_indexer_covariates(covariates) if covariates is not None else [] + prompt = load_search_prompt(config.local_search.prompt) + + logger.debug("Executing streaming local search query: %s", query) + search_engine = get_local_search_engine( + config=config, + reports=read_indexer_reports(community_reports, communities, community_level), + text_units=read_indexer_text_units(text_units), + entities=entities_, + relationships=read_indexer_relationships(relationships), + covariates={"claims": covariates_}, + description_embedding_store=description_embedding_store, + response_type=response_type, + system_prompt=prompt, + callbacks=callbacks, + ) + return search_engine.stream_search(query=query) + + +@validate_call(config={"arbitrary_types_allowed": True}) +async def drift_search( + config: GraphRagConfig, + entities: pd.DataFrame, + communities: pd.DataFrame, + community_reports: pd.DataFrame, + text_units: pd.DataFrame, + relationships: pd.DataFrame, + community_level: int, + response_type: str, + query: str, + callbacks: list[QueryCallbacks] | None = None, + verbose: bool = False, +) -> tuple[ + str | dict[str, Any] | list[dict[str, Any]], + str | list[pd.DataFrame] | dict[str, pd.DataFrame], +]: + """Perform a DRIFT search and return the context data and response. + + Parameters + ---------- + - config (GraphRagConfig): A graphrag configuration (from settings.yaml) + - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) + - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) + - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) + - relationships (pd.DataFrame): A DataFrame containing the final relationships (from relationships.parquet) + - community_level (int): The community level to search at. + - query (str): The user query to search for. + + Returns + ------- + TODO: Document the search response type and format. + """ + init_loggers(config=config, verbose=verbose, filename="query.log") + + callbacks = callbacks or [] + full_response = "" + context_data = {} + + def on_context(context: Any) -> None: + nonlocal context_data + context_data = context + + local_callbacks = NoopQueryCallbacks() + local_callbacks.on_context = on_context + callbacks.append(local_callbacks) + + logger.debug("Executing drift search query: %s", query) + async for chunk in drift_search_streaming( + config=config, + entities=entities, + communities=communities, + community_reports=community_reports, + text_units=text_units, + relationships=relationships, + community_level=community_level, + response_type=response_type, + query=query, + callbacks=callbacks, + ): + full_response += chunk + logger.debug("Query response: %s", truncate(full_response, 400)) + return full_response, context_data + + +@validate_call(config={"arbitrary_types_allowed": True}) +def drift_search_streaming( + config: GraphRagConfig, + entities: pd.DataFrame, + communities: pd.DataFrame, + community_reports: pd.DataFrame, + text_units: pd.DataFrame, + relationships: pd.DataFrame, + community_level: int, + response_type: str, + query: str, + callbacks: list[QueryCallbacks] | None = None, + verbose: bool = False, +) -> AsyncGenerator: + """Perform a DRIFT search and return the context data and response. + + Parameters + ---------- + - config (GraphRagConfig): A graphrag configuration (from settings.yaml) + - entities (pd.DataFrame): A DataFrame containing the final entities (from entities.parquet) + - community_reports (pd.DataFrame): A DataFrame containing the final community reports (from community_reports.parquet) + - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) + - relationships (pd.DataFrame): A DataFrame containing the final relationships (from relationships.parquet) + - community_level (int): The community level to search at. + - query (str): The user query to search for. + + Returns + ------- + TODO: Document the search response type and format. + """ + init_loggers(config=config, verbose=verbose, filename="query.log") + + msg = f"Vector Store Args: {redact(config.vector_store.model_dump())}" + logger.debug(msg) + + description_embedding_store = get_embedding_store( + config=config.vector_store, + embedding_name=entity_description_embedding, + ) + + full_content_embedding_store = get_embedding_store( + config=config.vector_store, + embedding_name=community_full_content_embedding, + ) + + entities_ = read_indexer_entities(entities, communities, community_level) + reports = read_indexer_reports(community_reports, communities, community_level) + read_indexer_report_embeddings(reports, full_content_embedding_store) + prompt = load_search_prompt(config.drift_search.prompt) + reduce_prompt = load_search_prompt(config.drift_search.reduce_prompt) + + logger.debug("Executing streaming drift search query: %s", query) + search_engine = get_drift_search_engine( + config=config, + reports=reports, + text_units=read_indexer_text_units(text_units), + entities=entities_, + relationships=read_indexer_relationships(relationships), + description_embedding_store=description_embedding_store, + local_system_prompt=prompt, + reduce_system_prompt=reduce_prompt, + response_type=response_type, + callbacks=callbacks, + ) + return search_engine.stream_search(query=query) + + +@validate_call(config={"arbitrary_types_allowed": True}) +async def basic_search( + config: GraphRagConfig, + text_units: pd.DataFrame, + response_type: str, + query: str, + callbacks: list[QueryCallbacks] | None = None, + verbose: bool = False, +) -> tuple[ + str | dict[str, Any] | list[dict[str, Any]], + str | list[pd.DataFrame] | dict[str, pd.DataFrame], +]: + """Perform a basic search and return the context data and response. + + Parameters + ---------- + - config (GraphRagConfig): A graphrag configuration (from settings.yaml) + - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) + - query (str): The user query to search for. + + Returns + ------- + TODO: Document the search response type and format. + """ + init_loggers(config=config, verbose=verbose, filename="query.log") + + callbacks = callbacks or [] + full_response = "" + context_data = {} + + def on_context(context: Any) -> None: + nonlocal context_data + context_data = context + + local_callbacks = NoopQueryCallbacks() + local_callbacks.on_context = on_context + callbacks.append(local_callbacks) + + logger.debug("Executing basic search query: %s", query) + async for chunk in basic_search_streaming( + config=config, + text_units=text_units, + response_type=response_type, + query=query, + callbacks=callbacks, + ): + full_response += chunk + logger.debug("Query response: %s", truncate(full_response, 400)) + return full_response, context_data + + +@validate_call(config={"arbitrary_types_allowed": True}) +def basic_search_streaming( + config: GraphRagConfig, + text_units: pd.DataFrame, + response_type: str, + query: str, + callbacks: list[QueryCallbacks] | None = None, + verbose: bool = False, +) -> AsyncGenerator: + """Perform a local search and return the context data and response via a generator. + + Parameters + ---------- + - config (GraphRagConfig): A graphrag configuration (from settings.yaml) + - text_units (pd.DataFrame): A DataFrame containing the final text units (from text_units.parquet) + - query (str): The user query to search for. + + Returns + ------- + TODO: Document the search response type and format. + """ + init_loggers(config=config, verbose=verbose, filename="query.log") + + msg = f"Vector Store Args: {redact(config.vector_store.model_dump())}" + logger.debug(msg) + + embedding_store = get_embedding_store( + config=config.vector_store, + embedding_name=text_unit_text_embedding, + ) + + prompt = load_search_prompt(config.basic_search.prompt) + + logger.debug("Executing streaming basic search query: %s", query) + search_engine = get_basic_search_engine( + config=config, + text_units=read_indexer_text_units(text_units), + text_unit_embeddings=embedding_store, + response_type=response_type, + system_prompt=prompt, + callbacks=callbacks, + ) + return search_engine.stream_search(query=query) diff --git a/graphrag/factory/__init__.py b/packages/graphrag/graphrag/cache/__init__.py similarity index 78% rename from graphrag/factory/__init__.py rename to packages/graphrag/graphrag/cache/__init__.py index 55bd738251..60ffdf7a1f 100644 --- a/graphrag/factory/__init__.py +++ b/packages/graphrag/graphrag/cache/__init__.py @@ -1,4 +1,4 @@ # Copyright (c) 2025 Microsoft Corporation. # Licensed under the MIT License -"""Factory module.""" +"""Cache module.""" diff --git a/packages/graphrag/graphrag/cache/cache_key_creator.py b/packages/graphrag/graphrag/cache/cache_key_creator.py new file mode 100644 index 0000000000..704035f253 --- /dev/null +++ b/packages/graphrag/graphrag/cache/cache_key_creator.py @@ -0,0 +1,44 @@ +# Copyright (c) 2025 Microsoft Corporation. +# Licensed under the MIT License + +"""Cache key creation for Graphrag.""" + +from typing import Any + +from graphrag_llm.cache import create_cache_key + +_CACHE_VERSION = 4 +""" +If there's a breaking change in what we cache, we should increment this version number to invalidate existing caches. + +fnllm was on cache version 2 and though we generate +similar cache keys, the objects stored in cache by fnllm and litellm are different. +Using litellm model providers will not be able to reuse caches generated by fnllm +thus we start with version 3 for litellm. + +graphrag-llm package is now on version 4. +This is to account for changes to the ModelConfig that affect the cache key and +occurred when pulling this package out of graphrag. +graphrag-llm, now that is supports metrics, also caches metrics which were not cached before. +""" + + +def cache_key_creator( + input_args: dict[str, Any], +) -> str: + """Generate a cache key based on input arguments. + + Args + ____ + input_args: dict[str, Any] + The input arguments for the model call. + + Returns + ------- + str + The generated cache key in the format + `{prefix}_{data_hash}_v{version}` if prefix is provided. + """ + base_key = create_cache_key(input_args) + + return f"{base_key}_v{_CACHE_VERSION}" diff --git a/graphrag/callbacks/__init__.py b/packages/graphrag/graphrag/callbacks/__init__.py similarity index 100% rename from graphrag/callbacks/__init__.py rename to packages/graphrag/graphrag/callbacks/__init__.py diff --git a/graphrag/callbacks/console_workflow_callbacks.py b/packages/graphrag/graphrag/callbacks/console_workflow_callbacks.py similarity index 90% rename from graphrag/callbacks/console_workflow_callbacks.py rename to packages/graphrag/graphrag/callbacks/console_workflow_callbacks.py index dbe7e0d552..547c5ed258 100644 --- a/graphrag/callbacks/console_workflow_callbacks.py +++ b/packages/graphrag/graphrag/callbacks/console_workflow_callbacks.py @@ -37,6 +37,10 @@ def workflow_end(self, name: str, instance: object) -> None: if self._verbose: print(instance) + def pipeline_error(self, error: BaseException) -> None: + """Execute this callback when an error occurs in the pipeline.""" + print(f"Pipeline error: {error}") + def progress(self, progress: Progress) -> None: """Handle when progress occurs.""" complete = progress.completed_items or 0 diff --git a/graphrag/callbacks/llm_callbacks.py b/packages/graphrag/graphrag/callbacks/llm_callbacks.py similarity index 100% rename from graphrag/callbacks/llm_callbacks.py rename to packages/graphrag/graphrag/callbacks/llm_callbacks.py diff --git a/graphrag/callbacks/noop_query_callbacks.py b/packages/graphrag/graphrag/callbacks/noop_query_callbacks.py similarity index 100% rename from graphrag/callbacks/noop_query_callbacks.py rename to packages/graphrag/graphrag/callbacks/noop_query_callbacks.py diff --git a/graphrag/callbacks/noop_workflow_callbacks.py b/packages/graphrag/graphrag/callbacks/noop_workflow_callbacks.py similarity index 89% rename from graphrag/callbacks/noop_workflow_callbacks.py rename to packages/graphrag/graphrag/callbacks/noop_workflow_callbacks.py index 9f9ac2aee0..19aba39a0c 100644 --- a/graphrag/callbacks/noop_workflow_callbacks.py +++ b/packages/graphrag/graphrag/callbacks/noop_workflow_callbacks.py @@ -25,3 +25,6 @@ def workflow_end(self, name: str, instance: object) -> None: def progress(self, progress: Progress) -> None: """Handle when progress occurs.""" + + def pipeline_error(self, error: BaseException) -> None: + """Execute this callback when an error occurs in the pipeline.""" diff --git a/graphrag/callbacks/query_callbacks.py b/packages/graphrag/graphrag/callbacks/query_callbacks.py similarity index 100% rename from graphrag/callbacks/query_callbacks.py rename to packages/graphrag/graphrag/callbacks/query_callbacks.py diff --git a/graphrag/callbacks/workflow_callbacks.py b/packages/graphrag/graphrag/callbacks/workflow_callbacks.py similarity index 89% rename from graphrag/callbacks/workflow_callbacks.py rename to packages/graphrag/graphrag/callbacks/workflow_callbacks.py index 0429cff809..3fb09710f9 100644 --- a/graphrag/callbacks/workflow_callbacks.py +++ b/packages/graphrag/graphrag/callbacks/workflow_callbacks.py @@ -35,3 +35,7 @@ def workflow_end(self, name: str, instance: object) -> None: def progress(self, progress: Progress) -> None: """Handle when progress occurs.""" ... + + def pipeline_error(self, error: BaseException) -> None: + """Execute this callback when an error occurs in the pipeline.""" + ... diff --git a/graphrag/callbacks/workflow_callbacks_manager.py b/packages/graphrag/graphrag/callbacks/workflow_callbacks_manager.py similarity index 85% rename from graphrag/callbacks/workflow_callbacks_manager.py rename to packages/graphrag/graphrag/callbacks/workflow_callbacks_manager.py index 1ca0c097e5..9d3803f1be 100644 --- a/graphrag/callbacks/workflow_callbacks_manager.py +++ b/packages/graphrag/graphrag/callbacks/workflow_callbacks_manager.py @@ -1,7 +1,7 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing the WorkflowCallbacks registry.""" +"""A module containing 'WorkflowCallbacksManager' model.""" from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks from graphrag.index.typing.pipeline_run_result import PipelineRunResult @@ -50,3 +50,9 @@ def progress(self, progress: Progress) -> None: for callback in self._callbacks: if hasattr(callback, "progress"): callback.progress(progress) + + def pipeline_error(self, error: BaseException) -> None: + """Execute this callback when an error occurs in the pipeline.""" + for callback in self._callbacks: + if hasattr(callback, "pipeline_error"): + callback.pipeline_error(error) diff --git a/graphrag/cli/__init__.py b/packages/graphrag/graphrag/cli/__init__.py similarity index 100% rename from graphrag/cli/__init__.py rename to packages/graphrag/graphrag/cli/__init__.py diff --git a/graphrag/cli/index.py b/packages/graphrag/graphrag/cli/index.py similarity index 69% rename from graphrag/cli/index.py rename to packages/graphrag/graphrag/cli/index.py index b5464d2544..94a40cde48 100644 --- a/graphrag/cli/index.py +++ b/packages/graphrag/graphrag/cli/index.py @@ -9,9 +9,11 @@ import warnings from pathlib import Path +from graphrag_cache.cache_type import CacheType + import graphrag.api as api from graphrag.callbacks.console_workflow_callbacks import ConsoleWorkflowCallbacks -from graphrag.config.enums import CacheType, IndexingMethod +from graphrag.config.enums import IndexingMethod from graphrag.config.load_config import load_config from graphrag.index.validate_config import validate_config_names from graphrag.utils.cli import redact @@ -43,26 +45,17 @@ def index_cli( root_dir: Path, method: IndexingMethod, verbose: bool, - memprofile: bool, cache: bool, - config_filepath: Path | None, dry_run: bool, skip_validation: bool, - output_dir: Path | None, ): """Run the pipeline with the given config.""" - cli_overrides = {} - if output_dir: - cli_overrides["output.base_dir"] = str(output_dir) - cli_overrides["reporting.base_dir"] = str(output_dir) - cli_overrides["update_index_output.base_dir"] = str(output_dir) - config = load_config(root_dir, config_filepath, cli_overrides) + config = load_config(root_dir=root_dir) _run_index( config=config, method=method, is_update_run=False, verbose=verbose, - memprofile=memprofile, cache=cache, dry_run=dry_run, skip_validation=skip_validation, @@ -73,27 +66,19 @@ def update_cli( root_dir: Path, method: IndexingMethod, verbose: bool, - memprofile: bool, cache: bool, - config_filepath: Path | None, skip_validation: bool, - output_dir: Path | None, ): """Run the pipeline with the given config.""" - cli_overrides = {} - if output_dir: - cli_overrides["output.base_dir"] = str(output_dir) - cli_overrides["reporting.base_dir"] = str(output_dir) - cli_overrides["update_index_output.base_dir"] = str(output_dir) - - config = load_config(root_dir, config_filepath, cli_overrides) + config = load_config( + root_dir=root_dir, + ) _run_index( config=config, method=method, is_update_run=True, verbose=verbose, - memprofile=memprofile, cache=cache, dry_run=False, skip_validation=skip_validation, @@ -105,7 +90,6 @@ def _run_index( method, is_update_run, verbose, - memprofile, cache, dry_run, skip_validation, @@ -120,7 +104,7 @@ def _run_index( ) if not cache: - config.cache.type = CacheType.none + config.cache.type = CacheType.Noop if not skip_validation: validate_config_names(config) @@ -142,20 +126,10 @@ def _run_index( config=config, method=method, is_update_run=is_update_run, - memory_profile=memprofile, callbacks=[ConsoleWorkflowCallbacks(verbose=verbose)], verbose=verbose, ) ) - encountered_errors = any( - output.errors and len(output.errors) > 0 for output in outputs - ) - - if encountered_errors: - logger.error( - "Errors occurred during the pipeline run, see logs for more details." - ) - else: - logger.info("All workflows completed successfully.") + encountered_errors = any(output.error is not None for output in outputs) sys.exit(1 if encountered_errors else 0) diff --git a/graphrag/cli/initialize.py b/packages/graphrag/graphrag/cli/initialize.py similarity index 75% rename from graphrag/cli/initialize.py rename to packages/graphrag/graphrag/cli/initialize.py index 09215f8c5d..3bf0c69c76 100644 --- a/graphrag/cli/initialize.py +++ b/packages/graphrag/graphrag/cli/initialize.py @@ -6,6 +6,7 @@ import logging from pathlib import Path +from graphrag.config.defaults import graphrag_config_defaults from graphrag.config.init_content import INIT_DOTENV, INIT_YAML from graphrag.prompts.index.community_report import ( COMMUNITY_REPORT_PROMPT, @@ -34,7 +35,9 @@ logger = logging.getLogger(__name__) -def initialize_project_at(path: Path, force: bool) -> None: +def initialize_project_at( + path: Path, force: bool, model: str, embedding_model: str +) -> None: """ Initialize the project at the given path. @@ -51,26 +54,30 @@ def initialize_project_at(path: Path, force: bool) -> None: If the project already exists and force is False. """ logger.info("Initializing project at %s", path) - root = Path(path) - if not root.exists(): - root.mkdir(parents=True, exist_ok=True) + root = Path(path).resolve() + root.mkdir(parents=True, exist_ok=True) settings_yaml = root / "settings.yaml" if settings_yaml.exists() and not force: msg = f"Project already initialized at {root}" raise ValueError(msg) - with settings_yaml.open("wb") as file: - file.write(INIT_YAML.encode(encoding="utf-8", errors="strict")) + input_path = ( + root / (graphrag_config_defaults.input_storage.base_dir or "input") + ).resolve() + input_path.mkdir(parents=True, exist_ok=True) + # using replace with custom tokens instead of format here because we have a placeholder for GRAPHRAG_API_KEY that is used later for .env overlay + formatted = INIT_YAML.replace("", model).replace( + "", embedding_model + ) + settings_yaml.write_text(formatted, encoding="utf-8", errors="strict") dotenv = root / ".env" if not dotenv.exists() or force: - with dotenv.open("wb") as file: - file.write(INIT_DOTENV.encode(encoding="utf-8", errors="strict")) + dotenv.write_text(INIT_DOTENV, encoding="utf-8", errors="strict") prompts_dir = root / "prompts" - if not prompts_dir.exists(): - prompts_dir.mkdir(parents=True, exist_ok=True) + prompts_dir.mkdir(parents=True, exist_ok=True) prompts = { "extract_graph": GRAPH_EXTRACTION_PROMPT, @@ -91,5 +98,4 @@ def initialize_project_at(path: Path, force: bool) -> None: for name, content in prompts.items(): prompt_file = prompts_dir / f"{name}.txt" if not prompt_file.exists() or force: - with prompt_file.open("wb") as file: - file.write(content.encode(encoding="utf-8", errors="strict")) + prompt_file.write_text(content, encoding="utf-8", errors="strict") diff --git a/graphrag/cli/main.py b/packages/graphrag/graphrag/cli/main.py similarity index 81% rename from graphrag/cli/main.py rename to packages/graphrag/graphrag/cli/main.py index bc0e9f39ac..5259cf4b85 100644 --- a/graphrag/cli/main.py +++ b/packages/graphrag/graphrag/cli/main.py @@ -10,7 +10,11 @@ import typer -from graphrag.config.defaults import graphrag_config_defaults +from graphrag.config.defaults import ( + DEFAULT_COMPLETION_MODEL, + DEFAULT_EMBEDDING_MODEL, + graphrag_config_defaults, +) from graphrag.config.enums import IndexingMethod, SearchMethod from graphrag.prompt_tune.defaults import LIMIT, MAX_TOKEN_COUNT, N_SUBSET_MAX, K from graphrag.prompt_tune.types import DocSelectionType @@ -64,9 +68,9 @@ def completer(incomplete: str) -> list[str]: # Apply wildcard matching if required if match_wildcard: completions = filter( - lambda i: wildcard_match(i, match_wildcard) - if match_wildcard - else False, + lambda i: ( + wildcard_match(i, match_wildcard) if match_wildcard else False + ), completions, ) @@ -94,15 +98,28 @@ def completer(incomplete: str) -> list[str]: @app.command("init") def _initialize_cli( root: Path = typer.Option( - Path(), + Path.cwd(), "--root", "-r", help="The project root directory.", dir_okay=True, writable=True, + file_okay=False, resolve_path=True, autocompletion=ROOT_AUTOCOMPLETE, ), + model: str = typer.Option( + DEFAULT_COMPLETION_MODEL, + "--model", + "-m", + prompt="Specify the default chat model to use", + ), + embedding_model: str = typer.Option( + DEFAULT_EMBEDDING_MODEL, + "--embedding", + "-e", + prompt="Specify the default embedding model to use", + ), force: bool = typer.Option( False, "--force", @@ -113,28 +130,21 @@ def _initialize_cli( """Generate a default configuration file.""" from graphrag.cli.initialize import initialize_project_at - initialize_project_at(path=root, force=force) + initialize_project_at( + path=root, force=force, model=model, embedding_model=embedding_model + ) @app.command("index") def _index_cli( - config: Path | None = typer.Option( - None, - "--config", - "-c", - help="The configuration to use.", - exists=True, - file_okay=True, - readable=True, - autocompletion=CONFIG_AUTOCOMPLETE, - ), root: Path = typer.Option( - Path(), + Path.cwd(), "--root", "-r", help="The project root directory.", exists=True, dir_okay=True, + file_okay=False, writable=True, resolve_path=True, autocompletion=ROOT_AUTOCOMPLETE, @@ -151,11 +161,6 @@ def _index_cli( "-v", help="Run the indexing pipeline with verbose logging", ), - memprofile: bool = typer.Option( - False, - "--memprofile", - help="Run the indexing pipeline with memory profiling", - ), dry_run: bool = typer.Option( False, "--dry-run", @@ -174,18 +179,6 @@ def _index_cli( "--skip-validation", help="Skip any preflight validation. Useful when running no LLM steps.", ), - output: Path | None = typer.Option( - None, - "--output", - "-o", - help=( - "Indexing pipeline output directory. " - "Overrides output.base_dir in the configuration file." - ), - dir_okay=True, - writable=True, - resolve_path=True, - ), ) -> None: """Build a knowledge graph index.""" from graphrag.cli.index import index_cli @@ -193,35 +186,23 @@ def _index_cli( index_cli( root_dir=root, verbose=verbose, - memprofile=memprofile, cache=cache, - config_filepath=config, dry_run=dry_run, skip_validation=skip_validation, - output_dir=output, method=method, ) @app.command("update") def _update_cli( - config: Path | None = typer.Option( - None, - "--config", - "-c", - help="The configuration to use.", - exists=True, - file_okay=True, - readable=True, - autocompletion=CONFIG_AUTOCOMPLETE, - ), root: Path = typer.Option( - Path(), + Path.cwd(), "--root", "-r", help="The project root directory.", exists=True, dir_okay=True, + file_okay=False, writable=True, resolve_path=True, autocompletion=ROOT_AUTOCOMPLETE, @@ -238,11 +219,6 @@ def _update_cli( "-v", help="Run the indexing pipeline with verbose logging.", ), - memprofile: bool = typer.Option( - False, - "--memprofile", - help="Run the indexing pipeline with memory profiling.", - ), cache: bool = typer.Option( True, "--cache/--no-cache", @@ -253,18 +229,6 @@ def _update_cli( "--skip-validation", help="Skip any preflight validation. Useful when running no LLM steps.", ), - output: Path | None = typer.Option( - None, - "--output", - "-o", - help=( - "Indexing pipeline output directory. " - "Overrides output.base_dir in the configuration file." - ), - dir_okay=True, - writable=True, - resolve_path=True, - ), ) -> None: """ Update an existing knowledge graph index. @@ -276,11 +240,8 @@ def _update_cli( update_cli( root_dir=root, verbose=verbose, - memprofile=memprofile, cache=cache, - config_filepath=config, skip_validation=skip_validation, - output_dir=output, method=method, ) @@ -288,26 +249,17 @@ def _update_cli( @app.command("prompt-tune") def _prompt_tune_cli( root: Path = typer.Option( - Path(), + Path.cwd(), "--root", "-r", help="The project root directory.", exists=True, dir_okay=True, + file_okay=False, writable=True, resolve_path=True, autocompletion=ROOT_AUTOCOMPLETE, ), - config: Path | None = typer.Option( - None, - "--config", - "-c", - help="The configuration to use.", - exists=True, - file_okay=True, - readable=True, - autocompletion=CONFIG_AUTOCOMPLETE, - ), verbose: bool = typer.Option( False, "--verbose", @@ -354,14 +306,14 @@ def _prompt_tune_cli( help="The minimum number of examples to generate/include in the entity extraction prompt.", ), chunk_size: int = typer.Option( - graphrag_config_defaults.chunks.size, + graphrag_config_defaults.chunking.size, "--chunk-size", - help="The size of each example text chunk. Overrides chunks.size in the configuration file.", + help="The size of each example text chunk. Overrides chunking.size in the configuration file.", ), overlap: int = typer.Option( - graphrag_config_defaults.chunks.overlap, + graphrag_config_defaults.chunking.overlap, "--overlap", - help="The overlap size for chunking documents. Overrides chunks.overlap in the configuration file.", + help="The overlap size for chunking documents. Overrides chunking.overlap in the configuration file.", ), language: str | None = typer.Option( None, @@ -392,7 +344,6 @@ def _prompt_tune_cli( loop.run_until_complete( prompt_tune( root=root, - config=config, domain=domain, verbose=verbose, selection_method=selection_method, @@ -412,28 +363,27 @@ def _prompt_tune_cli( @app.command("query") def _query_cli( + query: str = typer.Argument( + help="The query to execute.", + ), + root: Path = typer.Option( + Path.cwd(), + "--root", + "-r", + help="The project root directory.", + exists=True, + dir_okay=True, + file_okay=False, + writable=True, + resolve_path=True, + autocompletion=ROOT_AUTOCOMPLETE, + ), method: SearchMethod = typer.Option( - ..., + SearchMethod.GLOBAL.value, "--method", "-m", help="The query algorithm to use.", ), - query: str = typer.Option( - ..., - "--query", - "-q", - help="The query to execute.", - ), - config: Path | None = typer.Option( - None, - "--config", - "-c", - help="The configuration to use.", - exists=True, - file_okay=True, - readable=True, - autocompletion=CONFIG_AUTOCOMPLETE, - ), verbose: bool = typer.Option( False, "--verbose", @@ -451,17 +401,6 @@ def _query_cli( resolve_path=True, autocompletion=ROOT_AUTOCOMPLETE, ), - root: Path = typer.Option( - Path(), - "--root", - "-r", - help="The project root directory.", - exists=True, - dir_okay=True, - writable=True, - resolve_path=True, - autocompletion=ROOT_AUTOCOMPLETE, - ), community_level: int = typer.Option( 2, "--community-level", @@ -500,7 +439,6 @@ def _query_cli( match method: case SearchMethod.LOCAL: run_local_search( - config_filepath=config, data_dir=data, root_dir=root, community_level=community_level, @@ -511,7 +449,6 @@ def _query_cli( ) case SearchMethod.GLOBAL: run_global_search( - config_filepath=config, data_dir=data, root_dir=root, community_level=community_level, @@ -523,7 +460,6 @@ def _query_cli( ) case SearchMethod.DRIFT: run_drift_search( - config_filepath=config, data_dir=data, root_dir=root, community_level=community_level, @@ -534,9 +470,9 @@ def _query_cli( ) case SearchMethod.BASIC: run_basic_search( - config_filepath=config, data_dir=data, root_dir=root, + response_type=response_type, streaming=streaming, query=query, verbose=verbose, diff --git a/graphrag/cli/prompt_tune.py b/packages/graphrag/graphrag/cli/prompt_tune.py similarity index 90% rename from graphrag/cli/prompt_tune.py rename to packages/graphrag/graphrag/cli/prompt_tune.py index 2646776f6f..249dd7cd07 100644 --- a/graphrag/cli/prompt_tune.py +++ b/packages/graphrag/graphrag/cli/prompt_tune.py @@ -24,7 +24,6 @@ async def prompt_tune( root: Path, - config: Path | None, domain: str | None, verbose: bool, selection_method: api.DocSelectionType, @@ -43,7 +42,6 @@ async def prompt_tune( Parameters ---------- - - config: The configuration file. - root: The root directory. - domain: The domain to map the input documents to. - verbose: Enable verbose logging. @@ -58,15 +56,16 @@ async def prompt_tune( - k: The number of documents to select when using auto selection method. - min_examples_required: The minimum number of examples required for entity extraction prompts. """ - root_path = Path(root).resolve() - graph_config = load_config(root_path, config) + graph_config = load_config( + root_dir=root, + ) # override chunking config in the configuration - if chunk_size != graph_config.chunks.size: - graph_config.chunks.size = chunk_size + if chunk_size != graph_config.chunking.size: + graph_config.chunking.size = chunk_size - if overlap != graph_config.chunks.overlap: - graph_config.chunks.overlap = overlap + if overlap != graph_config.chunking.overlap: + graph_config.chunking.overlap = overlap # configure the root logger with the specified log level from graphrag.logger.standard_logging import init_loggers @@ -82,8 +81,6 @@ async def prompt_tune( prompts = await api.generate_indexing_prompts( config=graph_config, - chunk_size=chunk_size, - overlap=overlap, limit=limit, selection_method=selection_method, domain=domain, diff --git a/packages/graphrag/graphrag/cli/query.py b/packages/graphrag/graphrag/cli/query.py new file mode 100644 index 0000000000..ae06a88c95 --- /dev/null +++ b/packages/graphrag/graphrag/cli/query.py @@ -0,0 +1,396 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""CLI implementation of the query subcommand.""" + +import asyncio +import sys +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from graphrag_storage import create_storage + +import graphrag.api as api +from graphrag.callbacks.noop_query_callbacks import NoopQueryCallbacks +from graphrag.config.load_config import load_config +from graphrag.config.models.graph_rag_config import GraphRagConfig +from graphrag.utils.storage import load_table_from_storage, storage_has_table + +if TYPE_CHECKING: + import pandas as pd + +# ruff: noqa: T201 + + +def run_global_search( + data_dir: Path | None, + root_dir: Path, + community_level: int | None, + dynamic_community_selection: bool, + response_type: str, + streaming: bool, + query: str, + verbose: bool, +): + """Perform a global search with a given query. + + Loads index files required for global search and calls the Query API. + """ + cli_overrides: dict[str, Any] = {} + if data_dir: + cli_overrides["output_storage"] = {"base_dir": str(data_dir)} + config = load_config( + root_dir=root_dir, + cli_overrides=cli_overrides, + ) + + dataframe_dict = _resolve_output_files( + config=config, + output_list=[ + "entities", + "communities", + "community_reports", + ], + optional_list=[], + ) + + entities: pd.DataFrame = dataframe_dict["entities"] + communities: pd.DataFrame = dataframe_dict["communities"] + community_reports: pd.DataFrame = dataframe_dict["community_reports"] + + if streaming: + + async def run_streaming_search(): + full_response = "" + context_data = {} + + def on_context(context: Any) -> None: + nonlocal context_data + context_data = context + + callbacks = NoopQueryCallbacks() + callbacks.on_context = on_context + + async for stream_chunk in api.global_search_streaming( + config=config, + entities=entities, + communities=communities, + community_reports=community_reports, + community_level=community_level, + dynamic_community_selection=dynamic_community_selection, + response_type=response_type, + query=query, + callbacks=[callbacks], + verbose=verbose, + ): + full_response += stream_chunk + print(stream_chunk, end="") + sys.stdout.flush() + print() + return full_response, context_data + + return asyncio.run(run_streaming_search()) + # not streaming + response, context_data = asyncio.run( + api.global_search( + config=config, + entities=entities, + communities=communities, + community_reports=community_reports, + community_level=community_level, + dynamic_community_selection=dynamic_community_selection, + response_type=response_type, + query=query, + verbose=verbose, + ) + ) + print(response) + + return response, context_data + + +def run_local_search( + data_dir: Path | None, + root_dir: Path, + community_level: int, + response_type: str, + streaming: bool, + query: str, + verbose: bool, +): + """Perform a local search with a given query. + + Loads index files required for local search and calls the Query API. + """ + cli_overrides: dict[str, Any] = {} + if data_dir: + cli_overrides["output_storage"] = {"base_dir": str(data_dir)} + config = load_config( + root_dir=root_dir, + cli_overrides=cli_overrides, + ) + + dataframe_dict = _resolve_output_files( + config=config, + output_list=[ + "communities", + "community_reports", + "text_units", + "relationships", + "entities", + ], + optional_list=[ + "covariates", + ], + ) + + communities: pd.DataFrame = dataframe_dict["communities"] + community_reports: pd.DataFrame = dataframe_dict["community_reports"] + text_units: pd.DataFrame = dataframe_dict["text_units"] + relationships: pd.DataFrame = dataframe_dict["relationships"] + entities: pd.DataFrame = dataframe_dict["entities"] + covariates: pd.DataFrame | None = dataframe_dict["covariates"] + + if streaming: + + async def run_streaming_search(): + full_response = "" + context_data = {} + + def on_context(context: Any) -> None: + nonlocal context_data + context_data = context + + callbacks = NoopQueryCallbacks() + callbacks.on_context = on_context + + async for stream_chunk in api.local_search_streaming( + config=config, + entities=entities, + communities=communities, + community_reports=community_reports, + text_units=text_units, + relationships=relationships, + covariates=covariates, + community_level=community_level, + response_type=response_type, + query=query, + callbacks=[callbacks], + verbose=verbose, + ): + full_response += stream_chunk + print(stream_chunk, end="") + sys.stdout.flush() + print() + return full_response, context_data + + return asyncio.run(run_streaming_search()) + # not streaming + response, context_data = asyncio.run( + api.local_search( + config=config, + entities=entities, + communities=communities, + community_reports=community_reports, + text_units=text_units, + relationships=relationships, + covariates=covariates, + community_level=community_level, + response_type=response_type, + query=query, + verbose=verbose, + ) + ) + print(response) + + return response, context_data + + +def run_drift_search( + data_dir: Path | None, + root_dir: Path, + community_level: int, + response_type: str, + streaming: bool, + query: str, + verbose: bool, +): + """Perform a local search with a given query. + + Loads index files required for local search and calls the Query API. + """ + cli_overrides: dict[str, Any] = {} + if data_dir: + cli_overrides["output_storage"] = {"base_dir": str(data_dir)} + config = load_config( + root_dir=root_dir, + cli_overrides=cli_overrides, + ) + + dataframe_dict = _resolve_output_files( + config=config, + output_list=[ + "communities", + "community_reports", + "text_units", + "relationships", + "entities", + ], + ) + + communities: pd.DataFrame = dataframe_dict["communities"] + community_reports: pd.DataFrame = dataframe_dict["community_reports"] + text_units: pd.DataFrame = dataframe_dict["text_units"] + relationships: pd.DataFrame = dataframe_dict["relationships"] + entities: pd.DataFrame = dataframe_dict["entities"] + + if streaming: + + async def run_streaming_search(): + full_response = "" + context_data = {} + + def on_context(context: Any) -> None: + nonlocal context_data + context_data = context + + callbacks = NoopQueryCallbacks() + callbacks.on_context = on_context + + async for stream_chunk in api.drift_search_streaming( + config=config, + entities=entities, + communities=communities, + community_reports=community_reports, + text_units=text_units, + relationships=relationships, + community_level=community_level, + response_type=response_type, + query=query, + callbacks=[callbacks], + verbose=verbose, + ): + full_response += stream_chunk + print(stream_chunk, end="") + sys.stdout.flush() + print() + return full_response, context_data + + return asyncio.run(run_streaming_search()) + + # not streaming + response, context_data = asyncio.run( + api.drift_search( + config=config, + entities=entities, + communities=communities, + community_reports=community_reports, + text_units=text_units, + relationships=relationships, + community_level=community_level, + response_type=response_type, + query=query, + verbose=verbose, + ) + ) + print(response) + + return response, context_data + + +def run_basic_search( + data_dir: Path | None, + root_dir: Path, + response_type: str, + streaming: bool, + query: str, + verbose: bool, +): + """Perform a basics search with a given query. + + Loads index files required for basic search and calls the Query API. + """ + cli_overrides: dict[str, Any] = {} + if data_dir: + cli_overrides["output_storage"] = {"base_dir": str(data_dir)} + config = load_config( + root_dir=root_dir, + cli_overrides=cli_overrides, + ) + + dataframe_dict = _resolve_output_files( + config=config, + output_list=[ + "text_units", + ], + ) + + text_units: pd.DataFrame = dataframe_dict["text_units"] + + if streaming: + + async def run_streaming_search(): + full_response = "" + context_data = {} + + def on_context(context: Any) -> None: + nonlocal context_data + context_data = context + + callbacks = NoopQueryCallbacks() + callbacks.on_context = on_context + + async for stream_chunk in api.basic_search_streaming( + config=config, + text_units=text_units, + response_type=response_type, + query=query, + callbacks=[callbacks], + verbose=verbose, + ): + full_response += stream_chunk + print(stream_chunk, end="") + sys.stdout.flush() + print() + return full_response, context_data + + return asyncio.run(run_streaming_search()) + # not streaming + response, context_data = asyncio.run( + api.basic_search( + config=config, + text_units=text_units, + response_type=response_type, + query=query, + verbose=verbose, + ) + ) + print(response) + + return response, context_data + + +def _resolve_output_files( + config: GraphRagConfig, + output_list: list[str], + optional_list: list[str] | None = None, +) -> dict[str, Any]: + """Read indexing output files to a dataframe dict.""" + dataframe_dict = {} + storage_obj = create_storage(config.output_storage) + for name in output_list: + df_value = asyncio.run(load_table_from_storage(name=name, storage=storage_obj)) + dataframe_dict[name] = df_value + + # for optional output files, set the dict entry to None instead of erroring out if it does not exist + if optional_list: + for optional_file in optional_list: + file_exists = asyncio.run(storage_has_table(optional_file, storage_obj)) + if file_exists: + df_value = asyncio.run( + load_table_from_storage(name=optional_file, storage=storage_obj) + ) + dataframe_dict[optional_file] = df_value + else: + dataframe_dict[optional_file] = None + return dataframe_dict diff --git a/graphrag/config/__init__.py b/packages/graphrag/graphrag/config/__init__.py similarity index 100% rename from graphrag/config/__init__.py rename to packages/graphrag/graphrag/config/__init__.py diff --git a/graphrag/config/defaults.py b/packages/graphrag/graphrag/config/defaults.py similarity index 59% rename from graphrag/config/defaults.py rename to packages/graphrag/graphrag/config/defaults.py index 6ec2e2d2bb..640933581a 100644 --- a/graphrag/config/defaults.py +++ b/packages/graphrag/graphrag/config/defaults.py @@ -3,73 +3,43 @@ """Common default configuration values.""" -from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path from typing import ClassVar +from graphrag_cache import CacheType +from graphrag_chunking.chunk_strategy_type import ChunkerType +from graphrag_input import InputType +from graphrag_llm.config import AuthMethod +from graphrag_storage import StorageType +from graphrag_vectors import VectorStoreType + from graphrag.config.embeddings import default_embeddings from graphrag.config.enums import ( AsyncType, - AuthType, - CacheType, - ChunkStrategyType, - InputFileType, - ModelType, NounPhraseExtractorType, ReportingType, - StorageType, - VectorStoreType, ) from graphrag.index.operations.build_noun_graph.np_extractors.stop_words import ( EN_STOP_WORDS, ) -from graphrag.language_model.providers.litellm.services.rate_limiter.rate_limiter import ( - RateLimiter, -) -from graphrag.language_model.providers.litellm.services.rate_limiter.static_rate_limiter import ( - StaticRateLimiter, -) -from graphrag.language_model.providers.litellm.services.retry.exponential_retry import ( - ExponentialRetry, -) -from graphrag.language_model.providers.litellm.services.retry.incremental_wait_retry import ( - IncrementalWaitRetry, -) -from graphrag.language_model.providers.litellm.services.retry.native_wait_retry import ( - NativeRetry, -) -from graphrag.language_model.providers.litellm.services.retry.random_wait_retry import ( - RandomWaitRetry, -) -from graphrag.language_model.providers.litellm.services.retry.retry import Retry +DEFAULT_INPUT_BASE_DIR = "input" DEFAULT_OUTPUT_BASE_DIR = "output" -DEFAULT_CHAT_MODEL_ID = "default_chat_model" -DEFAULT_CHAT_MODEL_TYPE = ModelType.Chat -DEFAULT_CHAT_MODEL = "gpt-4-turbo-preview" -DEFAULT_CHAT_MODEL_AUTH_TYPE = AuthType.APIKey +DEFAULT_CACHE_BASE_DIR = "cache" +DEFAULT_UPDATE_OUTPUT_BASE_DIR = "update_output" +DEFAULT_COMPLETION_MODEL_ID = "default_completion_model" +DEFAULT_COMPLETION_MODEL_AUTH_TYPE = AuthMethod.ApiKey +DEFAULT_COMPLETION_MODEL = "gpt-4.1" DEFAULT_EMBEDDING_MODEL_ID = "default_embedding_model" -DEFAULT_EMBEDDING_MODEL_TYPE = ModelType.Embedding -DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small" -DEFAULT_EMBEDDING_MODEL_AUTH_TYPE = AuthType.APIKey +DEFAULT_EMBEDDING_MODEL_AUTH_TYPE = AuthMethod.ApiKey +DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large" DEFAULT_MODEL_PROVIDER = "openai" -DEFAULT_VECTOR_STORE_ID = "default_vector_store" -ENCODING_MODEL = "cl100k_base" +ENCODING_MODEL = "o200k_base" COGNITIVE_SERVICES_AUDIENCE = "https://cognitiveservices.azure.com/.default" - -DEFAULT_RETRY_SERVICES: dict[str, Callable[..., Retry]] = { - "native": NativeRetry, - "exponential_backoff": ExponentialRetry, - "random_wait": RandomWaitRetry, - "incremental_wait": IncrementalWaitRetry, -} - -DEFAULT_RATE_LIMITER_SERVICES: dict[str, Callable[..., RateLimiter]] = { - "static": StaticRateLimiter, -} +DEFAULT_ENTITY_TYPES = ["organization", "person", "geo", "event"] @dataclass @@ -79,33 +49,19 @@ class BasicSearchDefaults: prompt: None = None k: int = 10 max_context_tokens: int = 12_000 - chat_model_id: str = DEFAULT_CHAT_MODEL_ID + completion_model_id: str = DEFAULT_COMPLETION_MODEL_ID embedding_model_id: str = DEFAULT_EMBEDDING_MODEL_ID @dataclass -class CacheDefaults: - """Default values for cache.""" - - type: ClassVar[CacheType] = CacheType.file - base_dir: str = "cache" - connection_string: None = None - container_name: None = None - storage_account_blob_url: None = None - cosmosdb_account_url: None = None - - -@dataclass -class ChunksDefaults: - """Default values for chunks.""" +class ChunkingDefaults: + """Default values for chunking.""" + type: str = ChunkerType.Tokens size: int = 1200 overlap: int = 100 - group_by_columns: list[str] = field(default_factory=lambda: ["id"]) - strategy: ClassVar[ChunkStrategyType] = ChunkStrategyType.tokens - encoding_model: str = "cl100k_base" - prepend_metadata: bool = False - chunk_size_includes_metadata: bool = False + encoding_model: str = ENCODING_MODEL + prepend_metadata: None = None @dataclass @@ -125,8 +81,8 @@ class CommunityReportDefaults: text_prompt: None = None max_length: int = 2000 max_input_length: int = 8000 - strategy: None = None - model_id: str = DEFAULT_CHAT_MODEL_ID + completion_model_id: str = DEFAULT_COMPLETION_MODEL_ID + model_instance_name: str = "community_reporting" @dataclass @@ -154,35 +110,19 @@ class DriftSearchDefaults: local_search_n: int = 1 local_search_llm_max_gen_tokens: int | None = None local_search_llm_max_gen_completion_tokens: int | None = None - chat_model_id: str = DEFAULT_CHAT_MODEL_ID + completion_model_id: str = DEFAULT_COMPLETION_MODEL_ID embedding_model_id: str = DEFAULT_EMBEDDING_MODEL_ID -@dataclass -class EmbedGraphDefaults: - """Default values for embedding graph.""" - - enabled: bool = False - dimensions: int = 1536 - num_walks: int = 10 - walk_length: int = 40 - window_size: int = 2 - iterations: int = 3 - random_seed: int = 597832 - use_lcc: bool = True - - @dataclass class EmbedTextDefaults: """Default values for embedding text.""" - model: str = "text-embedding-3-small" + embedding_model_id: str = DEFAULT_EMBEDDING_MODEL_ID + model_instance_name: str = "text_embedding" batch_size: int = 16 batch_max_tokens: int = 8191 - model_id: str = DEFAULT_EMBEDDING_MODEL_ID names: list[str] = field(default_factory=lambda: default_embeddings) - strategy: None = None - vector_store_id: str = DEFAULT_VECTOR_STORE_ID @dataclass @@ -195,8 +135,8 @@ class ExtractClaimsDefaults: "Any claims or facts that could be relevant to information discovery." ) max_gleanings: int = 1 - strategy: None = None - model_id: str = DEFAULT_CHAT_MODEL_ID + completion_model_id: str = DEFAULT_COMPLETION_MODEL_ID + model_instance_name: str = "extract_claims" @dataclass @@ -208,8 +148,8 @@ class ExtractGraphDefaults: default_factory=lambda: ["organization", "person", "geo", "event"] ) max_gleanings: int = 1 - strategy: None = None - model_id: str = DEFAULT_CHAT_MODEL_ID + completion_model_id: str = DEFAULT_COMPLETION_MODEL_ID + model_instance_name: str = "extract_graph" @dataclass @@ -266,75 +206,54 @@ class GlobalSearchDefaults: dynamic_search_num_repeats: int = 1 dynamic_search_use_summary: bool = False dynamic_search_max_level: int = 2 - chat_model_id: str = DEFAULT_CHAT_MODEL_ID + completion_model_id: str = DEFAULT_COMPLETION_MODEL_ID @dataclass class StorageDefaults: """Default values for storage.""" - type: ClassVar[StorageType] = StorageType.file - base_dir: str = DEFAULT_OUTPUT_BASE_DIR - connection_string: None = None - container_name: None = None - storage_account_blob_url: None = None - cosmosdb_account_url: None = None + type: str = StorageType.File + encoding: str | None = None + base_dir: str | None = None + azure_connection_string: None = None + azure_container_name: None = None + azure_account_url: None = None + azure_cosmosdb_account_url: None = None + + +@dataclass +class InputDefaults: + """Default values for input.""" + + type: ClassVar[InputType] = InputType.Text + encoding: str | None = None + file_pattern: None = None + id_column: None = None + title_column: None = None + text_column: None = None @dataclass class InputStorageDefaults(StorageDefaults): """Default values for input storage.""" - base_dir: str = "input" + base_dir: str | None = DEFAULT_INPUT_BASE_DIR @dataclass -class InputDefaults: - """Default values for input.""" +class CacheStorageDefaults(StorageDefaults): + """Default values for cache storage.""" - storage: InputStorageDefaults = field(default_factory=InputStorageDefaults) - file_type: ClassVar[InputFileType] = InputFileType.text - encoding: str = "utf-8" - file_pattern: str = "" - file_filter: None = None - text_column: str = "text" - title_column: None = None - metadata: None = None + base_dir: str | None = DEFAULT_CACHE_BASE_DIR @dataclass -class LanguageModelDefaults: - """Default values for language model.""" - - api_key: None = None - auth_type: ClassVar[AuthType] = AuthType.APIKey - model_provider: str | None = None - encoding_model: str = "" - max_tokens: int | None = None - temperature: float = 0 - max_completion_tokens: int | None = None - reasoning_effort: str | None = None - top_p: float = 1 - n: int = 1 - frequency_penalty: float = 0.0 - presence_penalty: float = 0.0 - request_timeout: float = 180.0 - api_base: None = None - api_version: None = None - deployment_name: None = None - organization: None = None - proxy: None = None - audience: None = None - model_supports_json: None = None - tokens_per_minute: None = None - requests_per_minute: None = None - rate_limit_strategy: str | None = "static" - retry_strategy: str = "exponential_backoff" - max_retries: int = 10 - max_retry_wait: float = 10.0 - concurrent_requests: int = 25 - responses: None = None - async_mode: AsyncType = AsyncType.Threaded +class CacheDefaults: + """Default values for cache.""" + + type: CacheType = CacheType.Json + storage: CacheStorageDefaults = field(default_factory=CacheStorageDefaults) @dataclass @@ -348,15 +267,15 @@ class LocalSearchDefaults: top_k_entities: int = 10 top_k_relationships: int = 10 max_context_tokens: int = 12_000 - chat_model_id: str = DEFAULT_CHAT_MODEL_ID + completion_model_id: str = DEFAULT_COMPLETION_MODEL_ID embedding_model_id: str = DEFAULT_EMBEDDING_MODEL_ID @dataclass -class OutputDefaults(StorageDefaults): +class OutputStorageDefaults(StorageDefaults): """Default values for output.""" - base_dir: str = DEFAULT_OUTPUT_BASE_DIR + base_dir: str | None = DEFAULT_OUTPUT_BASE_DIR @dataclass @@ -399,22 +318,15 @@ class SummarizeDescriptionsDefaults: prompt: None = None max_length: int = 500 max_input_tokens: int = 4_000 - strategy: None = None - model_id: str = DEFAULT_CHAT_MODEL_ID - - -@dataclass -class UmapDefaults: - """Default values for UMAP.""" - - enabled: bool = False + completion_model_id: str = DEFAULT_COMPLETION_MODEL_ID + model_instance_name: str = "summarize_descriptions" @dataclass -class UpdateIndexOutputDefaults(StorageDefaults): +class UpdateOutputStorageDefaults(StorageDefaults): """Default values for update index output.""" - base_dir: str = "update_output" + base_dir: str | None = DEFAULT_UPDATE_OUTPUT_BASE_DIR @dataclass @@ -423,33 +335,28 @@ class VectorStoreDefaults: type: ClassVar[str] = VectorStoreType.LanceDB.value db_uri: str = str(Path(DEFAULT_OUTPUT_BASE_DIR) / "lancedb") - container_name: str = "default" - overwrite: bool = True - url: None = None - api_key: None = None - audience: None = None - database_name: None = None - schema: None = None @dataclass class GraphRagConfigDefaults: """Default values for GraphRAG.""" - root_dir: str = "" models: dict = field(default_factory=dict) + completion_models: dict = field(default_factory=dict) + embedding_models: dict = field(default_factory=dict) + concurrent_requests: int = 25 + async_mode: AsyncType = AsyncType.Threaded reporting: ReportingDefaults = field(default_factory=ReportingDefaults) - storage: StorageDefaults = field(default_factory=StorageDefaults) - output: OutputDefaults = field(default_factory=OutputDefaults) - outputs: None = None - update_index_output: UpdateIndexOutputDefaults = field( - default_factory=UpdateIndexOutputDefaults + input_storage: InputStorageDefaults = field(default_factory=InputStorageDefaults) + output_storage: OutputStorageDefaults = field(default_factory=OutputStorageDefaults) + update_output_storage: UpdateOutputStorageDefaults = field( + default_factory=UpdateOutputStorageDefaults ) cache: CacheDefaults = field(default_factory=CacheDefaults) input: InputDefaults = field(default_factory=InputDefaults) - embed_graph: EmbedGraphDefaults = field(default_factory=EmbedGraphDefaults) + embed_text: EmbedTextDefaults = field(default_factory=EmbedTextDefaults) - chunks: ChunksDefaults = field(default_factory=ChunksDefaults) + chunking: ChunkingDefaults = field(default_factory=ChunkingDefaults) snapshots: SnapshotsDefaults = field(default_factory=SnapshotsDefaults) extract_graph: ExtractGraphDefaults = field(default_factory=ExtractGraphDefaults) extract_graph_nlp: ExtractGraphNLPDefaults = field( @@ -464,17 +371,15 @@ class GraphRagConfigDefaults: extract_claims: ExtractClaimsDefaults = field(default_factory=ExtractClaimsDefaults) prune_graph: PruneGraphDefaults = field(default_factory=PruneGraphDefaults) cluster_graph: ClusterGraphDefaults = field(default_factory=ClusterGraphDefaults) - umap: UmapDefaults = field(default_factory=UmapDefaults) local_search: LocalSearchDefaults = field(default_factory=LocalSearchDefaults) global_search: GlobalSearchDefaults = field(default_factory=GlobalSearchDefaults) drift_search: DriftSearchDefaults = field(default_factory=DriftSearchDefaults) basic_search: BasicSearchDefaults = field(default_factory=BasicSearchDefaults) - vector_store: dict[str, VectorStoreDefaults] = field( - default_factory=lambda: {DEFAULT_VECTOR_STORE_ID: VectorStoreDefaults()} + vector_store: VectorStoreDefaults = field( + default_factory=lambda: VectorStoreDefaults() ) workflows: None = None -language_model_defaults = LanguageModelDefaults() vector_store_defaults = VectorStoreDefaults() graphrag_config_defaults = GraphRagConfigDefaults() diff --git a/packages/graphrag/graphrag/config/embeddings.py b/packages/graphrag/graphrag/config/embeddings.py new file mode 100644 index 0000000000..7456ffca5a --- /dev/null +++ b/packages/graphrag/graphrag/config/embeddings.py @@ -0,0 +1,19 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing embeddings values.""" + +entity_description_embedding = "entity_description" +community_full_content_embedding = "community_full_content" +text_unit_text_embedding = "text_unit_text" + +all_embeddings: set[str] = { + entity_description_embedding, + community_full_content_embedding, + text_unit_text_embedding, +} +default_embeddings: list[str] = [ + entity_description_embedding, + community_full_content_embedding, + text_unit_text_embedding, +] diff --git a/packages/graphrag/graphrag/config/enums.py b/packages/graphrag/graphrag/config/enums.py new file mode 100644 index 0000000000..5084f2154b --- /dev/null +++ b/packages/graphrag/graphrag/config/enums.py @@ -0,0 +1,77 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing config enums.""" + +from __future__ import annotations + +from enum import Enum + + +class ReportingType(str, Enum): + """The reporting configuration type for the pipeline.""" + + file = "file" + """The file reporting configuration type.""" + blob = "blob" + """The blob reporting configuration type.""" + + def __repr__(self): + """Get a string representation.""" + return f'"{self.value}"' + + +class AsyncType(str, Enum): + """Enum for the type of async to use.""" + + AsyncIO = "asyncio" + Threaded = "threaded" + + +class SearchMethod(Enum): + """The type of search to run.""" + + LOCAL = "local" + GLOBAL = "global" + DRIFT = "drift" + BASIC = "basic" + + def __str__(self): + """Return the string representation of the enum value.""" + return self.value + + +class IndexingMethod(str, Enum): + """Enum for the type of indexing to perform.""" + + Standard = "standard" + """Traditional GraphRAG indexing, with all graph construction and summarization performed by a language model.""" + Fast = "fast" + """Fast indexing, using NLP for graph construction and language model for summarization.""" + StandardUpdate = "standard-update" + """Incremental update with standard indexing.""" + FastUpdate = "fast-update" + """Incremental update with fast indexing.""" + + +class NounPhraseExtractorType(str, Enum): + """Enum for the noun phrase extractor options.""" + + RegexEnglish = "regex_english" + """Standard extractor using regex. Fastest, but limited to English.""" + Syntactic = "syntactic_parser" + """Noun phrase extractor based on dependency parsing and NER using SpaCy.""" + CFG = "cfg" + """Noun phrase extractor combining CFG-based noun-chunk extraction and NER.""" + + +class ModularityMetric(str, Enum): + """Enum for the modularity metric to use.""" + + Graph = "graph" + """Graph modularity metric.""" + + LCC = "lcc" + + WeightedComponents = "weighted_components" + """Weighted components modularity metric.""" diff --git a/graphrag/config/errors.py b/packages/graphrag/graphrag/config/errors.py similarity index 80% rename from graphrag/config/errors.py rename to packages/graphrag/graphrag/config/errors.py index 32a20b838c..6dbe25ad40 100644 --- a/graphrag/config/errors.py +++ b/packages/graphrag/graphrag/config/errors.py @@ -33,15 +33,6 @@ def __init__(self, llm_type: str) -> None: super().__init__(msg) -class LanguageModelConfigMissingError(ValueError): - """Missing model configuration error.""" - - def __init__(self, key: str = "") -> None: - """Init method definition.""" - msg = f'A {key} model configuration is required. Please rerun `graphrag init` and set models["{key}"] in settings.yaml.' - super().__init__(msg) - - class ConflictingSettingsError(ValueError): """Missing model configuration error.""" diff --git a/graphrag/config/init_content.py b/packages/graphrag/graphrag/config/init_content.py similarity index 52% rename from graphrag/config/init_content.py rename to packages/graphrag/graphrag/config/init_content.py index fd16f20d35..9973d1920f 100644 --- a/graphrag/config/init_content.py +++ b/packages/graphrag/graphrag/config/init_content.py @@ -6,7 +6,6 @@ import graphrag.config.defaults as defs from graphrag.config.defaults import ( graphrag_config_defaults, - language_model_defaults, vector_store_defaults, ) @@ -17,117 +16,98 @@ ### LLM settings ### ## There are a number of settings to tune the threading and token limits for LLM calls - check the docs. -models: - {defs.DEFAULT_CHAT_MODEL_ID}: - type: {defs.DEFAULT_CHAT_MODEL_TYPE.value} +completion_models: + {defs.DEFAULT_COMPLETION_MODEL_ID}: model_provider: {defs.DEFAULT_MODEL_PROVIDER} - auth_type: {defs.DEFAULT_CHAT_MODEL_AUTH_TYPE.value} # or azure_managed_identity + model: + auth_method: {defs.DEFAULT_COMPLETION_MODEL_AUTH_TYPE} # or azure_managed_identity api_key: ${{GRAPHRAG_API_KEY}} # set this in the generated .env file, or remove if managed identity - model: {defs.DEFAULT_CHAT_MODEL} - # api_base: https://.openai.azure.com - # api_version: 2024-05-01-preview - model_supports_json: true # recommended if this is available for your model. - concurrent_requests: {language_model_defaults.concurrent_requests} - async_mode: {language_model_defaults.async_mode.value} # or asyncio - retry_strategy: {language_model_defaults.retry_strategy} - max_retries: {language_model_defaults.max_retries} - tokens_per_minute: null - requests_per_minute: null + retry: + type: exponential_backoff + +embedding_models: {defs.DEFAULT_EMBEDDING_MODEL_ID}: - type: {defs.DEFAULT_EMBEDDING_MODEL_TYPE.value} model_provider: {defs.DEFAULT_MODEL_PROVIDER} - auth_type: {defs.DEFAULT_EMBEDDING_MODEL_AUTH_TYPE.value} + model: + auth_method: {defs.DEFAULT_EMBEDDING_MODEL_AUTH_TYPE} api_key: ${{GRAPHRAG_API_KEY}} - model: {defs.DEFAULT_EMBEDDING_MODEL} - # api_base: https://.openai.azure.com - # api_version: 2024-05-01-preview - concurrent_requests: {language_model_defaults.concurrent_requests} - async_mode: {language_model_defaults.async_mode.value} # or asyncio - retry_strategy: {language_model_defaults.retry_strategy} - max_retries: {language_model_defaults.max_retries} - tokens_per_minute: null - requests_per_minute: null - -### Input settings ### + retry: + type: exponential_backoff + +### Document processing settings ### input: - storage: - type: {graphrag_config_defaults.input.storage.type.value} # or blob - base_dir: "{graphrag_config_defaults.input.storage.base_dir}" - file_type: {graphrag_config_defaults.input.file_type.value} # [csv, text, json] + type: {graphrag_config_defaults.input.type.value} # [csv, text, json, jsonl] -chunks: - size: {graphrag_config_defaults.chunks.size} - overlap: {graphrag_config_defaults.chunks.overlap} - group_by_columns: [{",".join(graphrag_config_defaults.chunks.group_by_columns)}] +chunking: + type: {graphrag_config_defaults.chunking.type} + size: {graphrag_config_defaults.chunking.size} + overlap: {graphrag_config_defaults.chunking.overlap} + encoding_model: {graphrag_config_defaults.chunking.encoding_model} -### Output/storage settings ### +### Storage settings ### ## If blob storage is specified in the following four sections, ## connection_string and container_name must be provided -output: - type: {graphrag_config_defaults.output.type.value} # [file, blob, cosmosdb] - base_dir: "{graphrag_config_defaults.output.base_dir}" - -cache: - type: {graphrag_config_defaults.cache.type.value} # [file, blob, cosmosdb] - base_dir: "{graphrag_config_defaults.cache.base_dir}" +input_storage: + type: {graphrag_config_defaults.input_storage.type} # [file, blob, cosmosdb] + base_dir: "{graphrag_config_defaults.input_storage.base_dir}" + +output_storage: + type: {graphrag_config_defaults.output_storage.type} # [file, blob, cosmosdb] + base_dir: "{graphrag_config_defaults.output_storage.base_dir}" reporting: type: {graphrag_config_defaults.reporting.type.value} # [file, blob] base_dir: "{graphrag_config_defaults.reporting.base_dir}" +cache: + type: {graphrag_config_defaults.cache.type} # [json, memory, none] + storage: + type: {graphrag_config_defaults.cache.storage.type} # [file, blob, cosmosdb] + base_dir: "{graphrag_config_defaults.cache.storage.base_dir}" + vector_store: - {defs.DEFAULT_VECTOR_STORE_ID}: - type: {vector_store_defaults.type} - db_uri: {vector_store_defaults.db_uri} - container_name: {vector_store_defaults.container_name} + type: {vector_store_defaults.type} + db_uri: {vector_store_defaults.db_uri} ### Workflow settings ### embed_text: - model_id: {graphrag_config_defaults.embed_text.model_id} - vector_store_id: {graphrag_config_defaults.embed_text.vector_store_id} + embedding_model_id: {graphrag_config_defaults.embed_text.embedding_model_id} extract_graph: - model_id: {graphrag_config_defaults.extract_graph.model_id} + completion_model_id: {graphrag_config_defaults.extract_graph.completion_model_id} prompt: "prompts/extract_graph.txt" entity_types: [{",".join(graphrag_config_defaults.extract_graph.entity_types)}] max_gleanings: {graphrag_config_defaults.extract_graph.max_gleanings} summarize_descriptions: - model_id: {graphrag_config_defaults.summarize_descriptions.model_id} + completion_model_id: {graphrag_config_defaults.summarize_descriptions.completion_model_id} prompt: "prompts/summarize_descriptions.txt" max_length: {graphrag_config_defaults.summarize_descriptions.max_length} extract_graph_nlp: text_analyzer: extractor_type: {graphrag_config_defaults.extract_graph_nlp.text_analyzer.extractor_type.value} # [regex_english, syntactic_parser, cfg] - async_mode: {graphrag_config_defaults.extract_graph_nlp.async_mode.value} # or asyncio cluster_graph: max_cluster_size: {graphrag_config_defaults.cluster_graph.max_cluster_size} extract_claims: enabled: false - model_id: {graphrag_config_defaults.extract_claims.model_id} + completion_model_id: {graphrag_config_defaults.extract_claims.completion_model_id} prompt: "prompts/extract_claims.txt" description: "{graphrag_config_defaults.extract_claims.description}" max_gleanings: {graphrag_config_defaults.extract_claims.max_gleanings} community_reports: - model_id: {graphrag_config_defaults.community_reports.model_id} + completion_model_id: {graphrag_config_defaults.community_reports.completion_model_id} graph_prompt: "prompts/community_report_graph.txt" text_prompt: "prompts/community_report_text.txt" max_length: {graphrag_config_defaults.community_reports.max_length} max_input_length: {graphrag_config_defaults.community_reports.max_input_length} -embed_graph: - enabled: false # if true, will generate node2vec embeddings for nodes - -umap: - enabled: false # if true, will generate UMAP embeddings for nodes (embed_graph must also be enabled) - snapshots: graphml: false embeddings: false @@ -137,24 +117,24 @@ ## See the config docs: https://microsoft.github.io/graphrag/config/yaml/#query local_search: - chat_model_id: {graphrag_config_defaults.local_search.chat_model_id} + completion_model_id: {graphrag_config_defaults.local_search.completion_model_id} embedding_model_id: {graphrag_config_defaults.local_search.embedding_model_id} prompt: "prompts/local_search_system_prompt.txt" global_search: - chat_model_id: {graphrag_config_defaults.global_search.chat_model_id} + completion_model_id: {graphrag_config_defaults.global_search.completion_model_id} map_prompt: "prompts/global_search_map_system_prompt.txt" reduce_prompt: "prompts/global_search_reduce_system_prompt.txt" knowledge_prompt: "prompts/global_search_knowledge_system_prompt.txt" drift_search: - chat_model_id: {graphrag_config_defaults.drift_search.chat_model_id} + completion_model_id: {graphrag_config_defaults.drift_search.completion_model_id} embedding_model_id: {graphrag_config_defaults.drift_search.embedding_model_id} prompt: "prompts/drift_search_system_prompt.txt" reduce_prompt: "prompts/drift_search_reduce_prompt.txt" basic_search: - chat_model_id: {graphrag_config_defaults.basic_search.chat_model_id} + completion_model_id: {graphrag_config_defaults.basic_search.completion_model_id} embedding_model_id: {graphrag_config_defaults.basic_search.embedding_model_id} prompt: "prompts/basic_search_system_prompt.txt" """ diff --git a/packages/graphrag/graphrag/config/load_config.py b/packages/graphrag/graphrag/config/load_config.py new file mode 100644 index 0000000000..104bd59d43 --- /dev/null +++ b/packages/graphrag/graphrag/config/load_config.py @@ -0,0 +1,47 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Default method for loading config.""" + +from pathlib import Path +from typing import Any + +from graphrag_common.config import load_config as lc + +from graphrag.config.models.graph_rag_config import GraphRagConfig + + +def load_config( + root_dir: str | Path, + cli_overrides: dict[str, Any] | None = None, +) -> GraphRagConfig: + """Load configuration from a file. + + Parameters + ---------- + root_dir : str | Path + The root directory of the project. + Searches for settings.[yaml|yml|json] config files. + cli_overrides : dict[str, Any] | None + A nested dictionary of cli overrides. + Example: {'output': {'base_dir': 'override_value'}} + + Returns + ------- + GraphRagConfig + The loaded configuration. + + Raises + ------ + FileNotFoundError + If the config file is not found. + ConfigParsingError + If there was an error parsing the config file or its environment variables. + ValidationError + If there are pydantic validation errors when instantiating the config. + """ + return lc( + config_initializer=GraphRagConfig, + config_path=root_dir, + overrides=cli_overrides, + ) diff --git a/graphrag/config/models/__init__.py b/packages/graphrag/graphrag/config/models/__init__.py similarity index 100% rename from graphrag/config/models/__init__.py rename to packages/graphrag/graphrag/config/models/__init__.py diff --git a/graphrag/config/models/basic_search_config.py b/packages/graphrag/graphrag/config/models/basic_search_config.py similarity index 90% rename from graphrag/config/models/basic_search_config.py rename to packages/graphrag/graphrag/config/models/basic_search_config.py index 66a1e68577..5b48b74d82 100644 --- a/graphrag/config/models/basic_search_config.py +++ b/packages/graphrag/graphrag/config/models/basic_search_config.py @@ -15,9 +15,9 @@ class BasicSearchConfig(BaseModel): description="The basic search prompt to use.", default=graphrag_config_defaults.basic_search.prompt, ) - chat_model_id: str = Field( + completion_model_id: str = Field( description="The model ID to use for basic search.", - default=graphrag_config_defaults.basic_search.chat_model_id, + default=graphrag_config_defaults.basic_search.completion_model_id, ) embedding_model_id: str = Field( description="The model ID to use for text embeddings.", diff --git a/graphrag/config/models/cluster_graph_config.py b/packages/graphrag/graphrag/config/models/cluster_graph_config.py similarity index 100% rename from graphrag/config/models/cluster_graph_config.py rename to packages/graphrag/graphrag/config/models/cluster_graph_config.py diff --git a/graphrag/config/models/community_reports_config.py b/packages/graphrag/graphrag/config/models/community_reports_config.py similarity index 55% rename from graphrag/config/models/community_reports_config.py rename to packages/graphrag/graphrag/config/models/community_reports_config.py index b4e9259489..604eafe484 100644 --- a/graphrag/config/models/community_reports_config.py +++ b/packages/graphrag/graphrag/config/models/community_reports_config.py @@ -3,20 +3,36 @@ """Parameterization settings for the default configuration.""" +from dataclasses import dataclass from pathlib import Path from pydantic import BaseModel, Field from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.models.language_model_config import LanguageModelConfig +from graphrag.prompts.index.community_report import COMMUNITY_REPORT_PROMPT +from graphrag.prompts.index.community_report_text_units import ( + COMMUNITY_REPORT_TEXT_PROMPT, +) + + +@dataclass +class CommunityReportPrompts: + """Community report prompt templates.""" + + graph_prompt: str + text_prompt: str class CommunityReportsConfig(BaseModel): """Configuration section for community reports.""" - model_id: str = Field( + completion_model_id: str = Field( description="The model ID to use for community reports.", - default=graphrag_config_defaults.community_reports.model_id, + default=graphrag_config_defaults.community_reports.completion_model_id, + ) + model_instance_name: str = Field( + description="The model singleton instance name. This primarily affects the cache storage partitioning.", + default=graphrag_config_defaults.community_reports.model_instance_name, ) graph_prompt: str | None = Field( description="The community report extraction prompt to use for graph-based summarization.", @@ -34,32 +50,14 @@ class CommunityReportsConfig(BaseModel): description="The maximum input length in tokens to use when generating reports.", default=graphrag_config_defaults.community_reports.max_input_length, ) - strategy: dict | None = Field( - description="The override strategy to use.", - default=graphrag_config_defaults.community_reports.strategy, - ) - - def resolved_strategy( - self, root_dir: str, model_config: LanguageModelConfig - ) -> dict: - """Get the resolved community report extraction strategy.""" - from graphrag.index.operations.summarize_communities.typing import ( - CreateCommunityReportsStrategyType, - ) - return self.strategy or { - "type": CreateCommunityReportsStrategyType.graph_intelligence, - "llm": model_config.model_dump(), - "graph_prompt": (Path(root_dir) / self.graph_prompt).read_text( - encoding="utf-8" - ) + def resolved_prompts(self) -> CommunityReportPrompts: + """Get the resolved community report extraction prompts.""" + return CommunityReportPrompts( + graph_prompt=Path(self.graph_prompt).read_text(encoding="utf-8") if self.graph_prompt - else None, - "text_prompt": (Path(root_dir) / self.text_prompt).read_text( - encoding="utf-8" - ) + else COMMUNITY_REPORT_PROMPT, + text_prompt=Path(self.text_prompt).read_text(encoding="utf-8") if self.text_prompt - else None, - "max_report_length": self.max_length, - "max_input_length": self.max_input_length, - } + else COMMUNITY_REPORT_TEXT_PROMPT, + ) diff --git a/graphrag/config/models/drift_search_config.py b/packages/graphrag/graphrag/config/models/drift_search_config.py similarity index 97% rename from graphrag/config/models/drift_search_config.py rename to packages/graphrag/graphrag/config/models/drift_search_config.py index a6edf66474..ce77fc6b1f 100644 --- a/graphrag/config/models/drift_search_config.py +++ b/packages/graphrag/graphrag/config/models/drift_search_config.py @@ -19,9 +19,9 @@ class DRIFTSearchConfig(BaseModel): description="The drift search reduce prompt to use.", default=graphrag_config_defaults.drift_search.reduce_prompt, ) - chat_model_id: str = Field( + completion_model_id: str = Field( description="The model ID to use for drift search.", - default=graphrag_config_defaults.drift_search.chat_model_id, + default=graphrag_config_defaults.drift_search.completion_model_id, ) embedding_model_id: str = Field( description="The model ID to use for drift search.", diff --git a/packages/graphrag/graphrag/config/models/embed_text_config.py b/packages/graphrag/graphrag/config/models/embed_text_config.py new file mode 100644 index 0000000000..9720c5aec4 --- /dev/null +++ b/packages/graphrag/graphrag/config/models/embed_text_config.py @@ -0,0 +1,33 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Parameterization settings for the default configuration.""" + +from pydantic import BaseModel, Field + +from graphrag.config.defaults import graphrag_config_defaults + + +class EmbedTextConfig(BaseModel): + """Configuration section for text embeddings.""" + + embedding_model_id: str = Field( + description="The model ID to use for text embeddings.", + default=graphrag_config_defaults.embed_text.embedding_model_id, + ) + model_instance_name: str = Field( + description="The model singleton instance name. This primarily affects the cache storage partitioning.", + default=graphrag_config_defaults.embed_text.model_instance_name, + ) + batch_size: int = Field( + description="The batch size to use.", + default=graphrag_config_defaults.embed_text.batch_size, + ) + batch_max_tokens: int = Field( + description="The batch max tokens to use.", + default=graphrag_config_defaults.embed_text.batch_max_tokens, + ) + names: list[str] = Field( + description="The specific embeddings to perform.", + default=graphrag_config_defaults.embed_text.names, + ) diff --git a/graphrag/config/models/extract_claims_config.py b/packages/graphrag/graphrag/config/models/extract_claims_config.py similarity index 55% rename from graphrag/config/models/extract_claims_config.py rename to packages/graphrag/graphrag/config/models/extract_claims_config.py index 166cc29d4e..db8e525a70 100644 --- a/graphrag/config/models/extract_claims_config.py +++ b/packages/graphrag/graphrag/config/models/extract_claims_config.py @@ -3,24 +3,36 @@ """Parameterization settings for the default configuration.""" +from dataclasses import dataclass from pathlib import Path from pydantic import BaseModel, Field from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.models.language_model_config import LanguageModelConfig +from graphrag.prompts.index.extract_claims import EXTRACT_CLAIMS_PROMPT -class ClaimExtractionConfig(BaseModel): +@dataclass +class ClaimExtractionPrompts: + """Claim extraction prompt templates.""" + + extraction_prompt: str + + +class ExtractClaimsConfig(BaseModel): """Configuration section for claim extraction.""" enabled: bool = Field( description="Whether claim extraction is enabled.", default=graphrag_config_defaults.extract_claims.enabled, ) - model_id: str = Field( + completion_model_id: str = Field( description="The model ID to use for claim extraction.", - default=graphrag_config_defaults.extract_claims.model_id, + default=graphrag_config_defaults.extract_claims.completion_model_id, + ) + model_instance_name: str = Field( + description="The model singleton instance name. This primarily affects the cache storage partitioning.", + default=graphrag_config_defaults.extract_claims.model_instance_name, ) prompt: str | None = Field( description="The claim extraction prompt to use.", @@ -34,22 +46,11 @@ class ClaimExtractionConfig(BaseModel): description="The maximum number of entity gleanings to use.", default=graphrag_config_defaults.extract_claims.max_gleanings, ) - strategy: dict | None = Field( - description="The override strategy to use.", - default=graphrag_config_defaults.extract_claims.strategy, - ) - def resolved_strategy( - self, root_dir: str, model_config: LanguageModelConfig - ) -> dict: - """Get the resolved claim extraction strategy.""" - return self.strategy or { - "llm": model_config.model_dump(), - "extraction_prompt": (Path(root_dir) / self.prompt).read_text( - encoding="utf-8" - ) + def resolved_prompts(self) -> ClaimExtractionPrompts: + """Get the resolved claim extraction prompts.""" + return ClaimExtractionPrompts( + extraction_prompt=Path(self.prompt).read_text(encoding="utf-8") if self.prompt - else None, - "claim_description": self.description, - "max_gleanings": self.max_gleanings, - } + else EXTRACT_CLAIMS_PROMPT, + ) diff --git a/graphrag/config/models/extract_graph_config.py b/packages/graphrag/graphrag/config/models/extract_graph_config.py similarity index 50% rename from graphrag/config/models/extract_graph_config.py rename to packages/graphrag/graphrag/config/models/extract_graph_config.py index 915ff5d8a5..22323a998b 100644 --- a/graphrag/config/models/extract_graph_config.py +++ b/packages/graphrag/graphrag/config/models/extract_graph_config.py @@ -3,20 +3,32 @@ """Parameterization settings for the default configuration.""" +from dataclasses import dataclass from pathlib import Path from pydantic import BaseModel, Field from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.models.language_model_config import LanguageModelConfig +from graphrag.prompts.index.extract_graph import GRAPH_EXTRACTION_PROMPT + + +@dataclass +class ExtractGraphPrompts: + """Graph extraction prompt templates.""" + + extraction_prompt: str class ExtractGraphConfig(BaseModel): """Configuration section for entity extraction.""" - model_id: str = Field( + completion_model_id: str = Field( description="The model ID to use for text embeddings.", - default=graphrag_config_defaults.extract_graph.model_id, + default=graphrag_config_defaults.extract_graph.completion_model_id, + ) + model_instance_name: str = Field( + description="The model singleton instance name. This primarily affects the cache storage partitioning.", + default=graphrag_config_defaults.extract_graph.model_instance_name, ) prompt: str | None = Field( description="The entity extraction prompt to use.", @@ -30,26 +42,11 @@ class ExtractGraphConfig(BaseModel): description="The maximum number of entity gleanings to use.", default=graphrag_config_defaults.extract_graph.max_gleanings, ) - strategy: dict | None = Field( - description="Override the default entity extraction strategy", - default=graphrag_config_defaults.extract_graph.strategy, - ) - - def resolved_strategy( - self, root_dir: str, model_config: LanguageModelConfig - ) -> dict: - """Get the resolved entity extraction strategy.""" - from graphrag.index.operations.extract_graph.typing import ( - ExtractEntityStrategyType, - ) - return self.strategy or { - "type": ExtractEntityStrategyType.graph_intelligence, - "llm": model_config.model_dump(), - "extraction_prompt": (Path(root_dir) / self.prompt).read_text( - encoding="utf-8" - ) + def resolved_prompts(self) -> ExtractGraphPrompts: + """Get the resolved graph extraction prompts.""" + return ExtractGraphPrompts( + extraction_prompt=Path(self.prompt).read_text(encoding="utf-8") if self.prompt - else None, - "max_gleanings": self.max_gleanings, - } + else GRAPH_EXTRACTION_PROMPT, + ) diff --git a/graphrag/config/models/extract_graph_nlp_config.py b/packages/graphrag/graphrag/config/models/extract_graph_nlp_config.py similarity index 100% rename from graphrag/config/models/extract_graph_nlp_config.py rename to packages/graphrag/graphrag/config/models/extract_graph_nlp_config.py diff --git a/graphrag/config/models/global_search_config.py b/packages/graphrag/graphrag/config/models/global_search_config.py similarity index 96% rename from graphrag/config/models/global_search_config.py rename to packages/graphrag/graphrag/config/models/global_search_config.py index c350efcea6..1b48500ed2 100644 --- a/graphrag/config/models/global_search_config.py +++ b/packages/graphrag/graphrag/config/models/global_search_config.py @@ -19,9 +19,9 @@ class GlobalSearchConfig(BaseModel): description="The global search reducer to use.", default=graphrag_config_defaults.global_search.reduce_prompt, ) - chat_model_id: str = Field( + completion_model_id: str = Field( description="The model ID to use for global search.", - default=graphrag_config_defaults.global_search.chat_model_id, + default=graphrag_config_defaults.global_search.completion_model_id, ) knowledge_prompt: str | None = Field( description="The global search general prompt to use.", diff --git a/packages/graphrag/graphrag/config/models/graph_rag_config.py b/packages/graphrag/graphrag/config/models/graph_rag_config.py new file mode 100644 index 0000000000..84fb2de884 --- /dev/null +++ b/packages/graphrag/graphrag/config/models/graph_rag_config.py @@ -0,0 +1,325 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Parameterization settings for the default configuration.""" + +from dataclasses import asdict +from pathlib import Path + +from devtools import pformat +from graphrag_cache import CacheConfig +from graphrag_chunking.chunking_config import ChunkingConfig +from graphrag_input import InputConfig +from graphrag_llm.config import ModelConfig +from graphrag_storage import StorageConfig, StorageType +from graphrag_vectors import IndexSchema, VectorStoreConfig, VectorStoreType +from pydantic import BaseModel, Field, model_validator + +from graphrag.config.defaults import graphrag_config_defaults +from graphrag.config.embeddings import all_embeddings +from graphrag.config.enums import AsyncType, ReportingType +from graphrag.config.models.basic_search_config import BasicSearchConfig +from graphrag.config.models.cluster_graph_config import ClusterGraphConfig +from graphrag.config.models.community_reports_config import CommunityReportsConfig +from graphrag.config.models.drift_search_config import DRIFTSearchConfig +from graphrag.config.models.embed_text_config import EmbedTextConfig +from graphrag.config.models.extract_claims_config import ExtractClaimsConfig +from graphrag.config.models.extract_graph_config import ExtractGraphConfig +from graphrag.config.models.extract_graph_nlp_config import ExtractGraphNLPConfig +from graphrag.config.models.global_search_config import GlobalSearchConfig +from graphrag.config.models.local_search_config import LocalSearchConfig +from graphrag.config.models.prune_graph_config import PruneGraphConfig +from graphrag.config.models.reporting_config import ReportingConfig +from graphrag.config.models.snapshots_config import SnapshotsConfig +from graphrag.config.models.summarize_descriptions_config import ( + SummarizeDescriptionsConfig, +) + + +class GraphRagConfig(BaseModel): + """Base class for the Default-Configuration parameterization settings.""" + + def __repr__(self) -> str: + """Get a string representation.""" + return pformat(self, highlight=False) + + def __str__(self): + """Get a string representation.""" + return self.model_dump_json(indent=4) + + completion_models: dict[str, ModelConfig] = Field( + description="Available completion model configurations.", + default=graphrag_config_defaults.completion_models, + ) + + embedding_models: dict[str, ModelConfig] = Field( + description="Available embedding model configurations.", + default=graphrag_config_defaults.embedding_models, + ) + + concurrent_requests: int = Field( + description="The default number of concurrent requests to make to language models.", + default=graphrag_config_defaults.concurrent_requests, + ) + + async_mode: AsyncType = Field( + description="The default asynchronous mode to use for language model requests.", + default=graphrag_config_defaults.async_mode, + ) + + input: InputConfig = Field( + description="The input configuration.", default=InputConfig() + ) + """The input configuration.""" + + input_storage: StorageConfig = Field( + description="The input storage configuration.", + default=StorageConfig( + base_dir=graphrag_config_defaults.input_storage.base_dir, + ), + ) + """The input storage configuration.""" + + def _validate_input_base_dir(self) -> None: + """Validate the input base directory.""" + if self.input_storage.type == StorageType.File: + if not self.input_storage.base_dir: + msg = "input storage base directory is required for file input storage. Please rerun `graphrag init` and set the input storage configuration." + raise ValueError(msg) + self.input_storage.base_dir = str( + Path(self.input_storage.base_dir).resolve() + ) + + chunking: ChunkingConfig = Field( + description="The chunking configuration to use.", + default=ChunkingConfig( + type=graphrag_config_defaults.chunking.type, + size=graphrag_config_defaults.chunking.size, + overlap=graphrag_config_defaults.chunking.overlap, + encoding_model=graphrag_config_defaults.chunking.encoding_model, + prepend_metadata=graphrag_config_defaults.chunking.prepend_metadata, + ), + ) + """The chunking configuration to use.""" + + output_storage: StorageConfig = Field( + description="The output configuration.", + default=StorageConfig( + base_dir=graphrag_config_defaults.output_storage.base_dir, + ), + ) + """The output configuration.""" + + def _validate_output_base_dir(self) -> None: + """Validate the output base directory.""" + if self.output_storage.type == StorageType.File: + if not self.output_storage.base_dir: + msg = "output base directory is required for file output. Please rerun `graphrag init` and set the output configuration." + raise ValueError(msg) + self.output_storage.base_dir = str( + Path(self.output_storage.base_dir).resolve() + ) + + update_output_storage: StorageConfig = Field( + description="The output configuration for the updated index.", + default=StorageConfig( + base_dir=graphrag_config_defaults.update_output_storage.base_dir, + ), + ) + """The output configuration for the updated index.""" + + def _validate_update_output_storage_base_dir(self) -> None: + """Validate the update output base directory.""" + if self.update_output_storage.type == StorageType.File: + if not self.update_output_storage.base_dir: + msg = "update_output_storage base directory is required for file output. Please rerun `graphrag init` and set the update_output_storage configuration." + raise ValueError(msg) + self.update_output_storage.base_dir = str( + Path(self.update_output_storage.base_dir).resolve() + ) + + cache: CacheConfig = Field( + description="The cache configuration.", + default=CacheConfig(**asdict(graphrag_config_defaults.cache)), + ) + """The cache configuration.""" + + reporting: ReportingConfig = Field( + description="The reporting configuration.", default=ReportingConfig() + ) + """The reporting configuration.""" + + def _validate_reporting_base_dir(self) -> None: + """Validate the reporting base directory.""" + if self.reporting.type == ReportingType.file: + if self.reporting.base_dir.strip() == "": + msg = "Reporting base directory is required for file reporting. Please rerun `graphrag init` and set the reporting configuration." + raise ValueError(msg) + self.reporting.base_dir = str(Path(self.reporting.base_dir).resolve()) + + vector_store: VectorStoreConfig = Field( + description="The vector store configuration.", default=VectorStoreConfig() + ) + """The vector store configuration.""" + + workflows: list[str] | None = Field( + description="List of workflows to run, in execution order. This always overrides any built-in workflow methods.", + default=graphrag_config_defaults.workflows, + ) + """List of workflows to run, in execution order.""" + + embed_text: EmbedTextConfig = Field( + description="Text embedding configuration.", + default=EmbedTextConfig(), + ) + """Text embedding configuration.""" + + extract_graph: ExtractGraphConfig = Field( + description="The entity extraction configuration to use.", + default=ExtractGraphConfig(), + ) + """The entity extraction configuration to use.""" + + summarize_descriptions: SummarizeDescriptionsConfig = Field( + description="The description summarization configuration to use.", + default=SummarizeDescriptionsConfig(), + ) + """The description summarization configuration to use.""" + + extract_graph_nlp: ExtractGraphNLPConfig = Field( + description="The NLP-based graph extraction configuration to use.", + default=ExtractGraphNLPConfig(), + ) + """The NLP-based graph extraction configuration to use.""" + + prune_graph: PruneGraphConfig = Field( + description="The graph pruning configuration to use.", + default=PruneGraphConfig(), + ) + """The graph pruning configuration to use.""" + + cluster_graph: ClusterGraphConfig = Field( + description="The cluster graph configuration to use.", + default=ClusterGraphConfig(), + ) + """The cluster graph configuration to use.""" + + extract_claims: ExtractClaimsConfig = Field( + description="The claim extraction configuration to use.", + default=ExtractClaimsConfig( + enabled=graphrag_config_defaults.extract_claims.enabled, + ), + ) + """The claim extraction configuration to use.""" + + community_reports: CommunityReportsConfig = Field( + description="The community reports configuration to use.", + default=CommunityReportsConfig(), + ) + """The community reports configuration to use.""" + + snapshots: SnapshotsConfig = Field( + description="The snapshots configuration to use.", + default=SnapshotsConfig(), + ) + """The snapshots configuration to use.""" + + local_search: LocalSearchConfig = Field( + description="The local search configuration.", default=LocalSearchConfig() + ) + """The local search configuration.""" + + global_search: GlobalSearchConfig = Field( + description="The global search configuration.", default=GlobalSearchConfig() + ) + """The global search configuration.""" + + drift_search: DRIFTSearchConfig = Field( + description="The drift search configuration.", default=DRIFTSearchConfig() + ) + """The drift search configuration.""" + + basic_search: BasicSearchConfig = Field( + description="The basic search configuration.", default=BasicSearchConfig() + ) + """The basic search configuration.""" + + def _validate_vector_store(self) -> None: + """Validate the vector store configuration specifically in the GraphRAG context. This checks and sets required dynamic defaults for the embeddings we require.""" + self._validate_vector_store_db_uri() + # check and insert/overlay schemas for all of the core embeddings + # note that this does not require that they are used, only that they have a schema + # the embed_text block has the list of actual embeddings + if not self.vector_store.index_schema: + self.vector_store.index_schema = {} + for embedding in all_embeddings: + if embedding not in self.vector_store.index_schema: + self.vector_store.index_schema[embedding] = IndexSchema( + index_name=embedding, + ) + + def _validate_vector_store_db_uri(self) -> None: + """Validate the vector store configuration.""" + store = self.vector_store + if store.type == VectorStoreType.LanceDB: + if not store.db_uri or store.db_uri.strip == "": + store.db_uri = graphrag_config_defaults.vector_store.db_uri + store.db_uri = str(Path(store.db_uri).resolve()) + + def get_completion_model_config(self, model_id: str) -> ModelConfig: + """Get a completion model configuration by ID. + + Parameters + ---------- + model_id : str + The ID of the model to get. Should match an ID in the completion_models list. + + Returns + ------- + ModelConfig + The model configuration if found. + + Raises + ------ + ValueError + If the model ID is not found in the configuration. + """ + if model_id not in self.completion_models: + err_msg = f"Model ID {model_id} not found in completion_models. Please rerun `graphrag init` and set the completion_models configuration." + raise ValueError(err_msg) + + return self.completion_models[model_id] + + def get_embedding_model_config(self, model_id: str) -> ModelConfig: + """Get an embedding model configuration by ID. + + Parameters + ---------- + model_id : str + The ID of the model to get. Should match an ID in the embedding_models list. + + Returns + ------- + ModelConfig + The model configuration if found. + + Raises + ------ + ValueError + If the model ID is not found in the configuration. + """ + if model_id not in self.embedding_models: + err_msg = f"Model ID {model_id} not found in embedding_models. Please rerun `graphrag init` and set the embedding_models configuration." + raise ValueError(err_msg) + + return self.embedding_models[model_id] + + @model_validator(mode="after") + def _validate_model(self): + """Validate the model configuration.""" + self._validate_input_base_dir() + self._validate_reporting_base_dir() + self._validate_output_base_dir() + self._validate_update_output_storage_base_dir() + self._validate_vector_store() + return self diff --git a/graphrag/config/models/local_search_config.py b/packages/graphrag/graphrag/config/models/local_search_config.py similarity index 93% rename from graphrag/config/models/local_search_config.py rename to packages/graphrag/graphrag/config/models/local_search_config.py index 4cf31ffe0e..c9adbe9098 100644 --- a/graphrag/config/models/local_search_config.py +++ b/packages/graphrag/graphrag/config/models/local_search_config.py @@ -15,9 +15,9 @@ class LocalSearchConfig(BaseModel): description="The local search prompt to use.", default=graphrag_config_defaults.local_search.prompt, ) - chat_model_id: str = Field( + completion_model_id: str = Field( description="The model ID to use for local search.", - default=graphrag_config_defaults.local_search.chat_model_id, + default=graphrag_config_defaults.local_search.completion_model_id, ) embedding_model_id: str = Field( description="The model ID to use for text embeddings.", diff --git a/graphrag/config/models/prune_graph_config.py b/packages/graphrag/graphrag/config/models/prune_graph_config.py similarity index 100% rename from graphrag/config/models/prune_graph_config.py rename to packages/graphrag/graphrag/config/models/prune_graph_config.py diff --git a/graphrag/config/models/reporting_config.py b/packages/graphrag/graphrag/config/models/reporting_config.py similarity index 95% rename from graphrag/config/models/reporting_config.py rename to packages/graphrag/graphrag/config/models/reporting_config.py index 0e33736058..7443ea2961 100644 --- a/graphrag/config/models/reporting_config.py +++ b/packages/graphrag/graphrag/config/models/reporting_config.py @@ -28,7 +28,7 @@ class ReportingConfig(BaseModel): description="The reporting container name to use.", default=graphrag_config_defaults.reporting.container_name, ) - storage_account_blob_url: str | None = Field( + account_url: str | None = Field( description="The storage account blob url to use.", default=graphrag_config_defaults.reporting.storage_account_blob_url, ) diff --git a/graphrag/config/models/snapshots_config.py b/packages/graphrag/graphrag/config/models/snapshots_config.py similarity index 100% rename from graphrag/config/models/snapshots_config.py rename to packages/graphrag/graphrag/config/models/snapshots_config.py diff --git a/graphrag/config/models/summarize_descriptions_config.py b/packages/graphrag/graphrag/config/models/summarize_descriptions_config.py similarity index 56% rename from graphrag/config/models/summarize_descriptions_config.py rename to packages/graphrag/graphrag/config/models/summarize_descriptions_config.py index ef293f69c8..ac8f1dbca3 100644 --- a/graphrag/config/models/summarize_descriptions_config.py +++ b/packages/graphrag/graphrag/config/models/summarize_descriptions_config.py @@ -3,20 +3,32 @@ """Parameterization settings for the default configuration.""" +from dataclasses import dataclass from pathlib import Path from pydantic import BaseModel, Field from graphrag.config.defaults import graphrag_config_defaults -from graphrag.config.models.language_model_config import LanguageModelConfig +from graphrag.prompts.index.summarize_descriptions import SUMMARIZE_PROMPT + + +@dataclass +class SummarizeDescriptionsPrompts: + """Description summarization prompt templates.""" + + summarize_prompt: str class SummarizeDescriptionsConfig(BaseModel): """Configuration section for description summarization.""" - model_id: str = Field( + completion_model_id: str = Field( description="The model ID to use for summarization.", - default=graphrag_config_defaults.summarize_descriptions.model_id, + default=graphrag_config_defaults.summarize_descriptions.completion_model_id, + ) + model_instance_name: str = Field( + description="The model singleton instance name. This primarily affects the cache storage partitioning.", + default=graphrag_config_defaults.summarize_descriptions.model_instance_name, ) prompt: str | None = Field( description="The description summarization prompt to use.", @@ -30,27 +42,11 @@ class SummarizeDescriptionsConfig(BaseModel): description="Maximum tokens to submit from the input entity descriptions.", default=graphrag_config_defaults.summarize_descriptions.max_input_tokens, ) - strategy: dict | None = Field( - description="The override strategy to use.", - default=graphrag_config_defaults.summarize_descriptions.strategy, - ) - - def resolved_strategy( - self, root_dir: str, model_config: LanguageModelConfig - ) -> dict: - """Get the resolved description summarization strategy.""" - from graphrag.index.operations.summarize_descriptions.summarize_descriptions import ( - SummarizeStrategyType, - ) - return self.strategy or { - "type": SummarizeStrategyType.graph_intelligence, - "llm": model_config.model_dump(), - "summarize_prompt": (Path(root_dir) / self.prompt).read_text( - encoding="utf-8" - ) + def resolved_prompts(self) -> SummarizeDescriptionsPrompts: + """Get the resolved description summarization prompts.""" + return SummarizeDescriptionsPrompts( + summarize_prompt=Path(self.prompt).read_text(encoding="utf-8") if self.prompt - else None, - "max_summary_length": self.max_length, - "max_input_tokens": self.max_input_tokens, - } + else SUMMARIZE_PROMPT, + ) diff --git a/graphrag/data_model/__init__.py b/packages/graphrag/graphrag/data_model/__init__.py similarity index 100% rename from graphrag/data_model/__init__.py rename to packages/graphrag/graphrag/data_model/__init__.py diff --git a/graphrag/data_model/community.py b/packages/graphrag/graphrag/data_model/community.py similarity index 100% rename from graphrag/data_model/community.py rename to packages/graphrag/graphrag/data_model/community.py diff --git a/graphrag/data_model/community_report.py b/packages/graphrag/graphrag/data_model/community_report.py similarity index 100% rename from graphrag/data_model/community_report.py rename to packages/graphrag/graphrag/data_model/community_report.py diff --git a/graphrag/data_model/covariate.py b/packages/graphrag/graphrag/data_model/covariate.py similarity index 100% rename from graphrag/data_model/covariate.py rename to packages/graphrag/graphrag/data_model/covariate.py diff --git a/graphrag/data_model/document.py b/packages/graphrag/graphrag/data_model/document.py similarity index 100% rename from graphrag/data_model/document.py rename to packages/graphrag/graphrag/data_model/document.py diff --git a/graphrag/data_model/entity.py b/packages/graphrag/graphrag/data_model/entity.py similarity index 100% rename from graphrag/data_model/entity.py rename to packages/graphrag/graphrag/data_model/entity.py diff --git a/graphrag/data_model/identified.py b/packages/graphrag/graphrag/data_model/identified.py similarity index 100% rename from graphrag/data_model/identified.py rename to packages/graphrag/graphrag/data_model/identified.py diff --git a/graphrag/data_model/named.py b/packages/graphrag/graphrag/data_model/named.py similarity index 100% rename from graphrag/data_model/named.py rename to packages/graphrag/graphrag/data_model/named.py diff --git a/graphrag/data_model/relationship.py b/packages/graphrag/graphrag/data_model/relationship.py similarity index 100% rename from graphrag/data_model/relationship.py rename to packages/graphrag/graphrag/data_model/relationship.py diff --git a/graphrag/data_model/schemas.py b/packages/graphrag/graphrag/data_model/schemas.py similarity index 95% rename from graphrag/data_model/schemas.py rename to packages/graphrag/graphrag/data_model/schemas.py index 42d28d9029..c0926b9bb7 100644 --- a/graphrag/data_model/schemas.py +++ b/packages/graphrag/graphrag/data_model/schemas.py @@ -13,8 +13,6 @@ NODE_DEGREE = "degree" NODE_FREQUENCY = "frequency" NODE_DETAILS = "node_details" -NODE_X = "x" -NODE_Y = "y" # POST-PREP EDGE TABLE SCHEMA EDGE_SOURCE = "source" @@ -54,7 +52,7 @@ RELATIONSHIP_IDS = "relationship_ids" TEXT_UNIT_IDS = "text_unit_ids" COVARIATE_IDS = "covariate_ids" -DOCUMENT_IDS = "document_ids" +DOCUMENT_ID = "document_id" PERIOD = "period" SIZE = "size" @@ -66,7 +64,7 @@ N_TOKENS = "n_tokens" CREATION_DATE = "creation_date" -METADATA = "metadata" +RAW_DATA = "raw_data" # the following lists define the final content and ordering of columns in the data model parquet outputs ENTITIES_FINAL_COLUMNS = [ @@ -78,8 +76,6 @@ TEXT_UNIT_IDS, NODE_FREQUENCY, NODE_DEGREE, - NODE_X, - NODE_Y, ] RELATIONSHIPS_FINAL_COLUMNS = [ @@ -146,7 +142,7 @@ SHORT_ID, TEXT, N_TOKENS, - DOCUMENT_IDS, + DOCUMENT_ID, ENTITY_IDS, RELATIONSHIP_IDS, COVARIATE_IDS, @@ -159,5 +155,5 @@ TEXT, TEXT_UNIT_IDS, CREATION_DATE, - METADATA, + RAW_DATA, ] diff --git a/graphrag/data_model/text_unit.py b/packages/graphrag/graphrag/data_model/text_unit.py similarity index 89% rename from graphrag/data_model/text_unit.py rename to packages/graphrag/graphrag/data_model/text_unit.py index 07b1b9ae9c..55006ab15b 100644 --- a/graphrag/data_model/text_unit.py +++ b/packages/graphrag/graphrag/data_model/text_unit.py @@ -28,8 +28,8 @@ class TextUnit(Identified): n_tokens: int | None = None """The number of tokens in the text (optional).""" - document_ids: list[str] | None = None - """List of document IDs in which the text unit appears (optional).""" + document_id: str | None = None + """ID of the document in which the text unit appears (optional).""" attributes: dict[str, Any] | None = None """A dictionary of additional attributes associated with the text unit (optional).""" @@ -45,7 +45,7 @@ def from_dict( relationships_key: str = "relationship_ids", covariates_key: str = "covariate_ids", n_tokens_key: str = "n_tokens", - document_ids_key: str = "document_ids", + document_id_key: str = "document_id", attributes_key: str = "attributes", ) -> "TextUnit": """Create a new text unit from the dict data.""" @@ -57,6 +57,6 @@ def from_dict( relationship_ids=d.get(relationships_key), covariate_ids=d.get(covariates_key), n_tokens=d.get(n_tokens_key), - document_ids=d.get(document_ids_key), + document_id=d.get(document_id_key), attributes=d.get(attributes_key), ) diff --git a/graphrag/data_model/types.py b/packages/graphrag/graphrag/data_model/types.py similarity index 100% rename from graphrag/data_model/types.py rename to packages/graphrag/graphrag/data_model/types.py diff --git a/graphrag/index/__init__.py b/packages/graphrag/graphrag/index/__init__.py similarity index 100% rename from graphrag/index/__init__.py rename to packages/graphrag/graphrag/index/__init__.py diff --git a/graphrag/index/operations/__init__.py b/packages/graphrag/graphrag/index/operations/__init__.py similarity index 100% rename from graphrag/index/operations/__init__.py rename to packages/graphrag/graphrag/index/operations/__init__.py diff --git a/graphrag/index/operations/build_noun_graph/__init__.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/__init__.py similarity index 100% rename from graphrag/index/operations/build_noun_graph/__init__.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/__init__.py diff --git a/graphrag/index/operations/build_noun_graph/build_noun_graph.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/build_noun_graph.py similarity index 90% rename from graphrag/index/operations/build_noun_graph/build_noun_graph.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/build_noun_graph.py index dca2644ca9..32c2ef7b9d 100644 --- a/graphrag/index/operations/build_noun_graph/build_noun_graph.py +++ b/packages/graphrag/graphrag/index/operations/build_noun_graph/build_noun_graph.py @@ -7,9 +7,8 @@ import numpy as np import pandas as pd +from graphrag_cache import Cache -from graphrag.cache.noop_pipeline_cache import NoopPipelineCache -from graphrag.cache.pipeline_cache import PipelineCache from graphrag.config.enums import AsyncType from graphrag.index.operations.build_noun_graph.np_extractors.base import ( BaseNounPhraseExtractor, @@ -23,9 +22,9 @@ async def build_noun_graph( text_unit_df: pd.DataFrame, text_analyzer: BaseNounPhraseExtractor, normalize_edge_weights: bool, - num_threads: int = 4, - async_mode: AsyncType = AsyncType.Threaded, - cache: PipelineCache | None = None, + num_threads: int, + async_mode: AsyncType, + cache: Cache, ) -> tuple[pd.DataFrame, pd.DataFrame]: """Build a noun graph from text units.""" text_units = text_unit_df.loc[:, ["id", "text"]] @@ -43,9 +42,9 @@ async def build_noun_graph( async def _extract_nodes( text_unit_df: pd.DataFrame, text_analyzer: BaseNounPhraseExtractor, - num_threads: int = 4, - async_mode: AsyncType = AsyncType.Threaded, - cache: PipelineCache | None = None, + num_threads: int, + async_mode: AsyncType, + cache: Cache, ) -> pd.DataFrame: """ Extract initial nodes and edges from text units. @@ -53,7 +52,6 @@ async def _extract_nodes( Input: text unit df with schema [id, text, document_id] Returns a dataframe with schema [id, title, frequency, text_unit_ids]. """ - cache = cache or NoopPipelineCache() cache = cache.child("extract_noun_phrases") async def extract(row): @@ -66,7 +64,7 @@ async def extract(row): await cache.set(key, result) return result - text_unit_df["noun_phrases"] = await derive_from_rows( + text_unit_df["noun_phrases"] = await derive_from_rows( # type: ignore text_unit_df, extract, num_threads=num_threads, @@ -100,11 +98,14 @@ def _extract_edges( Input: nodes_df with schema [id, title, frequency, text_unit_ids] Returns: edges_df with schema [source, target, weight, text_unit_ids] """ + if nodes_df.empty: + return pd.DataFrame(columns=["source", "target", "weight", "text_unit_ids"]) + text_units_df = nodes_df.explode("text_unit_ids") text_units_df = text_units_df.rename(columns={"text_unit_ids": "text_unit_id"}) - text_units_df = ( - text_units_df.groupby("text_unit_id") + text_units_df + .groupby("text_unit_id") .agg({"title": lambda x: list(x) if len(x) > 1 else np.nan}) .reset_index() ) diff --git a/graphrag/index/operations/build_noun_graph/np_extractors/__init__.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/__init__.py similarity index 100% rename from graphrag/index/operations/build_noun_graph/np_extractors/__init__.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/__init__.py diff --git a/graphrag/index/operations/build_noun_graph/np_extractors/base.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/base.py similarity index 100% rename from graphrag/index/operations/build_noun_graph/np_extractors/base.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/base.py diff --git a/graphrag/index/operations/build_noun_graph/np_extractors/cfg_extractor.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/cfg_extractor.py similarity index 98% rename from graphrag/index/operations/build_noun_graph/np_extractors/cfg_extractor.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/cfg_extractor.py index 68c636ac03..60941eefc0 100644 --- a/graphrag/index/operations/build_noun_graph/np_extractors/cfg_extractor.py +++ b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/cfg_extractor.py @@ -165,7 +165,8 @@ def _tag_noun_phrases( return { "cleaned_tokens": cleaned_tokens, - "cleaned_text": self.word_delimiter.join(cleaned_tokens) + "cleaned_text": self.word_delimiter + .join(cleaned_tokens) .replace("\n", "") .upper(), "is_valid_entity": has_valid_entity, diff --git a/graphrag/index/operations/build_noun_graph/np_extractors/factory.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/factory.py similarity index 100% rename from graphrag/index/operations/build_noun_graph/np_extractors/factory.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/factory.py diff --git a/graphrag/index/operations/build_noun_graph/np_extractors/np_validator.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/np_validator.py similarity index 100% rename from graphrag/index/operations/build_noun_graph/np_extractors/np_validator.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/np_validator.py diff --git a/graphrag/index/operations/build_noun_graph/np_extractors/regex_extractor.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/regex_extractor.py similarity index 97% rename from graphrag/index/operations/build_noun_graph/np_extractors/regex_extractor.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/regex_extractor.py index 2f14b68e0d..b5490188b8 100644 --- a/graphrag/index/operations/build_noun_graph/np_extractors/regex_extractor.py +++ b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/regex_extractor.py @@ -110,7 +110,8 @@ def _tag_noun_phrases( ) and all(len(token) <= self.max_word_length for token in cleaned_tokens) return { "cleaned_tokens": cleaned_tokens, - "cleaned_text": self.word_delimiter.join(token for token in cleaned_tokens) + "cleaned_text": self.word_delimiter + .join(token for token in cleaned_tokens) .replace("\n", "") .upper(), "has_proper_nouns": has_proper_nouns, diff --git a/graphrag/index/operations/build_noun_graph/np_extractors/resource_loader.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/resource_loader.py similarity index 100% rename from graphrag/index/operations/build_noun_graph/np_extractors/resource_loader.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/resource_loader.py diff --git a/graphrag/index/operations/build_noun_graph/np_extractors/stop_words.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/stop_words.py similarity index 100% rename from graphrag/index/operations/build_noun_graph/np_extractors/stop_words.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/stop_words.py diff --git a/graphrag/index/operations/build_noun_graph/np_extractors/syntactic_parsing_extractor.py b/packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/syntactic_parsing_extractor.py similarity index 100% rename from graphrag/index/operations/build_noun_graph/np_extractors/syntactic_parsing_extractor.py rename to packages/graphrag/graphrag/index/operations/build_noun_graph/np_extractors/syntactic_parsing_extractor.py diff --git a/graphrag/index/operations/cluster_graph.py b/packages/graphrag/graphrag/index/operations/cluster_graph.py similarity index 94% rename from graphrag/index/operations/cluster_graph.py rename to packages/graphrag/graphrag/index/operations/cluster_graph.py index de16e2d35c..ff4632ea17 100644 --- a/graphrag/index/operations/cluster_graph.py +++ b/packages/graphrag/graphrag/index/operations/cluster_graph.py @@ -1,13 +1,13 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing cluster_graph, apply_clustering and run_layout methods definition.""" +"""A module containing cluster_graph method definition.""" import logging import networkx as nx -from graspologic.partition import hierarchical_leiden +from graphrag.index.utils.graphs import hierarchical_leiden from graphrag.index.utils.stable_lcc import stable_largest_connected_component Communities = list[tuple[int, int, int, list[str]]] diff --git a/graphrag/index/operations/compute_degree.py b/packages/graphrag/graphrag/index/operations/compute_degree.py similarity index 86% rename from graphrag/index/operations/compute_degree.py rename to packages/graphrag/graphrag/index/operations/compute_degree.py index b720bf6de5..8de95ad2e2 100644 --- a/graphrag/index/operations/compute_degree.py +++ b/packages/graphrag/graphrag/index/operations/compute_degree.py @@ -1,7 +1,7 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing create_graph definition.""" +"""A module containing compute_degree method definition.""" import networkx as nx import pandas as pd diff --git a/graphrag/index/operations/compute_edge_combined_degree.py b/packages/graphrag/graphrag/index/operations/compute_edge_combined_degree.py similarity index 94% rename from graphrag/index/operations/compute_edge_combined_degree.py rename to packages/graphrag/graphrag/index/operations/compute_edge_combined_degree.py index f6c75b008e..440fe39222 100644 --- a/graphrag/index/operations/compute_edge_combined_degree.py +++ b/packages/graphrag/graphrag/index/operations/compute_edge_combined_degree.py @@ -1,7 +1,7 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing compute_edge_combined_degree methods definition.""" +"""A module containing compute_edge_combined_degree method definition.""" from typing import cast diff --git a/graphrag/index/operations/create_graph.py b/packages/graphrag/graphrag/index/operations/create_graph.py similarity index 100% rename from graphrag/index/operations/create_graph.py rename to packages/graphrag/graphrag/index/operations/create_graph.py diff --git a/graphrag/index/operations/embed_text/__init__.py b/packages/graphrag/graphrag/index/operations/embed_text/__init__.py similarity index 100% rename from graphrag/index/operations/embed_text/__init__.py rename to packages/graphrag/graphrag/index/operations/embed_text/__init__.py diff --git a/packages/graphrag/graphrag/index/operations/embed_text/embed_text.py b/packages/graphrag/graphrag/index/operations/embed_text/embed_text.py new file mode 100644 index 0000000000..af0b79bbb5 --- /dev/null +++ b/packages/graphrag/graphrag/index/operations/embed_text/embed_text.py @@ -0,0 +1,89 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing embed_text method definition.""" + +import logging +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from graphrag_llm.tokenizer import Tokenizer +from graphrag_vectors import VectorStore, VectorStoreDocument + +from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks +from graphrag.index.operations.embed_text.run_embed_text import run_embed_text + +if TYPE_CHECKING: + from graphrag_llm.embedding import LLMEmbedding + +logger = logging.getLogger(__name__) + + +async def embed_text( + input: pd.DataFrame, + callbacks: WorkflowCallbacks, + model: "LLMEmbedding", + tokenizer: Tokenizer, + embed_column: str, + batch_size: int, + batch_max_tokens: int, + num_threads: int, + vector_store: VectorStore, + id_column: str = "id", +): + """Embed a piece of text into a vector space. The operation outputs a new column containing a mapping between doc_id and vector.""" + if embed_column not in input.columns: + msg = f"Column {embed_column} not found in input dataframe with columns {input.columns}" + raise ValueError(msg) + if id_column not in input.columns: + msg = f"Column {id_column} not found in input dataframe with columns {input.columns}" + raise ValueError(msg) + + vector_store.create_index() + + index = 0 + + all_results = [] + + num_total_batches = (input.shape[0] + batch_size - 1) // batch_size + while batch_size * index < input.shape[0]: + logger.info( + "uploading text embeddings batch %d/%d of size %d to vector store", + index + 1, + num_total_batches, + batch_size, + ) + batch = input.iloc[batch_size * index : batch_size * (index + 1)] + texts: list[str] = batch[embed_column].tolist() + ids: list[str] = batch[id_column].tolist() + result = await run_embed_text( + texts, + callbacks, + model, + tokenizer, + batch_size, + batch_max_tokens, + num_threads, + ) + if result.embeddings: + embeddings = [ + embedding for embedding in result.embeddings if embedding is not None + ] + all_results.extend(embeddings) + + vectors = result.embeddings or [] + documents: list[VectorStoreDocument] = [] + for doc_id, doc_vector in zip(ids, vectors, strict=True): + if type(doc_vector) is np.ndarray: + doc_vector = doc_vector.tolist() + document = VectorStoreDocument( + id=doc_id, + vector=doc_vector, + ) + documents.append(document) + + vector_store.load_documents(documents) + index += 1 + + return all_results diff --git a/graphrag/index/operations/embed_text/strategies/openai.py b/packages/graphrag/graphrag/index/operations/embed_text/run_embed_text.py similarity index 66% rename from graphrag/index/operations/embed_text/strategies/openai.py rename to packages/graphrag/graphrag/index/operations/embed_text/run_embed_text.py index ef8aadff6b..6755c146bb 100644 --- a/graphrag/index/operations/embed_text/strategies/openai.py +++ b/packages/graphrag/graphrag/index/operations/embed_text/run_embed_text.py @@ -1,59 +1,56 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing run method definition.""" +"""A module containing 'TextEmbeddingResult' model and run_embed_text method definition.""" import asyncio import logging -from typing import Any +from dataclasses import dataclass +from typing import TYPE_CHECKING import numpy as np +from graphrag_chunking.token_chunker import split_text_on_tokens +from graphrag_llm.tokenizer import Tokenizer -from graphrag.cache.pipeline_cache import PipelineCache from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks -from graphrag.config.models.language_model_config import LanguageModelConfig -from graphrag.index.operations.embed_text.strategies.typing import TextEmbeddingResult -from graphrag.index.text_splitting.text_splitting import TokenTextSplitter from graphrag.index.utils.is_null import is_null -from graphrag.language_model.manager import ModelManager -from graphrag.language_model.protocol.base import EmbeddingModel from graphrag.logger.progress import ProgressTicker, progress_ticker -from graphrag.tokenizer.get_tokenizer import get_tokenizer + +if TYPE_CHECKING: + from graphrag_llm.embedding import LLMEmbedding logger = logging.getLogger(__name__) -async def run( +@dataclass +class TextEmbeddingResult: + """Text embedding result class definition.""" + + embeddings: list[list[float] | None] | None + + +async def run_embed_text( input: list[str], callbacks: WorkflowCallbacks, - cache: PipelineCache, - args: dict[str, Any], + model: "LLMEmbedding", + tokenizer: Tokenizer, + batch_size: int, + batch_max_tokens: int, + num_threads: int, ) -> TextEmbeddingResult: """Run the Claim extraction chain.""" if is_null(input): return TextEmbeddingResult(embeddings=None) - batch_size = args.get("batch_size", 16) - batch_max_tokens = args.get("batch_max_tokens", 8191) - llm_config = args["llm"] - llm_config = LanguageModelConfig(**args["llm"]) - splitter = _get_splitter(llm_config, batch_max_tokens) - model = ModelManager().get_or_create_embedding_model( - name="text_embedding", - model_type=llm_config.type, - config=llm_config, - callbacks=callbacks, - cache=cache, - ) - semaphore: asyncio.Semaphore = asyncio.Semaphore(args.get("num_threads", 4)) + semaphore: asyncio.Semaphore = asyncio.Semaphore(num_threads) # Break up the input texts. The sizes here indicate how many snippets are in each input text - texts, input_sizes = _prepare_embed_texts(input, splitter) + texts, input_sizes = _prepare_embed_texts(input, tokenizer, batch_max_tokens) text_batches = _create_text_batches( texts, + tokenizer, batch_size, batch_max_tokens, - splitter, ) logger.info( "embedding %d inputs via %d snippets using %d batches. max_batch_size=%d, batch_max_tokens=%d", @@ -76,25 +73,16 @@ async def run( return TextEmbeddingResult(embeddings=embeddings) -def _get_splitter( - config: LanguageModelConfig, batch_max_tokens: int -) -> TokenTextSplitter: - return TokenTextSplitter( - tokenizer=get_tokenizer(model_config=config), - chunk_size=batch_max_tokens, - ) - - async def _execute( - model: EmbeddingModel, + model: "LLMEmbedding", chunks: list[list[str]], tick: ProgressTicker, semaphore: asyncio.Semaphore, ) -> list[list[float]]: async def embed(chunk: list[str]): async with semaphore: - chunk_embeddings = await model.aembed_batch(chunk) - result = np.array(chunk_embeddings) + embeddings_response = await model.embedding_async(input=chunk) + result = np.array(embeddings_response.embeddings) tick(1) return result @@ -106,9 +94,9 @@ async def embed(chunk: list[str]): def _create_text_batches( texts: list[str], + tokenizer: Tokenizer, max_batch_size: int, max_batch_tokens: int, - splitter: TokenTextSplitter, ) -> list[list[str]]: """Create batches of texts to embed.""" # https://learn.microsoft.com/en-us/azure/ai-services/openai/reference @@ -118,7 +106,7 @@ def _create_text_batches( current_batch_tokens = 0 for text in texts: - token_count = splitter.num_tokens(text) + token_count = tokenizer.num_tokens(text) if ( len(current_batch) >= max_batch_size or current_batch_tokens + token_count > max_batch_tokens @@ -137,18 +125,23 @@ def _create_text_batches( def _prepare_embed_texts( - input: list[str], splitter: TokenTextSplitter + input: list[str], + tokenizer: Tokenizer, + batch_max_tokens: int = 8191, + chunk_overlap: int = 100, ) -> tuple[list[str], list[int]]: sizes: list[int] = [] snippets: list[str] = [] for text in input: - # Split the input text and filter out any empty content - split_texts = splitter.split_text(text) - if split_texts is None: - continue + split_texts = split_text_on_tokens( + text, + chunk_size=batch_max_tokens, + chunk_overlap=chunk_overlap, + encode=tokenizer.encode, + decode=tokenizer.decode, + ) split_texts = [text for text in split_texts if len(text) > 0] - sizes.append(len(split_texts)) snippets.extend(split_texts) diff --git a/graphrag/index/operations/extract_covariates/__init__.py b/packages/graphrag/graphrag/index/operations/extract_covariates/__init__.py similarity index 100% rename from graphrag/index/operations/extract_covariates/__init__.py rename to packages/graphrag/graphrag/index/operations/extract_covariates/__init__.py diff --git a/packages/graphrag/graphrag/index/operations/extract_covariates/claim_extractor.py b/packages/graphrag/graphrag/index/operations/extract_covariates/claim_extractor.py new file mode 100644 index 0000000000..a334f360c2 --- /dev/null +++ b/packages/graphrag/graphrag/index/operations/extract_covariates/claim_extractor.py @@ -0,0 +1,193 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'ClaimExtractorResult' and 'ClaimExtractor' models.""" + +import logging +import traceback +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from graphrag_llm.utils import ( + CompletionMessagesBuilder, +) + +from graphrag.config.defaults import graphrag_config_defaults +from graphrag.index.typing.error_handler import ErrorHandlerFn +from graphrag.prompts.index.extract_claims import ( + CONTINUE_PROMPT, + LOOP_PROMPT, +) + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionResponse + +INPUT_TEXT_KEY = "input_text" +INPUT_ENTITY_SPEC_KEY = "entity_specs" +INPUT_CLAIM_DESCRIPTION_KEY = "claim_description" +INPUT_RESOLVED_ENTITIES_KEY = "resolved_entities" +RECORD_DELIMITER_KEY = "record_delimiter" +COMPLETION_DELIMITER_KEY = "completion_delimiter" +TUPLE_DELIMITER = "<|>" +RECORD_DELIMITER = "##" +COMPLETION_DELIMITER = "<|COMPLETE|>" +logger = logging.getLogger(__name__) + + +@dataclass +class ClaimExtractorResult: + """Claim extractor result class definition.""" + + output: list[dict] + source_docs: dict[str, Any] + + +class ClaimExtractor: + """Claim extractor class definition.""" + + _model: "LLMCompletion" + _extraction_prompt: str + _max_gleanings: int + _on_error: ErrorHandlerFn + + def __init__( + self, + model: "LLMCompletion", + extraction_prompt: str, + max_gleanings: int | None = None, + on_error: ErrorHandlerFn | None = None, + ): + """Init method definition.""" + self._model = model + self._extraction_prompt = extraction_prompt + self._max_gleanings = ( + max_gleanings + if max_gleanings is not None + else graphrag_config_defaults.extract_claims.max_gleanings + ) + self._on_error = on_error or (lambda _e, _s, _d: None) + + async def __call__( + self, + texts, + entity_spec, + resolved_entities, + claim_description, + ) -> ClaimExtractorResult: + """Call method definition.""" + source_doc_map = {} + all_claims: list[dict] = [] + for doc_index, text in enumerate(texts): + document_id = f"d{doc_index}" + try: + claims = await self._process_document( + text, claim_description, entity_spec + ) + all_claims += [ + self._clean_claim(c, document_id, resolved_entities) for c in claims + ] + source_doc_map[document_id] = text + except Exception as e: + logger.exception("error extracting claim") + self._on_error( + e, + traceback.format_exc(), + {"doc_index": doc_index, "text": text}, + ) + continue + + return ClaimExtractorResult( + output=all_claims, + source_docs=source_doc_map, + ) + + def _clean_claim( + self, claim: dict, document_id: str, resolved_entities: dict + ) -> dict: + # clean the parsed claims to remove any claims with status = False + obj = claim.get("object_id", claim.get("object")) + subject = claim.get("subject_id", claim.get("subject")) + + # If subject or object in resolved entities, then replace with resolved entity + obj = resolved_entities.get(obj, obj) + subject = resolved_entities.get(subject, subject) + claim["object_id"] = obj + claim["subject_id"] = subject + return claim + + async def _process_document( + self, text: str, claim_description: str, entity_spec: dict + ) -> list[dict]: + messages_builder = CompletionMessagesBuilder().add_user_message( + self._extraction_prompt.format(**{ + INPUT_TEXT_KEY: text, + INPUT_CLAIM_DESCRIPTION_KEY: claim_description, + INPUT_ENTITY_SPEC_KEY: entity_spec, + }) + ) + + response: LLMCompletionResponse = await self._model.completion_async( + messages=messages_builder.build(), + ) # type: ignore + results = response.content + messages_builder.add_assistant_message(results) + claims = results.strip().removesuffix(COMPLETION_DELIMITER) + + # if gleanings are specified, enter a loop to extract more claims + # there are two exit criteria: (a) we hit the configured max, (b) the model says there are no more claims + if self._max_gleanings > 0: + for i in range(self._max_gleanings): + messages_builder.add_user_message(CONTINUE_PROMPT) + response: LLMCompletionResponse = await self._model.completion_async( + messages=messages_builder.build(), + ) # type: ignore + extension = response.content + messages_builder.add_assistant_message(extension) + claims += RECORD_DELIMITER + extension.strip().removesuffix( + COMPLETION_DELIMITER + ) + + # If this isn't the last loop, check to see if we should continue + if i >= self._max_gleanings - 1: + break + + messages_builder.add_user_message(LOOP_PROMPT) + response: LLMCompletionResponse = await self._model.completion_async( + messages=messages_builder.build(), + ) # type: ignore + + if response.content != "Y": + break + + return self._parse_claim_tuples(results) + + def _parse_claim_tuples(self, claims: str) -> list[dict[str, Any]]: + """Parse claim tuples.""" + + def pull_field(index: int, fields: list[str]) -> str | None: + return fields[index].strip() if len(fields) > index else None + + result: list[dict[str, Any]] = [] + claims_values = ( + claims.strip().removesuffix(COMPLETION_DELIMITER).split(RECORD_DELIMITER) + ) + for claim in claims_values: + claim = claim.strip().removeprefix("(").removesuffix(")") + + # Ignore the completion delimiter + if claim == COMPLETION_DELIMITER: + continue + + claim_fields = claim.split(TUPLE_DELIMITER) + result.append({ + "subject_id": pull_field(0, claim_fields), + "object_id": pull_field(1, claim_fields), + "type": pull_field(2, claim_fields), + "status": pull_field(3, claim_fields), + "start_date": pull_field(4, claim_fields), + "end_date": pull_field(5, claim_fields), + "description": pull_field(6, claim_fields), + "source_text": pull_field(7, claim_fields), + }) + return result diff --git a/graphrag/index/operations/extract_covariates/extract_covariates.py b/packages/graphrag/graphrag/index/operations/extract_covariates/extract_covariates.py similarity index 56% rename from graphrag/index/operations/extract_covariates/extract_covariates.py rename to packages/graphrag/graphrag/index/operations/extract_covariates/extract_covariates.py index d29ca61e9d..2d366511e2 100644 --- a/graphrag/index/operations/extract_covariates/extract_covariates.py +++ b/packages/graphrag/graphrag/index/operations/extract_covariates/extract_covariates.py @@ -6,59 +6,51 @@ import logging from collections.abc import Iterable from dataclasses import asdict -from typing import Any +from typing import TYPE_CHECKING, Any import pandas as pd -from graphrag.cache.pipeline_cache import PipelineCache from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks -from graphrag.config.defaults import graphrag_config_defaults from graphrag.config.enums import AsyncType -from graphrag.config.models.language_model_config import LanguageModelConfig from graphrag.index.operations.extract_covariates.claim_extractor import ClaimExtractor from graphrag.index.operations.extract_covariates.typing import ( Covariate, CovariateExtractionResult, ) from graphrag.index.utils.derive_from_rows import derive_from_rows -from graphrag.language_model.manager import ModelManager - -logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion -DEFAULT_ENTITY_TYPES = ["organization", "person", "geo", "event"] +logger = logging.getLogger(__name__) async def extract_covariates( input: pd.DataFrame, callbacks: WorkflowCallbacks, - cache: PipelineCache, + model: "LLMCompletion", column: str, covariate_type: str, - strategy: dict[str, Any] | None, - async_mode: AsyncType = AsyncType.AsyncIO, - entity_types: list[str] | None = None, - num_threads: int = 4, + max_gleanings: int, + claim_description: str, + prompt: str, + entity_types: list[str], + num_threads: int, + async_type: AsyncType, ): """Extract claims from a piece of text.""" - logger.debug("extract_covariates strategy=%s", strategy) - if entity_types is None: - entity_types = DEFAULT_ENTITY_TYPES - resolved_entities_map = {} - strategy = strategy or {} - strategy_config = {**strategy} - async def run_strategy(row): text = row[column] result = await run_extract_claims( input=text, entity_types=entity_types, resolved_entities_map=resolved_entities_map, - callbacks=callbacks, - cache=cache, - strategy_config=strategy_config, + model=model, + max_gleanings=max_gleanings, + claim_description=claim_description, + prompt=prompt, ) return [ create_row_from_claim_data(row, item, covariate_type) @@ -69,8 +61,8 @@ async def run_strategy(row): input, run_strategy, callbacks, - async_type=async_mode, num_threads=num_threads, + async_type=async_type, progress_msg="extract covariates progress: ", ) return pd.DataFrame([item for row in results for item in row or []]) @@ -85,53 +77,29 @@ async def run_extract_claims( input: str | Iterable[str], entity_types: list[str], resolved_entities_map: dict[str, str], - callbacks: WorkflowCallbacks, - cache: PipelineCache, - strategy_config: dict[str, Any], + model: "LLMCompletion", + max_gleanings: int, + claim_description: str, + prompt: str, ) -> CovariateExtractionResult: """Run the Claim extraction chain.""" - llm_config = LanguageModelConfig(**strategy_config["llm"]) - llm = ModelManager().get_or_create_chat_model( - name="extract_claims", - model_type=llm_config.type, - config=llm_config, - callbacks=callbacks, - cache=cache, - ) - - extraction_prompt = strategy_config.get("extraction_prompt") - max_gleanings = strategy_config.get( - "max_gleanings", graphrag_config_defaults.extract_claims.max_gleanings - ) - tuple_delimiter = strategy_config.get("tuple_delimiter") - record_delimiter = strategy_config.get("record_delimiter") - completion_delimiter = strategy_config.get("completion_delimiter") - extractor = ClaimExtractor( - model_invoker=llm, - extraction_prompt=extraction_prompt, + model=model, + extraction_prompt=prompt, max_gleanings=max_gleanings, on_error=lambda e, s, d: logger.error( "Claim Extraction Error", exc_info=e, extra={"stack": s, "details": d} ), ) - claim_description = strategy_config.get("claim_description") - if claim_description is None: - msg = "claim_description is required for claim extraction" - raise ValueError(msg) - input = [input] if isinstance(input, str) else input - results = await extractor({ - "input_text": input, - "entity_specs": entity_types, - "resolved_entities": resolved_entities_map, - "claim_description": claim_description, - "tuple_delimiter": tuple_delimiter, - "record_delimiter": record_delimiter, - "completion_delimiter": completion_delimiter, - }) + results = await extractor( + texts=input, + entity_spec=entity_types, + resolved_entities=resolved_entities_map, + claim_description=claim_description, + ) claim_data = results.output return CovariateExtractionResult([create_covariate(item) for item in claim_data]) diff --git a/graphrag/index/operations/extract_covariates/typing.py b/packages/graphrag/graphrag/index/operations/extract_covariates/typing.py similarity index 93% rename from graphrag/index/operations/extract_covariates/typing.py rename to packages/graphrag/graphrag/index/operations/extract_covariates/typing.py index a524b2bc17..d5e82ad2fc 100644 --- a/graphrag/index/operations/extract_covariates/typing.py +++ b/packages/graphrag/graphrag/index/operations/extract_covariates/typing.py @@ -7,7 +7,8 @@ from dataclasses import dataclass from typing import Any -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag_cache import Cache + from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks @@ -42,7 +43,7 @@ class CovariateExtractionResult: list[str], dict[str, str], WorkflowCallbacks, - PipelineCache, + Cache, dict[str, Any], ], Awaitable[CovariateExtractionResult], diff --git a/graphrag/index/operations/extract_graph/__init__.py b/packages/graphrag/graphrag/index/operations/extract_graph/__init__.py similarity index 100% rename from graphrag/index/operations/extract_graph/__init__.py rename to packages/graphrag/graphrag/index/operations/extract_graph/__init__.py diff --git a/graphrag/index/operations/extract_graph/extract_graph.py b/packages/graphrag/graphrag/index/operations/extract_graph/extract_graph.py similarity index 50% rename from graphrag/index/operations/extract_graph/extract_graph.py rename to packages/graphrag/graphrag/index/operations/extract_graph/extract_graph.py index 76bcf40c76..7ab881c2ab 100644 --- a/graphrag/index/operations/extract_graph/extract_graph.py +++ b/packages/graphrag/graphrag/index/operations/extract_graph/extract_graph.py @@ -1,71 +1,60 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing entity_extract methods.""" +"""A module containing extract_graph method.""" import logging -from typing import Any +from typing import TYPE_CHECKING import pandas as pd -from graphrag.cache.pipeline_cache import PipelineCache from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks from graphrag.config.enums import AsyncType -from graphrag.index.operations.extract_graph.typing import ( - Document, - EntityExtractStrategy, - ExtractEntityStrategyType, -) +from graphrag.index.operations.extract_graph.graph_extractor import GraphExtractor from graphrag.index.utils.derive_from_rows import derive_from_rows -logger = logging.getLogger(__name__) - +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion -DEFAULT_ENTITY_TYPES = ["organization", "person", "geo", "event"] +logger = logging.getLogger(__name__) async def extract_graph( text_units: pd.DataFrame, callbacks: WorkflowCallbacks, - cache: PipelineCache, text_column: str, id_column: str, - strategy: dict[str, Any] | None, - async_mode: AsyncType = AsyncType.AsyncIO, - entity_types=DEFAULT_ENTITY_TYPES, - num_threads: int = 4, + model: "LLMCompletion", + prompt: str, + entity_types: list[str], + max_gleanings: int, + num_threads: int, + async_type: AsyncType, ) -> tuple[pd.DataFrame, pd.DataFrame]: """Extract a graph from a piece of text using a language model.""" - logger.debug("entity_extract strategy=%s", strategy) - if entity_types is None: - entity_types = DEFAULT_ENTITY_TYPES - strategy = strategy or {} - strategy_exec = _load_strategy( - strategy.get("type", ExtractEntityStrategyType.graph_intelligence) - ) - strategy_config = {**strategy} - num_started = 0 async def run_strategy(row): nonlocal num_started text = row[text_column] id = row[id_column] - result = await strategy_exec( - [Document(text=text, id=id)], - entity_types, - cache, - strategy_config, + result = await _run_extract_graph( + text=text, + source_id=id, + entity_types=entity_types, + model=model, + prompt=prompt, + max_gleanings=max_gleanings, ) num_started += 1 - return [result.entities, result.relationships, result.graph] + return result results = await derive_from_rows( text_units, run_strategy, callbacks, - async_type=async_mode, num_threads=num_threads, + async_type=async_type, progress_msg="extract graph progress: ", ) @@ -73,8 +62,8 @@ async def run_strategy(row): relationship_dfs = [] for result in results: if result: - entity_dfs.append(pd.DataFrame(result[0])) - relationship_dfs.append(pd.DataFrame(result[1])) + entity_dfs.append(result[0]) + relationship_dfs.append(result[1]) entities = _merge_entities(entity_dfs) relationships = _merge_relationships(relationship_dfs) @@ -82,25 +71,39 @@ async def run_strategy(row): return (entities, relationships) -def _load_strategy(strategy_type: ExtractEntityStrategyType) -> EntityExtractStrategy: - """Load strategy method definition.""" - match strategy_type: - case ExtractEntityStrategyType.graph_intelligence: - from graphrag.index.operations.extract_graph.graph_intelligence_strategy import ( - run_graph_intelligence, - ) +async def _run_extract_graph( + text: str, + source_id: str, + entity_types: list[str], + model: "LLMCompletion", + prompt: str, + max_gleanings: int, +) -> tuple[pd.DataFrame, pd.DataFrame]: + """Run the graph intelligence entity extraction strategy.""" + extractor = GraphExtractor( + model=model, + prompt=prompt, + max_gleanings=max_gleanings, + on_error=lambda e, s, d: logger.error( + "Entity Extraction Error", exc_info=e, extra={"stack": s, "details": d} + ), + ) + text = text.strip() - return run_graph_intelligence + entities_df, relationships_df = await extractor( + text, + entity_types=entity_types, + source_id=source_id, + ) - case _: - msg = f"Unknown strategy: {strategy_type}" - raise ValueError(msg) + return (entities_df, relationships_df) def _merge_entities(entity_dfs) -> pd.DataFrame: all_entities = pd.concat(entity_dfs, ignore_index=True) return ( - all_entities.groupby(["title", "type"], sort=False) + all_entities + .groupby(["title", "type"], sort=False) .agg( description=("description", list), text_unit_ids=("source_id", list), @@ -113,7 +116,8 @@ def _merge_entities(entity_dfs) -> pd.DataFrame: def _merge_relationships(relationship_dfs) -> pd.DataFrame: all_relationships = pd.concat(relationship_dfs, ignore_index=False) return ( - all_relationships.groupby(["source", "target"], sort=False) + all_relationships + .groupby(["source", "target"], sort=False) .agg( description=("description", list), text_unit_ids=("source_id", list), diff --git a/packages/graphrag/graphrag/index/operations/extract_graph/graph_extractor.py b/packages/graphrag/graphrag/index/operations/extract_graph/graph_extractor.py new file mode 100644 index 0000000000..ce78c0f6b9 --- /dev/null +++ b/packages/graphrag/graphrag/index/operations/extract_graph/graph_extractor.py @@ -0,0 +1,188 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Graph extraction helpers that return tabular data.""" + +import logging +import re +import traceback +from typing import TYPE_CHECKING, Any + +import pandas as pd +from graphrag_llm.utils import ( + CompletionMessagesBuilder, +) + +from graphrag.index.typing.error_handler import ErrorHandlerFn +from graphrag.index.utils.string import clean_str +from graphrag.prompts.index.extract_graph import ( + CONTINUE_PROMPT, + LOOP_PROMPT, +) + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionResponse + +INPUT_TEXT_KEY = "input_text" +RECORD_DELIMITER_KEY = "record_delimiter" +COMPLETION_DELIMITER_KEY = "completion_delimiter" +ENTITY_TYPES_KEY = "entity_types" +TUPLE_DELIMITER = "<|>" +RECORD_DELIMITER = "##" +COMPLETION_DELIMITER = "<|COMPLETE|>" + +logger = logging.getLogger(__name__) + + +class GraphExtractor: + """Unipartite graph extractor class definition.""" + + _model: "LLMCompletion" + _extraction_prompt: str + _max_gleanings: int + _on_error: ErrorHandlerFn + + def __init__( + self, + model: "LLMCompletion", + prompt: str, + max_gleanings: int, + on_error: ErrorHandlerFn | None = None, + ): + """Init method definition.""" + self._model = model + self._extraction_prompt = prompt + self._max_gleanings = max_gleanings + self._on_error = on_error or (lambda _e, _s, _d: None) + + async def __call__( + self, text: str, entity_types: list[str], source_id: str + ) -> tuple[pd.DataFrame, pd.DataFrame]: + """Extract entities and relationships from the supplied text.""" + try: + # Invoke the entity extraction + result = await self._process_document(text, entity_types) + except Exception as e: # pragma: no cover - defensive logging + logger.exception("error extracting graph") + self._on_error( + e, + traceback.format_exc(), + { + "source_id": source_id, + "text": text, + }, + ) + return _empty_entities_df(), _empty_relationships_df() + + return self._process_result( + result, + source_id, + TUPLE_DELIMITER, + RECORD_DELIMITER, + ) + + async def _process_document(self, text: str, entity_types: list[str]) -> str: + messages_builder = CompletionMessagesBuilder().add_user_message( + self._extraction_prompt.format(**{ + INPUT_TEXT_KEY: text, + ENTITY_TYPES_KEY: ",".join(entity_types), + }) + ) + + response: LLMCompletionResponse = await self._model.completion_async( + messages=messages_builder.build(), + ) # type: ignore + results = response.content + messages_builder.add_assistant_message(results) + + # if gleanings are specified, enter a loop to extract more entities + # there are two exit criteria: (a) we hit the configured max, (b) the model says there are no more entities + if self._max_gleanings > 0: + for i in range(self._max_gleanings): + messages_builder.add_user_message(CONTINUE_PROMPT) + response: LLMCompletionResponse = await self._model.completion_async( + messages=messages_builder.build(), + ) # type: ignore + response_text = response.content + messages_builder.add_assistant_message(response_text) + results += response_text + + # if this is the final glean, don't bother updating the continuation flag + if i >= self._max_gleanings - 1: + break + + messages_builder.add_user_message(LOOP_PROMPT) + response: LLMCompletionResponse = await self._model.completion_async( + messages=messages_builder.build(), + ) # type: ignore + if response.content != "Y": + break + + return results + + def _process_result( + self, + result: str, + source_id: str, + tuple_delimiter: str, + record_delimiter: str, + ) -> tuple[pd.DataFrame, pd.DataFrame]: + """Parse the result string into entity and relationship data frames.""" + entities: list[dict[str, Any]] = [] + relationships: list[dict[str, Any]] = [] + + records = [r.strip() for r in result.split(record_delimiter)] + + for raw_record in records: + record = re.sub(r"^\(|\)$", "", raw_record.strip()) + if not record or record == COMPLETION_DELIMITER: + continue + + record_attributes = record.split(tuple_delimiter) + record_type = record_attributes[0] + + if record_type == '"entity"' and len(record_attributes) >= 4: + entity_name = clean_str(record_attributes[1].upper()) + entity_type = clean_str(record_attributes[2].upper()) + entity_description = clean_str(record_attributes[3]) + entities.append({ + "title": entity_name, + "type": entity_type, + "description": entity_description, + "source_id": source_id, + }) + + if record_type == '"relationship"' and len(record_attributes) >= 5: + source = clean_str(record_attributes[1].upper()) + target = clean_str(record_attributes[2].upper()) + edge_description = clean_str(record_attributes[3]) + try: + weight = float(record_attributes[-1]) + except ValueError: + weight = 1.0 + + relationships.append({ + "source": source, + "target": target, + "description": edge_description, + "source_id": source_id, + "weight": weight, + }) + + entities_df = pd.DataFrame(entities) if entities else _empty_entities_df() + relationships_df = ( + pd.DataFrame(relationships) if relationships else _empty_relationships_df() + ) + + return entities_df, relationships_df + + +def _empty_entities_df() -> pd.DataFrame: + return pd.DataFrame(columns=["title", "type", "description", "source_id"]) + + +def _empty_relationships_df() -> pd.DataFrame: + return pd.DataFrame( + columns=["source", "target", "weight", "description", "source_id"] + ) diff --git a/graphrag/index/operations/finalize_community_reports.py b/packages/graphrag/graphrag/index/operations/finalize_community_reports.py similarity index 82% rename from graphrag/index/operations/finalize_community_reports.py rename to packages/graphrag/graphrag/index/operations/finalize_community_reports.py index 124e17e430..4446e3ee8f 100644 --- a/graphrag/index/operations/finalize_community_reports.py +++ b/packages/graphrag/graphrag/index/operations/finalize_community_reports.py @@ -3,11 +3,10 @@ """All the steps to transform final entities.""" -from uuid import uuid4 - import pandas as pd from graphrag.data_model.schemas import COMMUNITY_REPORTS_FINAL_COLUMNS +from graphrag.index.utils.hashing import gen_sha512_hash def finalize_community_reports( @@ -25,7 +24,9 @@ def finalize_community_reports( community_reports["community"] = community_reports["community"].astype(int) community_reports["human_readable_id"] = community_reports["community"] - community_reports["id"] = [uuid4().hex for _ in range(len(community_reports))] + community_reports["id"] = community_reports.apply( + lambda row: gen_sha512_hash(row, ["full_content"]), axis=1 + ) return community_reports.loc[ :, diff --git a/graphrag/index/operations/finalize_entities.py b/packages/graphrag/graphrag/index/operations/finalize_entities.py similarity index 59% rename from graphrag/index/operations/finalize_entities.py rename to packages/graphrag/graphrag/index/operations/finalize_entities.py index cd1dbb83eb..28ac2a55f0 100644 --- a/graphrag/index/operations/finalize_entities.py +++ b/packages/graphrag/graphrag/index/operations/finalize_entities.py @@ -7,38 +7,20 @@ import pandas as pd -from graphrag.config.models.embed_graph_config import EmbedGraphConfig from graphrag.data_model.schemas import ENTITIES_FINAL_COLUMNS from graphrag.index.operations.compute_degree import compute_degree from graphrag.index.operations.create_graph import create_graph -from graphrag.index.operations.embed_graph.embed_graph import embed_graph -from graphrag.index.operations.layout_graph.layout_graph import layout_graph def finalize_entities( entities: pd.DataFrame, relationships: pd.DataFrame, - embed_config: EmbedGraphConfig | None = None, - layout_enabled: bool = False, ) -> pd.DataFrame: """All the steps to transform final entities.""" graph = create_graph(relationships, edge_attr=["weight"]) - graph_embeddings = None - if embed_config is not None and embed_config.enabled: - graph_embeddings = embed_graph( - graph, - embed_config, - ) - layout = layout_graph( - graph, - layout_enabled, - embeddings=graph_embeddings, - ) degrees = compute_degree(graph) - final_entities = ( - entities.merge(layout, left_on="title", right_on="label", how="left") - .merge(degrees, on="title", how="left") - .drop_duplicates(subset="title") + final_entities = entities.merge(degrees, on="title", how="left").drop_duplicates( + subset="title" ) final_entities = final_entities.loc[entities["title"].notna()].reset_index() # disconnected nodes and those with no community even at level 0 can be missing degree diff --git a/graphrag/index/operations/finalize_relationships.py b/packages/graphrag/graphrag/index/operations/finalize_relationships.py similarity index 100% rename from graphrag/index/operations/finalize_relationships.py rename to packages/graphrag/graphrag/index/operations/finalize_relationships.py diff --git a/graphrag/index/operations/graph_to_dataframes.py b/packages/graphrag/graphrag/index/operations/graph_to_dataframes.py similarity index 95% rename from graphrag/index/operations/graph_to_dataframes.py rename to packages/graphrag/graphrag/index/operations/graph_to_dataframes.py index dbc608f640..632eb12586 100644 --- a/graphrag/index/operations/graph_to_dataframes.py +++ b/packages/graphrag/graphrag/index/operations/graph_to_dataframes.py @@ -1,7 +1,7 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing create_graph definition.""" +"""A module containing graph_to_dataframes method definition.""" import networkx as nx import pandas as pd diff --git a/graphrag/index/operations/prune_graph.py b/packages/graphrag/graphrag/index/operations/prune_graph.py similarity index 91% rename from graphrag/index/operations/prune_graph.py rename to packages/graphrag/graphrag/index/operations/prune_graph.py index d826558584..11a5335b4e 100644 --- a/graphrag/index/operations/prune_graph.py +++ b/packages/graphrag/graphrag/index/operations/prune_graph.py @@ -5,11 +5,11 @@ from typing import TYPE_CHECKING, cast -import graspologic as glc import networkx as nx import numpy as np import graphrag.data_model.schemas as schemas +from graphrag.index.utils.graphs import largest_connected_component if TYPE_CHECKING: from networkx.classes.reportviews import DegreeView @@ -50,7 +50,8 @@ def prune_graph( graph.remove_nodes_from([ node for node, data in graph.nodes(data=True) - if data[schemas.NODE_FREQUENCY] < min_node_freq + if schemas.NODE_FREQUENCY not in data + or data[schemas.NODE_FREQUENCY] < min_node_freq ]) if max_node_freq_std is not None: upper_threshold = _get_upper_threshold_by_std( @@ -64,6 +65,9 @@ def prune_graph( ]) # remove edges by min weight + if len(graph.edges) == 0: + return graph + if min_edge_weight_pct > 0: min_edge_weight = np.percentile( [data[schemas.EDGE_WEIGHT] for _, _, data in graph.edges(data=True)], @@ -78,7 +82,7 @@ def prune_graph( ]) if lcc_only: - return glc.utils.largest_connected_component(graph) # type: ignore + return largest_connected_component(graph) return graph diff --git a/graphrag/index/operations/snapshot_graphml.py b/packages/graphrag/graphrag/index/operations/snapshot_graphml.py similarity index 83% rename from graphrag/index/operations/snapshot_graphml.py rename to packages/graphrag/graphrag/index/operations/snapshot_graphml.py index c1eb9b0688..9124038401 100644 --- a/graphrag/index/operations/snapshot_graphml.py +++ b/packages/graphrag/graphrag/index/operations/snapshot_graphml.py @@ -4,14 +4,13 @@ """A module containing snapshot_graphml method definition.""" import networkx as nx - -from graphrag.storage.pipeline_storage import PipelineStorage +from graphrag_storage import Storage async def snapshot_graphml( input: str | nx.Graph, name: str, - storage: PipelineStorage, + storage: Storage, ) -> None: """Take a entire snapshot of a graph to standard graphml format.""" graphml = input if isinstance(input, str) else "\n".join(nx.generate_graphml(input)) diff --git a/graphrag/index/operations/summarize_communities/__init__.py b/packages/graphrag/graphrag/index/operations/summarize_communities/__init__.py similarity index 100% rename from graphrag/index/operations/summarize_communities/__init__.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/__init__.py diff --git a/graphrag/index/operations/summarize_communities/build_mixed_context.py b/packages/graphrag/graphrag/index/operations/summarize_communities/build_mixed_context.py similarity index 95% rename from graphrag/index/operations/summarize_communities/build_mixed_context.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/build_mixed_context.py index 0e0a4e39f0..27f4d35e7d 100644 --- a/graphrag/index/operations/summarize_communities/build_mixed_context.py +++ b/packages/graphrag/graphrag/index/operations/summarize_communities/build_mixed_context.py @@ -1,14 +1,15 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing the build_mixed_context method definition.""" + +"""A module containing build_mixed_context method definition.""" import pandas as pd +from graphrag_llm.tokenizer import Tokenizer import graphrag.data_model.schemas as schemas from graphrag.index.operations.summarize_communities.graph_context.sort_context import ( sort_context, ) -from graphrag.tokenizer.tokenizer import Tokenizer def build_mixed_context( diff --git a/graphrag/index/operations/summarize_communities/community_reports_extractor.py b/packages/graphrag/graphrag/index/operations/summarize_communities/community_reports_extractor.py similarity index 76% rename from graphrag/index/operations/summarize_communities/community_reports_extractor.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/community_reports_extractor.py index 1442a44a1b..4513abe95b 100644 --- a/graphrag/index/operations/summarize_communities/community_reports_extractor.py +++ b/packages/graphrag/graphrag/index/operations/summarize_communities/community_reports_extractor.py @@ -6,12 +6,14 @@ import logging import traceback from dataclasses import dataclass +from typing import TYPE_CHECKING from pydantic import BaseModel, Field from graphrag.index.typing.error_handler import ErrorHandlerFn -from graphrag.language_model.protocol.base import ChatModel -from graphrag.prompts.index.community_report import COMMUNITY_REPORT_PROMPT + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion logger = logging.getLogger(__name__) @@ -50,7 +52,7 @@ class CommunityReportsResult: class CommunityReportsExtractor: """Community reports extractor class definition.""" - _model: ChatModel + _model: "LLMCompletion" _extraction_prompt: str _output_formatter_prompt: str _on_error: ErrorHandlerFn @@ -58,16 +60,16 @@ class CommunityReportsExtractor: def __init__( self, - model_invoker: ChatModel, - extraction_prompt: str | None = None, + model: "LLMCompletion", + extraction_prompt: str, + max_report_length: int, on_error: ErrorHandlerFn | None = None, - max_report_length: int | None = None, ): """Init method definition.""" - self._model = model_invoker - self._extraction_prompt = extraction_prompt or COMMUNITY_REPORT_PROMPT + self._model = model + self._extraction_prompt = extraction_prompt self._on_error = on_error or (lambda _e, _s, _d: None) - self._max_report_length = max_report_length or 1500 + self._max_report_length = max_report_length async def __call__(self, input_text: str): """Call method definition.""" @@ -77,14 +79,12 @@ async def __call__(self, input_text: str): INPUT_TEXT_KEY: input_text, MAX_LENGTH_KEY: str(self._max_report_length), }) - response = await self._model.achat( - prompt, - json=True, # Leaving this as True to avoid creating new cache entries - name="create_community_report", - json_model=CommunityReportResponse, # A model is required when using json mode + response = await self._model.completion_async( + messages=prompt, + response_format=CommunityReportResponse, # A model is required when using json mode ) - output = response.parsed_response + output = response.formatted_response # type: ignore except Exception as e: logger.exception("error generating community report") self._on_error(e, traceback.format_exc(), None) diff --git a/graphrag/index/operations/summarize_communities/explode_communities.py b/packages/graphrag/graphrag/index/operations/summarize_communities/explode_communities.py similarity index 100% rename from graphrag/index/operations/summarize_communities/explode_communities.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/explode_communities.py diff --git a/graphrag/index/operations/summarize_communities/graph_context/__init__.py b/packages/graphrag/graphrag/index/operations/summarize_communities/graph_context/__init__.py similarity index 100% rename from graphrag/index/operations/summarize_communities/graph_context/__init__.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/graph_context/__init__.py diff --git a/graphrag/index/operations/summarize_communities/graph_context/context_builder.py b/packages/graphrag/graphrag/index/operations/summarize_communities/graph_context/context_builder.py similarity index 96% rename from graphrag/index/operations/summarize_communities/graph_context/context_builder.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/graph_context/context_builder.py index 34d281b3f8..f8e140fd98 100644 --- a/graphrag/index/operations/summarize_communities/graph_context/context_builder.py +++ b/packages/graphrag/graphrag/index/operations/summarize_communities/graph_context/context_builder.py @@ -7,6 +7,7 @@ from typing import cast import pandas as pd +from graphrag_llm.tokenizer import Tokenizer import graphrag.data_model.schemas as schemas from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks @@ -30,7 +31,6 @@ where_column_equals, ) from graphrag.logger.progress import progress_iterable -from graphrag.tokenizer.tokenizer import Tokenizer logger = logging.getLogger(__name__) @@ -79,7 +79,7 @@ def _prepare_reports_at_level( edge_df.loc[:, schemas.EDGE_SOURCE].isin(nodes_set) & edge_df.loc[:, schemas.EDGE_TARGET].isin(nodes_set) ] - level_edge_df.loc[:, schemas.EDGE_DETAILS] = level_edge_df.loc[ + level_edge_df.loc[:, schemas.EDGE_DETAILS] = level_edge_df.loc[ # type: ignore :, [ schemas.SHORT_ID, @@ -99,14 +99,16 @@ def _prepare_reports_at_level( # Merge node and edge details # Group edge details by node and aggregate into lists source_edges = ( - level_edge_df.groupby(schemas.EDGE_SOURCE) + level_edge_df + .groupby(schemas.EDGE_SOURCE) .agg({schemas.EDGE_DETAILS: "first"}) .reset_index() .rename(columns={schemas.EDGE_SOURCE: schemas.TITLE}) ) target_edges = ( - level_edge_df.groupby(schemas.EDGE_TARGET) + level_edge_df + .groupby(schemas.EDGE_TARGET) .agg({schemas.EDGE_DETAILS: "first"}) .reset_index() .rename(columns={schemas.EDGE_TARGET: schemas.TITLE}) @@ -129,7 +131,8 @@ def _prepare_reports_at_level( # Aggregate node and edge details merged_node_df = ( - merged_node_df.groupby([ + merged_node_df + .groupby([ schemas.TITLE, schemas.COMMUNITY_ID, schemas.COMMUNITY_LEVEL, @@ -155,8 +158,9 @@ def _prepare_reports_at_level( ) # Create the ALL_CONTEXT column - merged_node_df[schemas.ALL_CONTEXT] = ( - merged_node_df.loc[ + merged_node_df[schemas.ALL_CONTEXT] = ( # type: ignore + merged_node_df + .loc[ :, [ schemas.TITLE, @@ -175,7 +179,8 @@ def _prepare_reports_at_level( # group all node details by community community_df = ( - merged_node_df.groupby(schemas.COMMUNITY_ID) + merged_node_df + .groupby(schemas.COMMUNITY_ID) .agg({schemas.ALL_CONTEXT: list}) .reset_index() ) @@ -351,7 +356,8 @@ def _get_community_df( axis=1, ) community_df = ( - community_df.groupby(schemas.COMMUNITY_ID) + community_df + .groupby(schemas.COMMUNITY_ID) .agg({schemas.ALL_CONTEXT: list}) .reset_index() ) diff --git a/graphrag/index/operations/summarize_communities/graph_context/sort_context.py b/packages/graphrag/graphrag/index/operations/summarize_communities/graph_context/sort_context.py similarity index 99% rename from graphrag/index/operations/summarize_communities/graph_context/sort_context.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/graph_context/sort_context.py index 3b7863f2f8..18d843e4c5 100644 --- a/graphrag/index/operations/summarize_communities/graph_context/sort_context.py +++ b/packages/graphrag/graphrag/index/operations/summarize_communities/graph_context/sort_context.py @@ -3,9 +3,9 @@ """Sort context by degree in descending order.""" import pandas as pd +from graphrag_llm.tokenizer import Tokenizer import graphrag.data_model.schemas as schemas -from graphrag.tokenizer.tokenizer import Tokenizer def sort_context( diff --git a/graphrag/index/operations/summarize_communities/summarize_communities.py b/packages/graphrag/graphrag/index/operations/summarize_communities/summarize_communities.py similarity index 55% rename from graphrag/index/operations/summarize_communities/summarize_communities.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/summarize_communities.py index c31a4b0d77..7330b3f80e 100644 --- a/graphrag/index/operations/summarize_communities/summarize_communities.py +++ b/packages/graphrag/graphrag/index/operations/summarize_communities/summarize_communities.py @@ -1,29 +1,35 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing create_community_reports and load_strategy methods definition.""" +"""A module containing summarize_communities method definition.""" import logging from collections.abc import Callable +from typing import TYPE_CHECKING import pandas as pd +from graphrag_llm.tokenizer import Tokenizer import graphrag.data_model.schemas as schemas -from graphrag.cache.pipeline_cache import PipelineCache from graphrag.callbacks.noop_workflow_callbacks import NoopWorkflowCallbacks from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks from graphrag.config.enums import AsyncType +from graphrag.index.operations.summarize_communities.community_reports_extractor import ( + CommunityReportsExtractor, +) from graphrag.index.operations.summarize_communities.typing import ( CommunityReport, CommunityReportsStrategy, - CreateCommunityReportsStrategyType, + Finding, ) from graphrag.index.operations.summarize_communities.utils import ( get_levels, ) from graphrag.index.utils.derive_from_rows import derive_from_rows from graphrag.logger.progress import progress_ticker -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion logger = logging.getLogger(__name__) @@ -34,20 +40,20 @@ async def summarize_communities( local_contexts, level_context_builder: Callable, callbacks: WorkflowCallbacks, - cache: PipelineCache, - strategy: dict, + model: "LLMCompletion", + prompt: str, tokenizer: Tokenizer, max_input_length: int, - async_mode: AsyncType = AsyncType.AsyncIO, - num_threads: int = 4, + max_report_length: int, + num_threads: int, + async_type: AsyncType, ): """Generate community summaries.""" reports: list[CommunityReport | None] = [] tick = progress_ticker(callbacks.progress, len(local_contexts)) - strategy_exec = load_strategy(strategy["type"]) - strategy_config = {**strategy} community_hierarchy = ( - communities.explode("children") + communities + .explode("children") .rename({"children": "sub_community"}, axis=1) .loc[:, ["community", "level", "sub_community"]] ).dropna() @@ -70,13 +76,13 @@ async def summarize_communities( async def run_generate(record): result = await _generate_report( - strategy_exec, + run_extractor, community_id=record[schemas.COMMUNITY_ID], community_level=record[schemas.COMMUNITY_LEVEL], community_context=record[schemas.CONTEXT_STRING], - callbacks=callbacks, - cache=cache, - strategy=strategy_config, + model=model, + extraction_prompt=prompt, + max_report_length=max_report_length, ) tick() return result @@ -86,7 +92,7 @@ async def run_generate(record): run_generate, callbacks=NoopWorkflowCallbacks(), num_threads=num_threads, - async_type=async_mode, + async_type=async_type, progress_msg=f"level {levels[i]} summarize communities progress: ", ) reports.extend([lr for lr in local_reports if lr is not None]) @@ -96,35 +102,63 @@ async def run_generate(record): async def _generate_report( runner: CommunityReportsStrategy, - callbacks: WorkflowCallbacks, - cache: PipelineCache, - strategy: dict, + model: "LLMCompletion", + extraction_prompt: str, community_id: int, community_level: int, community_context: str, + max_report_length: int, ) -> CommunityReport | None: """Generate a report for a single community.""" return await runner( community_id, community_context, community_level, - callbacks, - cache, - strategy, + model, + extraction_prompt, + max_report_length, ) -def load_strategy( - strategy: CreateCommunityReportsStrategyType, -) -> CommunityReportsStrategy: - """Load strategy method definition.""" - match strategy: - case CreateCommunityReportsStrategyType.graph_intelligence: - from graphrag.index.operations.summarize_communities.strategies import ( - run_graph_intelligence, - ) +async def run_extractor( + community: str | int, + input: str, + level: int, + model: "LLMCompletion", + extraction_prompt: str, + max_report_length: int, +) -> CommunityReport | None: + """Run the graph intelligence entity extraction strategy.""" + extractor = CommunityReportsExtractor( + model, + extraction_prompt=extraction_prompt, + max_report_length=max_report_length, + on_error=lambda e, stack, _data: logger.error( + "Community Report Extraction Error", exc_info=e, extra={"stack": stack} + ), + ) + + try: + results = await extractor(input) + report = results.structured_output + if report is None: + logger.warning("No report found for community: %s", community) + return None - return run_graph_intelligence - case _: - msg = f"Unknown strategy: {strategy}" - raise ValueError(msg) + return CommunityReport( + community=community, + full_content=results.output, + level=level, + rank=report.rating, + title=report.title, + rating_explanation=report.rating_explanation, + summary=report.summary, + findings=[ + Finding(explanation=f.explanation, summary=f.summary) + for f in report.findings + ], + full_content_json=report.model_dump_json(indent=4), + ) + except Exception: + logger.exception("Error processing community: %s", community) + return None diff --git a/graphrag/index/operations/summarize_communities/text_unit_context/__init__.py b/packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/__init__.py similarity index 100% rename from graphrag/index/operations/summarize_communities/text_unit_context/__init__.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/__init__.py diff --git a/graphrag/index/operations/summarize_communities/text_unit_context/context_builder.py b/packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/context_builder.py similarity index 97% rename from graphrag/index/operations/summarize_communities/text_unit_context/context_builder.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/context_builder.py index 8d6e0ae2e0..c377417369 100644 --- a/graphrag/index/operations/summarize_communities/text_unit_context/context_builder.py +++ b/packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/context_builder.py @@ -7,6 +7,7 @@ from typing import cast import pandas as pd +from graphrag_llm.tokenizer import Tokenizer import graphrag.data_model.schemas as schemas from graphrag.index.operations.summarize_communities.build_mixed_context import ( @@ -18,7 +19,6 @@ from graphrag.index.operations.summarize_communities.text_unit_context.sort_context import ( sort_context, ) -from graphrag.tokenizer.tokenizer import Tokenizer logger = logging.getLogger(__name__) @@ -65,7 +65,8 @@ def build_local_context( ) context_df = ( - context_df.groupby([schemas.COMMUNITY_ID, schemas.COMMUNITY_LEVEL]) + context_df + .groupby([schemas.COMMUNITY_ID, schemas.COMMUNITY_LEVEL]) .agg({schemas.ALL_CONTEXT: list}) .reset_index() ) @@ -198,7 +199,8 @@ def build_level_context( axis=1, ) community_df = ( - community_df.groupby(schemas.COMMUNITY_ID) + community_df + .groupby(schemas.COMMUNITY_ID) .agg({schemas.ALL_CONTEXT: list}) .reset_index() ) diff --git a/graphrag/index/operations/summarize_communities/text_unit_context/prep_text_units.py b/packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/prep_text_units.py similarity index 94% rename from graphrag/index/operations/summarize_communities/text_unit_context/prep_text_units.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/prep_text_units.py index af31ee74ea..c1d9398733 100644 --- a/graphrag/index/operations/summarize_communities/text_unit_context/prep_text_units.py +++ b/packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/prep_text_units.py @@ -29,7 +29,8 @@ def prep_text_units( [schemas.TITLE, schemas.COMMUNITY_ID, schemas.NODE_DEGREE, schemas.ID] ] text_unit_degrees = ( - node_to_text_ids.groupby([schemas.COMMUNITY_ID, schemas.ID]) + node_to_text_ids + .groupby([schemas.COMMUNITY_ID, schemas.ID]) .agg({schemas.NODE_DEGREE: "sum"}) .reset_index() ) diff --git a/graphrag/index/operations/summarize_communities/text_unit_context/sort_context.py b/packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/sort_context.py similarity index 98% rename from graphrag/index/operations/summarize_communities/text_unit_context/sort_context.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/sort_context.py index 7bad931b59..b062551337 100644 --- a/graphrag/index/operations/summarize_communities/text_unit_context/sort_context.py +++ b/packages/graphrag/graphrag/index/operations/summarize_communities/text_unit_context/sort_context.py @@ -6,9 +6,9 @@ import logging import pandas as pd +from graphrag_llm.tokenizer import Tokenizer import graphrag.data_model.schemas as schemas -from graphrag.tokenizer.tokenizer import Tokenizer logger = logging.getLogger(__name__) diff --git a/graphrag/index/operations/summarize_communities/typing.py b/packages/graphrag/graphrag/index/operations/summarize_communities/typing.py similarity index 57% rename from graphrag/index/operations/summarize_communities/typing.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/typing.py index 6dddf3d6b1..73d8dd6f89 100644 --- a/graphrag/index/operations/summarize_communities/typing.py +++ b/packages/graphrag/graphrag/index/operations/summarize_communities/typing.py @@ -4,18 +4,14 @@ """A module containing 'Finding' and 'CommunityReport' models.""" from collections.abc import Awaitable, Callable -from enum import Enum -from typing import Any +from typing import TYPE_CHECKING, Any from typing_extensions import TypedDict -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion -ExtractedEntity = dict[str, Any] -StrategyConfig = dict[str, Any] RowContext = dict[str, Any] -EntityTypes = list[str] Claim = dict[str, Any] @@ -45,19 +41,9 @@ class CommunityReport(TypedDict): str | int, str, int, - WorkflowCallbacks, - PipelineCache, - StrategyConfig, + "LLMCompletion", + str, + int, ], Awaitable[CommunityReport | None], ] - - -class CreateCommunityReportsStrategyType(str, Enum): - """CreateCommunityReportsStrategyType class definition.""" - - graph_intelligence = "graph_intelligence" - - def __repr__(self): - """Get a string representation.""" - return f'"{self.value}"' diff --git a/graphrag/index/operations/summarize_communities/utils.py b/packages/graphrag/graphrag/index/operations/summarize_communities/utils.py similarity index 100% rename from graphrag/index/operations/summarize_communities/utils.py rename to packages/graphrag/graphrag/index/operations/summarize_communities/utils.py diff --git a/graphrag/index/operations/summarize_descriptions/__init__.py b/packages/graphrag/graphrag/index/operations/summarize_descriptions/__init__.py similarity index 100% rename from graphrag/index/operations/summarize_descriptions/__init__.py rename to packages/graphrag/graphrag/index/operations/summarize_descriptions/__init__.py diff --git a/graphrag/index/operations/summarize_descriptions/description_summary_extractor.py b/packages/graphrag/graphrag/index/operations/summarize_descriptions/description_summary_extractor.py similarity index 84% rename from graphrag/index/operations/summarize_descriptions/description_summary_extractor.py rename to packages/graphrag/graphrag/index/operations/summarize_descriptions/description_summary_extractor.py index 6a44ee1df7..f74a549ca6 100644 --- a/graphrag/index/operations/summarize_descriptions/description_summary_extractor.py +++ b/packages/graphrag/graphrag/index/operations/summarize_descriptions/description_summary_extractor.py @@ -1,15 +1,17 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""A module containing 'GraphExtractionResult' and 'GraphExtractor' models.""" +"""A module containing 'SummarizationResult' and 'SummarizeExtractor' models.""" import json from dataclasses import dataclass +from typing import TYPE_CHECKING from graphrag.index.typing.error_handler import ErrorHandlerFn -from graphrag.language_model.protocol.base import ChatModel -from graphrag.prompts.index.summarize_descriptions import SUMMARIZE_PROMPT -from graphrag.tokenizer.get_tokenizer import get_tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionResponse # these tokens are used in the prompt ENTITY_NAME_KEY = "entity_name" @@ -28,7 +30,7 @@ class SummarizationResult: class SummarizeExtractor: """Unipartite graph extractor class definition.""" - _model: ChatModel + _model: "LLMCompletion" _summarization_prompt: str _on_error: ErrorHandlerFn _max_summary_length: int @@ -36,17 +38,17 @@ class SummarizeExtractor: def __init__( self, - model_invoker: ChatModel, + model: "LLMCompletion", max_summary_length: int, max_input_tokens: int, - summarization_prompt: str | None = None, + summarization_prompt: str, on_error: ErrorHandlerFn | None = None, ): """Init method definition.""" # TODO: streamline construction - self._model = model_invoker - self._tokenizer = get_tokenizer(model_invoker.config) - self._summarization_prompt = summarization_prompt or SUMMARIZE_PROMPT + self._model = model + self._tokenizer = model.tokenizer + self._summarization_prompt = summarization_prompt self._on_error = on_error or (lambda _e, _s, _d: None) self._max_summary_length = max_summary_length self._max_input_tokens = max_input_tokens @@ -119,15 +121,14 @@ async def _summarize_descriptions_with_llm( self, id: str | tuple[str, str] | list[str], descriptions: list[str] ): """Summarize descriptions using the LLM.""" - response = await self._model.achat( - self._summarization_prompt.format(**{ + response: LLMCompletionResponse = await self._model.completion_async( + messages=self._summarization_prompt.format(**{ ENTITY_NAME_KEY: json.dumps(id, ensure_ascii=False), DESCRIPTION_LIST_KEY: json.dumps( sorted(descriptions), ensure_ascii=False ), MAX_LENGTH_KEY: self._max_summary_length, }), - name="summarize", - ) + ) # type: ignore # Calculate result - return str(response.output.content) + return response.content diff --git a/graphrag/index/operations/summarize_descriptions/summarize_descriptions.py b/packages/graphrag/graphrag/index/operations/summarize_descriptions/summarize_descriptions.py similarity index 66% rename from graphrag/index/operations/summarize_descriptions/summarize_descriptions.py rename to packages/graphrag/graphrag/index/operations/summarize_descriptions/summarize_descriptions.py index 780c94b329..a959afd79b 100644 --- a/graphrag/index/operations/summarize_descriptions/summarize_descriptions.py +++ b/packages/graphrag/graphrag/index/operations/summarize_descriptions/summarize_descriptions.py @@ -5,18 +5,22 @@ import asyncio import logging -from typing import Any +from typing import TYPE_CHECKING import pandas as pd -from graphrag.cache.pipeline_cache import PipelineCache from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks +from graphrag.index.operations.summarize_descriptions.description_summary_extractor import ( + SummarizeExtractor, +) from graphrag.index.operations.summarize_descriptions.typing import ( - SummarizationStrategy, - SummarizeStrategyType, + SummarizedDescriptionResult, ) from graphrag.logger.progress import ProgressTicker, progress_ticker +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + logger = logging.getLogger(__name__) @@ -24,17 +28,13 @@ async def summarize_descriptions( entities_df: pd.DataFrame, relationships_df: pd.DataFrame, callbacks: WorkflowCallbacks, - cache: PipelineCache, - strategy: dict[str, Any] | None = None, - num_threads: int = 4, + model: "LLMCompletion", + max_summary_length: int, + max_input_tokens: int, + prompt: str, + num_threads: int, ) -> tuple[pd.DataFrame, pd.DataFrame]: """Summarize entity and relationship descriptions from an entity graph, using a language model.""" - logger.debug("summarize_descriptions strategy=%s", strategy) - strategy = strategy or {} - strategy_exec = load_strategy( - strategy.get("type", SummarizeStrategyType.graph_intelligence) - ) - strategy_config = {**strategy} async def get_summarized( nodes: pd.DataFrame, edges: pd.DataFrame, semaphore: asyncio.Semaphore @@ -99,7 +99,14 @@ async def do_summarize_descriptions( semaphore: asyncio.Semaphore, ): async with semaphore: - results = await strategy_exec(id, descriptions, cache, strategy_config) + results = await run_summarize_descriptions( + id, + descriptions, + model, + max_summary_length, + max_input_tokens, + prompt, + ) ticker(1) return results @@ -108,15 +115,26 @@ async def do_summarize_descriptions( return await get_summarized(entities_df, relationships_df, semaphore) -def load_strategy(strategy_type: SummarizeStrategyType) -> SummarizationStrategy: - """Load strategy method definition.""" - match strategy_type: - case SummarizeStrategyType.graph_intelligence: - from graphrag.index.operations.summarize_descriptions.graph_intelligence_strategy import ( - run_graph_intelligence, - ) +async def run_summarize_descriptions( + id: str | tuple[str, str], + descriptions: list[str], + model: "LLMCompletion", + max_summary_length: int, + max_input_tokens: int, + prompt: str, +) -> SummarizedDescriptionResult: + """Run the graph intelligence entity extraction strategy.""" + extractor = SummarizeExtractor( + model=model, + summarization_prompt=prompt, + on_error=lambda e, stack, details: logger.error( + "Entity Extraction Error", + exc_info=e, + extra={"stack": stack, "details": details}, + ), + max_summary_length=max_summary_length, + max_input_tokens=max_input_tokens, + ) - return run_graph_intelligence - case _: - msg = f"Unknown strategy: {strategy_type}" - raise ValueError(msg) + result = await extractor(id=id, descriptions=descriptions) + return SummarizedDescriptionResult(id=result.id, description=result.description) diff --git a/packages/graphrag/graphrag/index/operations/summarize_descriptions/typing.py b/packages/graphrag/graphrag/index/operations/summarize_descriptions/typing.py new file mode 100644 index 0000000000..5a912caec4 --- /dev/null +++ b/packages/graphrag/graphrag/index/operations/summarize_descriptions/typing.py @@ -0,0 +1,21 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'SummarizedDescriptionResult' model.""" + +from dataclasses import dataclass +from typing import Any, NamedTuple + + +@dataclass +class SummarizedDescriptionResult: + """Entity summarization result class definition.""" + + id: str | tuple[str, str] + description: str + + +class DescriptionSummarizeRow(NamedTuple): + """DescriptionSummarizeRow class definition.""" + + graph: Any diff --git a/graphrag/index/run/__init__.py b/packages/graphrag/graphrag/index/run/__init__.py similarity index 100% rename from graphrag/index/run/__init__.py rename to packages/graphrag/graphrag/index/run/__init__.py diff --git a/graphrag/index/run/run_pipeline.py b/packages/graphrag/graphrag/index/run/run_pipeline.py similarity index 90% rename from graphrag/index/run/run_pipeline.py rename to packages/graphrag/graphrag/index/run/run_pipeline.py index f652db7acd..a4ce17582c 100644 --- a/graphrag/index/run/run_pipeline.py +++ b/packages/graphrag/graphrag/index/run/run_pipeline.py @@ -12,6 +12,8 @@ from typing import Any import pandas as pd +from graphrag_cache import create_cache +from graphrag_storage import Storage, create_storage from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks from graphrag.config.models.graph_rag_config import GraphRagConfig @@ -19,8 +21,6 @@ from graphrag.index.typing.context import PipelineRunContext from graphrag.index.typing.pipeline import Pipeline from graphrag.index.typing.pipeline_run_result import PipelineRunResult -from graphrag.storage.pipeline_storage import PipelineStorage -from graphrag.utils.api import create_cache_from_config, create_storage_from_config from graphrag.utils.storage import load_table_from_storage, write_table_to_storage logger = logging.getLogger(__name__) @@ -35,11 +35,9 @@ async def run_pipeline( input_documents: pd.DataFrame | None = None, ) -> AsyncIterable[PipelineRunResult]: """Run all workflows using a simplified pipeline.""" - root_dir = config.root_dir - - input_storage = create_storage_from_config(config.input.storage) - output_storage = create_storage_from_config(config.output) - cache = create_cache_from_config(config.cache, root_dir) + input_storage = create_storage(config.input_storage) + output_storage = create_storage(config.output_storage) + cache = create_cache(config.cache) # load existing state in case any workflows are stateful state_json = await output_storage.get("context.json") @@ -51,7 +49,7 @@ async def run_pipeline( if is_update_run: logger.info("Running incremental indexing.") - update_storage = create_storage_from_config(config.update_index_output) + update_storage = create_storage(config.update_output_storage) # we use this to store the new subset index, and will merge its content with the previous index update_timestamp = time.strftime("%Y%m%d-%H%M%S") timestamped_storage = update_storage.child(update_timestamp) @@ -121,7 +119,7 @@ async def _run_pipeline( result = await workflow_function(config, context) context.callbacks.workflow_end(name, result) yield PipelineRunResult( - workflow=name, result=result.result, state=context.state, errors=None + workflow=name, result=result.result, state=context.state, error=None ) context.stats.workflows[name] = {"overall": time.time() - work_time} if result.stop: @@ -135,7 +133,7 @@ async def _run_pipeline( except Exception as e: logger.exception("error running workflow %s", last_workflow) yield PipelineRunResult( - workflow=last_workflow, result=None, state=context.state, errors=[e] + workflow=last_workflow, result=None, state=context.state, error=e ) @@ -158,10 +156,10 @@ async def _dump_json(context: PipelineRunContext) -> None: async def _copy_previous_output( - storage: PipelineStorage, - copy_storage: PipelineStorage, + storage: Storage, + copy_storage: Storage, ): for file in storage.find(re.compile(r"\.parquet$")): - base_name = file[0].replace(".parquet", "") + base_name = file.replace(".parquet", "") table = await load_table_from_storage(base_name, storage) await write_table_to_storage(table, base_name, copy_storage) diff --git a/graphrag/index/run/utils.py b/packages/graphrag/graphrag/index/run/utils.py similarity index 63% rename from graphrag/index/run/utils.py rename to packages/graphrag/graphrag/index/run/utils.py index 52b1f0bd31..be6914a6d6 100644 --- a/graphrag/index/run/utils.py +++ b/packages/graphrag/graphrag/index/run/utils.py @@ -3,8 +3,11 @@ """Utility functions for the GraphRAG run module.""" -from graphrag.cache.memory_pipeline_cache import InMemoryCache -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag_cache import Cache +from graphrag_cache.memory_cache import MemoryCache +from graphrag_storage import Storage, create_storage +from graphrag_storage.memory_storage import MemoryStorage + from graphrag.callbacks.noop_workflow_callbacks import NoopWorkflowCallbacks from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks from graphrag.callbacks.workflow_callbacks_manager import WorkflowCallbacksManager @@ -12,26 +15,23 @@ from graphrag.index.typing.context import PipelineRunContext from graphrag.index.typing.state import PipelineState from graphrag.index.typing.stats import PipelineRunStats -from graphrag.storage.memory_pipeline_storage import MemoryPipelineStorage -from graphrag.storage.pipeline_storage import PipelineStorage -from graphrag.utils.api import create_storage_from_config def create_run_context( - input_storage: PipelineStorage | None = None, - output_storage: PipelineStorage | None = None, - previous_storage: PipelineStorage | None = None, - cache: PipelineCache | None = None, + input_storage: Storage | None = None, + output_storage: Storage | None = None, + previous_storage: Storage | None = None, + cache: Cache | None = None, callbacks: WorkflowCallbacks | None = None, stats: PipelineRunStats | None = None, state: PipelineState | None = None, ) -> PipelineRunContext: """Create the run context for the pipeline.""" return PipelineRunContext( - input_storage=input_storage or MemoryPipelineStorage(), - output_storage=output_storage or MemoryPipelineStorage(), - previous_storage=previous_storage or MemoryPipelineStorage(), - cache=cache or InMemoryCache(), + input_storage=input_storage or MemoryStorage(), + output_storage=output_storage or MemoryStorage(), + previous_storage=previous_storage or MemoryStorage(), + cache=cache or MemoryCache(), callbacks=callbacks or NoopWorkflowCallbacks(), stats=stats or PipelineRunStats(), state=state or {}, @@ -50,10 +50,10 @@ def create_callback_chain( def get_update_storages( config: GraphRagConfig, timestamp: str -) -> tuple[PipelineStorage, PipelineStorage, PipelineStorage]: +) -> tuple[Storage, Storage, Storage]: """Get storage objects for the update index run.""" - output_storage = create_storage_from_config(config.output) - update_storage = create_storage_from_config(config.update_index_output) + output_storage = create_storage(config.output_storage) + update_storage = create_storage(config.update_output_storage) timestamped_storage = update_storage.child(timestamp) delta_storage = timestamped_storage.child("delta") previous_storage = timestamped_storage.child("previous") diff --git a/graphrag/index/text_splitting/__init__.py b/packages/graphrag/graphrag/index/text_splitting/__init__.py similarity index 100% rename from graphrag/index/text_splitting/__init__.py rename to packages/graphrag/graphrag/index/text_splitting/__init__.py diff --git a/packages/graphrag/graphrag/index/text_splitting/text_splitting.py b/packages/graphrag/graphrag/index/text_splitting/text_splitting.py new file mode 100644 index 0000000000..0f42cd6ba5 --- /dev/null +++ b/packages/graphrag/graphrag/index/text_splitting/text_splitting.py @@ -0,0 +1,102 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing 'TokenTextSplitter' class and 'split_single_text_on_tokens' function.""" + +import logging +from abc import ABC +from collections.abc import Callable +from typing import cast + +import pandas as pd +from graphrag_llm.tokenizer import Tokenizer + +from graphrag.tokenizer.get_tokenizer import get_tokenizer + +EncodedText = list[int] +DecodeFn = Callable[[EncodedText], str] +EncodeFn = Callable[[str], EncodedText] +LengthFn = Callable[[str], int] + +logger = logging.getLogger(__name__) + + +class TokenTextSplitter(ABC): + """Token text splitter class definition.""" + + _chunk_size: int + _chunk_overlap: int + _length_function: LengthFn + _keep_separator: bool + _add_start_index: bool + _strip_whitespace: bool + + def __init__( + self, + # based on OpenAI embedding chunk size limits + # https://devblogs.microsoft.com/azure-sql/embedding-models-and-dimensions-optimizing-the-performance-resource-usage-ratio/ + chunk_size: int = 8191, + chunk_overlap: int = 100, + length_function: LengthFn = len, + keep_separator: bool = False, + add_start_index: bool = False, + strip_whitespace: bool = True, + tokenizer: Tokenizer | None = None, + ): + """Init method definition.""" + self._chunk_size = chunk_size + self._chunk_overlap = chunk_overlap + self._length_function = length_function + self._keep_separator = keep_separator + self._add_start_index = add_start_index + self._strip_whitespace = strip_whitespace + self._tokenizer = tokenizer or get_tokenizer() + + def num_tokens(self, text: str) -> int: + """Return the number of tokens in a string.""" + return self._tokenizer.num_tokens(text) + + def split_text(self, text: str | list[str]) -> list[str]: + """Split text method.""" + if isinstance(text, list): + text = " ".join(text) + elif cast("bool", pd.isna(text)) or text == "": + return [] + if not isinstance(text, str): + msg = f"Attempting to split a non-string value, actual is {type(text)}" + raise TypeError(msg) + + return split_single_text_on_tokens( + text, + chunk_overlap=self._chunk_overlap, + tokens_per_chunk=self._chunk_size, + decode=self._tokenizer.decode, + encode=self._tokenizer.encode, + ) + + +def split_single_text_on_tokens( + text: str, + tokens_per_chunk: int, + chunk_overlap: int, + encode: EncodeFn, + decode: DecodeFn, +) -> list[str]: + """Split a single text and return chunks using the tokenizer.""" + result = [] + input_ids = encode(text) + + start_idx = 0 + cur_idx = min(start_idx + tokens_per_chunk, len(input_ids)) + chunk_ids = input_ids[start_idx:cur_idx] + + while start_idx < len(input_ids): + chunk_text = decode(list(chunk_ids)) + result.append(chunk_text) # Append chunked text as string + if cur_idx == len(input_ids): + break + start_idx += tokens_per_chunk - chunk_overlap + cur_idx = min(start_idx + tokens_per_chunk, len(input_ids)) + chunk_ids = input_ids[start_idx:cur_idx] + + return result diff --git a/graphrag/index/typing/__init__.py b/packages/graphrag/graphrag/index/typing/__init__.py similarity index 100% rename from graphrag/index/typing/__init__.py rename to packages/graphrag/graphrag/index/typing/__init__.py diff --git a/graphrag/index/typing/context.py b/packages/graphrag/graphrag/index/typing/context.py similarity index 79% rename from graphrag/index/typing/context.py rename to packages/graphrag/graphrag/index/typing/context.py index ef2e1f7ea5..95e7f898f9 100644 --- a/graphrag/index/typing/context.py +++ b/packages/graphrag/graphrag/index/typing/context.py @@ -6,11 +6,11 @@ from dataclasses import dataclass -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag_cache import Cache from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks from graphrag.index.typing.state import PipelineState from graphrag.index.typing.stats import PipelineRunStats -from graphrag.storage.pipeline_storage import PipelineStorage +from graphrag_storage import Storage @dataclass @@ -18,13 +18,13 @@ class PipelineRunContext: """Provides the context for the current pipeline run.""" stats: PipelineRunStats - input_storage: PipelineStorage + input_storage: Storage "Storage for input documents." - output_storage: PipelineStorage + output_storage: Storage "Long-term storage for pipeline verbs to use. Items written here will be written to the storage provider." - previous_storage: PipelineStorage + previous_storage: Storage "Storage for previous pipeline run when running in update mode." - cache: PipelineCache + cache: Cache "Cache instance for reading previous LLM responses." callbacks: WorkflowCallbacks "Callbacks to be called during the pipeline run." diff --git a/graphrag/index/typing/error_handler.py b/packages/graphrag/graphrag/index/typing/error_handler.py similarity index 100% rename from graphrag/index/typing/error_handler.py rename to packages/graphrag/graphrag/index/typing/error_handler.py diff --git a/graphrag/index/typing/pipeline.py b/packages/graphrag/graphrag/index/typing/pipeline.py similarity index 100% rename from graphrag/index/typing/pipeline.py rename to packages/graphrag/graphrag/index/typing/pipeline.py diff --git a/graphrag/index/typing/pipeline_run_result.py b/packages/graphrag/graphrag/index/typing/pipeline_run_result.py similarity index 94% rename from graphrag/index/typing/pipeline_run_result.py rename to packages/graphrag/graphrag/index/typing/pipeline_run_result.py index f6a68d82a0..b39e030268 100644 --- a/graphrag/index/typing/pipeline_run_result.py +++ b/packages/graphrag/graphrag/index/typing/pipeline_run_result.py @@ -19,4 +19,4 @@ class PipelineRunResult: """The result of the workflow function. This can be anything - we use it only for logging downstream, and expect each workflow function to write official outputs to the provided storage.""" state: PipelineState """Ongoing pipeline context state object.""" - errors: list[BaseException] | None + error: BaseException | None diff --git a/graphrag/index/typing/state.py b/packages/graphrag/graphrag/index/typing/state.py similarity index 100% rename from graphrag/index/typing/state.py rename to packages/graphrag/graphrag/index/typing/state.py diff --git a/graphrag/index/typing/stats.py b/packages/graphrag/graphrag/index/typing/stats.py similarity index 100% rename from graphrag/index/typing/stats.py rename to packages/graphrag/graphrag/index/typing/stats.py diff --git a/graphrag/index/typing/workflow.py b/packages/graphrag/graphrag/index/typing/workflow.py similarity index 100% rename from graphrag/index/typing/workflow.py rename to packages/graphrag/graphrag/index/typing/workflow.py diff --git a/graphrag/index/update/__init__.py b/packages/graphrag/graphrag/index/update/__init__.py similarity index 100% rename from graphrag/index/update/__init__.py rename to packages/graphrag/graphrag/index/update/__init__.py diff --git a/graphrag/index/update/communities.py b/packages/graphrag/graphrag/index/update/communities.py similarity index 100% rename from graphrag/index/update/communities.py rename to packages/graphrag/graphrag/index/update/communities.py diff --git a/graphrag/index/update/entities.py b/packages/graphrag/graphrag/index/update/entities.py similarity index 96% rename from graphrag/index/update/entities.py rename to packages/graphrag/graphrag/index/update/entities.py index 7a74d1b6e0..fe9bb2347b 100644 --- a/graphrag/index/update/entities.py +++ b/packages/graphrag/graphrag/index/update/entities.py @@ -51,7 +51,8 @@ def _group_and_resolve_entities( # Group by title and resolve conflicts aggregated = ( - combined.groupby("title") + combined + .groupby("title") .agg({ "id": "first", "type": "first", @@ -60,8 +61,6 @@ def _group_and_resolve_entities( # Concatenate nd.array into a single list "text_unit_ids": lambda x: list(itertools.chain(*x.tolist())), "degree": "first", # todo: we could probably re-compute this with the entire new graph - "x": "first", - "y": "first", }) .reset_index() ) diff --git a/graphrag/index/update/incremental_index.py b/packages/graphrag/graphrag/index/update/incremental_index.py similarity index 87% rename from graphrag/index/update/incremental_index.py rename to packages/graphrag/graphrag/index/update/incremental_index.py index ac56e30df4..81f917e187 100644 --- a/graphrag/index/update/incremental_index.py +++ b/packages/graphrag/graphrag/index/update/incremental_index.py @@ -7,8 +7,8 @@ import numpy as np import pandas as pd +from graphrag_storage import Storage -from graphrag.storage.pipeline_storage import PipelineStorage from graphrag.utils.storage import ( load_table_from_storage, write_table_to_storage, @@ -31,16 +31,14 @@ class InputDelta: deleted_inputs: pd.DataFrame -async def get_delta_docs( - input_dataset: pd.DataFrame, storage: PipelineStorage -) -> InputDelta: +async def get_delta_docs(input_dataset: pd.DataFrame, storage: Storage) -> InputDelta: """Get the delta between the input dataset and the final documents. Parameters ---------- input_dataset : pd.DataFrame The input dataset. - storage : PipelineStorage + storage : Storage The Pipeline storage. Returns @@ -65,9 +63,9 @@ async def get_delta_docs( async def concat_dataframes( name: str, - previous_storage: PipelineStorage, - delta_storage: PipelineStorage, - output_storage: PipelineStorage, + previous_storage: Storage, + delta_storage: Storage, + output_storage: Storage, ) -> pd.DataFrame: """Concatenate dataframes.""" old_df = await load_table_from_storage(name, previous_storage) diff --git a/graphrag/index/update/relationships.py b/packages/graphrag/graphrag/index/update/relationships.py similarity index 97% rename from graphrag/index/update/relationships.py rename to packages/graphrag/graphrag/index/update/relationships.py index 8174d9cca3..e08cb8493a 100644 --- a/graphrag/index/update/relationships.py +++ b/packages/graphrag/graphrag/index/update/relationships.py @@ -50,7 +50,8 @@ def _update_and_merge_relationships( # Group by title and resolve conflicts aggregated = ( - merged_relationships.groupby(["source", "target"]) + merged_relationships + .groupby(["source", "target"]) .agg({ "id": "first", "human_readable_id": "first", diff --git a/graphrag/index/utils/__init__.py b/packages/graphrag/graphrag/index/utils/__init__.py similarity index 100% rename from graphrag/index/utils/__init__.py rename to packages/graphrag/graphrag/index/utils/__init__.py diff --git a/graphrag/index/utils/dataframes.py b/packages/graphrag/graphrag/index/utils/dataframes.py similarity index 100% rename from graphrag/index/utils/dataframes.py rename to packages/graphrag/graphrag/index/utils/dataframes.py diff --git a/graphrag/index/utils/derive_from_rows.py b/packages/graphrag/graphrag/index/utils/derive_from_rows.py similarity index 96% rename from graphrag/index/utils/derive_from_rows.py rename to packages/graphrag/graphrag/index/utils/derive_from_rows.py index 663d78c1c8..bb4e8a3016 100644 --- a/graphrag/index/utils/derive_from_rows.py +++ b/packages/graphrag/graphrag/index/utils/derive_from_rows.py @@ -1,7 +1,7 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""Apply a generic transform function to each row in a table.""" +"""A module containing derive_from_rows, derive_from_rows_asyncio_threads, and derive_from_rows_asyncio methods.""" import asyncio import inspect @@ -55,9 +55,6 @@ async def derive_from_rows( raise ValueError(msg) -"""A module containing the derive_from_rows_async method.""" - - async def derive_from_rows_asyncio_threads( input: pd.DataFrame, transform: Callable[[pd.Series], Awaitable[ItemType]], @@ -88,9 +85,6 @@ async def execute_task(task: Coroutine) -> ItemType | None: ) -"""A module containing the derive_from_rows_async method.""" - - async def derive_from_rows_asyncio( input: pd.DataFrame, transform: Callable[[pd.Series], Awaitable[ItemType]], diff --git a/graphrag/index/utils/dicts.py b/packages/graphrag/graphrag/index/utils/dicts.py similarity index 100% rename from graphrag/index/utils/dicts.py rename to packages/graphrag/graphrag/index/utils/dicts.py diff --git a/graphrag/index/utils/graphs.py b/packages/graphrag/graphrag/index/utils/graphs.py similarity index 61% rename from graphrag/index/utils/graphs.py rename to packages/graphrag/graphrag/index/utils/graphs.py index 6c06e05cbc..02f2693cbe 100644 --- a/graphrag/index/utils/graphs.py +++ b/packages/graphrag/graphrag/index/utils/graphs.py @@ -1,22 +1,136 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -"""Collection of graph utility functions.""" +""" +Collection of graph utility functions. + +These are largely copies/re-implementations of graspologic methods to avoid dependency issues. +""" import logging -from typing import cast +import math +from collections import defaultdict +from typing import Any, cast +import graspologic_native as gn import networkx as nx import numpy as np import pandas as pd -from graspologic.partition import hierarchical_leiden, modularity -from graspologic.utils import largest_connected_component from graphrag.config.enums import ModularityMetric logger = logging.getLogger(__name__) +def largest_connected_component(graph: nx.Graph) -> nx.Graph: + """Return the largest connected component of the graph.""" + graph = graph.copy() + lcc_nodes = max(nx.connected_components(graph), key=len) + lcc = graph.subgraph(lcc_nodes).copy() + lcc.remove_nodes_from([n for n in lcc if n not in lcc_nodes]) + return cast("nx.Graph", lcc) + + +def _nx_to_edge_list( + graph: nx.Graph, + weight_attribute: str = "weight", + weight_default: float = 1.0, +) -> list[tuple[str, str, float]]: + """ + Convert an undirected, non-multigraph networkx graph to a list of edges. + + Each edge is represented as a tuple of (source_str, target_str, weight). + """ + edge_list: list[tuple[str, str, float]] = [] + + # Decide how to retrieve the weight data + edge_iter = graph.edges(data=weight_attribute, default=weight_default) # type: ignore + + for source, target, weight in edge_iter: + source_str = str(source) + target_str = str(target) + edge_list.append((source_str, target_str, float(weight))) + + return edge_list + + +def hierarchical_leiden( + graph: nx.Graph, + max_cluster_size: int = 10, + random_seed: int | None = 0xDEADBEEF, +) -> list[gn.HierarchicalCluster]: + """Run hierarchical leiden on the graph.""" + return gn.hierarchical_leiden( + edges=_nx_to_edge_list(graph), + max_cluster_size=max_cluster_size, + seed=random_seed, + starting_communities=None, + resolution=1.0, + randomness=0.001, + use_modularity=True, + iterations=1, + ) + + +def modularity( + graph: nx.Graph, + partitions: dict[Any, int], + weight_attribute: str = "weight", + resolution: float = 1.0, +) -> float: + """Given an undirected graph and a dictionary of vertices to community ids, calculate the modularity.""" + components = _modularity_components(graph, partitions, weight_attribute, resolution) + return sum(components.values()) + + +def _modularity_component( + intra_community_degree: float, + total_community_degree: float, + network_degree_sum: float, + resolution: float, +) -> float: + community_degree_ratio = math.pow(total_community_degree, 2.0) / ( + 2.0 * network_degree_sum + ) + return (intra_community_degree - resolution * community_degree_ratio) / ( + 2.0 * network_degree_sum + ) + + +def _modularity_components( + graph: nx.Graph, + partitions: dict[Any, int], + weight_attribute: str = "weight", + resolution: float = 1.0, +) -> dict[int, float]: + total_edge_weight = 0.0 + communities = set(partitions.values()) + + degree_sums_within_community: dict[int, float] = defaultdict(lambda: 0.0) + degree_sums_for_community: dict[int, float] = defaultdict(lambda: 0.0) + for vertex, neighbor_vertex, weight in graph.edges(data=weight_attribute): + vertex_community = partitions[vertex] + neighbor_community = partitions[neighbor_vertex] + if vertex_community == neighbor_community: + if vertex == neighbor_vertex: + degree_sums_within_community[vertex_community] += weight + else: + degree_sums_within_community[vertex_community] += weight * 2.0 + degree_sums_for_community[vertex_community] += weight + degree_sums_for_community[neighbor_community] += weight + total_edge_weight += weight + + return { + comm: _modularity_component( + degree_sums_within_community[comm], + degree_sums_for_community[comm], + total_edge_weight, + resolution, + ) + for comm in communities + } + + def calculate_root_modularity( graph: nx.Graph, max_cluster_size: int = 10, @@ -26,7 +140,7 @@ def calculate_root_modularity( hcs = hierarchical_leiden( graph, max_cluster_size=max_cluster_size, random_seed=random_seed ) - root_clusters = hcs.first_level_hierarchical_clustering() + root_clusters = first_level_hierarchical_clustering(hcs) return modularity(graph, root_clusters) @@ -39,7 +153,7 @@ def calculate_leaf_modularity( hcs = hierarchical_leiden( graph, max_cluster_size=max_cluster_size, random_seed=random_seed ) - leaf_clusters = hcs.final_level_hierarchical_clustering() + leaf_clusters = final_level_hierarchical_clustering(hcs) return modularity(graph, leaf_clusters) @@ -147,9 +261,6 @@ def calculate_modularity( random_seed=random_seed, use_root_modularity=use_root_modularity, ) - case _: - msg = f"Unknown modularity metric type: {modularity_metric}" - raise ValueError(msg) def calculate_pmi_edge_weights( @@ -181,14 +292,16 @@ def calculate_pmi_edge_weights( edges_df["prop_weight"] = edges_df[edge_weight_col] / total_edge_weights edges_df = ( - edges_df.merge( + edges_df + .merge( copied_nodes_df, left_on=edge_source_col, right_on=node_name_col, how="left" ) .drop(columns=[node_name_col]) .rename(columns={"prop_occurrence": "source_prop"}) ) edges_df = ( - edges_df.merge( + edges_df + .merge( copied_nodes_df, left_on=edge_target_col, right_on=node_name_col, how="left" ) .drop(columns=[node_name_col]) @@ -227,8 +340,10 @@ def calculate_rrf_edge_weights( method="min", ascending=False ) edges_df[edge_weight_col] = edges_df.apply( - lambda x: (1 / (rrf_smoothing_factor + x["pmi_rank"])) - + (1 / (rrf_smoothing_factor + x["raw_weight_rank"])), + lambda x: ( + (1 / (rrf_smoothing_factor + x["pmi_rank"])) + + (1 / (rrf_smoothing_factor + x["raw_weight_rank"])) + ), axis=1, ) @@ -240,3 +355,32 @@ def get_upper_threshold_by_std(data: list[float] | list[int], std_trim: float) - mean = np.mean(data) std = np.std(data) return cast("float", mean + std_trim * std) + + +def first_level_hierarchical_clustering( + hcs: list[gn.HierarchicalCluster], +) -> dict[Any, int]: + """first_level_hierarchical_clustering. + + Returns + ------- + dict[Any, int] + The initial leiden algorithm clustering results as a dictionary + of node id to community id. + """ + return {entry.node: entry.cluster for entry in hcs if entry.level == 0} + + +def final_level_hierarchical_clustering( + hcs: list[gn.HierarchicalCluster], +) -> dict[Any, int]: + """ + final_level_hierarchical_clustering. + + Returns + ------- + dict[Any, int] + The last leiden algorithm clustering results as a dictionary + of node id to community id. + """ + return {entry.node: entry.cluster for entry in hcs if entry.is_final_cluster} diff --git a/graphrag/index/utils/hashing.py b/packages/graphrag/graphrag/index/utils/hashing.py similarity index 100% rename from graphrag/index/utils/hashing.py rename to packages/graphrag/graphrag/index/utils/hashing.py diff --git a/graphrag/index/utils/is_null.py b/packages/graphrag/graphrag/index/utils/is_null.py similarity index 100% rename from graphrag/index/utils/is_null.py rename to packages/graphrag/graphrag/index/utils/is_null.py diff --git a/graphrag/index/utils/stable_lcc.py b/packages/graphrag/graphrag/index/utils/stable_lcc.py similarity index 94% rename from graphrag/index/utils/stable_lcc.py rename to packages/graphrag/graphrag/index/utils/stable_lcc.py index 070311331b..806c0fd486 100644 --- a/graphrag/index/utils/stable_lcc.py +++ b/packages/graphrag/graphrag/index/utils/stable_lcc.py @@ -8,12 +8,11 @@ import networkx as nx +from graphrag.index.utils.graphs import largest_connected_component + def stable_largest_connected_component(graph: nx.Graph) -> nx.Graph: """Return the largest connected component of the graph, with nodes and edges sorted in a stable way.""" - # NOTE: The import is done here to reduce the initial import time of the module - from graspologic.utils import largest_connected_component - graph = graph.copy() graph = cast("nx.Graph", largest_connected_component(graph)) graph = normalize_node_names(graph) diff --git a/graphrag/index/utils/string.py b/packages/graphrag/graphrag/index/utils/string.py similarity index 100% rename from graphrag/index/utils/string.py rename to packages/graphrag/graphrag/index/utils/string.py diff --git a/graphrag/index/utils/uuid.py b/packages/graphrag/graphrag/index/utils/uuid.py similarity index 100% rename from graphrag/index/utils/uuid.py rename to packages/graphrag/graphrag/index/utils/uuid.py diff --git a/packages/graphrag/graphrag/index/validate_config.py b/packages/graphrag/graphrag/index/validate_config.py new file mode 100644 index 0000000000..4062b8de9a --- /dev/null +++ b/packages/graphrag/graphrag/index/validate_config.py @@ -0,0 +1,41 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing validate_config_names definition.""" + +import asyncio +import logging +import sys + +from graphrag_llm.completion import create_completion +from graphrag_llm.embedding import create_embedding + +from graphrag.config.models.graph_rag_config import GraphRagConfig + +logger = logging.getLogger(__name__) + + +def validate_config_names(parameters: GraphRagConfig) -> None: + """Validate config file for model deployment name typos, by running a quick test message for each.""" + for id, config in parameters.completion_models.items(): + llm = create_completion(config) + try: + llm.completion(messages="This is an LLM connectivity test. Say Hello World") + logger.info("LLM Config Params Validated") + except Exception as e: # noqa: BLE001 + logger.error(f"LLM configuration error detected.\n{e}") # noqa + print(f"Failed to validate language model ({id}) params", e) # noqa: T201 + sys.exit(1) + for id, config in parameters.embedding_models.items(): + embed_llm = create_embedding(config) + try: + asyncio.run( + embed_llm.embedding_async( + input=["This is an LLM Embedding Test String"] + ) + ) + logger.info("Embedding LLM Config Params Validated") + except Exception as e: # noqa: BLE001 + logger.error(f"Embedding configuration error detected.\n{e}") # noqa + print(f"Failed to validate embedding model ({id}) params", e) # noqa: T201 + sys.exit(1) diff --git a/graphrag/index/workflows/__init__.py b/packages/graphrag/graphrag/index/workflows/__init__.py similarity index 98% rename from graphrag/index/workflows/__init__.py rename to packages/graphrag/graphrag/index/workflows/__init__.py index 5567baceff..6dee90c097 100644 --- a/graphrag/index/workflows/__init__.py +++ b/packages/graphrag/graphrag/index/workflows/__init__.py @@ -74,7 +74,7 @@ ) # register all of our built-in workflows at once -PipelineFactory.register_all({ +PipelineFactory.register_all({ # noqa: RUF067 "load_input_documents": run_load_input_documents, "load_update_documents": run_load_update_documents, "create_base_text_units": run_create_base_text_units, diff --git a/packages/graphrag/graphrag/index/workflows/create_base_text_units.py b/packages/graphrag/graphrag/index/workflows/create_base_text_units.py new file mode 100644 index 0000000000..ec6abc2578 --- /dev/null +++ b/packages/graphrag/graphrag/index/workflows/create_base_text_units.py @@ -0,0 +1,118 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A module containing run_workflow method definition.""" + +import logging +from typing import Any, cast + +import pandas as pd +from graphrag_chunking.chunker import Chunker +from graphrag_chunking.chunker_factory import create_chunker +from graphrag_chunking.transformers import add_metadata +from graphrag_input import TextDocument +from graphrag_llm.tokenizer import Tokenizer + +from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks +from graphrag.config.models.graph_rag_config import GraphRagConfig +from graphrag.index.typing.context import PipelineRunContext +from graphrag.index.typing.workflow import WorkflowFunctionOutput +from graphrag.index.utils.hashing import gen_sha512_hash +from graphrag.logger.progress import progress_ticker +from graphrag.tokenizer.get_tokenizer import get_tokenizer +from graphrag.utils.storage import load_table_from_storage, write_table_to_storage + +logger = logging.getLogger(__name__) + + +async def run_workflow( + config: GraphRagConfig, + context: PipelineRunContext, +) -> WorkflowFunctionOutput: + """All the steps to transform base text_units.""" + logger.info("Workflow started: create_base_text_units") + documents = await load_table_from_storage("documents", context.output_storage) + + tokenizer = get_tokenizer(encoding_model=config.chunking.encoding_model) + chunker = create_chunker(config.chunking, tokenizer.encode, tokenizer.decode) + output = create_base_text_units( + documents, + context.callbacks, + tokenizer=tokenizer, + chunker=chunker, + prepend_metadata=config.chunking.prepend_metadata, + ) + + await write_table_to_storage(output, "text_units", context.output_storage) + + logger.info("Workflow completed: create_base_text_units") + return WorkflowFunctionOutput(result=output) + + +def create_base_text_units( + documents: pd.DataFrame, + callbacks: WorkflowCallbacks, + tokenizer: Tokenizer, + chunker: Chunker, + prepend_metadata: list[str] | None = None, +) -> pd.DataFrame: + """All the steps to transform base text_units.""" + documents.sort_values(by=["id"], ascending=[True], inplace=True) + + total_rows = len(documents) + tick = progress_ticker(callbacks.progress, total_rows) + + # Track progress of row-wise apply operation + logger.info("Starting chunking process for %d documents", total_rows) + + def chunker_with_logging(row: pd.Series, row_index: int) -> Any: + if prepend_metadata: + # create a standard text document for metadata plucking + # ignore any additional fields in case the input dataframe has extra columns + document = TextDocument( + id=row["id"], + title=row["title"], + text=row["text"], + creation_date=row["creation_date"], + raw_data=row["raw_data"], + ) + metadata = document.collect(prepend_metadata) + transformer = add_metadata( + metadata=metadata, line_delimiter=".\n" + ) # delim with . for back-compat older indexes + else: + transformer = None + + row["chunks"] = [ + chunk.text for chunk in chunker.chunk(row["text"], transform=transformer) + ] + + tick() + logger.info("chunker progress: %d/%d", row_index + 1, total_rows) + return row + + text_units = documents.apply( + lambda row: chunker_with_logging(row, row.name), axis=1 + ) + + text_units = cast("pd.DataFrame", text_units[["id", "chunks"]]) + text_units = text_units.explode("chunks") + text_units.rename( + columns={ + "id": "document_id", + "chunks": "text", + }, + inplace=True, + ) + + text_units["id"] = text_units.apply( + lambda row: gen_sha512_hash(row, ["text"]), axis=1 + ) + # get a final token measurement + text_units["n_tokens"] = text_units["text"].apply( + lambda x: len(tokenizer.encode(x)) + ) + + return cast( + "pd.DataFrame", text_units[text_units["text"].notna()].reset_index(drop=True) + ) diff --git a/graphrag/index/workflows/create_communities.py b/packages/graphrag/graphrag/index/workflows/create_communities.py similarity index 98% rename from graphrag/index/workflows/create_communities.py rename to packages/graphrag/graphrag/index/workflows/create_communities.py index c06d5f4b28..4394593e99 100644 --- a/graphrag/index/workflows/create_communities.py +++ b/packages/graphrag/graphrag/index/workflows/create_communities.py @@ -96,7 +96,8 @@ def create_communities( matched = targets.loc[targets["community_x"] == targets["community_y"]] text_units = matched.explode("text_unit_ids") grouped = ( - text_units.groupby(["community_x", "level_x", "parent_x"]) + text_units + .groupby(["community_x", "level_x", "parent_x"]) .agg(relationship_ids=("id", list), text_unit_ids=("text_unit_ids", list)) .reset_index() ) diff --git a/graphrag/index/workflows/create_community_reports.py b/packages/graphrag/graphrag/index/workflows/create_community_reports.py similarity index 75% rename from graphrag/index/workflows/create_community_reports.py rename to packages/graphrag/graphrag/index/workflows/create_community_reports.py index bf98655c01..abfdeca45a 100644 --- a/graphrag/index/workflows/create_community_reports.py +++ b/packages/graphrag/graphrag/index/workflows/create_community_reports.py @@ -4,16 +4,17 @@ """A module containing run_workflow method definition.""" import logging +from typing import TYPE_CHECKING import pandas as pd +from graphrag_llm.completion import create_completion +from graphrag_llm.tokenizer import Tokenizer import graphrag.data_model.schemas as schemas -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag.cache.cache_key_creator import cache_key_creator from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks -from graphrag.config.defaults import graphrag_config_defaults from graphrag.config.enums import AsyncType from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.config.models.language_model_config import LanguageModelConfig from graphrag.index.operations.finalize_community_reports import ( finalize_community_reports, ) @@ -29,13 +30,15 @@ ) from graphrag.index.typing.context import PipelineRunContext from graphrag.index.typing.workflow import WorkflowFunctionOutput -from graphrag.tokenizer.get_tokenizer import get_tokenizer from graphrag.utils.storage import ( load_table_from_storage, storage_has_table, write_table_to_storage, ) +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + logger = logging.getLogger(__name__) @@ -54,25 +57,32 @@ async def run_workflow( ): claims = await load_table_from_storage("covariates", context.output_storage) - community_reports_llm_settings = config.get_language_model_config( - config.community_reports.model_id + model_config = config.get_completion_model_config( + config.community_reports.completion_model_id ) - async_mode = community_reports_llm_settings.async_mode - num_threads = community_reports_llm_settings.concurrent_requests - summarization_strategy = config.community_reports.resolved_strategy( - config.root_dir, community_reports_llm_settings + prompts = config.community_reports.resolved_prompts() + + model = create_completion( + model_config, + cache=context.cache.child(config.community_reports.model_instance_name), + cache_key_creator=cache_key_creator, ) + tokenizer = model.tokenizer + output = await create_community_reports( edges_input=edges, entities=entities, communities=communities, claims_input=claims, callbacks=context.callbacks, - cache=context.cache, - summarization_strategy=summarization_strategy, - async_mode=async_mode, - num_threads=num_threads, + model=model, + tokenizer=tokenizer, + prompt=prompts.graph_prompt, + max_input_length=config.community_reports.max_input_length, + max_report_length=config.community_reports.max_length, + num_threads=config.concurrent_requests, + async_type=config.async_mode, ) await write_table_to_storage(output, "community_reports", context.output_storage) @@ -87,10 +97,13 @@ async def create_community_reports( communities: pd.DataFrame, claims_input: pd.DataFrame | None, callbacks: WorkflowCallbacks, - cache: PipelineCache, - summarization_strategy: dict, - async_mode: AsyncType = AsyncType.AsyncIO, - num_threads: int = 4, + model: "LLMCompletion", + tokenizer: Tokenizer, + prompt: str, + max_input_length: int, + max_report_length: int, + num_threads: int, + async_type: AsyncType, ) -> pd.DataFrame: """All the steps to transform community reports.""" nodes = explode_communities(communities, entities) @@ -102,15 +115,6 @@ async def create_community_reports( if claims_input is not None: claims = _prep_claims(claims_input) - summarization_strategy["extraction_prompt"] = summarization_strategy["graph_prompt"] - - model_config = LanguageModelConfig(**summarization_strategy["llm"]) - tokenizer = get_tokenizer(model_config) - - max_input_length = summarization_strategy.get( - "max_input_length", graphrag_config_defaults.community_reports.max_input_length - ) - local_contexts = build_local_context( nodes, edges, @@ -126,12 +130,13 @@ async def create_community_reports( local_contexts, build_level_context, callbacks, - cache, - summarization_strategy, + model=model, + prompt=prompt, tokenizer=tokenizer, max_input_length=max_input_length, - async_mode=async_mode, + max_report_length=max_report_length, num_threads=num_threads, + async_type=async_type, ) return finalize_community_reports(community_reports, communities) @@ -145,7 +150,7 @@ def _prep_nodes(input: pd.DataFrame) -> pd.DataFrame: ) # Create NODE_DETAILS column - input.loc[:, schemas.NODE_DETAILS] = input.loc[ + input.loc[:, schemas.NODE_DETAILS] = input.loc[ # type: ignore :, [ schemas.SHORT_ID, @@ -163,7 +168,7 @@ def _prep_edges(input: pd.DataFrame) -> pd.DataFrame: input.fillna(value={schemas.DESCRIPTION: "No Description"}, inplace=True) # Create EDGE_DETAILS column - input.loc[:, schemas.EDGE_DETAILS] = input.loc[ + input.loc[:, schemas.EDGE_DETAILS] = input.loc[ # type: ignore :, [ schemas.SHORT_ID, @@ -182,7 +187,7 @@ def _prep_claims(input: pd.DataFrame) -> pd.DataFrame: input.fillna(value={schemas.DESCRIPTION: "No Description"}, inplace=True) # Create CLAIM_DETAILS column - input.loc[:, schemas.CLAIM_DETAILS] = input.loc[ + input.loc[:, schemas.CLAIM_DETAILS] = input.loc[ # type: ignore :, [ schemas.SHORT_ID, diff --git a/graphrag/index/workflows/create_community_reports_text.py b/packages/graphrag/graphrag/index/workflows/create_community_reports_text.py similarity index 67% rename from graphrag/index/workflows/create_community_reports_text.py rename to packages/graphrag/graphrag/index/workflows/create_community_reports_text.py index 20286f7c55..8a6be96e68 100644 --- a/graphrag/index/workflows/create_community_reports_text.py +++ b/packages/graphrag/graphrag/index/workflows/create_community_reports_text.py @@ -4,15 +4,16 @@ """A module containing run_workflow method definition.""" import logging +from typing import TYPE_CHECKING import pandas as pd +from graphrag_llm.completion import create_completion +from graphrag_llm.tokenizer import Tokenizer -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag.cache.cache_key_creator import cache_key_creator from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks -from graphrag.config.defaults import graphrag_config_defaults from graphrag.config.enums import AsyncType from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.config.models.language_model_config import LanguageModelConfig from graphrag.index.operations.finalize_community_reports import ( finalize_community_reports, ) @@ -28,9 +29,11 @@ ) from graphrag.index.typing.context import PipelineRunContext from graphrag.index.typing.workflow import WorkflowFunctionOutput -from graphrag.tokenizer.get_tokenizer import get_tokenizer from graphrag.utils.storage import load_table_from_storage, write_table_to_storage +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + logger = logging.getLogger(__name__) @@ -45,24 +48,31 @@ async def run_workflow( text_units = await load_table_from_storage("text_units", context.output_storage) - community_reports_llm_settings = config.get_language_model_config( - config.community_reports.model_id + model_config = config.get_completion_model_config( + config.community_reports.completion_model_id ) - async_mode = community_reports_llm_settings.async_mode - num_threads = community_reports_llm_settings.concurrent_requests - summarization_strategy = config.community_reports.resolved_strategy( - config.root_dir, community_reports_llm_settings + model = create_completion( + model_config, + cache=context.cache.child(config.community_reports.model_instance_name), + cache_key_creator=cache_key_creator, ) + tokenizer = model.tokenizer + + prompts = config.community_reports.resolved_prompts() + output = await create_community_reports_text( entities, communities, text_units, context.callbacks, - context.cache, - summarization_strategy, - async_mode=async_mode, - num_threads=num_threads, + model=model, + tokenizer=tokenizer, + prompt=prompts.text_prompt, + max_input_length=config.community_reports.max_input_length, + max_report_length=config.community_reports.max_length, + num_threads=config.concurrent_requests, + async_type=config.async_mode, ) await write_table_to_storage(output, "community_reports", context.output_storage) @@ -76,23 +86,17 @@ async def create_community_reports_text( communities: pd.DataFrame, text_units: pd.DataFrame, callbacks: WorkflowCallbacks, - cache: PipelineCache, - summarization_strategy: dict, - async_mode: AsyncType = AsyncType.AsyncIO, - num_threads: int = 4, + model: "LLMCompletion", + tokenizer: Tokenizer, + prompt: str, + max_input_length: int, + max_report_length: int, + num_threads: int, + async_type: AsyncType, ) -> pd.DataFrame: """All the steps to transform community reports.""" nodes = explode_communities(communities, entities) - summarization_strategy["extraction_prompt"] = summarization_strategy["text_prompt"] - - max_input_length = summarization_strategy.get( - "max_input_length", graphrag_config_defaults.community_reports.max_input_length - ) - - model_config = LanguageModelConfig(**summarization_strategy["llm"]) - tokenizer = get_tokenizer(model_config) - local_contexts = build_local_context( communities, text_units, nodes, tokenizer, max_input_length ) @@ -103,12 +107,13 @@ async def create_community_reports_text( local_contexts, build_level_context, callbacks, - cache, - summarization_strategy, + model=model, + prompt=prompt, tokenizer=tokenizer, max_input_length=max_input_length, - async_mode=async_mode, + max_report_length=max_report_length, num_threads=num_threads, + async_type=async_type, ) return finalize_community_reports(community_reports, communities) diff --git a/graphrag/index/workflows/create_final_documents.py b/packages/graphrag/graphrag/index/workflows/create_final_documents.py similarity index 81% rename from graphrag/index/workflows/create_final_documents.py rename to packages/graphrag/graphrag/index/workflows/create_final_documents.py index af81e8dfa8..554fbc4254 100644 --- a/graphrag/index/workflows/create_final_documents.py +++ b/packages/graphrag/graphrag/index/workflows/create_final_documents.py @@ -37,19 +37,15 @@ def create_final_documents( documents: pd.DataFrame, text_units: pd.DataFrame ) -> pd.DataFrame: """All the steps to transform final documents.""" - exploded = ( - text_units.explode("document_ids") - .loc[:, ["id", "document_ids", "text"]] - .rename( - columns={ - "document_ids": "chunk_doc_id", - "id": "chunk_id", - "text": "chunk_text", - } - ) + renamed = text_units.loc[:, ["id", "document_id", "text"]].rename( + columns={ + "document_id": "chunk_doc_id", + "id": "chunk_id", + "text": "chunk_text", + } ) - joined = exploded.merge( + joined = renamed.merge( documents, left_on="chunk_doc_id", right_on="id", @@ -71,7 +67,7 @@ def create_final_documents( rejoined["id"] = rejoined["id"].astype(str) rejoined["human_readable_id"] = rejoined.index - if "metadata" not in rejoined.columns: - rejoined["metadata"] = pd.Series(dtype="object") + if "raw_data" not in rejoined.columns: + rejoined["raw_data"] = pd.Series(dtype="object") return rejoined.loc[:, DOCUMENTS_FINAL_COLUMNS] diff --git a/graphrag/index/workflows/create_final_text_units.py b/packages/graphrag/graphrag/index/workflows/create_final_text_units.py similarity index 93% rename from graphrag/index/workflows/create_final_text_units.py rename to packages/graphrag/graphrag/index/workflows/create_final_text_units.py index 373b9b4cb4..c16e08bb7c 100644 --- a/graphrag/index/workflows/create_final_text_units.py +++ b/packages/graphrag/graphrag/index/workflows/create_final_text_units.py @@ -59,7 +59,7 @@ def create_final_text_units( final_covariates: pd.DataFrame | None, ) -> pd.DataFrame: """All the steps to transform the text units.""" - selected = text_units.loc[:, ["id", "text", "document_ids", "n_tokens"]] + selected = text_units.loc[:, ["id", "text", "document_id", "n_tokens"]] selected["human_readable_id"] = selected.index entity_join = _entities(final_entities) @@ -88,7 +88,8 @@ def _entities(df: pd.DataFrame) -> pd.DataFrame: unrolled = selected.explode(["text_unit_ids"]).reset_index(drop=True) return ( - unrolled.groupby("text_unit_ids", sort=False) + unrolled + .groupby("text_unit_ids", sort=False) .agg(entity_ids=("id", "unique")) .reset_index() .rename(columns={"text_unit_ids": "id"}) @@ -100,7 +101,8 @@ def _relationships(df: pd.DataFrame) -> pd.DataFrame: unrolled = selected.explode(["text_unit_ids"]).reset_index(drop=True) return ( - unrolled.groupby("text_unit_ids", sort=False) + unrolled + .groupby("text_unit_ids", sort=False) .agg(relationship_ids=("id", "unique")) .reset_index() .rename(columns={"text_unit_ids": "id"}) @@ -111,7 +113,8 @@ def _covariates(df: pd.DataFrame) -> pd.DataFrame: selected = df.loc[:, ["id", "text_unit_id"]] return ( - selected.groupby("text_unit_id", sort=False) + selected + .groupby("text_unit_id", sort=False) .agg(covariate_ids=("id", "unique")) .reset_index() .rename(columns={"text_unit_id": "id"}) diff --git a/graphrag/index/workflows/extract_covariates.py b/packages/graphrag/graphrag/index/workflows/extract_covariates.py similarity index 62% rename from graphrag/index/workflows/extract_covariates.py rename to packages/graphrag/graphrag/index/workflows/extract_covariates.py index 63adb1d0b9..18b470a8b1 100644 --- a/graphrag/index/workflows/extract_covariates.py +++ b/packages/graphrag/graphrag/index/workflows/extract_covariates.py @@ -4,13 +4,15 @@ """A module containing run_workflow method definition.""" import logging -from typing import Any +from typing import TYPE_CHECKING from uuid import uuid4 import pandas as pd +from graphrag_llm.completion import create_completion -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag.cache.cache_key_creator import cache_key_creator from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks +from graphrag.config.defaults import DEFAULT_ENTITY_TYPES from graphrag.config.enums import AsyncType from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.data_model.schemas import COVARIATES_FINAL_COLUMNS @@ -21,6 +23,9 @@ from graphrag.index.typing.workflow import WorkflowFunctionOutput from graphrag.utils.storage import load_table_from_storage, write_table_to_storage +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + logger = logging.getLogger(__name__) @@ -34,25 +39,29 @@ async def run_workflow( if config.extract_claims.enabled: text_units = await load_table_from_storage("text_units", context.output_storage) - extract_claims_llm_settings = config.get_language_model_config( - config.extract_claims.model_id + model_config = config.get_completion_model_config( + config.extract_claims.completion_model_id ) - extraction_strategy = config.extract_claims.resolved_strategy( - config.root_dir, extract_claims_llm_settings + + model = create_completion( + model_config, + cache=context.cache.child(config.extract_claims.model_instance_name), + cache_key_creator=cache_key_creator, ) - async_mode = extract_claims_llm_settings.async_mode - num_threads = extract_claims_llm_settings.concurrent_requests + prompts = config.extract_claims.resolved_prompts() output = await extract_covariates( - text_units, - context.callbacks, - context.cache, - "claim", - extraction_strategy, - async_mode=async_mode, - entity_types=None, - num_threads=num_threads, + text_units=text_units, + callbacks=context.callbacks, + model=model, + covariate_type="claim", + max_gleanings=config.extract_claims.max_gleanings, + claim_description=config.extract_claims.description, + prompt=prompts.extraction_prompt, + entity_types=DEFAULT_ENTITY_TYPES, + num_threads=config.concurrent_requests, + async_type=config.async_mode, ) await write_table_to_storage(output, "covariates", context.output_storage) @@ -64,27 +73,32 @@ async def run_workflow( async def extract_covariates( text_units: pd.DataFrame, callbacks: WorkflowCallbacks, - cache: PipelineCache, + model: "LLMCompletion", covariate_type: str, - extraction_strategy: dict[str, Any] | None, - async_mode: AsyncType = AsyncType.AsyncIO, - entity_types: list[str] | None = None, - num_threads: int = 4, + max_gleanings: int, + claim_description: str, + prompt: str, + entity_types: list[str], + num_threads: int, + async_type: AsyncType, ) -> pd.DataFrame: """All the steps to extract and format covariates.""" # reassign the id because it will be overwritten in the output by a covariate one # this also results in text_unit_id being copied to the output covariate table text_units["text_unit_id"] = text_units["id"] + covariates = await extractor( input=text_units, callbacks=callbacks, - cache=cache, + model=model, column="text", covariate_type=covariate_type, - strategy=extraction_strategy, - async_mode=async_mode, + max_gleanings=max_gleanings, + claim_description=claim_description, + prompt=prompt, entity_types=entity_types, num_threads=num_threads, + async_type=async_type, ) text_units.drop(columns=["text_unit_id"], inplace=True) # don't pollute the global covariates["id"] = covariates["covariate_type"].apply(lambda _x: str(uuid4())) diff --git a/graphrag/index/workflows/extract_graph.py b/packages/graphrag/graphrag/index/workflows/extract_graph.py similarity index 58% rename from graphrag/index/workflows/extract_graph.py rename to packages/graphrag/graphrag/index/workflows/extract_graph.py index 592502f6da..6d6520e401 100644 --- a/graphrag/index/workflows/extract_graph.py +++ b/packages/graphrag/graphrag/index/workflows/extract_graph.py @@ -4,11 +4,12 @@ """A module containing run_workflow method definition.""" import logging -from typing import Any +from typing import TYPE_CHECKING import pandas as pd +from graphrag_llm.completion import create_completion -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag.cache.cache_key_creator import cache_key_creator from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks from graphrag.config.enums import AsyncType from graphrag.config.models.graph_rag_config import GraphRagConfig @@ -22,6 +23,9 @@ from graphrag.index.typing.workflow import WorkflowFunctionOutput from graphrag.utils.storage import load_table_from_storage, write_table_to_storage +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + logger = logging.getLogger(__name__) @@ -33,30 +37,40 @@ async def run_workflow( logger.info("Workflow started: extract_graph") text_units = await load_table_from_storage("text_units", context.output_storage) - extract_graph_llm_settings = config.get_language_model_config( - config.extract_graph.model_id + extraction_model_config = config.get_completion_model_config( + config.extract_graph.completion_model_id ) - extraction_strategy = config.extract_graph.resolved_strategy( - config.root_dir, extract_graph_llm_settings + extraction_prompts = config.extract_graph.resolved_prompts() + extraction_model = create_completion( + extraction_model_config, + cache=context.cache.child(config.extract_graph.model_instance_name), + cache_key_creator=cache_key_creator, ) - summarization_llm_settings = config.get_language_model_config( - config.summarize_descriptions.model_id + summarization_model_config = config.get_completion_model_config( + config.summarize_descriptions.completion_model_id ) - summarization_strategy = config.summarize_descriptions.resolved_strategy( - config.root_dir, summarization_llm_settings + summarization_prompts = config.summarize_descriptions.resolved_prompts() + summarization_model = create_completion( + summarization_model_config, + cache=context.cache.child(config.summarize_descriptions.model_instance_name), + cache_key_creator=cache_key_creator, ) entities, relationships, raw_entities, raw_relationships = await extract_graph( text_units=text_units, callbacks=context.callbacks, - cache=context.cache, - extraction_strategy=extraction_strategy, - extraction_num_threads=extract_graph_llm_settings.concurrent_requests, - extraction_async_mode=extract_graph_llm_settings.async_mode, + extraction_model=extraction_model, + extraction_prompt=extraction_prompts.extraction_prompt, entity_types=config.extract_graph.entity_types, - summarization_strategy=summarization_strategy, - summarization_num_threads=summarization_llm_settings.concurrent_requests, + max_gleanings=config.extract_graph.max_gleanings, + extraction_num_threads=config.concurrent_requests, + extraction_async_type=config.async_mode, + summarization_model=summarization_model, + max_summary_length=config.summarize_descriptions.max_length, + max_input_tokens=config.summarize_descriptions.max_input_tokens, + summarization_prompt=summarization_prompts.summarize_prompt, + summarization_num_threads=config.concurrent_requests, ) await write_table_to_storage(entities, "entities", context.output_storage) @@ -82,36 +96,41 @@ async def run_workflow( async def extract_graph( text_units: pd.DataFrame, callbacks: WorkflowCallbacks, - cache: PipelineCache, - extraction_strategy: dict[str, Any] | None = None, - extraction_num_threads: int = 4, - extraction_async_mode: AsyncType = AsyncType.AsyncIO, - entity_types: list[str] | None = None, - summarization_strategy: dict[str, Any] | None = None, - summarization_num_threads: int = 4, + extraction_model: "LLMCompletion", + extraction_prompt: str, + entity_types: list[str], + max_gleanings: int, + extraction_num_threads: int, + extraction_async_type: AsyncType, + summarization_model: "LLMCompletion", + max_summary_length: int, + max_input_tokens: int, + summarization_prompt: str, + summarization_num_threads: int, ) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]: """All the steps to create the base entity graph.""" # this returns a graph for each text unit, to be merged later extracted_entities, extracted_relationships = await extractor( text_units=text_units, callbacks=callbacks, - cache=cache, text_column="text", id_column="id", - strategy=extraction_strategy, - async_mode=extraction_async_mode, + model=extraction_model, + prompt=extraction_prompt, entity_types=entity_types, + max_gleanings=max_gleanings, num_threads=extraction_num_threads, + async_type=extraction_async_type, ) - if not _validate_data(extracted_entities): - error_msg = "Entity Extraction failed. No entities detected during extraction." + if len(extracted_entities) == 0: + error_msg = "Graph Extraction failed. No entities detected during extraction." logger.error(error_msg) raise ValueError(error_msg) - if not _validate_data(extracted_relationships): + if len(extracted_relationships) == 0: error_msg = ( - "Entity Extraction failed. No relationships detected during extraction." + "Graph Extraction failed. No relationships detected during extraction." ) logger.error(error_msg) raise ValueError(error_msg) @@ -124,9 +143,11 @@ async def extract_graph( extracted_entities=extracted_entities, extracted_relationships=extracted_relationships, callbacks=callbacks, - cache=cache, - summarization_strategy=summarization_strategy, - summarization_num_threads=summarization_num_threads, + model=summarization_model, + max_summary_length=max_summary_length, + max_input_tokens=max_input_tokens, + summarization_prompt=summarization_prompt, + num_threads=summarization_num_threads, ) return (entities, relationships, raw_entities, raw_relationships) @@ -136,18 +157,22 @@ async def get_summarized_entities_relationships( extracted_entities: pd.DataFrame, extracted_relationships: pd.DataFrame, callbacks: WorkflowCallbacks, - cache: PipelineCache, - summarization_strategy: dict[str, Any] | None = None, - summarization_num_threads: int = 4, + model: "LLMCompletion", + max_summary_length: int, + max_input_tokens: int, + summarization_prompt: str, + num_threads: int, ) -> tuple[pd.DataFrame, pd.DataFrame]: """Summarize the entities and relationships.""" entity_summaries, relationship_summaries = await summarize_descriptions( entities_df=extracted_entities, relationships_df=extracted_relationships, callbacks=callbacks, - cache=cache, - strategy=summarization_strategy, - num_threads=summarization_num_threads, + model=model, + max_summary_length=max_summary_length, + max_input_tokens=max_input_tokens, + prompt=summarization_prompt, + num_threads=num_threads, ) relationships = extracted_relationships.drop(columns=["description"]).merge( @@ -157,8 +182,3 @@ async def get_summarized_entities_relationships( extracted_entities.drop(columns=["description"], inplace=True) entities = extracted_entities.merge(entity_summaries, on="title", how="left") return entities, relationships - - -def _validate_data(df: pd.DataFrame) -> bool: - """Validate that the dataframe has data.""" - return len(df) > 0 diff --git a/graphrag/index/workflows/extract_graph_nlp.py b/packages/graphrag/graphrag/index/workflows/extract_graph_nlp.py similarity index 64% rename from graphrag/index/workflows/extract_graph_nlp.py rename to packages/graphrag/graphrag/index/workflows/extract_graph_nlp.py index 90afedf46c..38810e5de4 100644 --- a/graphrag/index/workflows/extract_graph_nlp.py +++ b/packages/graphrag/graphrag/index/workflows/extract_graph_nlp.py @@ -6,11 +6,14 @@ import logging import pandas as pd +from graphrag_cache import Cache -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.config.models.extract_graph_nlp_config import ExtractGraphNLPConfig +from graphrag.config.enums import AsyncType from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.index.operations.build_noun_graph.build_noun_graph import build_noun_graph +from graphrag.index.operations.build_noun_graph.np_extractors.base import ( + BaseNounPhraseExtractor, +) from graphrag.index.operations.build_noun_graph.np_extractors.factory import ( create_noun_phrase_extractor, ) @@ -29,10 +32,16 @@ async def run_workflow( logger.info("Workflow started: extract_graph_nlp") text_units = await load_table_from_storage("text_units", context.output_storage) + text_analyzer_config = config.extract_graph_nlp.text_analyzer + text_analyzer = create_noun_phrase_extractor(text_analyzer_config) + entities, relationships = await extract_graph_nlp( text_units, context.cache, - extraction_config=config.extract_graph_nlp, + text_analyzer=text_analyzer, + normalize_edge_weights=config.extract_graph_nlp.normalize_edge_weights, + num_threads=config.extract_graph_nlp.concurrent_requests, + async_type=config.extract_graph_nlp.async_mode, ) await write_table_to_storage(entities, "entities", context.output_storage) @@ -50,21 +59,35 @@ async def run_workflow( async def extract_graph_nlp( text_units: pd.DataFrame, - cache: PipelineCache, - extraction_config: ExtractGraphNLPConfig, + cache: Cache, + text_analyzer: BaseNounPhraseExtractor, + normalize_edge_weights: bool, + num_threads: int, + async_type: AsyncType, ) -> tuple[pd.DataFrame, pd.DataFrame]: """All the steps to create the base entity graph.""" - text_analyzer_config = extraction_config.text_analyzer - text_analyzer = create_noun_phrase_extractor(text_analyzer_config) extracted_nodes, extracted_edges = await build_noun_graph( text_units, text_analyzer=text_analyzer, - normalize_edge_weights=extraction_config.normalize_edge_weights, - num_threads=extraction_config.concurrent_requests, - async_mode=extraction_config.async_mode, + normalize_edge_weights=normalize_edge_weights, + num_threads=num_threads, + async_mode=async_type, cache=cache, ) + if len(extracted_nodes) == 0: + error_msg = ( + "NLP Graph Extraction failed. No entities detected during extraction." + ) + logger.error(error_msg) + raise ValueError(error_msg) + + if len(extracted_edges) == 0: + error_msg = ( + "NLP Graph Extraction failed. No relationships detected during extraction." + ) + logger.error(error_msg) + # add in any other columns required by downstream workflows extracted_nodes["type"] = "NOUN PHRASE" extracted_nodes["description"] = "" diff --git a/graphrag/index/workflows/factory.py b/packages/graphrag/graphrag/index/workflows/factory.py similarity index 100% rename from graphrag/index/workflows/factory.py rename to packages/graphrag/graphrag/index/workflows/factory.py diff --git a/graphrag/index/workflows/finalize_graph.py b/packages/graphrag/graphrag/index/workflows/finalize_graph.py similarity index 83% rename from graphrag/index/workflows/finalize_graph.py rename to packages/graphrag/graphrag/index/workflows/finalize_graph.py index 66827774f2..49529aea3a 100644 --- a/graphrag/index/workflows/finalize_graph.py +++ b/packages/graphrag/graphrag/index/workflows/finalize_graph.py @@ -7,7 +7,6 @@ import pandas as pd -from graphrag.config.models.embed_graph_config import EmbedGraphConfig from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.index.operations.create_graph import create_graph from graphrag.index.operations.finalize_entities import finalize_entities @@ -34,8 +33,6 @@ async def run_workflow( final_entities, final_relationships = finalize_graph( entities, relationships, - embed_config=config.embed_graph, - layout_enabled=config.umap.enabled, ) await write_table_to_storage(final_entities, "entities", context.output_storage) @@ -44,7 +41,6 @@ async def run_workflow( ) if config.snapshots.graphml: - # todo: extract graphs at each level, and add in meta like descriptions graph = create_graph(final_relationships, edge_attr=["weight"]) await snapshot_graphml( @@ -65,12 +61,8 @@ async def run_workflow( def finalize_graph( entities: pd.DataFrame, relationships: pd.DataFrame, - embed_config: EmbedGraphConfig | None = None, - layout_enabled: bool = False, ) -> tuple[pd.DataFrame, pd.DataFrame]: """All the steps to finalize the entity and relationship formats.""" - final_entities = finalize_entities( - entities, relationships, embed_config, layout_enabled - ) + final_entities = finalize_entities(entities, relationships) final_relationships = finalize_relationships(relationships) return (final_entities, final_relationships) diff --git a/graphrag/index/workflows/generate_text_embeddings.py b/packages/graphrag/graphrag/index/workflows/generate_text_embeddings.py similarity index 62% rename from graphrag/index/workflows/generate_text_embeddings.py rename to packages/graphrag/graphrag/index/workflows/generate_text_embeddings.py index 9e6dde6d34..16b726028e 100644 --- a/graphrag/index/workflows/generate_text_embeddings.py +++ b/packages/graphrag/graphrag/index/workflows/generate_text_embeddings.py @@ -4,22 +4,23 @@ """A module containing run_workflow method definition.""" import logging +from typing import TYPE_CHECKING import pandas as pd +from graphrag_llm.embedding import create_embedding +from graphrag_llm.tokenizer import Tokenizer +from graphrag_vectors import ( + VectorStoreConfig, + create_vector_store, +) -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag.cache.cache_key_creator import cache_key_creator from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks from graphrag.config.embeddings import ( community_full_content_embedding, - community_summary_embedding, - community_title_embedding, - document_text_embedding, entity_description_embedding, - entity_title_embedding, - relationship_description_embedding, text_unit_text_embedding, ) -from graphrag.config.get_embedding_settings import get_embedding_settings from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.index.operations.embed_text.embed_text import embed_text from graphrag.index.typing.context import PipelineRunContext @@ -29,6 +30,9 @@ write_table_to_storage, ) +if TYPE_CHECKING: + from graphrag_llm.embedding import LLMEmbedding + logger = logging.getLogger(__name__) @@ -40,44 +44,41 @@ async def run_workflow( logger.info("Workflow started: generate_text_embeddings") embedded_fields = config.embed_text.names logger.info("Embedding the following fields: %s", embedded_fields) - documents = None - relationships = None text_units = None entities = None community_reports = None - if document_text_embedding in embedded_fields: - documents = await load_table_from_storage("documents", context.output_storage) - if relationship_description_embedding in embedded_fields: - relationships = await load_table_from_storage( - "relationships", context.output_storage - ) if text_unit_text_embedding in embedded_fields: text_units = await load_table_from_storage("text_units", context.output_storage) - if ( - entity_title_embedding in embedded_fields - or entity_description_embedding in embedded_fields - ): + if entity_description_embedding in embedded_fields: entities = await load_table_from_storage("entities", context.output_storage) - if ( - community_title_embedding in embedded_fields - or community_summary_embedding in embedded_fields - or community_full_content_embedding in embedded_fields - ): + if community_full_content_embedding in embedded_fields: community_reports = await load_table_from_storage( "community_reports", context.output_storage ) - text_embed = get_embedding_settings(config) + model_config = config.get_embedding_model_config( + config.embed_text.embedding_model_id + ) + + model = create_embedding( + model_config, + cache=context.cache.child(config.embed_text.model_instance_name), + cache_key_creator=cache_key_creator, + ) + + tokenizer = model.tokenizer output = await generate_text_embeddings( - documents=documents, - relationships=relationships, text_units=text_units, entities=entities, community_reports=community_reports, callbacks=context.callbacks, - cache=context.cache, - text_embed_config=text_embed, + model=model, + tokenizer=tokenizer, + batch_size=config.embed_text.batch_size, + batch_max_tokens=config.embed_text.batch_max_tokens, + num_threads=config.concurrent_requests, + vector_store_config=config.vector_store, embedded_fields=embedded_fields, ) @@ -94,38 +95,26 @@ async def run_workflow( async def generate_text_embeddings( - documents: pd.DataFrame | None, - relationships: pd.DataFrame | None, text_units: pd.DataFrame | None, entities: pd.DataFrame | None, community_reports: pd.DataFrame | None, callbacks: WorkflowCallbacks, - cache: PipelineCache, - text_embed_config: dict, + model: "LLMEmbedding", + tokenizer: Tokenizer, + batch_size: int, + batch_max_tokens: int, + num_threads: int, + vector_store_config: VectorStoreConfig, embedded_fields: list[str], ) -> dict[str, pd.DataFrame]: """All the steps to generate all embeddings.""" embedding_param_map = { - document_text_embedding: { - "data": documents.loc[:, ["id", "text"]] if documents is not None else None, - "embed_column": "text", - }, - relationship_description_embedding: { - "data": relationships.loc[:, ["id", "description"]] - if relationships is not None - else None, - "embed_column": "description", - }, text_unit_text_embedding: { "data": text_units.loc[:, ["id", "text"]] if text_units is not None else None, "embed_column": "text", }, - entity_title_embedding: { - "data": entities.loc[:, ["id", "title"]] if entities is not None else None, - "embed_column": "title", - }, entity_description_embedding: { "data": entities.loc[:, ["id", "title", "description"]].assign( title_description=lambda df: df["title"] + ":" + df["description"] @@ -134,18 +123,6 @@ async def generate_text_embeddings( else None, "embed_column": "title_description", }, - community_title_embedding: { - "data": community_reports.loc[:, ["id", "title"]] - if community_reports is not None - else None, - "embed_column": "title", - }, - community_summary_embedding: { - "data": community_reports.loc[:, ["id", "summary"]] - if community_reports is not None - else None, - "embed_column": "summary", - }, community_full_content_embedding: { "data": community_reports.loc[:, ["id", "full_content"]] if community_reports is not None @@ -164,8 +141,12 @@ async def generate_text_embeddings( outputs[field] = await _run_embeddings( name=field, callbacks=callbacks, - cache=cache, - text_embed_config=text_embed_config, + model=model, + tokenizer=tokenizer, + vector_store_config=vector_store_config, + batch_size=batch_size, + batch_max_tokens=batch_max_tokens, + num_threads=num_threads, **embedding_param_map[field], ) return outputs @@ -176,17 +157,29 @@ async def _run_embeddings( data: pd.DataFrame, embed_column: str, callbacks: WorkflowCallbacks, - cache: PipelineCache, - text_embed_config: dict, + model: "LLMEmbedding", + tokenizer: Tokenizer, + batch_size: int, + batch_max_tokens: int, + num_threads: int, + vector_store_config: VectorStoreConfig, ) -> pd.DataFrame: """All the steps to generate single embedding.""" + vector_store = create_vector_store( + vector_store_config, vector_store_config.index_schema[name] + ) + vector_store.connect() + data["embedding"] = await embed_text( input=data, callbacks=callbacks, - cache=cache, + model=model, + tokenizer=tokenizer, embed_column=embed_column, - embedding_name=name, - strategy=text_embed_config["strategy"], + batch_size=batch_size, + batch_max_tokens=batch_max_tokens, + num_threads=num_threads, + vector_store=vector_store, ) return data.loc[:, ["id", "embedding"]] diff --git a/graphrag/index/workflows/load_input_documents.py b/packages/graphrag/graphrag/index/workflows/load_input_documents.py similarity index 67% rename from graphrag/index/workflows/load_input_documents.py rename to packages/graphrag/graphrag/index/workflows/load_input_documents.py index 33e14d0cb2..0a5aa65454 100644 --- a/graphrag/index/workflows/load_input_documents.py +++ b/packages/graphrag/graphrag/index/workflows/load_input_documents.py @@ -6,13 +6,11 @@ import logging import pandas as pd +from graphrag_input import InputReader, create_input_reader from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.config.models.input_config import InputConfig -from graphrag.index.input.factory import create_input from graphrag.index.typing.context import PipelineRunContext from graphrag.index.typing.workflow import WorkflowFunctionOutput -from graphrag.storage.pipeline_storage import PipelineStorage from graphrag.utils.storage import write_table_to_storage logger = logging.getLogger(__name__) @@ -23,10 +21,14 @@ async def run_workflow( context: PipelineRunContext, ) -> WorkflowFunctionOutput: """Load and parse input documents into a standard format.""" - output = await load_input_documents( - config.input, - context.input_storage, - ) + input_reader = create_input_reader(config.input, context.input_storage) + + output = await load_input_documents(input_reader) + + if len(output) == 0: + msg = "Error reading documents, please see logs." + logger.error(msg) + raise ValueError(msg) logger.info("Final # of rows loaded: %s", len(output)) context.stats.num_documents = len(output) @@ -36,8 +38,6 @@ async def run_workflow( return WorkflowFunctionOutput(result=output) -async def load_input_documents( - config: InputConfig, storage: PipelineStorage -) -> pd.DataFrame: +async def load_input_documents(input_reader: InputReader) -> pd.DataFrame: """Load and parse input documents into a standard format.""" - return await create_input(config, storage) + return pd.DataFrame(await input_reader.read_files()) diff --git a/graphrag/index/workflows/load_update_documents.py b/packages/graphrag/graphrag/index/workflows/load_update_documents.py similarity index 79% rename from graphrag/index/workflows/load_update_documents.py rename to packages/graphrag/graphrag/index/workflows/load_update_documents.py index fbe48b6419..1cab6cabfe 100644 --- a/graphrag/index/workflows/load_update_documents.py +++ b/packages/graphrag/graphrag/index/workflows/load_update_documents.py @@ -6,14 +6,14 @@ import logging import pandas as pd +from graphrag_input.input_reader import InputReader +from graphrag_input.input_reader_factory import create_input_reader +from graphrag_storage import Storage from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.config.models.input_config import InputConfig -from graphrag.index.input.factory import create_input from graphrag.index.typing.context import PipelineRunContext from graphrag.index.typing.workflow import WorkflowFunctionOutput from graphrag.index.update.incremental_index import get_delta_docs -from graphrag.storage.pipeline_storage import PipelineStorage from graphrag.utils.storage import write_table_to_storage logger = logging.getLogger(__name__) @@ -24,9 +24,9 @@ async def run_workflow( context: PipelineRunContext, ) -> WorkflowFunctionOutput: """Load and parse update-only input documents into a standard format.""" + input_reader = create_input_reader(config.input, context.input_storage) output = await load_update_documents( - config.input, - context.input_storage, + input_reader, context.previous_storage, ) @@ -43,12 +43,11 @@ async def run_workflow( async def load_update_documents( - config: InputConfig, - input_storage: PipelineStorage, - previous_storage: PipelineStorage, + input_reader: InputReader, + previous_storage: Storage, ) -> pd.DataFrame: """Load and parse update-only input documents into a standard format.""" - input_documents = await create_input(config, input_storage) + input_documents = pd.DataFrame(await input_reader.read_files()) # previous storage is the output of the previous run # we'll use this to diff the input from the prior delta_documents = await get_delta_docs(input_documents, previous_storage) diff --git a/graphrag/index/workflows/prune_graph.py b/packages/graphrag/graphrag/index/workflows/prune_graph.py similarity index 90% rename from graphrag/index/workflows/prune_graph.py rename to packages/graphrag/graphrag/index/workflows/prune_graph.py index 8bb48df7ee..5653eef49b 100644 --- a/graphrag/index/workflows/prune_graph.py +++ b/packages/graphrag/graphrag/index/workflows/prune_graph.py @@ -58,6 +58,7 @@ def prune_graph( """Prune a full graph based on graph statistics.""" # create a temporary graph to prune, then turn it back into dataframes graph = create_graph(relationships, edge_attr=["weight"], nodes=entities) + pruned = prune_graph_operation( graph, min_node_freq=pruning_config.min_node_freq, @@ -69,6 +70,16 @@ def prune_graph( lcc_only=pruning_config.lcc_only, ) + if len(pruned.nodes) == 0: + error_msg = "Graph Pruning failed. No entities remain." + logger.error(error_msg) + raise ValueError(error_msg) + + if len(pruned.edges) == 0: + error_msg = "Graph Pruning failed. No relationships remain." + logger.error(error_msg) + raise ValueError(error_msg) + pruned_nodes, pruned_edges = graph_to_dataframes( pruned, node_columns=["title"], edge_columns=["source", "target"] ) diff --git a/graphrag/index/workflows/update_clean_state.py b/packages/graphrag/graphrag/index/workflows/update_clean_state.py similarity index 100% rename from graphrag/index/workflows/update_clean_state.py rename to packages/graphrag/graphrag/index/workflows/update_clean_state.py diff --git a/graphrag/index/workflows/update_communities.py b/packages/graphrag/graphrag/index/workflows/update_communities.py similarity index 91% rename from graphrag/index/workflows/update_communities.py rename to packages/graphrag/graphrag/index/workflows/update_communities.py index b7e3e6a343..da4fdef147 100644 --- a/graphrag/index/workflows/update_communities.py +++ b/packages/graphrag/graphrag/index/workflows/update_communities.py @@ -5,12 +5,13 @@ import logging +from graphrag_storage import Storage + from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.index.run.utils import get_update_storages from graphrag.index.typing.context import PipelineRunContext from graphrag.index.typing.workflow import WorkflowFunctionOutput from graphrag.index.update.communities import _update_and_merge_communities -from graphrag.storage.pipeline_storage import PipelineStorage from graphrag.utils.storage import load_table_from_storage, write_table_to_storage logger = logging.getLogger(__name__) @@ -37,9 +38,9 @@ async def run_workflow( async def _update_communities( - previous_storage: PipelineStorage, - delta_storage: PipelineStorage, - output_storage: PipelineStorage, + previous_storage: Storage, + delta_storage: Storage, + output_storage: Storage, ) -> dict: """Update the communities output.""" old_communities = await load_table_from_storage("communities", previous_storage) diff --git a/graphrag/index/workflows/update_community_reports.py b/packages/graphrag/graphrag/index/workflows/update_community_reports.py similarity index 92% rename from graphrag/index/workflows/update_community_reports.py rename to packages/graphrag/graphrag/index/workflows/update_community_reports.py index 42576aca27..790f9fc296 100644 --- a/graphrag/index/workflows/update_community_reports.py +++ b/packages/graphrag/graphrag/index/workflows/update_community_reports.py @@ -6,13 +6,13 @@ import logging import pandas as pd +from graphrag_storage import Storage from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.index.run.utils import get_update_storages from graphrag.index.typing.context import PipelineRunContext from graphrag.index.typing.workflow import WorkflowFunctionOutput from graphrag.index.update.communities import _update_and_merge_community_reports -from graphrag.storage.pipeline_storage import PipelineStorage from graphrag.utils.storage import load_table_from_storage, write_table_to_storage logger = logging.getLogger(__name__) @@ -43,9 +43,9 @@ async def run_workflow( async def _update_community_reports( - previous_storage: PipelineStorage, - delta_storage: PipelineStorage, - output_storage: PipelineStorage, + previous_storage: Storage, + delta_storage: Storage, + output_storage: Storage, community_id_mapping: dict, ) -> pd.DataFrame: """Update the community reports output.""" diff --git a/graphrag/index/workflows/update_covariates.py b/packages/graphrag/graphrag/index/workflows/update_covariates.py similarity index 93% rename from graphrag/index/workflows/update_covariates.py rename to packages/graphrag/graphrag/index/workflows/update_covariates.py index f0bf29a6ae..09f8b4053d 100644 --- a/graphrag/index/workflows/update_covariates.py +++ b/packages/graphrag/graphrag/index/workflows/update_covariates.py @@ -7,12 +7,12 @@ import numpy as np import pandas as pd +from graphrag_storage import Storage from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.index.run.utils import get_update_storages from graphrag.index.typing.context import PipelineRunContext from graphrag.index.typing.workflow import WorkflowFunctionOutput -from graphrag.storage.pipeline_storage import PipelineStorage from graphrag.utils.storage import ( load_table_from_storage, storage_has_table, @@ -43,9 +43,9 @@ async def run_workflow( async def _update_covariates( - previous_storage: PipelineStorage, - delta_storage: PipelineStorage, - output_storage: PipelineStorage, + previous_storage: Storage, + delta_storage: Storage, + output_storage: Storage, ) -> None: """Update the covariates output.""" old_covariates = await load_table_from_storage("covariates", previous_storage) diff --git a/graphrag/index/workflows/update_entities_relationships.py b/packages/graphrag/graphrag/index/workflows/update_entities_relationships.py similarity index 78% rename from graphrag/index/workflows/update_entities_relationships.py rename to packages/graphrag/graphrag/index/workflows/update_entities_relationships.py index cd8ad82553..225c12d9b9 100644 --- a/graphrag/index/workflows/update_entities_relationships.py +++ b/packages/graphrag/graphrag/index/workflows/update_entities_relationships.py @@ -6,8 +6,11 @@ import logging import pandas as pd +from graphrag_cache import Cache +from graphrag_llm.completion import create_completion +from graphrag_storage import Storage -from graphrag.cache.pipeline_cache import PipelineCache +from graphrag.cache.cache_key_creator import cache_key_creator from graphrag.callbacks.workflow_callbacks import WorkflowCallbacks from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.index.run.utils import get_update_storages @@ -16,7 +19,6 @@ from graphrag.index.update.entities import _group_and_resolve_entities from graphrag.index.update.relationships import _update_and_merge_relationships from graphrag.index.workflows.extract_graph import get_summarized_entities_relationships -from graphrag.storage.pipeline_storage import PipelineStorage from graphrag.utils.storage import load_table_from_storage, write_table_to_storage logger = logging.getLogger(__name__) @@ -54,11 +56,11 @@ async def run_workflow( async def _update_entities_and_relationships( - previous_storage: PipelineStorage, - delta_storage: PipelineStorage, - output_storage: PipelineStorage, + previous_storage: Storage, + delta_storage: Storage, + output_storage: Storage, config: GraphRagConfig, - cache: PipelineCache, + cache: Cache, callbacks: WorkflowCallbacks, ) -> tuple[pd.DataFrame, pd.DataFrame, dict]: """Update Final Entities and Relationships output.""" @@ -77,11 +79,14 @@ async def _update_entities_and_relationships( delta_relationships, ) - summarization_llm_settings = config.get_language_model_config( - config.summarize_descriptions.model_id + summarization_model_config = config.get_completion_model_config( + config.summarize_descriptions.completion_model_id ) - summarization_strategy = config.summarize_descriptions.resolved_strategy( - config.root_dir, summarization_llm_settings + prompts = config.summarize_descriptions.resolved_prompts() + model = create_completion( + summarization_model_config, + cache=cache.child("summarize_descriptions"), + cache_key_creator=cache_key_creator, ) ( @@ -91,9 +96,11 @@ async def _update_entities_and_relationships( extracted_entities=merged_entities_df, extracted_relationships=merged_relationships_df, callbacks=callbacks, - cache=cache, - summarization_strategy=summarization_strategy, - summarization_num_threads=summarization_llm_settings.concurrent_requests, + model=model, + max_summary_length=config.summarize_descriptions.max_length, + max_input_tokens=config.summarize_descriptions.max_input_tokens, + summarization_prompt=prompts.summarize_prompt, + num_threads=config.concurrent_requests, ) # Save the updated entities back to storage diff --git a/graphrag/index/workflows/update_final_documents.py b/packages/graphrag/graphrag/index/workflows/update_final_documents.py similarity index 100% rename from graphrag/index/workflows/update_final_documents.py rename to packages/graphrag/graphrag/index/workflows/update_final_documents.py diff --git a/graphrag/index/workflows/update_text_embeddings.py b/packages/graphrag/graphrag/index/workflows/update_text_embeddings.py similarity index 73% rename from graphrag/index/workflows/update_text_embeddings.py rename to packages/graphrag/graphrag/index/workflows/update_text_embeddings.py index 11bce16d3e..375bb69df4 100644 --- a/graphrag/index/workflows/update_text_embeddings.py +++ b/packages/graphrag/graphrag/index/workflows/update_text_embeddings.py @@ -5,7 +5,9 @@ import logging -from graphrag.config.get_embedding_settings import get_embedding_settings +from graphrag_llm.embedding import create_embedding + +from graphrag.cache.cache_key_creator import cache_key_creator from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.index.run.utils import get_update_storages from graphrag.index.typing.context import PipelineRunContext @@ -25,9 +27,6 @@ async def run_workflow( output_storage, _, _ = get_update_storages( config, context.state["update_timestamp"] ) - - final_documents_df = context.state["incremental_update_final_documents"] - merged_relationships_df = context.state["incremental_update_merged_relationships"] merged_text_units = context.state["incremental_update_merged_text_units"] merged_entities_df = context.state["incremental_update_merged_entities"] merged_community_reports = context.state[ @@ -35,16 +34,30 @@ async def run_workflow( ] embedded_fields = config.embed_text.names - text_embed = get_embedding_settings(config) + + model_config = config.get_embedding_model_config( + config.embed_text.embedding_model_id + ) + + model = create_embedding( + model_config, + cache=context.cache.child("text_embedding"), + cache_key_creator=cache_key_creator, + ) + + tokenizer = model.tokenizer + result = await generate_text_embeddings( - documents=final_documents_df, - relationships=merged_relationships_df, text_units=merged_text_units, entities=merged_entities_df, community_reports=merged_community_reports, callbacks=context.callbacks, - cache=context.cache, - text_embed_config=text_embed, + model=model, + tokenizer=tokenizer, + batch_size=config.embed_text.batch_size, + batch_max_tokens=config.embed_text.batch_max_tokens, + num_threads=config.concurrent_requests, + vector_store_config=config.vector_store, embedded_fields=embedded_fields, ) if config.snapshots.embeddings: diff --git a/graphrag/index/workflows/update_text_units.py b/packages/graphrag/graphrag/index/workflows/update_text_units.py similarity index 94% rename from graphrag/index/workflows/update_text_units.py rename to packages/graphrag/graphrag/index/workflows/update_text_units.py index 392533f16b..c97f89ce7a 100644 --- a/graphrag/index/workflows/update_text_units.py +++ b/packages/graphrag/graphrag/index/workflows/update_text_units.py @@ -7,12 +7,12 @@ import numpy as np import pandas as pd +from graphrag_storage import Storage from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.index.run.utils import get_update_storages from graphrag.index.typing.context import PipelineRunContext from graphrag.index.typing.workflow import WorkflowFunctionOutput -from graphrag.storage.pipeline_storage import PipelineStorage from graphrag.utils.storage import load_table_from_storage, write_table_to_storage logger = logging.getLogger(__name__) @@ -40,9 +40,9 @@ async def run_workflow( async def _update_text_units( - previous_storage: PipelineStorage, - delta_storage: PipelineStorage, - output_storage: PipelineStorage, + previous_storage: Storage, + delta_storage: Storage, + output_storage: Storage, entity_id_mapping: dict, ) -> pd.DataFrame: """Update the text units output.""" diff --git a/graphrag/logger/__init__.py b/packages/graphrag/graphrag/logger/__init__.py similarity index 100% rename from graphrag/logger/__init__.py rename to packages/graphrag/graphrag/logger/__init__.py diff --git a/graphrag/logger/blob_workflow_logger.py b/packages/graphrag/graphrag/logger/blob_workflow_logger.py similarity index 89% rename from graphrag/logger/blob_workflow_logger.py rename to packages/graphrag/graphrag/logger/blob_workflow_logger.py index ae4893c6e8..fd7ccac4fe 100644 --- a/graphrag/logger/blob_workflow_logger.py +++ b/packages/graphrag/graphrag/logger/blob_workflow_logger.py @@ -26,7 +26,7 @@ def __init__( container_name: str | None, blob_name: str = "", base_dir: str | None = None, - storage_account_blob_url: str | None = None, + account_url: str | None = None, level: int = logging.NOTSET, ): """Create a new instance of the BlobWorkflowLogger class.""" @@ -35,24 +35,24 @@ def __init__( if container_name is None: msg = "No container name provided for blob storage." raise ValueError(msg) - if connection_string is None and storage_account_blob_url is None: + if connection_string is None and account_url is None: msg = "No storage account blob url provided for blob storage." raise ValueError(msg) self._connection_string = connection_string - self._storage_account_blob_url = storage_account_blob_url + self.account_url = account_url if self._connection_string: self._blob_service_client = BlobServiceClient.from_connection_string( self._connection_string ) else: - if storage_account_blob_url is None: - msg = "Either connection_string or storage_account_blob_url must be provided." + if account_url is None: + msg = "Either connection_string or account_url must be provided." raise ValueError(msg) self._blob_service_client = BlobServiceClient( - storage_account_blob_url, + account_url, credential=DefaultAzureCredential(), ) @@ -107,7 +107,7 @@ def _write_log(self, log: dict[str, Any]): self.__init__( self._connection_string, self._container_name, - storage_account_blob_url=self._storage_account_blob_url, + account_url=self.account_url, ) blob_client = self._blob_service_client.get_blob_client( diff --git a/packages/graphrag/graphrag/logger/factory.py b/packages/graphrag/graphrag/logger/factory.py new file mode 100644 index 0000000000..7b5d28ef5e --- /dev/null +++ b/packages/graphrag/graphrag/logger/factory.py @@ -0,0 +1,64 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Factory functions for creating a logger.""" + +from __future__ import annotations + +import logging +from pathlib import Path + +from graphrag_common.factory import Factory + +from graphrag.config.enums import ReportingType + +LOG_FORMAT = "%(asctime)s.%(msecs)04d - %(levelname)s - %(name)s - %(message)s" +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + + +class LoggerFactory(Factory[logging.Handler]): + """A factory class for logger implementations. + + Includes a method for users to register a custom logger implementation. + + Configuration arguments are passed to each logger implementation as kwargs + for individual enforcement of required/optional arguments. + + Note that because we rely on the built-in Python logging architecture, this factory does not return an instance, + it merely configures the logger to your specified storage location. + """ + + +# --- register built-in logger implementations --- +def create_file_logger(**kwargs) -> logging.Handler: + """Create a file-based logger.""" + base_dir = kwargs["base_dir"] + filename = kwargs["filename"] + log_dir = Path(base_dir) + log_dir.mkdir(parents=True, exist_ok=True) + log_file_path = log_dir / filename + + handler = logging.FileHandler(str(log_file_path), mode="a") + + formatter = logging.Formatter(fmt=LOG_FORMAT, datefmt=DATE_FORMAT) + handler.setFormatter(formatter) + + return handler + + +def create_blob_logger(**kwargs) -> logging.Handler: + """Create a blob storage-based logger.""" + from graphrag.logger.blob_workflow_logger import BlobWorkflowLogger + + return BlobWorkflowLogger( + connection_string=kwargs["connection_string"], + container_name=kwargs["container_name"], + base_dir=kwargs["base_dir"], + account_url=kwargs["account_url"], + ) + + +# --- register built-in implementations --- +logger_factory = LoggerFactory() +logger_factory.register(ReportingType.file.value, create_file_logger) +logger_factory.register(ReportingType.blob.value, create_blob_logger) diff --git a/graphrag/logger/progress.py b/packages/graphrag/graphrag/logger/progress.py similarity index 100% rename from graphrag/logger/progress.py rename to packages/graphrag/graphrag/logger/progress.py diff --git a/graphrag/logger/standard_logging.py b/packages/graphrag/graphrag/logger/standard_logging.py similarity index 76% rename from graphrag/logger/standard_logging.py rename to packages/graphrag/graphrag/logger/standard_logging.py index fea504c02f..de62f5e031 100644 --- a/graphrag/logger/standard_logging.py +++ b/packages/graphrag/graphrag/logger/standard_logging.py @@ -67,17 +67,25 @@ def init_loggers( log_level = logging.DEBUG if verbose else logging.INFO logger.setLevel(log_level) - # clear any existing handlers to avoid duplicate logs - if logger.hasHandlers(): - # Close file handlers properly before removing them - for handler in logger.handlers: - if isinstance(handler, logging.FileHandler): - handler.close() - logger.handlers.clear() + llm_logger = logging.getLogger("graphrag_llm") + llm_logger.setLevel(log_level) + + def _clear_handlers(logger: logging.Logger) -> None: + # clear any existing handlers to avoid duplicate logs + if logger.hasHandlers(): + # Close file handlers properly before removing them + for handler in logger.handlers: + if isinstance(handler, logging.FileHandler): + handler.close() + logger.handlers.clear() + + _clear_handlers(logger) + _clear_handlers(llm_logger) reporting_config = config.reporting config_dict = reporting_config.model_dump() - args = {**config_dict, "root_dir": config.root_dir, "filename": filename} + args = {**config_dict, "filename": filename} - handler = LoggerFactory.create_logger(reporting_config.type, args) + handler = LoggerFactory().create(reporting_config.type, args) logger.addHandler(handler) + llm_logger.addHandler(handler) diff --git a/graphrag/prompt_tune/__init__.py b/packages/graphrag/graphrag/prompt_tune/__init__.py similarity index 100% rename from graphrag/prompt_tune/__init__.py rename to packages/graphrag/graphrag/prompt_tune/__init__.py diff --git a/graphrag/prompt_tune/defaults.py b/packages/graphrag/graphrag/prompt_tune/defaults.py similarity index 91% rename from graphrag/prompt_tune/defaults.py rename to packages/graphrag/graphrag/prompt_tune/defaults.py index 6fb84f170a..2095b5c8ac 100644 --- a/graphrag/prompt_tune/defaults.py +++ b/packages/graphrag/graphrag/prompt_tune/defaults.py @@ -17,4 +17,4 @@ MIN_CHUNK_SIZE = 200 N_SUBSET_MAX = 300 MIN_CHUNK_OVERLAP = 0 -PROMPT_TUNING_MODEL_ID = "default_chat_model" +PROMPT_TUNING_MODEL_ID = "default_completion_model" diff --git a/graphrag/prompt_tune/generator/__init__.py b/packages/graphrag/graphrag/prompt_tune/generator/__init__.py similarity index 100% rename from graphrag/prompt_tune/generator/__init__.py rename to packages/graphrag/graphrag/prompt_tune/generator/__init__.py diff --git a/graphrag/prompt_tune/generator/community_report_rating.py b/packages/graphrag/graphrag/prompt_tune/generator/community_report_rating.py similarity index 66% rename from graphrag/prompt_tune/generator/community_report_rating.py rename to packages/graphrag/graphrag/prompt_tune/generator/community_report_rating.py index 22cf73105f..a4cae4c75f 100644 --- a/graphrag/prompt_tune/generator/community_report_rating.py +++ b/packages/graphrag/graphrag/prompt_tune/generator/community_report_rating.py @@ -2,21 +2,25 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License +from typing import TYPE_CHECKING -from graphrag.language_model.protocol.base import ChatModel from graphrag.prompt_tune.prompt.community_report_rating import ( GENERATE_REPORT_RATING_PROMPT, ) +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionResponse + async def generate_community_report_rating( - model: ChatModel, domain: str, persona: str, docs: str | list[str] + model: "LLMCompletion", domain: str, persona: str, docs: str | list[str] ) -> str: """Generate an LLM persona to use for GraphRAG prompts. Parameters ---------- - - llm (CompletionLLM): The LLM to use for generation + - model (LLMCompletion): The LLM to use for generation - domain (str): The domain to generate a rating for - persona (str): The persona to generate a rating for for - docs (str | list[str]): Documents used to contextualize the rating @@ -30,6 +34,8 @@ async def generate_community_report_rating( domain=domain, persona=persona, input_text=docs_str ) - response = await model.achat(domain_prompt) + response: LLMCompletionResponse = await model.completion_async( + messages=domain_prompt + ) # type: ignore - return str(response.output.content).strip() + return response.content diff --git a/graphrag/prompt_tune/generator/community_report_summarization.py b/packages/graphrag/graphrag/prompt_tune/generator/community_report_summarization.py similarity index 100% rename from graphrag/prompt_tune/generator/community_report_summarization.py rename to packages/graphrag/graphrag/prompt_tune/generator/community_report_summarization.py diff --git a/graphrag/prompt_tune/generator/community_reporter_role.py b/packages/graphrag/graphrag/prompt_tune/generator/community_reporter_role.py similarity index 66% rename from graphrag/prompt_tune/generator/community_reporter_role.py rename to packages/graphrag/graphrag/prompt_tune/generator/community_reporter_role.py index d3c90d181e..b38d678b06 100644 --- a/graphrag/prompt_tune/generator/community_reporter_role.py +++ b/packages/graphrag/graphrag/prompt_tune/generator/community_reporter_role.py @@ -3,20 +3,25 @@ """Generate a community reporter role for community summarization.""" -from graphrag.language_model.protocol.base import ChatModel +from typing import TYPE_CHECKING + from graphrag.prompt_tune.prompt.community_reporter_role import ( GENERATE_COMMUNITY_REPORTER_ROLE_PROMPT, ) +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionResponse + async def generate_community_reporter_role( - model: ChatModel, domain: str, persona: str, docs: str | list[str] + model: "LLMCompletion", domain: str, persona: str, docs: str | list[str] ) -> str: """Generate an LLM persona to use for GraphRAG prompts. Parameters ---------- - - llm (CompletionLLM): The LLM to use for generation + - model (LLMCompletion): The LLM to use for generation - domain (str): The domain to generate a persona for - persona (str): The persona to generate a role for - docs (str | list[str]): The domain to generate a persona for @@ -30,6 +35,8 @@ async def generate_community_reporter_role( domain=domain, persona=persona, input_text=docs_str ) - response = await model.achat(domain_prompt) + response: LLMCompletionResponse = await model.completion_async( + messages=domain_prompt + ) # type: ignore - return str(response.output.content) + return response.content diff --git a/graphrag/prompt_tune/generator/domain.py b/packages/graphrag/graphrag/prompt_tune/generator/domain.py similarity index 55% rename from graphrag/prompt_tune/generator/domain.py rename to packages/graphrag/graphrag/prompt_tune/generator/domain.py index 7838594ccc..1135d67d9f 100644 --- a/graphrag/prompt_tune/generator/domain.py +++ b/packages/graphrag/graphrag/prompt_tune/generator/domain.py @@ -3,16 +3,21 @@ """Domain generation for GraphRAG prompts.""" -from graphrag.language_model.protocol.base import ChatModel +from typing import TYPE_CHECKING + from graphrag.prompt_tune.prompt.domain import GENERATE_DOMAIN_PROMPT +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionResponse + -async def generate_domain(model: ChatModel, docs: str | list[str]) -> str: +async def generate_domain(model: "LLMCompletion", docs: str | list[str]) -> str: """Generate an LLM persona to use for GraphRAG prompts. Parameters ---------- - - llm (CompletionLLM): The LLM to use for generation + - model (LLMCompletion): The LLM to use for generation - docs (str | list[str]): The domain to generate a persona for Returns @@ -22,6 +27,8 @@ async def generate_domain(model: ChatModel, docs: str | list[str]) -> str: docs_str = " ".join(docs) if isinstance(docs, list) else docs domain_prompt = GENERATE_DOMAIN_PROMPT.format(input_text=docs_str) - response = await model.achat(domain_prompt) + response: LLMCompletionResponse = await model.completion_async( + messages=domain_prompt + ) # type: ignore - return str(response.output.content) + return response.content diff --git a/graphrag/prompt_tune/generator/entity_relationship.py b/packages/graphrag/graphrag/prompt_tune/generator/entity_relationship.py similarity index 70% rename from graphrag/prompt_tune/generator/entity_relationship.py rename to packages/graphrag/graphrag/prompt_tune/generator/entity_relationship.py index 70225cbb02..9282366e45 100644 --- a/graphrag/prompt_tune/generator/entity_relationship.py +++ b/packages/graphrag/graphrag/prompt_tune/generator/entity_relationship.py @@ -4,19 +4,27 @@ """Entity relationship example generation module.""" import asyncio +from typing import TYPE_CHECKING + +from graphrag_llm.utils import ( + CompletionMessagesBuilder, +) -from graphrag.language_model.protocol.base import ChatModel from graphrag.prompt_tune.prompt.entity_relationship import ( ENTITY_RELATIONSHIPS_GENERATION_JSON_PROMPT, ENTITY_RELATIONSHIPS_GENERATION_PROMPT, UNTYPED_ENTITY_RELATIONSHIPS_GENERATION_PROMPT, ) +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionResponse + MAX_EXAMPLES = 5 async def generate_entity_relationship_examples( - model: ChatModel, + model: "LLMCompletion", persona: str, entity_types: str | list[str] | None, docs: str | list[str], @@ -29,7 +37,8 @@ async def generate_entity_relationship_examples( on the json_mode parameter. """ docs_list = [docs] if isinstance(docs, str) else docs - history = [{"content": persona, "role": "system"}] + + msg_builder = CompletionMessagesBuilder().add_system_message(persona) if entity_types: entity_types_str = ( @@ -57,9 +66,13 @@ async def generate_entity_relationship_examples( messages = messages[:MAX_EXAMPLES] tasks = [ - model.achat(message, history=history, json=json_mode) for message in messages + model.completion_async( + messages=msg_builder.add_user_message(message).build(), + response_format_json_object=json_mode, + ) + for message in messages ] - responses = await asyncio.gather(*tasks) + responses: list[LLMCompletionResponse] = await asyncio.gather(*tasks) # type: ignore - return [str(response.output.content) for response in responses] + return [response.content for response in responses] diff --git a/graphrag/prompt_tune/generator/entity_summarization_prompt.py b/packages/graphrag/graphrag/prompt_tune/generator/entity_summarization_prompt.py similarity index 100% rename from graphrag/prompt_tune/generator/entity_summarization_prompt.py rename to packages/graphrag/graphrag/prompt_tune/generator/entity_summarization_prompt.py diff --git a/graphrag/prompt_tune/generator/entity_types.py b/packages/graphrag/graphrag/prompt_tune/generator/entity_types.py similarity index 59% rename from graphrag/prompt_tune/generator/entity_types.py rename to packages/graphrag/graphrag/prompt_tune/generator/entity_types.py index d68ab52115..21c58086e7 100644 --- a/graphrag/prompt_tune/generator/entity_types.py +++ b/packages/graphrag/graphrag/prompt_tune/generator/entity_types.py @@ -3,15 +3,23 @@ """Entity type generation module for fine-tuning.""" +from typing import TYPE_CHECKING + +from graphrag_llm.utils import ( + CompletionMessagesBuilder, +) from pydantic import BaseModel -from graphrag.language_model.protocol.base import ChatModel from graphrag.prompt_tune.defaults import DEFAULT_TASK from graphrag.prompt_tune.prompt.entity_types import ( ENTITY_TYPE_GENERATION_JSON_PROMPT, ENTITY_TYPE_GENERATION_PROMPT, ) +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionResponse + class EntityTypesResponse(BaseModel): """Entity types response model.""" @@ -20,7 +28,7 @@ class EntityTypesResponse(BaseModel): async def generate_entity_types( - model: ChatModel, + model: "LLMCompletion", domain: str, persona: str, docs: str | list[str], @@ -43,17 +51,24 @@ async def generate_entity_types( else ENTITY_TYPE_GENERATION_PROMPT ).format(task=formatted_task, input_text=docs_str) - history = [{"role": "system", "content": persona}] + messages = ( + CompletionMessagesBuilder() + .add_system_message(persona) + .add_user_message(entity_types_prompt) + .build() + ) if json_mode: - response = await model.achat( - entity_types_prompt, - history=history, - json=json_mode, - json_model=EntityTypesResponse, - ) - parsed_model = response.parsed_response + response: LLMCompletionResponse[ + EntityTypesResponse + ] = await model.completion_async( + messages=messages, + response_format=EntityTypesResponse, + ) # type: ignore + parsed_model = response.formatted_response return parsed_model.entity_types if parsed_model else [] - response = await model.achat(entity_types_prompt, history=history, json=json_mode) - return str(response.output.content) + non_json_response: LLMCompletionResponse = await model.completion_async( + messages=messages + ) # type: ignore + return non_json_response.content diff --git a/graphrag/prompt_tune/generator/extract_graph_prompt.py b/packages/graphrag/graphrag/prompt_tune/generator/extract_graph_prompt.py similarity index 98% rename from graphrag/prompt_tune/generator/extract_graph_prompt.py rename to packages/graphrag/graphrag/prompt_tune/generator/extract_graph_prompt.py index db0f87cf97..46707c45bf 100644 --- a/graphrag/prompt_tune/generator/extract_graph_prompt.py +++ b/packages/graphrag/graphrag/prompt_tune/generator/extract_graph_prompt.py @@ -5,6 +5,8 @@ from pathlib import Path +from graphrag_llm.tokenizer import Tokenizer + from graphrag.prompt_tune.template.extract_graph import ( EXAMPLE_EXTRACTION_TEMPLATE, GRAPH_EXTRACTION_JSON_PROMPT, @@ -13,7 +15,6 @@ UNTYPED_GRAPH_EXTRACTION_PROMPT, ) from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer EXTRACT_GRAPH_FILENAME = "extract_graph.txt" diff --git a/graphrag/prompt_tune/generator/language.py b/packages/graphrag/graphrag/prompt_tune/generator/language.py similarity index 55% rename from graphrag/prompt_tune/generator/language.py rename to packages/graphrag/graphrag/prompt_tune/generator/language.py index 5c00fd6b5a..029f31180b 100644 --- a/graphrag/prompt_tune/generator/language.py +++ b/packages/graphrag/graphrag/prompt_tune/generator/language.py @@ -3,16 +3,21 @@ """Language detection for GraphRAG prompts.""" -from graphrag.language_model.protocol.base import ChatModel +from typing import TYPE_CHECKING + from graphrag.prompt_tune.prompt.language import DETECT_LANGUAGE_PROMPT +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionResponse + -async def detect_language(model: ChatModel, docs: str | list[str]) -> str: +async def detect_language(model: "LLMCompletion", docs: str | list[str]) -> str: """Detect input language to use for GraphRAG prompts. Parameters ---------- - - llm (CompletionLLM): The LLM to use for generation + - model (LLMCompletion): The LLM to use for generation - docs (str | list[str]): The docs to detect language from Returns @@ -22,6 +27,8 @@ async def detect_language(model: ChatModel, docs: str | list[str]) -> str: docs_str = " ".join(docs) if isinstance(docs, list) else docs language_prompt = DETECT_LANGUAGE_PROMPT.format(input_text=docs_str) - response = await model.achat(language_prompt) + response: LLMCompletionResponse = await model.completion_async( + messages=language_prompt + ) # type: ignore - return str(response.output.content) + return response.content diff --git a/graphrag/prompt_tune/generator/persona.py b/packages/graphrag/graphrag/prompt_tune/generator/persona.py similarity index 60% rename from graphrag/prompt_tune/generator/persona.py rename to packages/graphrag/graphrag/prompt_tune/generator/persona.py index b9bf485d8d..b6fdc18ff2 100644 --- a/graphrag/prompt_tune/generator/persona.py +++ b/packages/graphrag/graphrag/prompt_tune/generator/persona.py @@ -3,25 +3,32 @@ """Persona generating module for fine-tuning GraphRAG prompts.""" -from graphrag.language_model.protocol.base import ChatModel +from typing import TYPE_CHECKING + from graphrag.prompt_tune.defaults import DEFAULT_TASK from graphrag.prompt_tune.prompt.persona import GENERATE_PERSONA_PROMPT +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionResponse + async def generate_persona( - model: ChatModel, domain: str, task: str = DEFAULT_TASK + model: "LLMCompletion", domain: str, task: str = DEFAULT_TASK ) -> str: """Generate an LLM persona to use for GraphRAG prompts. Parameters ---------- - - llm (CompletionLLM): The LLM to use for generation + - model (LLMCompletion): The LLM to use for generation - domain (str): The domain to generate a persona for - task (str): The task to generate a persona for. Default is DEFAULT_TASK """ formatted_task = task.format(domain=domain) persona_prompt = GENERATE_PERSONA_PROMPT.format(sample_task=formatted_task) - response = await model.achat(persona_prompt) + response: LLMCompletionResponse = await model.completion_async( + messages=persona_prompt + ) # type: ignore - return str(response.output.content) + return response.content diff --git a/graphrag/prompt_tune/loader/__init__.py b/packages/graphrag/graphrag/prompt_tune/loader/__init__.py similarity index 100% rename from graphrag/prompt_tune/loader/__init__.py rename to packages/graphrag/graphrag/prompt_tune/loader/__init__.py diff --git a/graphrag/prompt_tune/loader/input.py b/packages/graphrag/graphrag/prompt_tune/loader/input.py similarity index 65% rename from graphrag/prompt_tune/loader/input.py rename to packages/graphrag/graphrag/prompt_tune/loader/input.py index 29010b05a8..0cfdb2299a 100644 --- a/graphrag/prompt_tune/loader/input.py +++ b/packages/graphrag/graphrag/prompt_tune/loader/input.py @@ -4,16 +4,19 @@ """Input loading module.""" import logging +from typing import Any import numpy as np import pandas as pd +from graphrag_chunking.chunker_factory import create_chunker +from graphrag_input import create_input_reader +from graphrag_llm.embedding import create_embedding +from graphrag_storage import create_storage -from graphrag.cache.noop_pipeline_cache import NoopPipelineCache from graphrag.callbacks.noop_workflow_callbacks import NoopWorkflowCallbacks from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.index.input.factory import create_input -from graphrag.index.operations.embed_text.strategies.openai import ( - run as run_embed_text, +from graphrag.index.operations.embed_text.run_embed_text import ( + run_embed_text, ) from graphrag.index.workflows.create_base_text_units import create_base_text_units from graphrag.prompt_tune.defaults import ( @@ -22,12 +25,11 @@ K, ) from graphrag.prompt_tune.types import DocSelectionType -from graphrag.utils.api import create_storage_from_config def _sample_chunks_from_embeddings( text_chunks: pd.DataFrame, - embeddings: np.ndarray[float, np.dtype[np.float_]], + embeddings: np.ndarray[Any, np.dtype[np.float64]], k: int = K, ) -> pd.DataFrame: """Sample text chunks from embeddings.""" @@ -43,28 +45,24 @@ async def load_docs_in_chunks( select_method: DocSelectionType, limit: int, logger: logging.Logger, - chunk_size: int, - overlap: int, n_subset_max: int = N_SUBSET_MAX, k: int = K, ) -> list[str]: """Load docs into chunks for generating prompts.""" - embeddings_llm_settings = config.get_language_model_config( - config.embed_text.model_id + embeddings_llm_settings = config.get_embedding_model_config( + config.embed_text.embedding_model_id ) - input_storage = create_storage_from_config(config.input.storage) - dataset = await create_input(config.input, input_storage) - chunk_config = config.chunks + model = create_embedding(embeddings_llm_settings) + tokenizer = model.tokenizer + chunker = create_chunker(config.chunking, tokenizer.encode, tokenizer.decode) + input_storage = create_storage(config.input_storage) + input_reader = create_input_reader(config.input, input_storage) + dataset = await input_reader.read_files() chunks_df = create_base_text_units( - documents=dataset, + documents=pd.DataFrame(dataset), callbacks=NoopWorkflowCallbacks(), - group_by_columns=chunk_config.group_by_columns, - size=chunk_size, - overlap=overlap, - encoding_model=chunk_config.encoding_model, - strategy=chunk_config.strategy, - prepend_metadata=chunk_config.prepend_metadata, - chunk_size_includes_metadata=chunk_config.chunk_size_includes_metadata, + tokenizer=tokenizer, + chunker=chunker, ) # Depending on the select method, build the dataset @@ -89,13 +87,11 @@ async def load_docs_in_chunks( embedding_results = await run_embed_text( sampled_text_chunks, callbacks=NoopWorkflowCallbacks(), - cache=NoopPipelineCache(), - args={ - "llm": embeddings_llm_settings.model_dump(), - "num_threads": embeddings_llm_settings.concurrent_requests, - "batch_size": config.embed_text.batch_size, - "batch_max_tokens": config.embed_text.batch_max_tokens, - }, + model=model, + tokenizer=tokenizer, + batch_size=config.embed_text.batch_size, + batch_max_tokens=config.embed_text.batch_max_tokens, + num_threads=config.concurrent_requests, ) embeddings = np.array(embedding_results.embeddings) chunks_df = _sample_chunks_from_embeddings(chunks_df, embeddings, k=k) diff --git a/graphrag/prompt_tune/prompt/__init__.py b/packages/graphrag/graphrag/prompt_tune/prompt/__init__.py similarity index 100% rename from graphrag/prompt_tune/prompt/__init__.py rename to packages/graphrag/graphrag/prompt_tune/prompt/__init__.py diff --git a/graphrag/prompt_tune/prompt/community_report_rating.py b/packages/graphrag/graphrag/prompt_tune/prompt/community_report_rating.py similarity index 100% rename from graphrag/prompt_tune/prompt/community_report_rating.py rename to packages/graphrag/graphrag/prompt_tune/prompt/community_report_rating.py diff --git a/graphrag/prompt_tune/prompt/community_reporter_role.py b/packages/graphrag/graphrag/prompt_tune/prompt/community_reporter_role.py similarity index 100% rename from graphrag/prompt_tune/prompt/community_reporter_role.py rename to packages/graphrag/graphrag/prompt_tune/prompt/community_reporter_role.py diff --git a/graphrag/prompt_tune/prompt/domain.py b/packages/graphrag/graphrag/prompt_tune/prompt/domain.py similarity index 100% rename from graphrag/prompt_tune/prompt/domain.py rename to packages/graphrag/graphrag/prompt_tune/prompt/domain.py diff --git a/graphrag/prompt_tune/prompt/entity_relationship.py b/packages/graphrag/graphrag/prompt_tune/prompt/entity_relationship.py similarity index 57% rename from graphrag/prompt_tune/prompt/entity_relationship.py rename to packages/graphrag/graphrag/prompt_tune/prompt/entity_relationship.py index 66eefa9947..ec7dca55a4 100644 --- a/graphrag/prompt_tune/prompt/entity_relationship.py +++ b/packages/graphrag/graphrag/prompt_tune/prompt/entity_relationship.py @@ -12,7 +12,7 @@ - entity_name: Name of the entity, capitalized - entity_type: One of the following types: [{entity_types}] - entity_description: Comprehensive description of the entity's attributes and activities -Format each entity as ("entity"{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}) +Format each entity as ("entity"<|><|><|>) 2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other. For each pair of related entities, extract the following information: @@ -20,13 +20,13 @@ - target_entity: name of the target entity, as identified in step 1 - relationship_description: explanation as to why you think the source entity and the target entity are related to each other - relationship_strength: an integer score between 1 to 10, indicating strength of the relationship between the source entity and target entity -Format each relationship as ("relationship"{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}) +Format each relationship as ("relationship"<|><|><|><|>) -3. Return output in {language} as a single list of all the entities and relationships identified in steps 1 and 2. Use {{record_delimiter}} as the list delimiter. +3. Return output in {language} as a single list of all the entities and relationships identified in steps 1 and 2. Use ## as the list delimiter. 4. If you have to translate into {language}, just translate the descriptions, nothing else! -5. When finished, output {{completion_delimiter}}. +5. When finished, output <|COMPLETE|>. ###################### -Examples- @@ -37,14 +37,14 @@ The Verdantis's Central Institution is scheduled to meet on Monday and Thursday, with the institution planning to release its latest policy decision on Thursday at 1:30 p.m. PDT, followed by a press conference where Central Institution Chair Martin Smith will take questions. Investors expect the Market Strategy Committee to hold its benchmark interest rate steady in a range of 3.5%-3.75%. ###################### Output: -("entity"{{tuple_delimiter}}CENTRAL INSTITUTION{{tuple_delimiter}}ORGANIZATION{{tuple_delimiter}}The Central Institution is the Federal Reserve of Verdantis, which is setting interest rates on Monday and Thursday) -{{record_delimiter}} -("entity"{{tuple_delimiter}}MARTIN SMITH{{tuple_delimiter}}PERSON{{tuple_delimiter}}Martin Smith is the chair of the Central Institution) -{{record_delimiter}} -("entity"{{tuple_delimiter}}MARKET STRATEGY COMMITTEE{{tuple_delimiter}}ORGANIZATION{{tuple_delimiter}}The Central Institution committee makes key decisions about interest rates and the growth of Verdantis's money supply) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}MARTIN SMITH{{tuple_delimiter}}CENTRAL INSTITUTION{{tuple_delimiter}}Martin Smith is the Chair of the Central Institution and will answer questions at a press conference{{tuple_delimiter}}9) -{{completion_delimiter}} +("entity"<|>CENTRAL INSTITUTION<|>ORGANIZATION<|>The Central Institution is the Federal Reserve of Verdantis, which is setting interest rates on Monday and Thursday) +## +("entity"<|>MARTIN SMITH<|>PERSON<|>Martin Smith is the chair of the Central Institution) +## +("entity"<|>MARKET STRATEGY COMMITTEE<|>ORGANIZATION<|>The Central Institution committee makes key decisions about interest rates and the growth of Verdantis's money supply) +## +("relationship"<|>MARTIN SMITH<|>CENTRAL INSTITUTION<|>Martin Smith is the Chair of the Central Institution and will answer questions at a press conference<|>9) +<|COMPLETE|> ###################### Example 2: @@ -55,12 +55,12 @@ TechGlobal, a formerly public company, was taken private by Vision Holdings in 2014. The well-established chip designer says it powers 85% of premium smartphones. ###################### Output: -("entity"{{tuple_delimiter}}TECHGLOBAL{{tuple_delimiter}}ORGANIZATION{{tuple_delimiter}}TechGlobal is a stock now listed on the Global Exchange which powers 85% of premium smartphones) -{{record_delimiter}} -("entity"{{tuple_delimiter}}VISION HOLDINGS{{tuple_delimiter}}ORGANIZATION{{tuple_delimiter}}Vision Holdings is a firm that previously owned TechGlobal) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}TECHGLOBAL{{tuple_delimiter}}VISION HOLDINGS{{tuple_delimiter}}Vision Holdings formerly owned TechGlobal from 2014 until present{{tuple_delimiter}}5) -{{completion_delimiter}} +("entity"<|>TECHGLOBAL<|>ORGANIZATION<|>TechGlobal is a stock now listed on the Global Exchange which powers 85% of premium smartphones) +## +("entity"<|>VISION HOLDINGS<|>ORGANIZATION<|>Vision Holdings is a firm that previously owned TechGlobal) +## +("relationship"<|>TECHGLOBAL<|>VISION HOLDINGS<|>Vision Holdings formerly owned TechGlobal from 2014 until present<|>5) +<|COMPLETE|> ###################### Example 3: @@ -77,47 +77,47 @@ The Aurelians include 39-year-old businessman Samuel Namara, who has been held in Tiruzia's Alhamia Prison, as well as journalist Durke Bataglani, 59, and environmentalist Meggie Tazbah, 53, who also holds Bratinas nationality. ###################### Output: -("entity"{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}GEO{{tuple_delimiter}}Firuzabad held Aurelians as hostages) -{{record_delimiter}} -("entity"{{tuple_delimiter}}AURELIA{{tuple_delimiter}}GEO{{tuple_delimiter}}Country seeking to release hostages) -{{record_delimiter}} -("entity"{{tuple_delimiter}}QUINTARA{{tuple_delimiter}}GEO{{tuple_delimiter}}Country that negotiated a swap of money in exchange for hostages) -{{record_delimiter}} -{{record_delimiter}} -("entity"{{tuple_delimiter}}TIRUZIA{{tuple_delimiter}}GEO{{tuple_delimiter}}Capital of Firuzabad where the Aurelians were being held) -{{record_delimiter}} -("entity"{{tuple_delimiter}}KROHAARA{{tuple_delimiter}}GEO{{tuple_delimiter}}Capital city in Quintara) -{{record_delimiter}} -("entity"{{tuple_delimiter}}CASHION{{tuple_delimiter}}GEO{{tuple_delimiter}}Capital city in Aurelia) -{{record_delimiter}} -("entity"{{tuple_delimiter}}SAMUEL NAMARA{{tuple_delimiter}}PERSON{{tuple_delimiter}}Aurelian who spent time in Tiruzia's Alhamia Prison) -{{record_delimiter}} -("entity"{{tuple_delimiter}}ALHAMIA PRISON{{tuple_delimiter}}GEO{{tuple_delimiter}}Prison in Tiruzia) -{{record_delimiter}} -("entity"{{tuple_delimiter}}DURKE BATAGLANI{{tuple_delimiter}}PERSON{{tuple_delimiter}}Aurelian journalist who was held hostage) -{{record_delimiter}} -("entity"{{tuple_delimiter}}MEGGIE TAZBAH{{tuple_delimiter}}PERSON{{tuple_delimiter}}Bratinas national and environmentalist who was held hostage) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}AURELIA{{tuple_delimiter}}Firuzabad negotiated a hostage exchange with Aurelia{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}QUINTARA{{tuple_delimiter}}AURELIA{{tuple_delimiter}}Quintara brokered the hostage exchange between Firuzabad and Aurelia{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}QUINTARA{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}Quintara brokered the hostage exchange between Firuzabad and Aurelia{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}SAMUEL NAMARA{{tuple_delimiter}}ALHAMIA PRISON{{tuple_delimiter}}Samuel Namara was a prisoner at Alhamia prison{{tuple_delimiter}}8) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}SAMUEL NAMARA{{tuple_delimiter}}MEGGIE TAZBAH{{tuple_delimiter}}Samuel Namara and Meggie Tazbah were exchanged in the same hostage release{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}SAMUEL NAMARA{{tuple_delimiter}}DURKE BATAGLANI{{tuple_delimiter}}Samuel Namara and Durke Bataglani were exchanged in the same hostage release{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}MEGGIE TAZBAH{{tuple_delimiter}}DURKE BATAGLANI{{tuple_delimiter}}Meggie Tazbah and Durke Bataglani were exchanged in the same hostage release{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}SAMUEL NAMARA{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}Samuel Namara was a hostage in Firuzabad{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}MEGGIE TAZBAH{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}Meggie Tazbah was a hostage in Firuzabad{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}DURKE BATAGLANI{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}Durke Bataglani was a hostage in Firuzabad{{tuple_delimiter}}2) -{{completion_delimiter}} +("entity"<|>FIRUZABAD<|>GEO<|>Firuzabad held Aurelians as hostages) +## +("entity"<|>AURELIA<|>GEO<|>Country seeking to release hostages) +## +("entity"<|>QUINTARA<|>GEO<|>Country that negotiated a swap of money in exchange for hostages) +## +## +("entity"<|>TIRUZIA<|>GEO<|>Capital of Firuzabad where the Aurelians were being held) +## +("entity"<|>KROHAARA<|>GEO<|>Capital city in Quintara) +## +("entity"<|>CASHION<|>GEO<|>Capital city in Aurelia) +## +("entity"<|>SAMUEL NAMARA<|>PERSON<|>Aurelian who spent time in Tiruzia's Alhamia Prison) +## +("entity"<|>ALHAMIA PRISON<|>GEO<|>Prison in Tiruzia) +## +("entity"<|>DURKE BATAGLANI<|>PERSON<|>Aurelian journalist who was held hostage) +## +("entity"<|>MEGGIE TAZBAH<|>PERSON<|>Bratinas national and environmentalist who was held hostage) +## +("relationship"<|>FIRUZABAD<|>AURELIA<|>Firuzabad negotiated a hostage exchange with Aurelia<|>2) +## +("relationship"<|>QUINTARA<|>AURELIA<|>Quintara brokered the hostage exchange between Firuzabad and Aurelia<|>2) +## +("relationship"<|>QUINTARA<|>FIRUZABAD<|>Quintara brokered the hostage exchange between Firuzabad and Aurelia<|>2) +## +("relationship"<|>SAMUEL NAMARA<|>ALHAMIA PRISON<|>Samuel Namara was a prisoner at Alhamia prison<|>8) +## +("relationship"<|>SAMUEL NAMARA<|>MEGGIE TAZBAH<|>Samuel Namara and Meggie Tazbah were exchanged in the same hostage release<|>2) +## +("relationship"<|>SAMUEL NAMARA<|>DURKE BATAGLANI<|>Samuel Namara and Durke Bataglani were exchanged in the same hostage release<|>2) +## +("relationship"<|>MEGGIE TAZBAH<|>DURKE BATAGLANI<|>Meggie Tazbah and Durke Bataglani were exchanged in the same hostage release<|>2) +## +("relationship"<|>SAMUEL NAMARA<|>FIRUZABAD<|>Samuel Namara was a hostage in Firuzabad<|>2) +## +("relationship"<|>MEGGIE TAZBAH<|>FIRUZABAD<|>Meggie Tazbah was a hostage in Firuzabad<|>2) +## +("relationship"<|>DURKE BATAGLANI<|>FIRUZABAD<|>Durke Bataglani was a hostage in Firuzabad<|>2) +<|COMPLETE|> -Real Data- ###################### @@ -242,7 +242,7 @@ - entity_name: Name of the entity, capitalized - entity_type: Suggest several labels or categories for the entity. The categories should not be specific, but should be as general as possible. - entity_description: Comprehensive description of the entity's attributes and activities -Format each entity as ("entity"{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}) +Format each entity as ("entity"<|><|><|>) 2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other. For each pair of related entities, extract the following information: @@ -250,13 +250,13 @@ - target_entity: name of the target entity, as identified in step 1 - relationship_description: explanation as to why you think the source entity and the target entity are related to each other - relationship_strength: a numeric score indicating strength of the relationship between the source entity and target entity -Format each relationship as ("relationship"{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}) +Format each relationship as ("relationship"<|><|><|><|>) -3. Return output in {language} as a single list of all the entities and relationships identified in steps 1 and 2. Use **{{record_delimiter}}** as the list delimiter. +3. Return output in {language} as a single list of all the entities and relationships identified in steps 1 and 2. Use **##** as the list delimiter. 4. If you have to translate into {language}, just translate the descriptions, nothing else! -5. When finished, output {{completion_delimiter}}. +5. When finished, output <|COMPLETE|>. ###################### -Examples- @@ -266,14 +266,14 @@ The Verdantis's Central Institution is scheduled to meet on Monday and Thursday, with the institution planning to release its latest policy decision on Thursday at 1:30 p.m. PDT, followed by a press conference where Central Institution Chair Martin Smith will take questions. Investors expect the Market Strategy Committee to hold its benchmark interest rate steady in a range of 3.5%-3.75%. ###################### Output: -("entity"{{tuple_delimiter}}CENTRAL INSTITUTION{{tuple_delimiter}}ORGANIZATION{{tuple_delimiter}}The Central Institution is the Federal Reserve of Verdantis, which is setting interest rates on Monday and Thursday) -{{record_delimiter}} -("entity"{{tuple_delimiter}}MARTIN SMITH{{tuple_delimiter}}PERSON{{tuple_delimiter}}Martin Smith is the chair of the Central Institution) -{{record_delimiter}} -("entity"{{tuple_delimiter}}MARKET STRATEGY COMMITTEE{{tuple_delimiter}}ORGANIZATION{{tuple_delimiter}}The Central Institution committee makes key decisions about interest rates and the growth of Verdantis's money supply) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}MARTIN SMITH{{tuple_delimiter}}CENTRAL INSTITUTION{{tuple_delimiter}}Martin Smith is the Chair of the Central Institution and will answer questions at a press conference{{tuple_delimiter}}9) -{{completion_delimiter}} +("entity"<|>CENTRAL INSTITUTION<|>ORGANIZATION<|>The Central Institution is the Federal Reserve of Verdantis, which is setting interest rates on Monday and Thursday) +## +("entity"<|>MARTIN SMITH<|>PERSON<|>Martin Smith is the chair of the Central Institution) +## +("entity"<|>MARKET STRATEGY COMMITTEE<|>ORGANIZATION<|>The Central Institution committee makes key decisions about interest rates and the growth of Verdantis's money supply) +## +("relationship"<|>MARTIN SMITH<|>CENTRAL INSTITUTION<|>Martin Smith is the Chair of the Central Institution and will answer questions at a press conference<|>9) +<|COMPLETE|> ###################### Example 2: @@ -283,12 +283,12 @@ TechGlobal, a formerly public company, was taken private by Vision Holdings in 2014. The well-established chip designer says it powers 85% of premium smartphones. ###################### Output: -("entity"{{tuple_delimiter}}TECHGLOBAL{{tuple_delimiter}}ORGANIZATION{{tuple_delimiter}}TechGlobal is a stock now listed on the Global Exchange which powers 85% of premium smartphones) -{{record_delimiter}} -("entity"{{tuple_delimiter}}VISION HOLDINGS{{tuple_delimiter}}ORGANIZATION{{tuple_delimiter}}Vision Holdings is a firm that previously owned TechGlobal) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}TECHGLOBAL{{tuple_delimiter}}VISION HOLDINGS{{tuple_delimiter}}Vision Holdings formerly owned TechGlobal from 2014 until present{{tuple_delimiter}}5) -{{completion_delimiter}} +("entity"<|>TECHGLOBAL<|>ORGANIZATION<|>TechGlobal is a stock now listed on the Global Exchange which powers 85% of premium smartphones) +## +("entity"<|>VISION HOLDINGS<|>ORGANIZATION<|>Vision Holdings is a firm that previously owned TechGlobal) +## +("relationship"<|>TECHGLOBAL<|>VISION HOLDINGS<|>Vision Holdings formerly owned TechGlobal from 2014 until present<|>5) +<|COMPLETE|> ###################### Example 3: @@ -304,47 +304,47 @@ The Aurelians include 39-year-old businessman Samuel Namara, who has been held in Tiruzia's Alhamia Prison, as well as journalist Durke Bataglani, 59, and environmentalist Meggie Tazbah, 53, who also holds Bratinas nationality. ###################### Output: -("entity"{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}GEO{{tuple_delimiter}}Firuzabad held Aurelians as hostages) -{{record_delimiter}} -("entity"{{tuple_delimiter}}AURELIA{{tuple_delimiter}}GEO{{tuple_delimiter}}Country seeking to release hostages) -{{record_delimiter}} -("entity"{{tuple_delimiter}}QUINTARA{{tuple_delimiter}}GEO{{tuple_delimiter}}Country that negotiated a swap of money in exchange for hostages) -{{record_delimiter}} -{{record_delimiter}} -("entity"{{tuple_delimiter}}TIRUZIA{{tuple_delimiter}}GEO{{tuple_delimiter}}Capital of Firuzabad where the Aurelians were being held) -{{record_delimiter}} -("entity"{{tuple_delimiter}}KROHAARA{{tuple_delimiter}}GEO{{tuple_delimiter}}Capital city in Quintara) -{{record_delimiter}} -("entity"{{tuple_delimiter}}CASHION{{tuple_delimiter}}GEO{{tuple_delimiter}}Capital city in Aurelia) -{{record_delimiter}} -("entity"{{tuple_delimiter}}SAMUEL NAMARA{{tuple_delimiter}}PERSON{{tuple_delimiter}}Aurelian who spent time in Tiruzia's Alhamia Prison) -{{record_delimiter}} -("entity"{{tuple_delimiter}}ALHAMIA PRISON{{tuple_delimiter}}GEO{{tuple_delimiter}}Prison in Tiruzia) -{{record_delimiter}} -("entity"{{tuple_delimiter}}DURKE BATAGLANI{{tuple_delimiter}}PERSON{{tuple_delimiter}}Aurelian journalist who was held hostage) -{{record_delimiter}} -("entity"{{tuple_delimiter}}MEGGIE TAZBAH{{tuple_delimiter}}PERSON{{tuple_delimiter}}Bratinas national and environmentalist who was held hostage) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}AURELIA{{tuple_delimiter}}Firuzabad negotiated a hostage exchange with Aurelia{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}QUINTARA{{tuple_delimiter}}AURELIA{{tuple_delimiter}}Quintara brokered the hostage exchange between Firuzabad and Aurelia{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}QUINTARA{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}Quintara brokered the hostage exchange between Firuzabad and Aurelia{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}SAMUEL NAMARA{{tuple_delimiter}}ALHAMIA PRISON{{tuple_delimiter}}Samuel Namara was a prisoner at Alhamia prison{{tuple_delimiter}}8) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}SAMUEL NAMARA{{tuple_delimiter}}MEGGIE TAZBAH{{tuple_delimiter}}Samuel Namara and Meggie Tazbah were exchanged in the same hostage release{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}SAMUEL NAMARA{{tuple_delimiter}}DURKE BATAGLANI{{tuple_delimiter}}Samuel Namara and Durke Bataglani were exchanged in the same hostage release{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}MEGGIE TAZBAH{{tuple_delimiter}}DURKE BATAGLANI{{tuple_delimiter}}Meggie Tazbah and Durke Bataglani were exchanged in the same hostage release{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}SAMUEL NAMARA{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}Samuel Namara was a hostage in Firuzabad{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}MEGGIE TAZBAH{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}Meggie Tazbah was a hostage in Firuzabad{{tuple_delimiter}}2) -{{record_delimiter}} -("relationship"{{tuple_delimiter}}DURKE BATAGLANI{{tuple_delimiter}}FIRUZABAD{{tuple_delimiter}}Durke Bataglani was a hostage in Firuzabad{{tuple_delimiter}}2) -{{completion_delimiter}} +("entity"<|>FIRUZABAD<|>GEO<|>Firuzabad held Aurelians as hostages) +## +("entity"<|>AURELIA<|>GEO<|>Country seeking to release hostages) +## +("entity"<|>QUINTARA<|>GEO<|>Country that negotiated a swap of money in exchange for hostages) +## +## +("entity"<|>TIRUZIA<|>GEO<|>Capital of Firuzabad where the Aurelians were being held) +## +("entity"<|>KROHAARA<|>GEO<|>Capital city in Quintara) +## +("entity"<|>CASHION<|>GEO<|>Capital city in Aurelia) +## +("entity"<|>SAMUEL NAMARA<|>PERSON<|>Aurelian who spent time in Tiruzia's Alhamia Prison) +## +("entity"<|>ALHAMIA PRISON<|>GEO<|>Prison in Tiruzia) +## +("entity"<|>DURKE BATAGLANI<|>PERSON<|>Aurelian journalist who was held hostage) +## +("entity"<|>MEGGIE TAZBAH<|>PERSON<|>Bratinas national and environmentalist who was held hostage) +## +("relationship"<|>FIRUZABAD<|>AURELIA<|>Firuzabad negotiated a hostage exchange with Aurelia<|>2) +## +("relationship"<|>QUINTARA<|>AURELIA<|>Quintara brokered the hostage exchange between Firuzabad and Aurelia<|>2) +## +("relationship"<|>QUINTARA<|>FIRUZABAD<|>Quintara brokered the hostage exchange between Firuzabad and Aurelia<|>2) +## +("relationship"<|>SAMUEL NAMARA<|>ALHAMIA PRISON<|>Samuel Namara was a prisoner at Alhamia prison<|>8) +## +("relationship"<|>SAMUEL NAMARA<|>MEGGIE TAZBAH<|>Samuel Namara and Meggie Tazbah were exchanged in the same hostage release<|>2) +## +("relationship"<|>SAMUEL NAMARA<|>DURKE BATAGLANI<|>Samuel Namara and Durke Bataglani were exchanged in the same hostage release<|>2) +## +("relationship"<|>MEGGIE TAZBAH<|>DURKE BATAGLANI<|>Meggie Tazbah and Durke Bataglani were exchanged in the same hostage release<|>2) +## +("relationship"<|>SAMUEL NAMARA<|>FIRUZABAD<|>Samuel Namara was a hostage in Firuzabad<|>2) +## +("relationship"<|>MEGGIE TAZBAH<|>FIRUZABAD<|>Meggie Tazbah was a hostage in Firuzabad<|>2) +## +("relationship"<|>DURKE BATAGLANI<|>FIRUZABAD<|>Durke Bataglani was a hostage in Firuzabad<|>2) +<|COMPLETE|> ###################### -Real Data- diff --git a/graphrag/prompt_tune/prompt/entity_types.py b/packages/graphrag/graphrag/prompt_tune/prompt/entity_types.py similarity index 100% rename from graphrag/prompt_tune/prompt/entity_types.py rename to packages/graphrag/graphrag/prompt_tune/prompt/entity_types.py diff --git a/graphrag/prompt_tune/prompt/language.py b/packages/graphrag/graphrag/prompt_tune/prompt/language.py similarity index 100% rename from graphrag/prompt_tune/prompt/language.py rename to packages/graphrag/graphrag/prompt_tune/prompt/language.py diff --git a/graphrag/prompt_tune/prompt/persona.py b/packages/graphrag/graphrag/prompt_tune/prompt/persona.py similarity index 100% rename from graphrag/prompt_tune/prompt/persona.py rename to packages/graphrag/graphrag/prompt_tune/prompt/persona.py diff --git a/graphrag/prompt_tune/template/__init__.py b/packages/graphrag/graphrag/prompt_tune/template/__init__.py similarity index 100% rename from graphrag/prompt_tune/template/__init__.py rename to packages/graphrag/graphrag/prompt_tune/template/__init__.py diff --git a/graphrag/prompt_tune/template/community_report_summarization.py b/packages/graphrag/graphrag/prompt_tune/template/community_report_summarization.py similarity index 98% rename from graphrag/prompt_tune/template/community_report_summarization.py rename to packages/graphrag/graphrag/prompt_tune/template/community_report_summarization.py index b0d037ab6b..32888fb137 100644 --- a/graphrag/prompt_tune/template/community_report_summarization.py +++ b/packages/graphrag/graphrag/prompt_tune/template/community_report_summarization.py @@ -56,13 +56,13 @@ Entities -id,entity,description +human_readable_id,title,description 5,VERDANT OASIS PLAZA,Verdant Oasis Plaza is the location of the Unity March 6,HARMONY ASSEMBLY,Harmony Assembly is an organization that is holding a march at Verdant Oasis Plaza Relationships -id,source,target,description +human_readable_id,source,target,description 37,VERDANT OASIS PLAZA,UNITY MARCH,Verdant Oasis Plaza is the location of the Unity March 38,VERDANT OASIS PLAZA,HARMONY ASSEMBLY,Harmony Assembly is holding a march at Verdant Oasis Plaza 39,VERDANT OASIS PLAZA,UNITY MARCH,The Unity March is taking place at Verdant Oasis Plaza diff --git a/graphrag/prompt_tune/template/entity_summarization.py b/packages/graphrag/graphrag/prompt_tune/template/entity_summarization.py similarity index 100% rename from graphrag/prompt_tune/template/entity_summarization.py rename to packages/graphrag/graphrag/prompt_tune/template/entity_summarization.py diff --git a/graphrag/prompt_tune/template/extract_graph.py b/packages/graphrag/graphrag/prompt_tune/template/extract_graph.py similarity index 84% rename from graphrag/prompt_tune/template/extract_graph.py rename to packages/graphrag/graphrag/prompt_tune/template/extract_graph.py index 32d8756ec2..58a095cd0d 100644 --- a/graphrag/prompt_tune/template/extract_graph.py +++ b/packages/graphrag/graphrag/prompt_tune/template/extract_graph.py @@ -12,7 +12,7 @@ - entity_name: Name of the entity, capitalized - entity_type: One of the following types: [{entity_types}] - entity_description: Comprehensive description of the entity's attributes and activities -Format each entity as ("entity"{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}) +Format each entity as ("entity"<|><|><|>) 2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other. For each pair of related entities, extract the following information: @@ -20,13 +20,13 @@ - target_entity: name of the target entity, as identified in step 1 - relationship_description: explanation as to why you think the source entity and the target entity are related to each other - relationship_strength: an integer score between 1 to 10, indicating strength of the relationship between the source entity and target entity -Format each relationship as ("relationship"{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}) +Format each relationship as ("relationship"<|><|><|><|>) -3. Return output in {language} as a single list of all the entities and relationships identified in steps 1 and 2. Use **{{record_delimiter}}** as the list delimiter. +3. Return output in {language} as a single list of all the entities and relationships identified in steps 1 and 2. Use **##** as the list delimiter. 4. If you have to translate into {language}, just translate the descriptions, nothing else! -5. When finished, output {{completion_delimiter}}. +5. When finished, output <|COMPLETE|>. -Examples- ###################### @@ -113,7 +113,7 @@ - entity_name: Name of the entity, capitalized - entity_type: Suggest several labels or categories for the entity. The categories should not be specific, but should be as general as possible. - entity_description: Comprehensive description of the entity's attributes and activities -Format each entity as ("entity"{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}) +Format each entity as ("entity"<|><|><|>) 2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other. For each pair of related entities, extract the following information: @@ -121,13 +121,13 @@ - target_entity: name of the target entity, as identified in step 1 - relationship_description: explanation as to why you think the source entity and the target entity are related to each other - relationship_strength: a numeric score indicating strength of the relationship between the source entity and target entity -Format each relationship as ("relationship"{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}{{tuple_delimiter}}) +Format each relationship as ("relationship"<|><|><|><|>) -3. Return output in {language} as a single list of all the entities and relationships identified in steps 1 and 2. Use **{{record_delimiter}}** as the list delimiter. +3. Return output in {language} as a single list of all the entities and relationships identified in steps 1 and 2. Use **##** as the list delimiter. 4. If you have to translate into {language}, just translate the descriptions, nothing else! -5. When finished, output {{completion_delimiter}}. +5. When finished, output <|COMPLETE|>. -Examples- ###################### diff --git a/graphrag/prompt_tune/types.py b/packages/graphrag/graphrag/prompt_tune/types.py similarity index 100% rename from graphrag/prompt_tune/types.py rename to packages/graphrag/graphrag/prompt_tune/types.py diff --git a/graphrag/prompts/__init__.py b/packages/graphrag/graphrag/prompts/__init__.py similarity index 100% rename from graphrag/prompts/__init__.py rename to packages/graphrag/graphrag/prompts/__init__.py diff --git a/graphrag/prompts/index/__init__.py b/packages/graphrag/graphrag/prompts/index/__init__.py similarity index 100% rename from graphrag/prompts/index/__init__.py rename to packages/graphrag/graphrag/prompts/index/__init__.py diff --git a/graphrag/prompts/index/community_report.py b/packages/graphrag/graphrag/prompts/index/community_report.py similarity index 99% rename from graphrag/prompts/index/community_report.py rename to packages/graphrag/graphrag/prompts/index/community_report.py index c3a7702ba0..b1f0f8abe2 100644 --- a/graphrag/prompts/index/community_report.py +++ b/packages/graphrag/graphrag/prompts/index/community_report.py @@ -59,13 +59,13 @@ Entities -id,entity,description +human_readable_id,title,description 5,VERDANT OASIS PLAZA,Verdant Oasis Plaza is the location of the Unity March 6,HARMONY ASSEMBLY,Harmony Assembly is an organization that is holding a march at Verdant Oasis Plaza Relationships -id,source,target,description +human_readable_id,source,target,description 37,VERDANT OASIS PLAZA,UNITY MARCH,Verdant Oasis Plaza is the location of the Unity March 38,VERDANT OASIS PLAZA,HARMONY ASSEMBLY,Harmony Assembly is holding a march at Verdant Oasis Plaza 39,VERDANT OASIS PLAZA,UNITY MARCH,The Unity March is taking place at Verdant Oasis Plaza diff --git a/graphrag/prompts/index/community_report_text_units.py b/packages/graphrag/graphrag/prompts/index/community_report_text_units.py similarity index 100% rename from graphrag/prompts/index/community_report_text_units.py rename to packages/graphrag/graphrag/prompts/index/community_report_text_units.py diff --git a/graphrag/prompts/index/extract_claims.py b/packages/graphrag/graphrag/prompts/index/extract_claims.py similarity index 64% rename from graphrag/prompts/index/extract_claims.py rename to packages/graphrag/graphrag/prompts/index/extract_claims.py index 5e0e5570c6..59b19c9061 100644 --- a/graphrag/prompts/index/extract_claims.py +++ b/packages/graphrag/graphrag/prompts/index/extract_claims.py @@ -22,11 +22,11 @@ - Claim Date: Period (start_date, end_date) when the claim was made. Both start_date and end_date should be in ISO-8601 format. If the claim was made on a single date rather than a date range, set the same date for both start_date and end_date. If date is unknown, return **NONE**. - Claim Source Text: List of **all** quotes from the original text that are relevant to the claim. -Format each claim as ({tuple_delimiter}{tuple_delimiter}{tuple_delimiter}{tuple_delimiter}{tuple_delimiter}{tuple_delimiter}{tuple_delimiter}) +Format each claim as (<|><|><|><|><|><|><|>) -3. Return output in English as a single list of all the claims identified in steps 1 and 2. Use **{record_delimiter}** as the list delimiter. +3. Return output in English as a single list of all the claims identified in steps 1 and 2. Use **##** as the list delimiter. -4. When finished, output {completion_delimiter} +4. When finished, output <|COMPLETE|> -Examples- Example 1: @@ -35,8 +35,8 @@ Text: According to an article on 2022/01/10, Company A was fined for bid rigging while participating in multiple public tenders published by Government Agency B. The company is owned by Person C who was suspected of engaging in corruption activities in 2015. Output: -(COMPANY A{tuple_delimiter}GOVERNMENT AGENCY B{tuple_delimiter}ANTI-COMPETITIVE PRACTICES{tuple_delimiter}TRUE{tuple_delimiter}2022-01-10T00:00:00{tuple_delimiter}2022-01-10T00:00:00{tuple_delimiter}Company A was found to engage in anti-competitive practices because it was fined for bid rigging in multiple public tenders published by Government Agency B according to an article published on 2022/01/10{tuple_delimiter}According to an article published on 2022/01/10, Company A was fined for bid rigging while participating in multiple public tenders published by Government Agency B.) -{completion_delimiter} +(COMPANY A<|>GOVERNMENT AGENCY B<|>ANTI-COMPETITIVE PRACTICES<|>TRUE<|>2022-01-10T00:00:00<|>2022-01-10T00:00:00<|>Company A was found to engage in anti-competitive practices because it was fined for bid rigging in multiple public tenders published by Government Agency B according to an article published on 2022/01/10<|>According to an article published on 2022/01/10, Company A was fined for bid rigging while participating in multiple public tenders published by Government Agency B.) +<|COMPLETE|> Example 2: Entity specification: Company A, Person C @@ -44,10 +44,10 @@ Text: According to an article on 2022/01/10, Company A was fined for bid rigging while participating in multiple public tenders published by Government Agency B. The company is owned by Person C who was suspected of engaging in corruption activities in 2015. Output: -(COMPANY A{tuple_delimiter}GOVERNMENT AGENCY B{tuple_delimiter}ANTI-COMPETITIVE PRACTICES{tuple_delimiter}TRUE{tuple_delimiter}2022-01-10T00:00:00{tuple_delimiter}2022-01-10T00:00:00{tuple_delimiter}Company A was found to engage in anti-competitive practices because it was fined for bid rigging in multiple public tenders published by Government Agency B according to an article published on 2022/01/10{tuple_delimiter}According to an article published on 2022/01/10, Company A was fined for bid rigging while participating in multiple public tenders published by Government Agency B.) -{record_delimiter} -(PERSON C{tuple_delimiter}NONE{tuple_delimiter}CORRUPTION{tuple_delimiter}SUSPECTED{tuple_delimiter}2015-01-01T00:00:00{tuple_delimiter}2015-12-30T00:00:00{tuple_delimiter}Person C was suspected of engaging in corruption activities in 2015{tuple_delimiter}The company is owned by Person C who was suspected of engaging in corruption activities in 2015) -{completion_delimiter} +(COMPANY A<|>GOVERNMENT AGENCY B<|>ANTI-COMPETITIVE PRACTICES<|>TRUE<|>2022-01-10T00:00:00<|>2022-01-10T00:00:00<|>Company A was found to engage in anti-competitive practices because it was fined for bid rigging in multiple public tenders published by Government Agency B according to an article published on 2022/01/10<|>According to an article published on 2022/01/10, Company A was fined for bid rigging while participating in multiple public tenders published by Government Agency B.) +## +(PERSON C<|>NONE<|>CORRUPTION<|>SUSPECTED<|>2015-01-01T00:00:00<|>2015-12-30T00:00:00<|>Person C was suspected of engaging in corruption activities in 2015<|>The company is owned by Person C who was suspected of engaging in corruption activities in 2015) +<|COMPLETE|> -Real Data- Use the following input for your answer. diff --git a/packages/graphrag/graphrag/prompts/index/extract_graph.py b/packages/graphrag/graphrag/prompts/index/extract_graph.py new file mode 100644 index 0000000000..91157937d4 --- /dev/null +++ b/packages/graphrag/graphrag/prompts/index/extract_graph.py @@ -0,0 +1,129 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""A file containing prompts definition.""" + +GRAPH_EXTRACTION_PROMPT = """ +-Goal- +Given a text document that is potentially relevant to this activity and a list of entity types, identify all entities of those types from the text and all relationships among the identified entities. + +-Steps- +1. Identify all entities. For each identified entity, extract the following information: +- entity_name: Name of the entity, capitalized +- entity_type: One of the following types: [{entity_types}] +- entity_description: Comprehensive description of the entity's attributes and activities +Format each entity as ("entity"<|><|><|>) + +2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other. +For each pair of related entities, extract the following information: +- source_entity: name of the source entity, as identified in step 1 +- target_entity: name of the target entity, as identified in step 1 +- relationship_description: explanation as to why you think the source entity and the target entity are related to each other +- relationship_strength: a numeric score indicating strength of the relationship between the source entity and target entity + Format each relationship as ("relationship"<|><|><|><|>) + +3. Return output in English as a single list of all the entities and relationships identified in steps 1 and 2. Use **##** as the list delimiter. + +4. When finished, output <|COMPLETE|> + +###################### +-Examples- +###################### +Example 1: +Entity_types: ORGANIZATION,PERSON +Text: +The Verdantis's Central Institution is scheduled to meet on Monday and Thursday, with the institution planning to release its latest policy decision on Thursday at 1:30 p.m. PDT, followed by a press conference where Central Institution Chair Martin Smith will take questions. Investors expect the Market Strategy Committee to hold its benchmark interest rate steady in a range of 3.5%-3.75%. +###################### +Output: +("entity"<|>CENTRAL INSTITUTION<|>ORGANIZATION<|>The Central Institution is the Federal Reserve of Verdantis, which is setting interest rates on Monday and Thursday) +## +("entity"<|>MARTIN SMITH<|>PERSON<|>Martin Smith is the chair of the Central Institution) +## +("entity"<|>MARKET STRATEGY COMMITTEE<|>ORGANIZATION<|>The Central Institution committee makes key decisions about interest rates and the growth of Verdantis's money supply) +## +("relationship"<|>MARTIN SMITH<|>CENTRAL INSTITUTION<|>Martin Smith is the Chair of the Central Institution and will answer questions at a press conference<|>9) +<|COMPLETE|> + +###################### +Example 2: +Entity_types: ORGANIZATION +Text: +TechGlobal's (TG) stock skyrocketed in its opening day on the Global Exchange Thursday. But IPO experts warn that the semiconductor corporation's debut on the public markets isn't indicative of how other newly listed companies may perform. + +TechGlobal, a formerly public company, was taken private by Vision Holdings in 2014. The well-established chip designer says it powers 85% of premium smartphones. +###################### +Output: +("entity"<|>TECHGLOBAL<|>ORGANIZATION<|>TechGlobal is a stock now listed on the Global Exchange which powers 85% of premium smartphones) +## +("entity"<|>VISION HOLDINGS<|>ORGANIZATION<|>Vision Holdings is a firm that previously owned TechGlobal) +## +("relationship"<|>TECHGLOBAL<|>VISION HOLDINGS<|>Vision Holdings formerly owned TechGlobal from 2014 until present<|>5) +<|COMPLETE|> + +###################### +Example 3: +Entity_types: ORGANIZATION,GEO,PERSON +Text: +Five Aurelians jailed for 8 years in Firuzabad and widely regarded as hostages are on their way home to Aurelia. + +The swap orchestrated by Quintara was finalized when $8bn of Firuzi funds were transferred to financial institutions in Krohaara, the capital of Quintara. + +The exchange initiated in Firuzabad's capital, Tiruzia, led to the four men and one woman, who are also Firuzi nationals, boarding a chartered flight to Krohaara. + +They were welcomed by senior Aurelian officials and are now on their way to Aurelia's capital, Cashion. + +The Aurelians include 39-year-old businessman Samuel Namara, who has been held in Tiruzia's Alhamia Prison, as well as journalist Durke Bataglani, 59, and environmentalist Meggie Tazbah, 53, who also holds Bratinas nationality. +###################### +Output: +("entity"<|>FIRUZABAD<|>GEO<|>Firuzabad held Aurelians as hostages) +## +("entity"<|>AURELIA<|>GEO<|>Country seeking to release hostages) +## +("entity"<|>QUINTARA<|>GEO<|>Country that negotiated a swap of money in exchange for hostages) +## +## +("entity"<|>TIRUZIA<|>GEO<|>Capital of Firuzabad where the Aurelians were being held) +## +("entity"<|>KROHAARA<|>GEO<|>Capital city in Quintara) +## +("entity"<|>CASHION<|>GEO<|>Capital city in Aurelia) +## +("entity"<|>SAMUEL NAMARA<|>PERSON<|>Aurelian who spent time in Tiruzia's Alhamia Prison) +## +("entity"<|>ALHAMIA PRISON<|>GEO<|>Prison in Tiruzia) +## +("entity"<|>DURKE BATAGLANI<|>PERSON<|>Aurelian journalist who was held hostage) +## +("entity"<|>MEGGIE TAZBAH<|>PERSON<|>Bratinas national and environmentalist who was held hostage) +## +("relationship"<|>FIRUZABAD<|>AURELIA<|>Firuzabad negotiated a hostage exchange with Aurelia<|>2) +## +("relationship"<|>QUINTARA<|>AURELIA<|>Quintara brokered the hostage exchange between Firuzabad and Aurelia<|>2) +## +("relationship"<|>QUINTARA<|>FIRUZABAD<|>Quintara brokered the hostage exchange between Firuzabad and Aurelia<|>2) +## +("relationship"<|>SAMUEL NAMARA<|>ALHAMIA PRISON<|>Samuel Namara was a prisoner at Alhamia prison<|>8) +## +("relationship"<|>SAMUEL NAMARA<|>MEGGIE TAZBAH<|>Samuel Namara and Meggie Tazbah were exchanged in the same hostage release<|>2) +## +("relationship"<|>SAMUEL NAMARA<|>DURKE BATAGLANI<|>Samuel Namara and Durke Bataglani were exchanged in the same hostage release<|>2) +## +("relationship"<|>MEGGIE TAZBAH<|>DURKE BATAGLANI<|>Meggie Tazbah and Durke Bataglani were exchanged in the same hostage release<|>2) +## +("relationship"<|>SAMUEL NAMARA<|>FIRUZABAD<|>Samuel Namara was a hostage in Firuzabad<|>2) +## +("relationship"<|>MEGGIE TAZBAH<|>FIRUZABAD<|>Meggie Tazbah was a hostage in Firuzabad<|>2) +## +("relationship"<|>DURKE BATAGLANI<|>FIRUZABAD<|>Durke Bataglani was a hostage in Firuzabad<|>2) +<|COMPLETE|> + +###################### +-Real Data- +###################### +Entity_types: {entity_types} +Text: {input_text} +###################### +Output:""" + +CONTINUE_PROMPT = "MANY entities and relationships were missed in the last extraction. Remember to ONLY emit entities that match any of the previously extracted types. Add them below using the same format:\n" +LOOP_PROMPT = "It appears some entities and relationships may have still been missed. Answer Y if there are still entities or relationships that need to be added, or N if there are none. Please answer with a single letter Y or N.\n" diff --git a/graphrag/prompts/index/summarize_descriptions.py b/packages/graphrag/graphrag/prompts/index/summarize_descriptions.py similarity index 100% rename from graphrag/prompts/index/summarize_descriptions.py rename to packages/graphrag/graphrag/prompts/index/summarize_descriptions.py diff --git a/graphrag/prompts/query/__init__.py b/packages/graphrag/graphrag/prompts/query/__init__.py similarity index 100% rename from graphrag/prompts/query/__init__.py rename to packages/graphrag/graphrag/prompts/query/__init__.py diff --git a/graphrag/prompts/query/basic_search_system_prompt.py b/packages/graphrag/graphrag/prompts/query/basic_search_system_prompt.py similarity index 96% rename from graphrag/prompts/query/basic_search_system_prompt.py rename to packages/graphrag/graphrag/prompts/query/basic_search_system_prompt.py index a20fb6ad10..bc37c70d98 100644 --- a/graphrag/prompts/query/basic_search_system_prompt.py +++ b/packages/graphrag/graphrag/prompts/query/basic_search_system_prompt.py @@ -27,7 +27,7 @@ "Person X is the owner of Company Y and subject to many allegations of wrongdoing [Data: Sources (2, 7, 64, 46, 34, +more)]. He is also CEO of company X [Data: Sources (1, 3)]" -where 1, 2, 3, 7, 34, 46, and 64 represent the source id taken from the "source_id" column in the provided tables. +where 1, 2, 3, 7, 34, 46, and 64 represent the source id taken from the "id" column in the provided tables. Do not include information where the supporting evidence for it is not provided. @@ -60,7 +60,7 @@ "Person X is the owner of Company Y and subject to many allegations of wrongdoing [Data: Sources (2, 7, 64, 46, 34, +more)]. He is also CEO of company X [Data: Sources (1, 3)]" -where 1, 2, 3, 7, 34, 46, and 64 represent the source id taken from the "source_id" column in the provided tables. +where 1, 2, 3, 7, 34, 46, and 64 represent the source id taken from the "id" column in the provided tables. Do not include information where the supporting evidence for it is not provided. diff --git a/graphrag/prompts/query/drift_search_system_prompt.py b/packages/graphrag/graphrag/prompts/query/drift_search_system_prompt.py similarity index 94% rename from graphrag/prompts/query/drift_search_system_prompt.py rename to packages/graphrag/graphrag/prompts/query/drift_search_system_prompt.py index 3faae89a0e..4cb220f69d 100644 --- a/graphrag/prompts/query/drift_search_system_prompt.py +++ b/packages/graphrag/graphrag/prompts/query/drift_search_system_prompt.py @@ -65,7 +65,7 @@ Add sections and commentary to the response as appropriate for the length and format. -Additionally provide a score between 0 and 100 representing how well the response addresses the overall research question: {global_query}. Based on your response, suggest up to five follow-up questions that could be asked to further explore the topic as it relates to the overall research question. Do not include scores or follow up questions in the 'response' field of the JSON, add them to the respective 'score' and 'follow_up_queries' keys of the JSON output. Format your response in JSON with the following keys and values: +Additionally provide a score between 0 and 100 representing how well the response addresses the overall research question: {global_query}. Based on your response, suggest up to {followups} follow-up questions that could be asked to further explore the topic as it relates to the overall research question. Do not include scores or follow up questions in the 'response' field of the JSON, add them to the respective 'score' and 'follow_up_queries' keys of the JSON output. Format your response in JSON with the following keys and values: {{'response': str, Put your answer, formatted in markdown, here. Do not answer the global query in this section. 'score': int, diff --git a/graphrag/prompts/query/global_search_knowledge_system_prompt.py b/packages/graphrag/graphrag/prompts/query/global_search_knowledge_system_prompt.py similarity index 100% rename from graphrag/prompts/query/global_search_knowledge_system_prompt.py rename to packages/graphrag/graphrag/prompts/query/global_search_knowledge_system_prompt.py diff --git a/graphrag/prompts/query/global_search_map_system_prompt.py b/packages/graphrag/graphrag/prompts/query/global_search_map_system_prompt.py similarity index 100% rename from graphrag/prompts/query/global_search_map_system_prompt.py rename to packages/graphrag/graphrag/prompts/query/global_search_map_system_prompt.py diff --git a/graphrag/prompts/query/global_search_reduce_system_prompt.py b/packages/graphrag/graphrag/prompts/query/global_search_reduce_system_prompt.py similarity index 100% rename from graphrag/prompts/query/global_search_reduce_system_prompt.py rename to packages/graphrag/graphrag/prompts/query/global_search_reduce_system_prompt.py diff --git a/graphrag/prompts/query/local_search_system_prompt.py b/packages/graphrag/graphrag/prompts/query/local_search_system_prompt.py similarity index 100% rename from graphrag/prompts/query/local_search_system_prompt.py rename to packages/graphrag/graphrag/prompts/query/local_search_system_prompt.py diff --git a/graphrag/prompts/query/question_gen_system_prompt.py b/packages/graphrag/graphrag/prompts/query/question_gen_system_prompt.py similarity index 100% rename from graphrag/prompts/query/question_gen_system_prompt.py rename to packages/graphrag/graphrag/prompts/query/question_gen_system_prompt.py diff --git a/graphrag/py.typed b/packages/graphrag/graphrag/py.typed similarity index 100% rename from graphrag/py.typed rename to packages/graphrag/graphrag/py.typed diff --git a/graphrag/query/__init__.py b/packages/graphrag/graphrag/query/__init__.py similarity index 100% rename from graphrag/query/__init__.py rename to packages/graphrag/graphrag/query/__init__.py diff --git a/graphrag/query/context_builder/__init__.py b/packages/graphrag/graphrag/query/context_builder/__init__.py similarity index 100% rename from graphrag/query/context_builder/__init__.py rename to packages/graphrag/graphrag/query/context_builder/__init__.py diff --git a/graphrag/query/context_builder/builders.py b/packages/graphrag/graphrag/query/context_builder/builders.py similarity index 100% rename from graphrag/query/context_builder/builders.py rename to packages/graphrag/graphrag/query/context_builder/builders.py diff --git a/graphrag/query/context_builder/community_context.py b/packages/graphrag/graphrag/query/context_builder/community_context.py similarity index 99% rename from graphrag/query/context_builder/community_context.py rename to packages/graphrag/graphrag/query/context_builder/community_context.py index 4917be0186..2d3238b0d0 100644 --- a/graphrag/query/context_builder/community_context.py +++ b/packages/graphrag/graphrag/query/context_builder/community_context.py @@ -8,11 +8,11 @@ from typing import Any, cast import pandas as pd +from graphrag_llm.tokenizer import Tokenizer from graphrag.data_model.community_report import CommunityReport from graphrag.data_model.entity import Entity from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer logger = logging.getLogger(__name__) diff --git a/graphrag/query/context_builder/conversation_history.py b/packages/graphrag/graphrag/query/context_builder/conversation_history.py similarity index 99% rename from graphrag/query/context_builder/conversation_history.py rename to packages/graphrag/graphrag/query/context_builder/conversation_history.py index c20998121c..1170c7c0da 100644 --- a/graphrag/query/context_builder/conversation_history.py +++ b/packages/graphrag/graphrag/query/context_builder/conversation_history.py @@ -7,9 +7,9 @@ from enum import Enum import pandas as pd +from graphrag_llm.tokenizer import Tokenizer from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer """ Enum for conversation roles diff --git a/graphrag/query/context_builder/dynamic_community_selection.py b/packages/graphrag/graphrag/query/context_builder/dynamic_community_selection.py similarity index 94% rename from graphrag/query/context_builder/dynamic_community_selection.py rename to packages/graphrag/graphrag/query/context_builder/dynamic_community_selection.py index 904478f738..0ef22b9176 100644 --- a/graphrag/query/context_builder/dynamic_community_selection.py +++ b/packages/graphrag/graphrag/query/context_builder/dynamic_community_selection.py @@ -8,14 +8,17 @@ from collections import Counter from copy import deepcopy from time import time -from typing import Any +from typing import TYPE_CHECKING, Any + +from graphrag_llm.tokenizer import Tokenizer from graphrag.data_model.community import Community from graphrag.data_model.community_report import CommunityReport -from graphrag.language_model.protocol.base import ChatModel from graphrag.query.context_builder.rate_prompt import RATE_QUERY from graphrag.query.context_builder.rate_relevancy import rate_relevancy -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion logger = logging.getLogger(__name__) @@ -30,7 +33,7 @@ def __init__( self, community_reports: list[CommunityReport], communities: list[Community], - model: ChatModel, + model: "LLMCompletion", tokenizer: Tokenizer, rate_query: str = RATE_QUERY, use_summary: bool = False, @@ -123,8 +126,10 @@ async def select(self, query: str) -> tuple[list[CommunityReport], dict[str, Any # TODO check why some sub_communities are NOT in report_df if community in self.communities: for child in self.communities[community].children: - if child in self.reports: - communities_to_rate.append(child) + # Convert child to string to match self.reports key type + child_str = str(child) + if child_str in self.reports: + communities_to_rate.append(child_str) else: logger.debug( "dynamic community selection: cannot find community %s in reports", diff --git a/graphrag/query/context_builder/entity_extraction.py b/packages/graphrag/graphrag/query/context_builder/entity_extraction.py similarity index 92% rename from graphrag/query/context_builder/entity_extraction.py rename to packages/graphrag/graphrag/query/context_builder/entity_extraction.py index 0dd03ba281..6eab03b5cc 100644 --- a/graphrag/query/context_builder/entity_extraction.py +++ b/packages/graphrag/graphrag/query/context_builder/entity_extraction.py @@ -4,16 +4,20 @@ """Orchestration Context Builders.""" from enum import Enum +from typing import TYPE_CHECKING + +from graphrag_vectors import VectorStore from graphrag.data_model.entity import Entity from graphrag.data_model.relationship import Relationship -from graphrag.language_model.protocol.base import EmbeddingModel from graphrag.query.input.retrieval.entities import ( get_entity_by_id, get_entity_by_key, get_entity_by_name, ) -from graphrag.vector_stores.base import BaseVectorStore + +if TYPE_CHECKING: + from graphrag_llm.embedding import LLMEmbedding class EntityVectorStoreKey(str, Enum): @@ -36,8 +40,8 @@ def from_string(value: str) -> "EntityVectorStoreKey": def map_query_to_entities( query: str, - text_embedding_vectorstore: BaseVectorStore, - text_embedder: EmbeddingModel, + text_embedding_vectorstore: VectorStore, + text_embedder: "LLMEmbedding", all_entities_dict: dict[str, Entity], embedding_vectorstore_key: str = EntityVectorStoreKey.ID, include_entity_names: list[str] | None = None, @@ -57,7 +61,7 @@ def map_query_to_entities( # oversample to account for excluded entities search_results = text_embedding_vectorstore.similarity_search_by_text( text=query, - text_embedder=lambda t: text_embedder.embed(t), + text_embedder=lambda t: text_embedder.embedding(input=[t]).first_embedding, k=k * oversample_scaler, ) for result in search_results: diff --git a/graphrag/query/context_builder/local_context.py b/packages/graphrag/graphrag/query/context_builder/local_context.py similarity index 99% rename from graphrag/query/context_builder/local_context.py rename to packages/graphrag/graphrag/query/context_builder/local_context.py index a2d8a54533..b84566bde0 100644 --- a/graphrag/query/context_builder/local_context.py +++ b/packages/graphrag/graphrag/query/context_builder/local_context.py @@ -7,6 +7,7 @@ from typing import Any, cast import pandas as pd +from graphrag_llm.tokenizer import Tokenizer from graphrag.data_model.covariate import Covariate from graphrag.data_model.entity import Entity @@ -24,7 +25,6 @@ to_relationship_dataframe, ) from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer def build_entity_context( diff --git a/graphrag/query/context_builder/rate_prompt.py b/packages/graphrag/graphrag/query/context_builder/rate_prompt.py similarity index 100% rename from graphrag/query/context_builder/rate_prompt.py rename to packages/graphrag/graphrag/query/context_builder/rate_prompt.py diff --git a/graphrag/query/context_builder/rate_relevancy.py b/packages/graphrag/graphrag/query/context_builder/rate_relevancy.py similarity index 73% rename from graphrag/query/context_builder/rate_relevancy.py rename to packages/graphrag/graphrag/query/context_builder/rate_relevancy.py index 6fa128999f..16474369ac 100644 --- a/graphrag/query/context_builder/rate_relevancy.py +++ b/packages/graphrag/graphrag/query/context_builder/rate_relevancy.py @@ -6,14 +6,20 @@ import asyncio import logging from contextlib import nullcontext -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np +from graphrag_llm.tokenizer import Tokenizer +from graphrag_llm.utils import ( + CompletionMessagesBuilder, + gather_completion_response_async, +) -from graphrag.language_model.protocol.base import ChatModel from graphrag.query.context_builder.rate_prompt import RATE_QUERY from graphrag.query.llm.text_utils import try_parse_json_object -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion logger = logging.getLogger(__name__) @@ -21,7 +27,7 @@ async def rate_relevancy( query: str, description: str, - model: ChatModel, + model: "LLMCompletion", tokenizer: Tokenizer, rate_query: str = RATE_QUERY, num_repeats: int = 1, @@ -42,18 +48,21 @@ async def rate_relevancy( semaphore: asyncio.Semaphore to limit the number of concurrent LLM calls (default: None) """ llm_calls, prompt_tokens, output_tokens, ratings = 0, 0, 0, [] - messages = [ - { - "role": "system", - "content": rate_query.format(description=description, question=query), - }, - ] + + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(rate_query.format(description=description, question=query)) + .add_user_message(query) + ) + for _ in range(num_repeats): async with semaphore if semaphore is not None else nullcontext(): - model_response = await model.achat( - prompt=query, history=messages, model_parameters=model_params, json=True + model_response = await model.completion_async( + messages=messages_builder.build(), + response_format_json_object=True, + **model_params, ) - response = model_response.output.content + response = await gather_completion_response_async(model_response) try: _, parsed_response = try_parse_json_object(response) ratings.append(parsed_response["rating"]) @@ -63,7 +72,7 @@ async def rate_relevancy( logger.warning("Error parsing json response, defaulting to rating 1") ratings.append(1) llm_calls += 1 - prompt_tokens += tokenizer.num_tokens(messages[0]["content"]) + prompt_tokens += tokenizer.num_prompt_tokens(messages_builder.build()) output_tokens += tokenizer.num_tokens(response) # select the decision with the most votes options, counts = np.unique(ratings, return_counts=True) diff --git a/graphrag/query/context_builder/source_context.py b/packages/graphrag/graphrag/query/context_builder/source_context.py similarity index 98% rename from graphrag/query/context_builder/source_context.py rename to packages/graphrag/graphrag/query/context_builder/source_context.py index eaf308f629..ff121b8cdd 100644 --- a/graphrag/query/context_builder/source_context.py +++ b/packages/graphrag/graphrag/query/context_builder/source_context.py @@ -7,11 +7,11 @@ from typing import Any, cast import pandas as pd +from graphrag_llm.tokenizer import Tokenizer from graphrag.data_model.relationship import Relationship from graphrag.data_model.text_unit import TextUnit from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer """ Contain util functions to build text unit context for the search's system prompt diff --git a/graphrag/query/factory.py b/packages/graphrag/graphrag/query/factory.py similarity index 74% rename from graphrag/query/factory.py rename to packages/graphrag/graphrag/query/factory.py index 10dfd3ba1f..5655074ed0 100644 --- a/graphrag/query/factory.py +++ b/packages/graphrag/graphrag/query/factory.py @@ -3,6 +3,10 @@ """Query Factory methods to support CLI.""" +from graphrag_llm.completion import create_completion +from graphrag_llm.embedding import create_embedding +from graphrag_vectors import VectorStore + from graphrag.callbacks.query_callbacks import QueryCallbacks from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.data_model.community import Community @@ -11,10 +15,6 @@ from graphrag.data_model.entity import Entity from graphrag.data_model.relationship import Relationship from graphrag.data_model.text_unit import TextUnit -from graphrag.language_model.manager import ModelManager -from graphrag.language_model.providers.fnllm.utils import ( - get_openai_model_parameters_from_config, -) from graphrag.query.context_builder.entity_extraction import EntityVectorStoreKey from graphrag.query.structured_search.basic_search.basic_context import ( BasicSearchContext, @@ -32,8 +32,6 @@ LocalSearchMixedContext, ) from graphrag.query.structured_search.local_search.search import LocalSearch -from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.vector_stores.base import BaseVectorStore def get_local_search_engine( @@ -44,34 +42,28 @@ def get_local_search_engine( relationships: list[Relationship], covariates: dict[str, list[Covariate]], response_type: str, - description_embedding_store: BaseVectorStore, + description_embedding_store: VectorStore, system_prompt: str | None = None, callbacks: list[QueryCallbacks] | None = None, ) -> LocalSearch: """Create a local search engine based on data + configuration.""" - model_settings = config.get_language_model_config(config.local_search.chat_model_id) - - chat_model = ModelManager().get_or_create_chat_model( - name="local_search_chat", - model_type=model_settings.type, - config=model_settings, + model_settings = config.get_completion_model_config( + config.local_search.completion_model_id ) - embedding_settings = config.get_language_model_config( + chat_model = create_completion(model_settings) + + embedding_settings = config.get_embedding_model_config( config.local_search.embedding_model_id ) - embedding_model = ModelManager().get_or_create_embedding_model( - name="local_search_embedding", - model_type=embedding_settings.type, - config=embedding_settings, - ) + embedding_model = create_embedding(embedding_settings) - tokenizer = get_tokenizer(model_config=model_settings) + tokenizer = chat_model.tokenizer ls_config = config.local_search - model_params = get_openai_model_parameters_from_config(model_settings) + model_params = model_settings.call_args return LocalSearch( model=chat_model, @@ -121,20 +113,16 @@ def get_global_search_engine( callbacks: list[QueryCallbacks] | None = None, ) -> GlobalSearch: """Create a global search engine based on data + configuration.""" - model_settings = config.get_language_model_config( - config.global_search.chat_model_id + model_settings = config.get_completion_model_config( + config.global_search.completion_model_id ) - model = ModelManager().get_or_create_chat_model( - name="global_search", - model_type=model_settings.type, - config=model_settings, - ) + model = create_completion(model_settings) - model_params = get_openai_model_parameters_from_config(model_settings) + model_params = model_settings.call_args # Here we get encoding based on specified encoding name - tokenizer = get_tokenizer(model_config=model_settings) + tokenizer = model.tokenizer gs_config = config.global_search dynamic_community_selection_kwargs = {} @@ -147,7 +135,7 @@ def get_global_search_engine( "keep_parent": gs_config.dynamic_search_keep_parent, "num_repeats": gs_config.dynamic_search_num_repeats, "use_summary": gs_config.dynamic_search_use_summary, - "concurrent_coroutines": model_settings.concurrent_requests, + "concurrent_coroutines": config.concurrent_requests, "threshold": gs_config.dynamic_search_threshold, "max_level": gs_config.dynamic_search_max_level, "model_params": {**model_params}, @@ -186,7 +174,7 @@ def get_global_search_engine( "max_context_tokens": gs_config.max_context_tokens, "context_name": "Reports", }, - concurrent_coroutines=model_settings.concurrent_requests, + concurrent_coroutines=config.concurrent_requests, response_type=response_type, callbacks=callbacks, ) @@ -198,34 +186,26 @@ def get_drift_search_engine( text_units: list[TextUnit], entities: list[Entity], relationships: list[Relationship], - description_embedding_store: BaseVectorStore, + description_embedding_store: VectorStore, response_type: str, local_system_prompt: str | None = None, reduce_system_prompt: str | None = None, callbacks: list[QueryCallbacks] | None = None, ) -> DRIFTSearch: """Create a local search engine based on data + configuration.""" - chat_model_settings = config.get_language_model_config( - config.drift_search.chat_model_id + chat_model_settings = config.get_completion_model_config( + config.drift_search.completion_model_id ) - chat_model = ModelManager().get_or_create_chat_model( - name="drift_search_chat", - model_type=chat_model_settings.type, - config=chat_model_settings, - ) + chat_model = create_completion(chat_model_settings) - embedding_model_settings = config.get_language_model_config( + embedding_model_settings = config.get_embedding_model_config( config.drift_search.embedding_model_id ) - embedding_model = ModelManager().get_or_create_embedding_model( - name="drift_search_embedding", - model_type=embedding_model_settings.type, - config=embedding_model_settings, - ) + embedding_model = create_embedding(embedding_model_settings) - tokenizer = get_tokenizer(model_config=chat_model_settings) + tokenizer = chat_model.tokenizer return DRIFTSearch( model=chat_model, @@ -249,38 +229,30 @@ def get_drift_search_engine( def get_basic_search_engine( text_units: list[TextUnit], - text_unit_embeddings: BaseVectorStore, + text_unit_embeddings: VectorStore, config: GraphRagConfig, + response_type: str, system_prompt: str | None = None, - response_type: str = "multiple paragraphs", callbacks: list[QueryCallbacks] | None = None, ) -> BasicSearch: """Create a basic search engine based on data + configuration.""" - chat_model_settings = config.get_language_model_config( - config.basic_search.chat_model_id + chat_model_settings = config.get_completion_model_config( + config.basic_search.completion_model_id ) - chat_model = ModelManager().get_or_create_chat_model( - name="basic_search_chat", - model_type=chat_model_settings.type, - config=chat_model_settings, - ) + chat_model = create_completion(chat_model_settings) - embedding_model_settings = config.get_language_model_config( + embedding_model_settings = config.get_embedding_model_config( config.basic_search.embedding_model_id ) - embedding_model = ModelManager().get_or_create_embedding_model( - name="basic_search_embedding", - model_type=embedding_model_settings.type, - config=embedding_model_settings, - ) + embedding_model = create_embedding(embedding_model_settings) - tokenizer = get_tokenizer(model_config=chat_model_settings) + tokenizer = chat_model.tokenizer bs_config = config.basic_search - model_params = get_openai_model_parameters_from_config(chat_model_settings) + model_params = chat_model_settings.call_args return BasicSearch( model=chat_model, diff --git a/graphrag/query/indexer_adapters.py b/packages/graphrag/graphrag/query/indexer_adapters.py similarity index 84% rename from graphrag/query/indexer_adapters.py rename to packages/graphrag/graphrag/query/indexer_adapters.py index 0c6e54a8af..c347fa11de 100644 --- a/graphrag/query/indexer_adapters.py +++ b/packages/graphrag/graphrag/query/indexer_adapters.py @@ -7,19 +7,17 @@ """ import logging -from typing import cast +from typing import TYPE_CHECKING, cast import pandas as pd +from graphrag_vectors import VectorStore -from graphrag.config.models.graph_rag_config import GraphRagConfig from graphrag.data_model.community import Community from graphrag.data_model.community_report import CommunityReport from graphrag.data_model.covariate import Covariate from graphrag.data_model.entity import Entity from graphrag.data_model.relationship import Relationship from graphrag.data_model.text_unit import TextUnit -from graphrag.language_model.manager import ModelManager -from graphrag.language_model.protocol.base import EmbeddingModel from graphrag.query.input.loaders.dfs import ( read_communities, read_community_reports, @@ -28,7 +26,9 @@ read_relationships, read_text_units, ) -from graphrag.vector_stores.base import BaseVectorStore + +if TYPE_CHECKING: + from graphrag_llm.embedding import LLMEmbedding logger = logging.getLogger(__name__) @@ -76,8 +76,6 @@ def read_indexer_reports( final_communities: pd.DataFrame, community_level: int | None, dynamic_community_selection: bool = False, - content_embedding_col: str = "full_content_embedding", - config: GraphRagConfig | None = None, ) -> list[CommunityReport]: """Read in the Community Reports from the raw indexing outputs. @@ -102,34 +100,12 @@ def read_indexer_reports( filtered_community_df, on="community", how="inner" ) - if config and ( - content_embedding_col not in reports_df.columns - or reports_df.loc[:, content_embedding_col].isna().any() - ): - # TODO: Find a way to retrieve the right embedding model id. - embedding_model_settings = config.get_language_model_config( - "default_embedding_model" - ) - embedder = ModelManager().get_or_create_embedding_model( - name="default_embedding", - model_type=embedding_model_settings.type, - config=embedding_model_settings, - ) - reports_df = embed_community_reports( - reports_df, embedder, embedding_col=content_embedding_col - ) - - return read_community_reports( - df=reports_df, - id_col="id", - short_id_col="community", - content_embedding_col=content_embedding_col, - ) + return read_community_reports(df=reports_df, id_col="id", short_id_col="community") def read_indexer_report_embeddings( community_reports: list[CommunityReport], - embeddings_store: BaseVectorStore, + embeddings_store: VectorStore, ): """Read in the Community Reports from the raw indexing outputs.""" for report in community_reports: @@ -218,7 +194,7 @@ def read_indexer_communities( def embed_community_reports( reports_df: pd.DataFrame, - embedder: EmbeddingModel, + embedder: "LLMEmbedding", source_col: str = "full_content", embedding_col: str = "full_content_embedding", ) -> pd.DataFrame: @@ -229,7 +205,7 @@ def embed_community_reports( if embedding_col not in reports_df.columns: reports_df[embedding_col] = reports_df.loc[:, source_col].apply( - lambda x: embedder.embed(x) + lambda x: embedder.embedding(input=[x]).first_embedding ) return reports_df diff --git a/graphrag/query/input/__init__.py b/packages/graphrag/graphrag/query/input/__init__.py similarity index 100% rename from graphrag/query/input/__init__.py rename to packages/graphrag/graphrag/query/input/__init__.py diff --git a/graphrag/query/input/loaders/__init__.py b/packages/graphrag/graphrag/query/input/loaders/__init__.py similarity index 100% rename from graphrag/query/input/loaders/__init__.py rename to packages/graphrag/graphrag/query/input/loaders/__init__.py diff --git a/graphrag/query/input/loaders/dfs.py b/packages/graphrag/graphrag/query/input/loaders/dfs.py similarity index 96% rename from graphrag/query/input/loaders/dfs.py rename to packages/graphrag/graphrag/query/input/loaders/dfs.py index 7182090cd2..a2b636f1dd 100644 --- a/graphrag/query/input/loaders/dfs.py +++ b/packages/graphrag/graphrag/query/input/loaders/dfs.py @@ -197,7 +197,6 @@ def read_community_reports( summary_col: str = "summary", content_col: str = "full_content", rank_col: str | None = "rank", - content_embedding_col: str | None = "full_content_embedding", attributes_cols: list[str] | None = None, ) -> list[CommunityReport]: """Read community reports from a dataframe using pre-converted records.""" @@ -213,9 +212,6 @@ def read_community_reports( summary=to_str(row, summary_col), full_content=to_str(row, content_col), rank=to_optional_float(row, rank_col), - full_content_embedding=to_optional_list( - row, content_embedding_col, item_type=float - ), attributes=( {col: row.get(col) for col in attributes_cols} if attributes_cols @@ -234,7 +230,7 @@ def read_text_units( relationships_col: str | None = "relationship_ids", covariates_col: str | None = "covariate_ids", tokens_col: str | None = "n_tokens", - document_ids_col: str | None = "document_ids", + document_id_col: str | None = "document_id", attributes_cols: list[str] | None = None, ) -> list[TextUnit]: """Read text units from a dataframe using pre-converted records.""" @@ -250,7 +246,7 @@ def read_text_units( row, covariates_col, key_type=str, value_type=str ), n_tokens=to_optional_int(row, tokens_col), - document_ids=to_optional_list(row, document_ids_col, item_type=str), + document_id=to_optional_str(row, document_id_col), attributes=( {col: row.get(col) for col in attributes_cols} if attributes_cols diff --git a/graphrag/query/input/loaders/utils.py b/packages/graphrag/graphrag/query/input/loaders/utils.py similarity index 100% rename from graphrag/query/input/loaders/utils.py rename to packages/graphrag/graphrag/query/input/loaders/utils.py diff --git a/graphrag/query/input/retrieval/__init__.py b/packages/graphrag/graphrag/query/input/retrieval/__init__.py similarity index 100% rename from graphrag/query/input/retrieval/__init__.py rename to packages/graphrag/graphrag/query/input/retrieval/__init__.py diff --git a/graphrag/query/input/retrieval/community_reports.py b/packages/graphrag/graphrag/query/input/retrieval/community_reports.py similarity index 100% rename from graphrag/query/input/retrieval/community_reports.py rename to packages/graphrag/graphrag/query/input/retrieval/community_reports.py diff --git a/graphrag/query/input/retrieval/covariates.py b/packages/graphrag/graphrag/query/input/retrieval/covariates.py similarity index 100% rename from graphrag/query/input/retrieval/covariates.py rename to packages/graphrag/graphrag/query/input/retrieval/covariates.py diff --git a/graphrag/query/input/retrieval/entities.py b/packages/graphrag/graphrag/query/input/retrieval/entities.py similarity index 100% rename from graphrag/query/input/retrieval/entities.py rename to packages/graphrag/graphrag/query/input/retrieval/entities.py diff --git a/graphrag/query/input/retrieval/relationships.py b/packages/graphrag/graphrag/query/input/retrieval/relationships.py similarity index 100% rename from graphrag/query/input/retrieval/relationships.py rename to packages/graphrag/graphrag/query/input/retrieval/relationships.py diff --git a/graphrag/query/input/retrieval/text_units.py b/packages/graphrag/graphrag/query/input/retrieval/text_units.py similarity index 100% rename from graphrag/query/input/retrieval/text_units.py rename to packages/graphrag/graphrag/query/input/retrieval/text_units.py diff --git a/graphrag/query/llm/__init__.py b/packages/graphrag/graphrag/query/llm/__init__.py similarity index 100% rename from graphrag/query/llm/__init__.py rename to packages/graphrag/graphrag/query/llm/__init__.py diff --git a/graphrag/query/llm/text_utils.py b/packages/graphrag/graphrag/query/llm/text_utils.py similarity index 93% rename from graphrag/query/llm/text_utils.py rename to packages/graphrag/graphrag/query/llm/text_utils.py index 5ff1983aa1..d36aaed70f 100644 --- a/graphrag/query/llm/text_utils.py +++ b/packages/graphrag/graphrag/query/llm/text_utils.py @@ -9,11 +9,10 @@ from collections.abc import Iterator from itertools import islice +from graphrag_llm.tokenizer import Tokenizer from json_repair import repair_json -import graphrag.config.defaults as defs from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer logger = logging.getLogger(__name__) @@ -36,7 +35,7 @@ def batched(iterable: Iterator, n: int): def chunk_text(text: str, max_tokens: int, tokenizer: Tokenizer | None = None): """Chunk text by token length.""" if tokenizer is None: - tokenizer = get_tokenizer(encoding_model=defs.ENCODING_MODEL) + tokenizer = get_tokenizer() tokens = tokenizer.encode(text) # type: ignore chunk_iterator = batched(iter(tokens), max_tokens) yield from (tokenizer.decode(list(chunk)) for chunk in chunk_iterator) @@ -63,7 +62,8 @@ def try_parse_json_object(input: str, verbose: bool = True) -> tuple[str, dict]: # Clean up json string. input = ( - input.replace("{{", "{") + input + .replace("{{", "{") .replace("}}", "}") .replace('"[{', "[{") .replace('}]"', "}]") diff --git a/graphrag/query/question_gen/__init__.py b/packages/graphrag/graphrag/query/question_gen/__init__.py similarity index 100% rename from graphrag/query/question_gen/__init__.py rename to packages/graphrag/graphrag/query/question_gen/__init__.py diff --git a/graphrag/query/question_gen/base.py b/packages/graphrag/graphrag/query/question_gen/base.py similarity index 84% rename from graphrag/query/question_gen/base.py rename to packages/graphrag/graphrag/query/question_gen/base.py index 2195aee52a..d45ec60317 100644 --- a/graphrag/query/question_gen/base.py +++ b/packages/graphrag/graphrag/query/question_gen/base.py @@ -5,15 +5,17 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING, Any + +from graphrag_llm.tokenizer import Tokenizer -from graphrag.language_model.protocol.base import ChatModel from graphrag.query.context_builder.builders import ( GlobalContextBuilder, LocalContextBuilder, ) -from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion @dataclass @@ -32,7 +34,7 @@ class BaseQuestionGen(ABC): def __init__( self, - model: ChatModel, + model: "LLMCompletion", context_builder: GlobalContextBuilder | LocalContextBuilder, tokenizer: Tokenizer | None = None, model_params: dict[str, Any] | None = None, @@ -40,7 +42,7 @@ def __init__( ): self.model = model self.context_builder = context_builder - self.tokenizer = tokenizer or get_tokenizer(model.config) + self.tokenizer = tokenizer or model.tokenizer self.model_params = model_params or {} self.context_builder_params = context_builder_params or {} diff --git a/graphrag/query/question_gen/local_gen.py b/packages/graphrag/graphrag/query/question_gen/local_gen.py similarity index 79% rename from graphrag/query/question_gen/local_gen.py rename to packages/graphrag/graphrag/query/question_gen/local_gen.py index 82795e9cf3..7e4fb3dadb 100644 --- a/graphrag/query/question_gen/local_gen.py +++ b/packages/graphrag/graphrag/query/question_gen/local_gen.py @@ -5,10 +5,12 @@ import logging import time -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast + +from graphrag_llm.tokenizer import Tokenizer +from graphrag_llm.utils import CompletionMessagesBuilder from graphrag.callbacks.llm_callbacks import BaseLLMCallback -from graphrag.language_model.protocol.base import ChatModel from graphrag.prompts.query.question_gen_system_prompt import QUESTION_SYSTEM_PROMPT from graphrag.query.context_builder.builders import ( ContextBuilderResult, @@ -18,7 +20,12 @@ ConversationHistory, ) from graphrag.query.question_gen.base import BaseQuestionGen, QuestionResult -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from collections.abc import AsyncIterator + + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionChunk logger = logging.getLogger(__name__) @@ -28,7 +35,7 @@ class LocalQuestionGen(BaseQuestionGen): def __init__( self, - model: ChatModel, + model: "LLMCompletion", context_builder: LocalContextBuilder, tokenizer: Tokenizer | None = None, system_prompt: str = QUESTION_SYSTEM_PROMPT, @@ -94,19 +101,28 @@ async def agenerate( system_prompt = self.system_prompt.format( context_data=context_data, question_count=question_count ) - question_messages = [ - {"role": "system", "content": system_prompt}, - ] + + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(system_prompt) + .add_user_message(question_text) + ) response = "" - async for chunk in self.model.achat_stream( - prompt=question_text, - history=question_messages, - model_parameters=self.model_params, - ): - response += chunk + + response_stream: AsyncIterator[ + LLMCompletionChunk + ] = await self.model.completion_async( + messages=messages_builder.build(), + stream=True, + **self.model_params, + ) # type: ignore + + async for chunk in response_stream: + response_text = chunk.choices[0].delta.content or "" + response += response_text for callback in self.callbacks: - callback.on_llm_new_token(chunk) + callback.on_llm_new_token(response_text) return QuestionResult( response=response.split("\n"), @@ -176,20 +192,28 @@ async def generate( system_prompt = self.system_prompt.format( context_data=context_data, question_count=question_count ) - question_messages = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": question_text}, - ] + + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(system_prompt) + .add_user_message(question_text) + ) response = "" - async for chunk in self.model.achat_stream( - prompt=question_text, - history=question_messages, - model_parameters=self.model_params, - ): - response += chunk + + response_stream: AsyncIterator[ + LLMCompletionChunk + ] = await self.model.completion_async( + messages=messages_builder.build(), + stream=True, + **self.model_params, + ) # type: ignore + + async for chunk in response_stream: + response_text = chunk.choices[0].delta.content or "" + response += response_text for callback in self.callbacks: - callback.on_llm_new_token(chunk) + callback.on_llm_new_token(response_text) return QuestionResult( response=response.split("\n"), diff --git a/graphrag/query/structured_search/__init__.py b/packages/graphrag/graphrag/query/structured_search/__init__.py similarity index 100% rename from graphrag/query/structured_search/__init__.py rename to packages/graphrag/graphrag/query/structured_search/__init__.py diff --git a/graphrag/query/structured_search/base.py b/packages/graphrag/graphrag/query/structured_search/base.py similarity index 89% rename from graphrag/query/structured_search/base.py rename to packages/graphrag/graphrag/query/structured_search/base.py index 753b419f69..1b5d1ca4eb 100644 --- a/graphrag/query/structured_search/base.py +++ b/packages/graphrag/graphrag/query/structured_search/base.py @@ -6,11 +6,11 @@ from abc import ABC, abstractmethod from collections.abc import AsyncGenerator from dataclasses import dataclass -from typing import Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar import pandas as pd +from graphrag_llm.tokenizer import Tokenizer -from graphrag.language_model.protocol.base import ChatModel from graphrag.query.context_builder.builders import ( BasicContextBuilder, DRIFTContextBuilder, @@ -20,8 +20,9 @@ from graphrag.query.context_builder.conversation_history import ( ConversationHistory, ) -from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion @dataclass @@ -57,7 +58,7 @@ class BaseSearch(ABC, Generic[T]): def __init__( self, - model: ChatModel, + model: "LLMCompletion", context_builder: T, tokenizer: Tokenizer | None = None, model_params: dict[str, Any] | None = None, @@ -65,7 +66,7 @@ def __init__( ): self.model = model self.context_builder = context_builder - self.tokenizer = tokenizer or get_tokenizer() + self.tokenizer = tokenizer or model.tokenizer self.model_params = model_params or {} self.context_builder_params = context_builder_params or {} diff --git a/graphrag/query/structured_search/basic_search/__init__.py b/packages/graphrag/graphrag/query/structured_search/basic_search/__init__.py similarity index 100% rename from graphrag/query/structured_search/basic_search/__init__.py rename to packages/graphrag/graphrag/query/structured_search/basic_search/__init__.py diff --git a/graphrag/query/structured_search/basic_search/basic_context.py b/packages/graphrag/graphrag/query/structured_search/basic_search/basic_context.py similarity index 73% rename from graphrag/query/structured_search/basic_search/basic_context.py rename to packages/graphrag/graphrag/query/structured_search/basic_search/basic_context.py index 8296de3c89..a6338cf1b0 100644 --- a/graphrag/query/structured_search/basic_search/basic_context.py +++ b/packages/graphrag/graphrag/query/structured_search/basic_search/basic_context.py @@ -4,20 +4,22 @@ """Basic Context Builder implementation.""" import logging -from typing import cast +from typing import TYPE_CHECKING, cast import pandas as pd +from graphrag_llm.tokenizer import Tokenizer +from graphrag_vectors import VectorStore from graphrag.data_model.text_unit import TextUnit -from graphrag.language_model.protocol.base import EmbeddingModel from graphrag.query.context_builder.builders import ( BasicContextBuilder, ContextBuilderResult, ) from graphrag.query.context_builder.conversation_history import ConversationHistory from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer -from graphrag.vector_stores.base import BaseVectorStore + +if TYPE_CHECKING: + from graphrag_llm.embedding import LLMEmbedding logger = logging.getLogger(__name__) @@ -27,8 +29,8 @@ class BasicSearchContext(BasicContextBuilder): def __init__( self, - text_embedder: EmbeddingModel, - text_unit_embeddings: BaseVectorStore, + text_embedder: "LLMEmbedding", + text_unit_embeddings: VectorStore, text_units: list[TextUnit] | None = None, tokenizer: Tokenizer | None = None, embedding_vectorstore_key: str = "id", @@ -38,7 +40,6 @@ def __init__( self.text_units = text_units self.text_unit_embeddings = text_unit_embeddings self.embedding_vectorstore_key = embedding_vectorstore_key - self.text_id_map = self._map_ids() def build_context( self, @@ -48,7 +49,7 @@ def build_context( max_context_tokens: int = 12_000, context_name: str = "Sources", column_delimiter: str = "|", - text_id_col: str = "source_id", + text_id_col: str = "id", text_col: str = "text", **kwargs, ) -> ContextBuilderResult: @@ -56,17 +57,20 @@ def build_context( if query != "": related_texts = self.text_unit_embeddings.similarity_search_by_text( text=query, - text_embedder=lambda t: self.text_embedder.embed(t), + text_embedder=lambda t: ( + self.text_embedder.embedding(input=[t]).first_embedding + ), k=k, ) - related_text_list = [ - { - text_id_col: self.text_id_map[f"{chunk.document.id}"], - text_col: chunk.document.text, - } - for chunk in related_texts + + text_unit_ids = {t.document.id for t in related_texts} + text_units_filtered = [] + text_units_filtered = [ + {text_id_col: t.short_id, text_col: t.text} + for t in self.text_units or [] + if t.id in text_unit_ids ] - related_text_df = pd.DataFrame(related_text_list) + related_text_df = pd.DataFrame(text_units_filtered) else: related_text_df = pd.DataFrame({ text_id_col: [], @@ -101,13 +105,5 @@ def build_context( return ContextBuilderResult( context_chunks=final_text, - context_records={context_name: final_text_df}, + context_records={context_name.lower(): final_text_df}, ) - - def _map_ids(self) -> dict[str, str]: - """Map id to short id in the text units.""" - id_map = {} - text_units = self.text_units or [] - for unit in text_units: - id_map[unit.id] = unit.short_id - return id_map diff --git a/graphrag/query/structured_search/basic_search/search.py b/packages/graphrag/graphrag/query/structured_search/basic_search/search.py similarity index 75% rename from graphrag/query/structured_search/basic_search/search.py rename to packages/graphrag/graphrag/query/structured_search/basic_search/search.py index ce5f656845..a8672fb4e8 100644 --- a/graphrag/query/structured_search/basic_search/search.py +++ b/packages/graphrag/graphrag/query/structured_search/basic_search/search.py @@ -5,18 +5,23 @@ import logging import time -from collections.abc import AsyncGenerator -from typing import Any +from collections.abc import AsyncGenerator, AsyncIterator +from typing import TYPE_CHECKING, Any + +from graphrag_llm.tokenizer import Tokenizer +from graphrag_llm.utils import CompletionMessagesBuilder from graphrag.callbacks.query_callbacks import QueryCallbacks -from graphrag.language_model.protocol.base import ChatModel from graphrag.prompts.query.basic_search_system_prompt import ( BASIC_SEARCH_SYSTEM_PROMPT, ) from graphrag.query.context_builder.builders import BasicContextBuilder from graphrag.query.context_builder.conversation_history import ConversationHistory from graphrag.query.structured_search.base import BaseSearch, SearchResult -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionChunk logger = logging.getLogger(__name__) """ @@ -29,7 +34,7 @@ class BasicSearch(BaseSearch[BasicContextBuilder]): def __init__( self, - model: ChatModel, + model: "LLMCompletion", context_builder: BasicContextBuilder, tokenizer: Tokenizer | None = None, system_prompt: str | None = None, @@ -77,19 +82,28 @@ async def search( context_data=context_result.context_chunks, response_type=self.response_type, ) - search_messages = [ - {"role": "system", "content": search_prompt}, - ] + + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(search_prompt) + .add_user_message(query) + ) response = "" - async for chunk in self.model.achat_stream( - prompt=query, - history=search_messages, - model_parameters=self.model_params, - ): + + response_stream: AsyncIterator[LLMCompletionChunk] = ( + self.model.completion_async( + messages=messages_builder.build(), + stream=True, + **self.model_params, + ) + ) # type: ignore + + async for chunk in response_stream: + response_text = chunk.choices[0].delta.content or "" for callback in self.callbacks: - callback.on_llm_new_token(chunk) - response += chunk + callback.on_llm_new_token(response_text) + response += response_text llm_calls["response"] = 1 prompt_tokens["response"] = len(self.tokenizer.encode(search_prompt)) @@ -143,18 +157,26 @@ async def stream_search( search_prompt = self.system_prompt.format( context_data=context_result.context_chunks, response_type=self.response_type ) - search_messages = [ - {"role": "system", "content": search_prompt}, - ] + + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(search_prompt) + .add_user_message(query) + ) for callback in self.callbacks: callback.on_context(context_result.context_records) - async for chunk_response in self.model.achat_stream( - prompt=query, - history=search_messages, - model_parameters=self.model_params, - ): + response_stream: AsyncIterator[ + LLMCompletionChunk + ] = await self.model.completion_async( + messages=messages_builder.build(), + stream=True, + **self.model_params, + ) # type: ignore + + async for chunk in response_stream: + response_text = chunk.choices[0].delta.content or "" for callback in self.callbacks: - callback.on_llm_new_token(chunk_response) - yield chunk_response + callback.on_llm_new_token(response_text) + yield response_text diff --git a/graphrag/query/structured_search/drift_search/__init__.py b/packages/graphrag/graphrag/query/structured_search/drift_search/__init__.py similarity index 100% rename from graphrag/query/structured_search/drift_search/__init__.py rename to packages/graphrag/graphrag/query/structured_search/drift_search/__init__.py diff --git a/graphrag/query/structured_search/drift_search/action.py b/packages/graphrag/graphrag/query/structured_search/drift_search/action.py similarity index 96% rename from graphrag/query/structured_search/drift_search/action.py rename to packages/graphrag/graphrag/query/structured_search/drift_search/action.py index 8f1da3d721..23cc811a39 100644 --- a/graphrag/query/structured_search/drift_search/action.py +++ b/packages/graphrag/graphrag/query/structured_search/drift_search/action.py @@ -50,7 +50,13 @@ def is_complete(self) -> bool: """Check if the action is complete (i.e., an answer is available).""" return self.answer is not None - async def search(self, search_engine: Any, global_query: str, scorer: Any = None): + async def search( + self, + search_engine: Any, + global_query: str, + k_followups: int, + scorer: Any = None, + ): """ Execute an asynchronous search using the search engine, and update the action with the results. @@ -71,7 +77,9 @@ async def search(self, search_engine: Any, global_query: str, scorer: Any = None return self search_result = await search_engine.search( - drift_query=global_query, query=self.query + query=self.query, + drift_query=global_query, + k_followups=k_followups, ) # Do not launch exception as it will roll up with other steps diff --git a/graphrag/query/structured_search/drift_search/drift_context.py b/packages/graphrag/graphrag/query/structured_search/drift_search/drift_context.py similarity index 93% rename from graphrag/query/structured_search/drift_search/drift_context.py rename to packages/graphrag/graphrag/query/structured_search/drift_search/drift_context.py index 4b4325ae2e..bacc39cf99 100644 --- a/graphrag/query/structured_search/drift_search/drift_context.py +++ b/packages/graphrag/graphrag/query/structured_search/drift_search/drift_context.py @@ -5,10 +5,12 @@ import logging from dataclasses import asdict -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np import pandas as pd +from graphrag_llm.tokenizer import Tokenizer +from graphrag_vectors import VectorStore from graphrag.config.models.drift_search_config import DRIFTSearchConfig from graphrag.data_model.community_report import CommunityReport @@ -16,7 +18,6 @@ from graphrag.data_model.entity import Entity from graphrag.data_model.relationship import Relationship from graphrag.data_model.text_unit import TextUnit -from graphrag.language_model.protocol.base import ChatModel, EmbeddingModel from graphrag.prompts.query.drift_search_system_prompt import ( DRIFT_LOCAL_SYSTEM_PROMPT, DRIFT_REDUCE_PROMPT, @@ -27,9 +28,10 @@ from graphrag.query.structured_search.local_search.mixed_context import ( LocalSearchMixedContext, ) -from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer -from graphrag.vector_stores.base import BaseVectorStore + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.embedding import LLMEmbedding logger = logging.getLogger(__name__) @@ -39,27 +41,27 @@ class DRIFTSearchContextBuilder(DRIFTContextBuilder): def __init__( self, - model: ChatModel, - text_embedder: EmbeddingModel, + model: "LLMCompletion", + config: DRIFTSearchConfig, + text_embedder: "LLMEmbedding", entities: list[Entity], - entity_text_embeddings: BaseVectorStore, + entity_text_embeddings: VectorStore, text_units: list[TextUnit] | None = None, reports: list[CommunityReport] | None = None, relationships: list[Relationship] | None = None, covariates: dict[str, list[Covariate]] | None = None, tokenizer: Tokenizer | None = None, embedding_vectorstore_key: str = EntityVectorStoreKey.ID, - config: DRIFTSearchConfig | None = None, local_system_prompt: str | None = None, local_mixed_context: LocalSearchMixedContext | None = None, reduce_system_prompt: str | None = None, response_type: str | None = None, ): """Initialize the DRIFT search context builder with necessary components.""" - self.config = config or DRIFTSearchConfig() + self.config = config self.model = model self.text_embedder = text_embedder - self.tokenizer = tokenizer or get_tokenizer() + self.tokenizer = tokenizer or model.tokenizer self.local_system_prompt = local_system_prompt or DRIFT_LOCAL_SYSTEM_PROMPT self.reduce_system_prompt = reduce_system_prompt or DRIFT_REDUCE_PROMPT diff --git a/graphrag/query/structured_search/drift_search/primer.py b/packages/graphrag/graphrag/query/structured_search/drift_search/primer.py similarity index 74% rename from graphrag/query/structured_search/drift_search/primer.py rename to packages/graphrag/graphrag/query/structured_search/drift_search/primer.py index 2a7f145711..36723064db 100644 --- a/graphrag/query/structured_search/drift_search/primer.py +++ b/packages/graphrag/graphrag/query/structured_search/drift_search/primer.py @@ -3,35 +3,53 @@ """Primer for DRIFT search.""" -import json import logging import secrets import time +from typing import TYPE_CHECKING import numpy as np import pandas as pd +from graphrag_llm.tokenizer import Tokenizer +from pydantic import BaseModel, Field from tqdm.asyncio import tqdm_asyncio from graphrag.config.models.drift_search_config import DRIFTSearchConfig from graphrag.data_model.community_report import CommunityReport -from graphrag.language_model.protocol.base import ChatModel, EmbeddingModel from graphrag.prompts.query.drift_search_system_prompt import ( DRIFT_PRIMER_PROMPT, ) from graphrag.query.structured_search.base import SearchResult -from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.embedding import LLMEmbedding + from graphrag_llm.types import LLMCompletionResponse logger = logging.getLogger(__name__) +class PrimerResponse(BaseModel): + """Response model for the primer.""" + + intermediate_answer: str = Field( + description="This answer should match the level of detail and length found in the community summaries. The intermediate answer should be exactly 2000 characters long. This must be formatted in markdown and must begin with a header that explains how the following text is related to the query.", + ) + score: int = Field( + description="A score on how well the intermediate answer addresses the query. A score of 0 indicates a poor, unfocused answer, while a score of 100 indicates a highly focused, relevant answer that addresses the query in its entirety." + ) + follow_up_queries: list[str] = Field( + description="A list of follow-up queries that could be asked to further explore the topic. These should be formatted as a list of strings. Generate at least five good follow-up queries." + ) + + class PrimerQueryProcessor: """Process the query by expanding it using community reports and generate follow-up actions.""" def __init__( self, - chat_model: ChatModel, - text_embedder: EmbeddingModel, + chat_model: "LLMCompletion", + text_embedder: "LLMEmbedding", reports: list[CommunityReport], tokenizer: Tokenizer | None = None, ): @@ -46,7 +64,7 @@ def __init__( """ self.chat_model = chat_model self.text_embedder = text_embedder - self.tokenizer = tokenizer or get_tokenizer() + self.tokenizer = tokenizer or chat_model.tokenizer self.reports = reports async def expand_query(self, query: str) -> tuple[str, dict[str, int]]: @@ -67,8 +85,10 @@ async def expand_query(self, query: str) -> tuple[str, dict[str, int]]: {template}\n" Ensure that the hypothetical answer does not reference new named entities that are not present in the original query.""" - model_response = await self.chat_model.achat(prompt) - text = model_response.output.content + model_response: LLMCompletionResponse = await self.chat_model.completion_async( + messages=prompt + ) # type: ignore + text = model_response.content prompt_tokens = len(self.tokenizer.encode(prompt)) output_tokens = len(self.tokenizer.encode(text)) @@ -95,7 +115,9 @@ async def __call__(self, query: str) -> tuple[list[float], dict[str, int]]: """ hyde_query, token_ct = await self.expand_query(query) logger.debug("Expanded query: %s", hyde_query) - return self.text_embedder.embed(hyde_query), token_ct + return self.text_embedder.embedding( + input=[hyde_query] + ).first_embedding, token_ct class DRIFTPrimer: @@ -104,7 +126,7 @@ class DRIFTPrimer: def __init__( self, config: DRIFTSearchConfig, - chat_model: ChatModel, + chat_model: "LLMCompletion", tokenizer: Tokenizer | None = None, ): """ @@ -117,7 +139,7 @@ def __init__( """ self.chat_model = chat_model self.config = config - self.tokenizer = tokenizer or get_tokenizer() + self.tokenizer = tokenizer or chat_model.tokenizer async def decompose_query( self, query: str, reports: pd.DataFrame @@ -137,15 +159,18 @@ async def decompose_query( prompt = DRIFT_PRIMER_PROMPT.format( query=query, community_reports=community_reports ) - model_response = await self.chat_model.achat(prompt, json=True) - response = model_response.output.content + model_response: LLMCompletionResponse[ + PrimerResponse + ] = await self.chat_model.completion_async( + messages=prompt, response_format=PrimerResponse + ) # type: ignore - parsed_response = json.loads(response) + parsed_response = model_response.formatted_response.model_dump() # type: ignore token_ct = { "llm_calls": 1, "prompt_tokens": len(self.tokenizer.encode(prompt)), - "output_tokens": len(self.tokenizer.encode(response)), + "output_tokens": len(self.tokenizer.encode(model_response.content)), } return parsed_response, token_ct diff --git a/graphrag/query/structured_search/drift_search/search.py b/packages/graphrag/graphrag/query/structured_search/drift_search/search.py similarity index 87% rename from graphrag/query/structured_search/drift_search/search.py rename to packages/graphrag/graphrag/query/structured_search/drift_search/search.py index ab1c85ccdb..070c5b5519 100644 --- a/graphrag/query/structured_search/drift_search/search.py +++ b/packages/graphrag/graphrag/query/structured_search/drift_search/search.py @@ -5,16 +5,17 @@ import logging import time -from collections.abc import AsyncGenerator -from typing import Any +from collections.abc import AsyncGenerator, AsyncIterator +from typing import TYPE_CHECKING, Any +from graphrag_llm.tokenizer import Tokenizer +from graphrag_llm.utils import ( + CompletionMessagesBuilder, + gather_completion_response_async, +) from tqdm.asyncio import tqdm_asyncio from graphrag.callbacks.query_callbacks import QueryCallbacks -from graphrag.language_model.protocol.base import ChatModel -from graphrag.language_model.providers.fnllm.utils import ( - get_openai_model_parameters_from_dict, -) from graphrag.query.context_builder.conversation_history import ConversationHistory from graphrag.query.context_builder.entity_extraction import EntityVectorStoreKey from graphrag.query.structured_search.base import BaseSearch, SearchResult @@ -25,8 +26,10 @@ from graphrag.query.structured_search.drift_search.primer import DRIFTPrimer from graphrag.query.structured_search.drift_search.state import QueryState from graphrag.query.structured_search.local_search.search import LocalSearch -from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionChunk logger = logging.getLogger(__name__) @@ -36,7 +39,7 @@ class DRIFTSearch(BaseSearch[DRIFTSearchContextBuilder]): def __init__( self, - model: ChatModel, + model: "LLMCompletion", context_builder: DRIFTSearchContextBuilder, tokenizer: Tokenizer | None = None, query_state: QueryState | None = None, @@ -55,7 +58,7 @@ def __init__( super().__init__(model, context_builder, tokenizer) self.context_builder = context_builder - self.tokenizer = tokenizer or get_tokenizer() + self.tokenizer = tokenizer or model.tokenizer self.query_state = query_state or QueryState() self.primer = DRIFTPrimer( config=self.context_builder.config, @@ -86,15 +89,13 @@ def init_local_search(self) -> LocalSearch: "max_context_tokens": self.context_builder.config.local_search_max_data_tokens, } - model_params = get_openai_model_parameters_from_dict({ - "model": self.model.config.model, - "max_tokens": self.context_builder.config.local_search_llm_max_gen_tokens, + model_params = { "temperature": self.context_builder.config.local_search_temperature, "n": self.context_builder.config.local_search_n, "top_p": self.context_builder.config.local_search_top_p, "max_completion_tokens": self.context_builder.config.local_search_llm_max_gen_completion_tokens, - "response_format": {"type": "json_object"}, - }) + "response_format_json_object": True, + } return LocalSearch( model=self.model, @@ -156,7 +157,11 @@ def _process_primer_results( raise ValueError(error_msg) async def _search_step( - self, global_query: str, search_engine: LocalSearch, actions: list[DriftAction] + self, + global_query: str, + k_followups: int, + search_engine: LocalSearch, + actions: list[DriftAction], ) -> list[DriftAction]: """ Perform an asynchronous search step by executing each DriftAction asynchronously. @@ -171,7 +176,11 @@ async def _search_step( list[DriftAction]: The results from executing the search actions asynchronously. """ tasks = [ - action.search(search_engine=search_engine, global_query=global_query) + action.search( + search_engine=search_engine, + global_query=global_query, + k_followups=k_followups, + ) for action in actions ] return await tqdm_asyncio.gather(*tasks, leave=False) @@ -241,7 +250,10 @@ async def search( ) # Process actions results = await self._search_step( - global_query=query, search_engine=self.local_search, actions=actions + global_query=query, + k_followups=self.context_builder.config.drift_k_followups, + search_engine=self.local_search, + actions=actions, ) # Update query state @@ -269,12 +281,10 @@ async def search( for callback in self.callbacks: callback.on_reduce_response_start(response_state) - model_params = get_openai_model_parameters_from_dict({ - "model": self.model.config.model, - "max_tokens": self.context_builder.config.reduce_max_tokens, + model_params = { "temperature": self.context_builder.config.reduce_temperature, "max_completion_tokens": self.context_builder.config.reduce_max_completion_tokens, - }) + } reduced_response = await self._reduce_response( responses=response_state, @@ -320,12 +330,10 @@ async def stream_search( for callback in self.callbacks: callback.on_reduce_response_start(result.response) - model_params = get_openai_model_parameters_from_dict({ - "model": self.model.config.model, - "max_tokens": self.context_builder.config.reduce_max_tokens, + model_params = { "temperature": self.context_builder.config.reduce_temperature, "max_completion_tokens": self.context_builder.config.reduce_max_completion_tokens, - }) + } full_response = "" async for resp in self._reduce_response_streaming( @@ -379,17 +387,19 @@ async def _reduce_response( context_data=reduce_responses, response_type=self.context_builder.response_type, ) - search_messages = [ - {"role": "system", "content": search_prompt}, - ] - model_response = await self.model.achat( - prompt=query, - history=search_messages, - model_parameters=llm_kwargs.get("model_params", {}), + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(search_prompt) + .add_user_message(query) + ) + + model_response = await self.model.completion_async( + messages=messages_builder.build(), + **llm_kwargs.get("model_params", {}), ) - reduced_response = model_response.output.content + reduced_response = await gather_completion_response_async(model_response) llm_calls["reduce"] = 1 prompt_tokens["reduce"] = len(self.tokenizer.encode(search_prompt)) + len( @@ -434,15 +444,21 @@ async def _reduce_response_streaming( context_data=reduce_responses, response_type=self.context_builder.response_type, ) - search_messages = [ - {"role": "system", "content": search_prompt}, - ] - async for response in self.model.achat_stream( - prompt=query, - history=search_messages, - model_parameters=model_params, - ): + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(search_prompt) + .add_user_message(query) + ) + + response_search: AsyncIterator[ + LLMCompletionChunk + ] = await self.model.completion_async( + messages=messages_builder.build(), stream=True, **model_params + ) # type: ignore + + async for chunk in response_search: + response_text = chunk.choices[0].delta.content or "" for callback in self.callbacks: - callback.on_llm_new_token(response) - yield response + callback.on_llm_new_token(response_text) + yield response_text diff --git a/graphrag/query/structured_search/drift_search/state.py b/packages/graphrag/graphrag/query/structured_search/drift_search/state.py similarity index 100% rename from graphrag/query/structured_search/drift_search/state.py rename to packages/graphrag/graphrag/query/structured_search/drift_search/state.py diff --git a/graphrag/query/structured_search/global_search/__init__.py b/packages/graphrag/graphrag/query/structured_search/global_search/__init__.py similarity index 100% rename from graphrag/query/structured_search/global_search/__init__.py rename to packages/graphrag/graphrag/query/structured_search/global_search/__init__.py diff --git a/graphrag/query/structured_search/global_search/community_context.py b/packages/graphrag/graphrag/query/structured_search/global_search/community_context.py similarity index 99% rename from graphrag/query/structured_search/global_search/community_context.py rename to packages/graphrag/graphrag/query/structured_search/global_search/community_context.py index 1709aab1a8..eb63073095 100644 --- a/graphrag/query/structured_search/global_search/community_context.py +++ b/packages/graphrag/graphrag/query/structured_search/global_search/community_context.py @@ -5,6 +5,8 @@ from typing import Any +from graphrag_llm.tokenizer import Tokenizer + from graphrag.data_model.community import Community from graphrag.data_model.community_report import CommunityReport from graphrag.data_model.entity import Entity @@ -20,7 +22,6 @@ ) from graphrag.query.structured_search.base import GlobalContextBuilder from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer class GlobalCommunityContext(GlobalContextBuilder): diff --git a/graphrag/query/structured_search/global_search/search.py b/packages/graphrag/graphrag/query/structured_search/global_search/search.py similarity index 88% rename from graphrag/query/structured_search/global_search/search.py rename to packages/graphrag/graphrag/query/structured_search/global_search/search.py index 86b95d0088..b84043dbde 100644 --- a/graphrag/query/structured_search/global_search/search.py +++ b/packages/graphrag/graphrag/query/structured_search/global_search/search.py @@ -7,14 +7,18 @@ import json import logging import time -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, AsyncIterator from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING, Any import pandas as pd +from graphrag_llm.tokenizer import Tokenizer +from graphrag_llm.utils import ( + CompletionMessagesBuilder, + gather_completion_response_async, +) from graphrag.callbacks.query_callbacks import QueryCallbacks -from graphrag.language_model.protocol.base import ChatModel from graphrag.prompts.query.global_search_knowledge_system_prompt import ( GENERAL_KNOWLEDGE_INSTRUCTION, ) @@ -31,7 +35,10 @@ ) from graphrag.query.llm.text_utils import try_parse_json_object from graphrag.query.structured_search.base import BaseSearch, SearchResult -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionChunk logger = logging.getLogger(__name__) @@ -50,7 +57,7 @@ class GlobalSearch(BaseSearch[GlobalContextBuilder]): def __init__( self, - model: ChatModel, + model: "LLMCompletion", context_builder: GlobalContextBuilder, tokenizer: Tokenizer | None = None, map_system_prompt: str | None = None, @@ -87,7 +94,7 @@ def __init__( self.map_llm_params = map_llm_params if map_llm_params else {} self.reduce_llm_params = reduce_llm_params if reduce_llm_params else {} if json_mode: - self.map_llm_params["response_format"] = {"type": "json_object"} + self.map_llm_params["response_format_json_object"] = True else: # remove response_format key if json_mode is False self.map_llm_params.pop("response_format", None) @@ -220,17 +227,20 @@ async def _map_response_single_batch( search_prompt = self.map_system_prompt.format( context_data=context_data, max_length=max_length ) - search_messages = [ - {"role": "system", "content": search_prompt}, - ] + + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(search_prompt) + .add_user_message(query) + ) + async with self.semaphore: - model_response = await self.model.achat( - prompt=query, - history=search_messages, - model_parameters=llm_kwargs, - json=True, + model_response = await self.model.completion_async( + messages=messages_builder.build(), + response_format_json_object=True, + **llm_kwargs, ) - search_response = model_response.output.content + search_response = await gather_completion_response_async(model_response) logger.debug("Map response: %s", search_response) try: # parse search response json @@ -376,20 +386,28 @@ async def _reduce_response( ) if self.allow_general_knowledge: search_prompt += "\n" + self.general_knowledge_inclusion_prompt - search_messages = [ - {"role": "system", "content": search_prompt}, - {"role": "user", "content": query}, - ] + + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(search_prompt) + .add_user_message(query) + ) search_response = "" - async for chunk_response in self.model.achat_stream( - prompt=query, - history=search_messages, - model_parameters=llm_kwargs, - ): - search_response += chunk_response + + response_search: AsyncIterator[ + LLMCompletionChunk + ] = await self.model.completion_async( + messages=messages_builder.build(), + stream=True, + **llm_kwargs, + ) # type: ignore + + async for chunk in response_search: + response_text = chunk.choices[0].delta.content or "" + search_response += response_text for callback in self.callbacks: - callback.on_llm_new_token(chunk_response) + callback.on_llm_new_token(response_text) return SearchResult( response=search_response, @@ -481,15 +499,23 @@ async def _stream_reduce_response( ) if self.allow_general_knowledge: search_prompt += "\n" + self.general_knowledge_inclusion_prompt - search_messages = [ - {"role": "system", "content": search_prompt}, - ] - async for chunk_response in self.model.achat_stream( - prompt=query, - history=search_messages, - **llm_kwargs, - ): + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(search_prompt) + .add_user_message(query) + ) + + response_search: AsyncIterator[ + LLMCompletionChunk + ] = await self.model.completion_async( + messages=messages_builder.build(), + stream=True, + **llm_kwargs.get("model_parameters", {}), + ) # type: ignore + + async for chunk in response_search: + response_text = chunk.choices[0].delta.content or "" for callback in self.callbacks: - callback.on_llm_new_token(chunk_response) - yield chunk_response + callback.on_llm_new_token(response_text) + yield response_text diff --git a/graphrag/query/structured_search/local_search/__init__.py b/packages/graphrag/graphrag/query/structured_search/local_search/__init__.py similarity index 100% rename from graphrag/query/structured_search/local_search/__init__.py rename to packages/graphrag/graphrag/query/structured_search/local_search/__init__.py diff --git a/graphrag/query/structured_search/local_search/mixed_context.py b/packages/graphrag/graphrag/query/structured_search/local_search/mixed_context.py similarity index 97% rename from graphrag/query/structured_search/local_search/mixed_context.py rename to packages/graphrag/graphrag/query/structured_search/local_search/mixed_context.py index 1ee8973fc4..34d8cf7d93 100644 --- a/graphrag/query/structured_search/local_search/mixed_context.py +++ b/packages/graphrag/graphrag/query/structured_search/local_search/mixed_context.py @@ -4,16 +4,17 @@ import logging from copy import deepcopy -from typing import Any +from typing import TYPE_CHECKING, Any import pandas as pd +from graphrag_llm.tokenizer import Tokenizer +from graphrag_vectors import VectorStore from graphrag.data_model.community_report import CommunityReport from graphrag.data_model.covariate import Covariate from graphrag.data_model.entity import Entity from graphrag.data_model.relationship import Relationship from graphrag.data_model.text_unit import TextUnit -from graphrag.language_model.protocol.base import EmbeddingModel from graphrag.query.context_builder.builders import ContextBuilderResult from graphrag.query.context_builder.community_context import ( build_community_context, @@ -41,8 +42,9 @@ from graphrag.query.input.retrieval.text_units import get_candidate_text_units from graphrag.query.structured_search.base import LocalContextBuilder from graphrag.tokenizer.get_tokenizer import get_tokenizer -from graphrag.tokenizer.tokenizer import Tokenizer -from graphrag.vector_stores.base import BaseVectorStore + +if TYPE_CHECKING: + from graphrag_llm.embedding import LLMEmbedding logger = logging.getLogger(__name__) @@ -53,8 +55,8 @@ class LocalSearchMixedContext(LocalContextBuilder): def __init__( self, entities: list[Entity], - entity_text_embeddings: BaseVectorStore, - text_embedder: EmbeddingModel, + entity_text_embeddings: VectorStore, + text_embedder: "LLMEmbedding", text_units: list[TextUnit] | None = None, community_reports: list[CommunityReport] | None = None, relationships: list[Relationship] | None = None, @@ -84,10 +86,6 @@ def __init__( self.tokenizer = tokenizer or get_tokenizer() self.embedding_vectorstore_key = embedding_vectorstore_key - def filter_by_entity_keys(self, entity_keys: list[int] | list[str]): - """Filter entity text embeddings by entity keys.""" - self.entity_text_embeddings.filter_by_id(entity_keys) - def build_context( self, query: str, diff --git a/graphrag/query/structured_search/local_search/search.py b/packages/graphrag/graphrag/query/structured_search/local_search/search.py similarity index 75% rename from graphrag/query/structured_search/local_search/search.py rename to packages/graphrag/graphrag/query/structured_search/local_search/search.py index fdd72949da..728bbcc246 100644 --- a/graphrag/query/structured_search/local_search/search.py +++ b/packages/graphrag/graphrag/query/structured_search/local_search/search.py @@ -5,11 +5,13 @@ import logging import time -from collections.abc import AsyncGenerator -from typing import Any +from collections.abc import AsyncGenerator, AsyncIterator +from typing import TYPE_CHECKING, Any + +from graphrag_llm.tokenizer import Tokenizer +from graphrag_llm.utils import CompletionMessagesBuilder from graphrag.callbacks.query_callbacks import QueryCallbacks -from graphrag.language_model.protocol.base import ChatModel from graphrag.prompts.query.local_search_system_prompt import ( LOCAL_SEARCH_SYSTEM_PROMPT, ) @@ -18,7 +20,10 @@ ConversationHistory, ) from graphrag.query.structured_search.base import BaseSearch, SearchResult -from graphrag.tokenizer.tokenizer import Tokenizer + +if TYPE_CHECKING: + from graphrag_llm.completion import LLMCompletion + from graphrag_llm.types import LLMCompletionChunk logger = logging.getLogger(__name__) @@ -28,7 +33,7 @@ class LocalSearch(BaseSearch[LocalContextBuilder]): def __init__( self, - model: ChatModel, + model: "LLMCompletion", context_builder: LocalContextBuilder, tokenizer: Tokenizer | None = None, system_prompt: str | None = None, @@ -76,26 +81,35 @@ async def search( context_data=context_result.context_chunks, response_type=self.response_type, global_query=drift_query, + followups=kwargs.get("k_followups", 0), ) else: search_prompt = self.system_prompt.format( context_data=context_result.context_chunks, response_type=self.response_type, ) - history_messages = [ - {"role": "system", "content": search_prompt}, - ] + + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(search_prompt) + .add_user_message(query) + ) full_response = "" - async for response in self.model.achat_stream( - prompt=query, - history=history_messages, - model_parameters=self.model_params, - ): - full_response += response + response: AsyncIterator[ + LLMCompletionChunk + ] = await self.model.completion_async( + messages=messages_builder.build(), + stream=True, + **self.model_params, + ) # type: ignore + + async for chunk in response: + response_text = chunk.choices[0].delta.content or "" + full_response += response_text for callback in self.callbacks: - callback.on_llm_new_token(response) + callback.on_llm_new_token(response_text) llm_calls["response"] = 1 prompt_tokens["response"] = len(self.tokenizer.encode(search_prompt)) @@ -146,18 +160,24 @@ async def stream_search( search_prompt = self.system_prompt.format( context_data=context_result.context_chunks, response_type=self.response_type ) - history_messages = [ - {"role": "system", "content": search_prompt}, - ] + + messages_builder = ( + CompletionMessagesBuilder() + .add_system_message(search_prompt) + .add_user_message(query) + ) for callback in self.callbacks: callback.on_context(context_result.context_records) - async for response in self.model.achat_stream( - prompt=query, - history=history_messages, - model_parameters=self.model_params, - ): + response: AsyncIterator[LLMCompletionChunk] = await self.model.completion_async( + messages=messages_builder.build(), + stream=True, + **self.model_params, + ) # type: ignore + + async for chunk in response: + response_text = chunk.choices[0].delta.content or "" for callback in self.callbacks: - callback.on_llm_new_token(response) - yield response + callback.on_llm_new_token(response_text) + yield response_text diff --git a/graphrag/tokenizer/__init__.py b/packages/graphrag/graphrag/tokenizer/__init__.py similarity index 100% rename from graphrag/tokenizer/__init__.py rename to packages/graphrag/graphrag/tokenizer/__init__.py diff --git a/graphrag/tokenizer/get_tokenizer.py b/packages/graphrag/graphrag/tokenizer/get_tokenizer.py similarity index 56% rename from graphrag/tokenizer/get_tokenizer.py rename to packages/graphrag/graphrag/tokenizer/get_tokenizer.py index 5d1ef40f0c..ed1ffec99c 100644 --- a/graphrag/tokenizer/get_tokenizer.py +++ b/packages/graphrag/graphrag/tokenizer/get_tokenizer.py @@ -3,16 +3,15 @@ """Get Tokenizer.""" +from graphrag_llm.config import ModelConfig, TokenizerConfig, TokenizerType +from graphrag_llm.tokenizer import Tokenizer, create_tokenizer + from graphrag.config.defaults import ENCODING_MODEL -from graphrag.config.models.language_model_config import LanguageModelConfig -from graphrag.tokenizer.litellm_tokenizer import LitellmTokenizer -from graphrag.tokenizer.tiktoken_tokenizer import TiktokenTokenizer -from graphrag.tokenizer.tokenizer import Tokenizer def get_tokenizer( - model_config: LanguageModelConfig | None = None, - encoding_model: str = ENCODING_MODEL, + model_config: "ModelConfig | None" = None, + encoding_model: str | None = None, ) -> Tokenizer: """ Get the tokenizer for the given model configuration or fallback to a tiktoken based tokenizer. @@ -32,10 +31,18 @@ def get_tokenizer( An instance of a Tokenizer. """ if model_config is not None: - if model_config.encoding_model.strip() != "": - # User has manually specified a tiktoken encoding model to use for the provided model configuration. - return TiktokenTokenizer(encoding_name=model_config.encoding_model) - - return LitellmTokenizer(model_name=model_config.model) - - return TiktokenTokenizer(encoding_name=encoding_model) + return create_tokenizer( + TokenizerConfig( + type=TokenizerType.LiteLLM, + model_id=f"{model_config.model_provider}/{model_config.model}", + ) + ) + + if encoding_model is None: + encoding_model = ENCODING_MODEL + return create_tokenizer( + TokenizerConfig( + type=TokenizerType.Tiktoken, + encoding_name=encoding_model, + ) + ) diff --git a/graphrag/utils/__init__.py b/packages/graphrag/graphrag/utils/__init__.py similarity index 100% rename from graphrag/utils/__init__.py rename to packages/graphrag/graphrag/utils/__init__.py diff --git a/packages/graphrag/graphrag/utils/api.py b/packages/graphrag/graphrag/utils/api.py new file mode 100644 index 0000000000..980997db06 --- /dev/null +++ b/packages/graphrag/graphrag/utils/api.py @@ -0,0 +1,75 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""API functions for the GraphRAG module.""" + +from pathlib import Path + +from graphrag_vectors import ( + VectorStore, + VectorStoreConfig, + create_vector_store, +) + + +def get_embedding_store( + config: VectorStoreConfig, + embedding_name: str, +) -> VectorStore: + """Get the embedding store.""" + embedding_store = create_vector_store(config, config.index_schema[embedding_name]) + embedding_store.connect() + + return embedding_store + + +def reformat_context_data(context_data: dict) -> dict: + """ + Reformats context_data for all query responses. + + Reformats a dictionary of dataframes into a dictionary of lists. + One list entry for each record. Records are grouped by original + dictionary keys. + + Note: depending on which query algorithm is used, the context_data may not + contain the same information (keys). In this case, the default behavior will be to + set these keys as empty lists to preserve a standard output format. + """ + final_format = { + "reports": [], + "entities": [], + "relationships": [], + "claims": [], + "sources": [], + } + for key in context_data: + records = ( + context_data[key].to_dict(orient="records") + if context_data[key] is not None and not isinstance(context_data[key], dict) + else context_data[key] + ) + if len(records) < 1: + continue + final_format[key] = records + return final_format + + +def load_search_prompt(prompt_config: str | None) -> str | None: + """ + Load the search prompt from disk if configured. + + If not, leave it empty - the search functions will load their defaults. + + """ + if prompt_config: + prompt_file = Path(prompt_config).resolve() + if prompt_file.exists(): + return prompt_file.read_bytes().decode(encoding="utf-8") + return None + + +def truncate(text: str, max_length: int) -> str: + """Truncate a string to a maximum length.""" + if len(text) <= max_length: + return text + return text[:max_length] + "...[truncated]" diff --git a/graphrag/utils/cli.py b/packages/graphrag/graphrag/utils/cli.py similarity index 100% rename from graphrag/utils/cli.py rename to packages/graphrag/graphrag/utils/cli.py diff --git a/graphrag/utils/storage.py b/packages/graphrag/graphrag/utils/storage.py similarity index 73% rename from graphrag/utils/storage.py rename to packages/graphrag/graphrag/utils/storage.py index 8534330a15..852d066091 100644 --- a/graphrag/utils/storage.py +++ b/packages/graphrag/graphrag/utils/storage.py @@ -7,13 +7,12 @@ from io import BytesIO import pandas as pd - -from graphrag.storage.pipeline_storage import PipelineStorage +from graphrag_storage import Storage logger = logging.getLogger(__name__) -async def load_table_from_storage(name: str, storage: PipelineStorage) -> pd.DataFrame: +async def load_table_from_storage(name: str, storage: Storage) -> pd.DataFrame: """Load a parquet from the storage instance.""" filename = f"{name}.parquet" if not await storage.has(filename): @@ -28,17 +27,17 @@ async def load_table_from_storage(name: str, storage: PipelineStorage) -> pd.Dat async def write_table_to_storage( - table: pd.DataFrame, name: str, storage: PipelineStorage + table: pd.DataFrame, name: str, storage: Storage ) -> None: """Write a table to storage.""" await storage.set(f"{name}.parquet", table.to_parquet()) -async def delete_table_from_storage(name: str, storage: PipelineStorage) -> None: +async def delete_table_from_storage(name: str, storage: Storage) -> None: """Delete a table to storage.""" await storage.delete(f"{name}.parquet") -async def storage_has_table(name: str, storage: PipelineStorage) -> bool: +async def storage_has_table(name: str, storage: Storage) -> bool: """Check if a table exists in storage.""" return await storage.has(f"{name}.parquet") diff --git a/packages/graphrag/pyproject.toml b/packages/graphrag/pyproject.toml new file mode 100644 index 0000000000..efa1a3fd39 --- /dev/null +++ b/packages/graphrag/pyproject.toml @@ -0,0 +1,70 @@ +[project] +name = "graphrag" +# Maintainers: do not change the version here manually +version = "2.7.1" +description = "GraphRAG: A graph-based retrieval-augmented generation (RAG) system." +authors = [ + {name = "Alonso Guevara Fernández", email = "alonsog@microsoft.com"}, + {name = "Andrés Morales Esquivel", email = "andresmor@microsoft.com"}, + {name = "Chris Trevino", email = "chtrevin@microsoft.com"}, + {name = "David Tittsworth", email = "datittsw@microsoft.com"}, + {name = "Dayenne de Souza", email = "ddesouza@microsoft.com"}, + {name = "Derek Worthen", email = "deworthe@microsoft.com"}, + {name = "Gaudy Blanco Meneses", email = "gaudyb@microsoft.com"}, + {name = "Ha Trinh", email = "trinhha@microsoft.com"}, + {name = "Jonathan Larson", email = "jolarso@microsoft.com"}, + {name = "Josh Bradley", email = "joshbradley@microsoft.com"}, + {name = "Kate Lytvynets", email = "kalytv@microsoft.com"}, + {name = "Kenny Zhang", email = "zhangken@microsoft.com"}, + {name = "Mónica Carvajal"}, + {name = "Nathan Evans", email = "naevans@microsoft.com"}, + {name = "Rodrigo Racanicci", email = "rracanicci@microsoft.com"}, + {name = "Sarah Smith", email = "smithsarah@microsoft.com"}, +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.11,<3.14" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +dependencies = [ + "azure-identity~=1.19", + "azure-search-documents~=11.5", + "azure-storage-blob~=12.24", + "devtools~=0.12", + "graphrag-cache==2.7.1", + "graphrag-common==2.7.1", + "graphrag-input==2.7.1", + "graphrag-llm==2.7.1", + "graphrag-storage==2.7.1", + "graphrag-vectors==2.7.1", + "graspologic-native~=1.2", + "json-repair~=0.30", + "networkx~=3.4", + "nltk==3.9.1", + "numpy~=2.1", + "pandas~=2.3", + "pyarrow~=22.0", + "pydantic~=2.10", + "spacy~=3.8", + "blis~=1.0", + "textblob~=0.18", + "tqdm~=4.67", + "typing-extensions~=4.12", + "typer~=0.16", +] + +[project.scripts] +graphrag = "graphrag.cli.main:app" + +[project.urls] +Source = "https://github.com/microsoft/graphrag" + +[build-system] +requires = ["hatchling>=1.27.0,<2.0.0"] +build-backend = "hatchling.build" + diff --git a/pyproject.toml b/pyproject.toml index 890b047fec..cf3a9aebb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "graphrag" -# Maintainers: do not change the version here manually, use ./scripts/release.sh -version = "2.7.1" +name = "graphrag-monorepo" +classifiers = ["Private :: Do Not Upload"] +version = "0.0.0" description = "GraphRAG: A graph-based retrieval-augmented generation (RAG) system." authors = [ {name = "Alonso Guevara Fernández", email = "alonsog@microsoft.com"}, @@ -21,90 +21,46 @@ authors = [ {name = "Rodrigo Racanicci", email = "rracanicci@microsoft.com"}, {name = "Sarah Smith", email = "smithsarah@microsoft.com"}, ] -license = "MIT" -readme = "README.md" -requires-python = ">=3.10,<3.13" -classifiers = [ - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", -] - -dependencies = [ - "environs>=11.0.0", - # Vector Stores - "azure-search-documents>=11.5.2", - "lancedb>=0.17.0", - # Async IO - "aiofiles>=24.1.0", - # LLM - "fnllm[azure,openai]>=0.4.1", - "json-repair>=0.30.3", - "openai>=1.68.0", - "nltk==3.9.1", - "tiktoken>=0.11.0", - # Data-Science - "numpy>=1.25.2", - "graspologic>=3.4.1", - "networkx>=3.4.2", - "pandas>=2.2.3,<3", - "pyarrow>=17.0.0", - "umap-learn>=0.5.6", - # Configuration - "pyyaml>=6.0.2", - "python-dotenv>=1.0.1", - "pydantic>=2.10.3", - "devtools>=0.12.2", - "typing-extensions>=4.12.2", - # Azure - "azure-cosmos>=4.9.0", - "azure-identity>=1.19.0", - "azure-storage-blob>=12.24.0", - "future>=1.0.0", # Needed until graspologic fixes their dependency - "typer>=0.16.0", - "tqdm>=4.67.1", - "textblob>=0.18.0.post0", - "spacy>=3.8.4", - "litellm>=1.77.1", -] +requires-python = ">=3.11,<3.14" [dependency-groups] dev = [ - "coverage>=7.6.9", - "ipykernel>=6.29.5", - "jupyter>=1.1.1", - "nbconvert>=7.16.4", - "poethepoet>=0.31.1", - "pandas-stubs>=2.3.0.250703", - "pyright>=1.1.390", - "pytest>=8.3.4", - "pytest-asyncio>=0.24.0", - "pytest-timeout>=2.3.1", - "ruff>=0.8.2", - "semversioner>=2.0.5", - "update-toml>=0.2.1", - "deptry>=0.21.1", - "mkdocs-material>=9.5.48", - "mkdocs-jupyter>=0.25.1", - "mkdocs-exclude-search>=0.6.6", - "pytest-dotenv>=0.5.2", - "mkdocs-typer>=0.0.3", + "coverage~=7.6", + "deptry~=0.21", + "ipykernel~=6.29", + "jupyter~=1.1", + "mkdocs-material~=9.5", + "mkdocs-jupyter~=0.25", + "mkdocs-exclude-search~=0.6", + "mkdocs-typer~=0.0.3", + "nbconvert~=7.16", + "pandas-stubs~=2.3", + "poethepoet~=0.31", + "pyright~=1.1", + "pytest~=8.3", + "pytest-asyncio~=0.24", + "pytest-dotenv~=0.5", + "pytest-timeout~=2.3", + "ruff~=0.8", + "semversioner~=2.0", + "update-toml~=0.2", + "pytest-xdist[psutil]~=3.8.0", ] -[project.scripts] -graphrag = "graphrag.cli.main:app" +[tool.uv] +package = false -[project.urls] -Source = "https://github.com/microsoft/graphrag" +[tool.uv.workspace] +members = ["packages/*"] -[build-system] -requires = ["setuptools>=64", "wheel"] -build-backend = "setuptools.build_meta" - -[tool.setuptools.packages.find] -include = ["graphrag*"] -exclude = ["examples_notebooks*", "tests*"] +[tool.uv.sources] +graphrag-chunking = { workspace = true } +graphrag-common = { workspace = true } +graphrag-input = { workspace = true } +graphrag-storage = { workspace = true } +graphrag-cache = { workspace = true } +graphrag-vectors = { workspace = true } +graphrag-llm = { workspace = true } # Keep poethepoet for task management to minimize changes [tool.poe.tasks] @@ -114,9 +70,19 @@ _ruff_check = 'ruff check .' _pyright = "pyright" _convert_local_search_nb = 'jupyter nbconvert --output-dir=docsite/posts/query/notebooks/ --output="{notebook_name}_nb" --template=docsite/nbdocsite_template --to markdown examples_notebooks/local_search.ipynb' _convert_global_search_nb = 'jupyter nbconvert --output-dir=docsite/posts/query/notebooks/ --output="{notebook_name}_nb" --template=docsite/nbdocsite_template --to markdown examples_notebooks/global_search.ipynb' +_copy_build_assets = "python -m scripts.copy_build_assets" _semversioner_release = "semversioner release" _semversioner_changelog = "semversioner changelog > CHANGELOG.md" -_semversioner_update_toml_version = "update-toml update --path project.version --value $(uv run semversioner current-version)" +# Add more update toml tasks as packages are added +_semversioner_update_graphrag_toml_version = "update-toml update --file packages/graphrag/pyproject.toml --path project.version --value $(uv run semversioner current-version)" +_semversioner_update_graphrag_chunking_toml_version = "update-toml update --file packages/graphrag-chunking/pyproject.toml --path project.version --value $(uv run semversioner current-version)" +_semversioner_update_graphrag_common_toml_version = "update-toml update --file packages/graphrag-common/pyproject.toml --path project.version --value $(uv run semversioner current-version)" +_semversioner_update_graphrag_storage_toml_version = "update-toml update --file packages/graphrag-storage/pyproject.toml --path project.version --value $(uv run semversioner current-version)" +_semversioner_update_graphrag_cache_toml_version = "update-toml update --file packages/graphrag-cache/pyproject.toml --path project.version --value $(uv run semversioner current-version)" +_semversioner_update_graphrag_input_toml_version = "update-toml update --file packages/graphrag-input/pyproject.toml --path project.version --value $(uv run semversioner current-version)" +_semversioner_update_graphrag_vectors_toml_version = "update-toml update --file packages/graphrag-vectors/pyproject.toml --path project.version --value $(uv run semversioner current-version)" +_semversioner_update_graphrag_llm_toml_version = "update-toml update --file packages/graphrag-llm/pyproject.toml --path project.version --value $(uv run semversioner current-version)" +_semversioner_update_workspace_dependency_versions = "python -m scripts.update_workspace_dependency_versions" semversioner_add = "semversioner add-change" coverage_report = 'coverage report --omit "**/tests/**" --show-missing' check_format = 'ruff format . --check' @@ -126,7 +92,7 @@ _test_all = "coverage run -m pytest ./tests" test_unit = "pytest ./tests/unit" test_integration = "pytest ./tests/integration" test_smoke = "pytest ./tests/smoke" -test_notebook = "pytest ./tests/notebook" +test_notebook = "pytest -n auto ./tests/notebook" test_verbs = "pytest ./tests/verbs" index = "python -m graphrag index" update = "python -m graphrag update" @@ -137,12 +103,27 @@ prompt_tune = "python -m graphrag prompt-tune" test_only = "pytest -s -k" serve_docs = "mkdocs serve" build_docs = "mkdocs build" +_build_packages = "uv build --all-packages" +_sync = "uv sync" + +[[tool.poe.tasks.build]] +sequence = ['_copy_build_assets', '_build_packages'] [[tool.poe.tasks.release]] sequence = [ '_semversioner_release', '_semversioner_changelog', - '_semversioner_update_toml_version', + # Add more update toml tasks as packages are added + '_semversioner_update_graphrag_toml_version', + '_semversioner_update_graphrag_common_toml_version', + '_semversioner_update_graphrag_chunking_toml_version', + '_semversioner_update_graphrag_input_toml_version', + '_semversioner_update_graphrag_storage_toml_version', + '_semversioner_update_graphrag_cache_toml_version', + "_semversioner_update_graphrag_vectors_toml_version", + '_semversioner_update_graphrag_llm_toml_version', + '_semversioner_update_workspace_dependency_versions', + '_sync', ] ignore_fail = 'return_non_zero' @@ -255,7 +236,14 @@ convention = "numpy" # https://github.com/microsoft/pyright/blob/9f81564a4685ff5c55edd3959f9b39030f590b2f/docs/configuration.md#sample-pyprojecttoml-file [tool.pyright] -include = ["graphrag", "tests", "examples_notebooks"] +include = [ + "packages/graphrag/graphrag", + "packages/graphrag-common/graphrag_common", + "packages/graphrag-storage/graphrag_storage", + "packages/graphrag-cache/graphrag_cache", + "packages/graphrag-llm/graphrag_llm", + "tests" +] exclude = ["**/node_modules", "**/__pycache__"] [tool.pytest.ini_options] diff --git a/graphrag/language_model/providers/fnllm/__init__.py b/scripts/__init__.py similarity index 70% rename from graphrag/language_model/providers/fnllm/__init__.py rename to scripts/__init__.py index 8132a1b5ab..4adce34aa7 100644 --- a/graphrag/language_model/providers/fnllm/__init__.py +++ b/scripts/__init__.py @@ -1,4 +1,5 @@ # Copyright (c) 2025 Microsoft Corporation. # Licensed under the MIT License -"""FNLLM provider module.""" + +"""GraphRAG Scripts module.""" diff --git a/scripts/copy_build_assets.py b/scripts/copy_build_assets.py new file mode 100644 index 0000000000..d1ac1d90e1 --- /dev/null +++ b/scripts/copy_build_assets.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025 Microsoft Corporation. +# Licensed under the MIT License + +"""Copy root build assets to package directories.""" + +import shutil +from pathlib import Path + + +def copy_build_assets(): + """Copy root build assets to package build directories so files are included in pypi distributions.""" + root_dir = Path(__file__).parent.parent + build_assets = ["LICENSE"] + + for package_dir in root_dir.glob("packages/*"): + if package_dir.is_dir(): + for asset in build_assets: + src = root_dir / asset + dest = package_dir / asset + if src.exists(): + shutil.copy(src, dest) + + +if __name__ == "__main__": + copy_build_assets() diff --git a/scripts/update_workspace_dependency_versions.py b/scripts/update_workspace_dependency_versions.py new file mode 100644 index 0000000000..199cf01173 --- /dev/null +++ b/scripts/update_workspace_dependency_versions.py @@ -0,0 +1,58 @@ +# Copyright (c) 2025 Microsoft Corporation. +# Licensed under the MIT License + +"""Update workspace dependency versions.""" + +import os +import re +import subprocess # noqa: S404 +from pathlib import Path + + +def _get_version() -> str: + command = ["uv", "run", "semversioner", "current-version"] + completion = subprocess.run(command, env=os.environ, capture_output=True, text=True) # noqa: S603 + if completion.returncode != 0: + msg = f"Failed to get current version with return code: {completion.returncode}" + raise RuntimeError(msg) + return completion.stdout.strip() + + +def _get_package_paths() -> list[Path]: + root_dir = Path(__file__).parent.parent + return [p.resolve() for p in root_dir.glob("packages/*") if p.is_dir()] + + +def update_workspace_dependency_versions(): + """Update dependency versions across workspace packages. + + Iterate through all the workspace packages and update cross-package + dependency versions to match the current version of the workspace. + """ + version = _get_version() + package_paths = _get_package_paths() + for package_path in package_paths: + current_package_name = package_path.name + toml_path = package_path / "pyproject.toml" + if not toml_path.exists() or not toml_path.is_file(): + continue + toml_contents = toml_path.read_text(encoding="utf-8") + + for other_package_path in package_paths: + other_package_name = other_package_path.name + if other_package_name == current_package_name: + continue + dep_pattern = rf"{other_package_name}\s*==\s*\d+\.\d+\.\d+" + + if re.search(dep_pattern, toml_contents): + toml_contents = re.sub( + dep_pattern, + f"{other_package_name}=={version}", + toml_contents, + ) + + toml_path.write_text(toml_contents, encoding="utf-8", newline="\n") + + +if __name__ == "__main__": + update_workspace_dependency_versions() diff --git a/tests/__init__.py b/tests/__init__.py index cbb9376b53..2d6d270212 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,13 +3,3 @@ """Tests for the GraphRAG LLM module.""" - -# Register MOCK providers -from graphrag.config.enums import ModelType -from graphrag.language_model.factory import ModelFactory -from tests.mock_provider import MockChatLLM, MockEmbeddingLLM - -ModelFactory.register_chat(ModelType.MockChat, lambda **kwargs: MockChatLLM(**kwargs)) -ModelFactory.register_embedding( - ModelType.MockEmbedding, lambda **kwargs: MockEmbeddingLLM(**kwargs) -) diff --git a/tests/fixtures/azure/config.json b/tests/fixtures/azure/config.json index 8adced2c08..774228d0ad 100644 --- a/tests/fixtures/azure/config.json +++ b/tests/fixtures/azure/config.json @@ -1,6 +1,7 @@ { "input_path": "./tests/fixtures/azure", - "input_file_type": "text", + "input_type": "text", + "index_method": "standard", "workflow_config": { "skip_assert": true, "azure": { diff --git a/tests/fixtures/azure/settings.yml b/tests/fixtures/azure/settings.yml index 80ba02e56b..961037cffa 100644 --- a/tests/fixtures/azure/settings.yml +++ b/tests/fixtures/azure/settings.yml @@ -2,11 +2,10 @@ extract_claims: enabled: true vector_store: - default_vector_store: - type: "azure_ai_search" - url: ${AZURE_AI_SEARCH_URL_ENDPOINT} - api_key: ${AZURE_AI_SEARCH_API_KEY} - container_name: "azure_ci" + type: "azure_ai_search" + url: ${AZURE_AI_SEARCH_URL_ENDPOINT} + api_key: ${AZURE_AI_SEARCH_API_KEY} + container_name: "azure_ci" input: storage: @@ -14,7 +13,7 @@ input: connection_string: ${LOCAL_BLOB_STORAGE_CONNECTION_STRING} container_name: azurefixture base_dir: input - file_type: text + type: text cache: diff --git a/tests/fixtures/min-csv/config.json b/tests/fixtures/min-csv/config.json index 7b1b4d61e6..99291c05cb 100644 --- a/tests/fixtures/min-csv/config.json +++ b/tests/fixtures/min-csv/config.json @@ -1,6 +1,7 @@ { "input_path": "./tests/fixtures/min-csv", - "input_file_type": "text", + "input_type": "text", + "index_method": "standard", "workflow_config": { "load_input_documents": { "max_runtime": 30 @@ -20,10 +21,6 @@ 100, 750 ], - "nan_allowed_columns": [ - "x", - "y" - ], "max_runtime": 30, "expected_artifacts": [ "entities.parquet", @@ -54,7 +51,7 @@ "period", "size" ], - "max_runtime": 300, + "max_runtime": 2000, "expected_artifacts": ["community_reports.parquet"] }, "create_final_text_units": { @@ -76,7 +73,7 @@ 15 ], "nan_allowed_columns": [ - "metadata" + "raw_data" ], "max_runtime": 30, "expected_artifacts": ["documents.parquet"] @@ -88,9 +85,9 @@ ], "max_runtime": 150, "expected_artifacts": [ - "embeddings.text_unit.text.parquet", - "embeddings.entity.description.parquet", - "embeddings.community.full_content.parquet" + "embeddings.text_unit_text.parquet", + "embeddings.entity_description.parquet", + "embeddings.community_full_content.parquet" ] } }, @@ -101,7 +98,7 @@ }, { "query": "What is the major conflict in this story and who are the protagonist and antagonist?", - "method": "global" + "method": "drift" } ], "slow": false diff --git a/tests/fixtures/min-csv/settings.yml b/tests/fixtures/min-csv/settings.yml index ee3ddd03f9..5692deb019 100644 --- a/tests/fixtures/min-csv/settings.yml +++ b/tests/fixtures/min-csv/settings.yml @@ -1,43 +1,37 @@ -models: - default_chat_model: - azure_auth_type: api_key - type: chat +completion_models: + default_completion_model: model_provider: azure api_key: ${GRAPHRAG_API_KEY} api_base: ${GRAPHRAG_API_BASE} api_version: "2025-04-01-preview" - deployment_name: gpt-4.1 model: gpt-4.1 - retry_strategy: exponential_backoff - tokens_per_minute: null - requests_per_minute: null - model_supports_json: true - concurrent_requests: 25 - async_mode: threaded + azure_deployment_name: gpt-4.1 + rate_limit: + type: sliding_window + tokens_per_period: 250_000 + requests_per_period: 250 +embedding_models: default_embedding_model: - azure_auth_type: api_key - type: embedding model_provider: azure api_key: ${GRAPHRAG_API_KEY} api_base: ${GRAPHRAG_API_BASE} api_version: "2025-04-01-preview" - deployment_name: text-embedding-ada-002 - model: text-embedding-ada-002 - retry_strategy: exponential_backoff - tokens_per_minute: null - requests_per_minute: null - concurrent_requests: 25 - async_mode: threaded + model: text-embedding-3-large + azure_deployment_name: text-embedding-3-large + rate_limit: + type: sliding_window + tokens_per_period: 250_000 + requests_per_period: 250 vector_store: - default_vector_store: - type: "lancedb" - db_uri: "./tests/fixtures/min-csv/lancedb" - container_name: "lancedb_ci" - overwrite: True + type: "lancedb" + db_uri: "./tests/fixtures/min-csv/lancedb" + overwrite: True + container_name: "lancedb_ci" input: - file_type: csv + type: csv + encoding: utf-8-sig snapshots: embeddings: true \ No newline at end of file diff --git a/tests/fixtures/text/config.json b/tests/fixtures/text/config.json index 5b5738b1c1..cc69e523ec 100644 --- a/tests/fixtures/text/config.json +++ b/tests/fixtures/text/config.json @@ -1,6 +1,7 @@ { "input_path": "./tests/fixtures/text", - "input_file_type": "text", + "input_type": "text", + "index_method": "fast", "workflow_config": { "load_input_documents": { "max_runtime": 30 @@ -8,17 +9,16 @@ "create_base_text_units": { "max_runtime": 30 }, - "extract_graph": { - "max_runtime": 500 + "extract_graph_nlp": { + "max_runtime": 30 + }, + "prune_graph": { + "max_runtime": 30 }, "finalize_graph": { "row_range": [ 10, - 200 - ], - "nan_allowed_columns": [ - "x", - "y" + 300 ], "max_runtime": 30, "expected_artifacts": [ @@ -26,23 +26,6 @@ "relationships.parquet" ] }, - "extract_covariates": { - "row_range": [ - 10, - 100 - ], - "nan_allowed_columns": [ - "type", - "description", - "object_id", - "status", - "start_date", - "end_date", - "source_text" - ], - "max_runtime": 300, - "expected_artifacts": ["covariates.parquet"] - }, "create_communities": { "row_range": [ 1, @@ -51,7 +34,7 @@ "max_runtime": 30, "expected_artifacts": ["communities.parquet"] }, - "create_community_reports": { + "create_community_reports_text": { "row_range": [ 1, 30 @@ -67,7 +50,7 @@ "period", "size" ], - "max_runtime": 300, + "max_runtime": 2000, "expected_artifacts": ["community_reports.parquet"] }, "create_final_text_units": { @@ -89,7 +72,7 @@ 1 ], "nan_allowed_columns": [ - "metadata" + "raw_data" ], "max_runtime": 30, "expected_artifacts": ["documents.parquet"] @@ -101,9 +84,9 @@ ], "max_runtime": 150, "expected_artifacts": [ - "embeddings.text_unit.text.parquet", - "embeddings.entity.description.parquet", - "embeddings.community.full_content.parquet" + "embeddings.text_unit_text.parquet", + "embeddings.entity_description.parquet", + "embeddings.community_full_content.parquet" ] } }, diff --git a/tests/fixtures/text/prompts/community_report.txt b/tests/fixtures/text/prompts/community_report.txt index f939001c5b..4d78837774 100644 --- a/tests/fixtures/text/prompts/community_report.txt +++ b/tests/fixtures/text/prompts/community_report.txt @@ -35,12 +35,12 @@ Text: Entities -id,entity,description +human_readable_id,title,description 5,ABILA CITY PARK,Abila City Park is the location of the POK rally Relationships -id,source,target,description +human_readable_id,source,target,description 37,ABILA CITY PARK,POK RALLY,Abila City Park is the location of the POK rally 38,ABILA CITY PARK,POK,POK is holding a rally in Abila City Park 39,ABILA CITY PARK,POKRALLY,The POKRally is taking place at Abila City Park diff --git a/tests/fixtures/text/settings.yml b/tests/fixtures/text/settings.yml index 163f92ccc4..6cf6f9074d 100644 --- a/tests/fixtures/text/settings.yml +++ b/tests/fixtures/text/settings.yml @@ -1,43 +1,33 @@ -models: - default_chat_model: - azure_auth_type: api_key - type: chat +completion_models: + default_completion_model: model_provider: azure api_key: ${GRAPHRAG_API_KEY} api_base: ${GRAPHRAG_API_BASE} api_version: "2025-04-01-preview" - deployment_name: gpt-4.1 model: gpt-4.1 - retry_strategy: exponential_backoff - tokens_per_minute: null - requests_per_minute: null - model_supports_json: true - concurrent_requests: 25 - async_mode: threaded + azure_deployment_name: gpt-4.1 + rate_limit: + type: sliding_window + tokens_per_period: 250_000 + requests_per_period: 250 +embedding_models: default_embedding_model: - azure_auth_type: api_key - type: embedding model_provider: azure api_key: ${GRAPHRAG_API_KEY} api_base: ${GRAPHRAG_API_BASE} api_version: "2025-04-01-preview" - deployment_name: text-embedding-ada-002 - model: text-embedding-ada-002 - retry_strategy: exponential_backoff - tokens_per_minute: null - requests_per_minute: null - concurrent_requests: 25 - async_mode: threaded + model: text-embedding-3-large + azure_deployment_name: text-embedding-3-large + rate_limit: + type: sliding_window + tokens_per_period: 250_000 + requests_per_period: 250 vector_store: - default_vector_store: - type: "azure_ai_search" - url: ${AZURE_AI_SEARCH_URL_ENDPOINT} - api_key: ${AZURE_AI_SEARCH_API_KEY} - container_name: "simple_text_ci" - -extract_claims: - enabled: true + type: "azure_ai_search" + url: ${AZURE_AI_SEARCH_URL_ENDPOINT} + api_key: ${AZURE_AI_SEARCH_API_KEY} + container_name: "simple_text_ci" community_reports: prompt: "prompts/community_report.txt" diff --git a/tests/integration/cache/test_factory.py b/tests/integration/cache/test_factory.py index 5dde4e2635..ef01a0a315 100644 --- a/tests/integration/cache/test_factory.py +++ b/tests/integration/cache/test_factory.py @@ -2,19 +2,18 @@ # Licensed under the MIT License """CacheFactory Tests. -These tests will test the CacheFactory class and the creation of each cache type that is natively supported. +These tests will test the CacheFactory() class and the creation of each cache type that is natively supported. """ import sys import pytest - -from graphrag.cache.factory import CacheFactory -from graphrag.cache.json_pipeline_cache import JsonPipelineCache -from graphrag.cache.memory_pipeline_cache import InMemoryCache -from graphrag.cache.noop_pipeline_cache import NoopPipelineCache -from graphrag.cache.pipeline_cache import PipelineCache -from graphrag.config.enums import CacheType +from graphrag_cache import Cache, CacheConfig, CacheType, create_cache, register_cache +from graphrag_cache.cache_factory import cache_factory +from graphrag_cache.json_cache import JsonCache +from graphrag_cache.memory_cache import MemoryCache +from graphrag_cache.noop_cache import NoopCache +from graphrag_storage import StorageConfig, StorageType, create_storage # cspell:disable-next-line well-known-key WELL_KNOWN_BLOB_STORAGE_KEY = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" @@ -23,31 +22,55 @@ def test_create_noop_cache(): - kwargs = {} - cache = CacheFactory.create_cache(CacheType.none.value, kwargs) - assert isinstance(cache, NoopPipelineCache) + cache = create_cache( + CacheConfig( + type=CacheType.Noop, + ) + ) + assert isinstance(cache, NoopCache) def test_create_memory_cache(): - kwargs = {} - cache = CacheFactory.create_cache(CacheType.memory.value, kwargs) - assert isinstance(cache, InMemoryCache) + cache = create_cache( + CacheConfig( + type=CacheType.Memory, + ) + ) + assert isinstance(cache, MemoryCache) def test_create_file_cache(): - kwargs = {"root_dir": "/tmp", "base_dir": "testcache"} - cache = CacheFactory.create_cache(CacheType.file.value, kwargs) - assert isinstance(cache, JsonPipelineCache) + storage = create_storage( + StorageConfig( + type=StorageType.Memory, + ) + ) + cache = create_cache( + CacheConfig( + type=CacheType.Json, + ), + storage=storage, + ) + assert isinstance(cache, JsonCache) def test_create_blob_cache(): - kwargs = { - "connection_string": WELL_KNOWN_BLOB_STORAGE_KEY, - "container_name": "testcontainer", - "base_dir": "testcache", - } - cache = CacheFactory.create_cache(CacheType.blob.value, kwargs) - assert isinstance(cache, JsonPipelineCache) + storage = create_storage( + StorageConfig( + type=StorageType.AzureBlob, + connection_string=WELL_KNOWN_BLOB_STORAGE_KEY, + container_name="testcontainer", + base_dir="testcache", + ) + ) + cache = create_cache( + CacheConfig( + type=CacheType.Json, + ), + storage=storage, + ) + + assert isinstance(cache, JsonCache) @pytest.mark.skipif( @@ -55,13 +78,21 @@ def test_create_blob_cache(): reason="cosmosdb emulator is only available on windows runners at this time", ) def test_create_cosmosdb_cache(): - kwargs = { - "connection_string": WELL_KNOWN_COSMOS_CONNECTION_STRING, - "base_dir": "testdatabase", - "container_name": "testcontainer", - } - cache = CacheFactory.create_cache(CacheType.cosmosdb.value, kwargs) - assert isinstance(cache, JsonPipelineCache) + storage = create_storage( + StorageConfig( + type=StorageType.AzureCosmos, + connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, + database_name="testdatabase", + container_name="testcontainer", + ) + ) + cache = create_cache( + CacheConfig( + type=CacheType.Json, + ), + storage=storage, + ) + assert isinstance(cache, JsonCache) def test_register_and_create_custom_cache(): @@ -69,14 +100,14 @@ def test_register_and_create_custom_cache(): from unittest.mock import MagicMock # Create a mock that satisfies the PipelineCache interface - custom_cache_class = MagicMock(spec=PipelineCache) + custom_cache_class = MagicMock(spec=Cache) # Make the mock return a mock instance when instantiated instance = MagicMock() instance.initialized = True custom_cache_class.return_value = instance - CacheFactory.register("custom", lambda **kwargs: custom_cache_class(**kwargs)) - cache = CacheFactory.create_cache("custom", {}) + register_cache("custom", lambda **kwargs: custom_cache_class(**kwargs)) + cache = create_cache(CacheConfig(type="custom")) assert custom_cache_class.called assert cache is instance @@ -84,58 +115,21 @@ def test_register_and_create_custom_cache(): assert cache.initialized is True # type: ignore # Attribute only exists on our mock # Check if it's in the list of registered cache types - assert "custom" in CacheFactory.get_cache_types() - assert CacheFactory.is_supported_type("custom") - - -def test_get_cache_types(): - cache_types = CacheFactory.get_cache_types() - # Check that built-in types are registered - assert CacheType.none.value in cache_types - assert CacheType.memory.value in cache_types - assert CacheType.file.value in cache_types - assert CacheType.blob.value in cache_types - assert CacheType.cosmosdb.value in cache_types + assert "custom" in cache_factory def test_create_unknown_cache(): - with pytest.raises(ValueError, match="Unknown cache type: unknown"): - CacheFactory.create_cache("unknown", {}) - - -def test_is_supported_type(): - # Test built-in types - assert CacheFactory.is_supported_type(CacheType.none.value) - assert CacheFactory.is_supported_type(CacheType.memory.value) - assert CacheFactory.is_supported_type(CacheType.file.value) - assert CacheFactory.is_supported_type(CacheType.blob.value) - assert CacheFactory.is_supported_type(CacheType.cosmosdb.value) - - # Test unknown type - assert not CacheFactory.is_supported_type("unknown") - - -def test_enum_and_string_compatibility(): - """Test that both enum and string types work for cache creation.""" - kwargs = {} - - # Test with enum - cache_enum = CacheFactory.create_cache(CacheType.memory, kwargs) - assert isinstance(cache_enum, InMemoryCache) - - # Test with string - cache_str = CacheFactory.create_cache("memory", kwargs) - assert isinstance(cache_str, InMemoryCache) - - # Both should create the same type - assert type(cache_enum) is type(cache_str) + with pytest.raises( + ValueError, + match="CacheConfig\\.type 'unknown' is not registered in the CacheFactory\\.", + ): + create_cache(CacheConfig(type="unknown")) def test_register_class_directly_works(): - """Test that registering a class directly works (CacheFactory allows this).""" - from graphrag.cache.pipeline_cache import PipelineCache + """Test that registering a class directly works (CacheFactory() allows this).""" - class CustomCache(PipelineCache): + class CustomCache(Cache): def __init__(self, **kwargs): pass @@ -157,13 +151,12 @@ async def clear(self): def child(self, name: str): return self - # CacheFactory allows registering classes directly (no TypeError) - CacheFactory.register("custom_class", CustomCache) + # CacheFactory() allows registering classes directly (no TypeError) + register_cache("custom_class", CustomCache) # Verify it was registered - assert "custom_class" in CacheFactory.get_cache_types() - assert CacheFactory.is_supported_type("custom_class") + assert "custom_class" in cache_factory # Test creating an instance - cache = CacheFactory.create_cache("custom_class", {}) + cache = create_cache(CacheConfig(type="custom_class")) assert isinstance(cache, CustomCache) diff --git a/tests/integration/language_model/test_factory.py b/tests/integration/language_model/test_factory.py index af503265d6..526cb3e8dd 100644 --- a/tests/integration/language_model/test_factory.py +++ b/tests/integration/language_model/test_factory.py @@ -6,92 +6,99 @@ These tests will test the LLMFactory class and the creation of custom and provided LLMs. """ -from collections.abc import AsyncGenerator, Generator -from typing import Any - -from graphrag.language_model.factory import ModelFactory -from graphrag.language_model.manager import ModelManager -from graphrag.language_model.response.base import ( - BaseModelOutput, - BaseModelResponse, - ModelResponse, -) - +from typing import TYPE_CHECKING, Any, Unpack -async def test_create_custom_chat_model(): - class CustomChatModel: +from graphrag_llm.completion import ( + LLMCompletion, + create_completion, + register_completion, +) +from graphrag_llm.config import ModelConfig +from graphrag_llm.embedding import LLMEmbedding, create_embedding, register_embedding + +if TYPE_CHECKING: + from collections.abc import AsyncIterator, Iterator + + from graphrag_llm.metrics import MetricsStore + from graphrag_llm.tokenizer import Tokenizer + from graphrag_llm.types import ( + LLMCompletionArgs, + LLMCompletionChunk, + LLMCompletionResponse, + LLMEmbeddingArgs, + LLMEmbeddingResponse, + ResponseFormat, + ) + + +def test_create_custom_chat_model(): + class CustomChatModel(LLMCompletion): config: Any def __init__(self, **kwargs): pass - async def achat( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> ModelResponse: - return BaseModelResponse(output=BaseModelOutput(content="content")) - - def chat( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> ModelResponse: - return BaseModelResponse( - output=BaseModelOutput( - content="content", full_response={"key": "value"} - ) - ) - - async def achat_stream( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> AsyncGenerator[str, None]: - yield "" - - def chat_stream( - self, prompt: str, history: list | None = None, **kwargs: Any - ) -> Generator[str, None]: ... - - ModelFactory.register_chat("custom_chat", CustomChatModel) - model = ModelManager().get_or_create_chat_model("custom", "custom_chat") + def supports_structured_response(self) -> bool: + return True + + def completion( + self, + /, + **kwargs: Unpack["LLMCompletionArgs[ResponseFormat]"], + ) -> "LLMCompletionResponse[ResponseFormat] | Iterator[LLMCompletionChunk]": ... + + async def completion_async( + self, + /, + **kwargs: Unpack["LLMCompletionArgs[ResponseFormat]"], + ) -> ( + "LLMCompletionResponse[ResponseFormat] | AsyncIterator[LLMCompletionChunk]" + ): ... + + @property + def metrics_store(self) -> "MetricsStore": ... + + @property + def tokenizer(self) -> "Tokenizer": ... + + register_completion("custom_chat", CustomChatModel) + + model = create_completion( + ModelConfig( + type="custom_chat", + model_provider="custom_provider", + model="custom_chat_model", + ) + ) assert isinstance(model, CustomChatModel) - response = await model.achat("prompt") - assert response.output.content == "content" - assert response.output.full_response is None - - response = model.chat("prompt") - assert response.output.content == "content" - assert response.output.full_response == {"key": "value"} - -async def test_create_custom_embedding_llm(): - class CustomEmbeddingModel: - config: Any - - def __init__(self, **kwargs): - pass - async def aembed(self, text: str, **kwargs) -> list[float]: - return [1.0] +def test_create_custom_embedding_llm(): + class CustomEmbeddingModel(LLMEmbedding): + def __init__(self, **kwargs): ... - def embed(self, text: str, **kwargs) -> list[float]: - return [1.0] + def embedding( + self, /, **kwargs: Unpack["LLMEmbeddingArgs"] + ) -> "LLMEmbeddingResponse": ... - async def aembed_batch( - self, text_list: list[str], **kwargs - ) -> list[list[float]]: - return [[1.0]] + async def embedding_async( + self, /, **kwargs: Unpack["LLMEmbeddingArgs"] + ) -> "LLMEmbeddingResponse": ... - def embed_batch(self, text_list: list[str], **kwargs) -> list[list[float]]: - return [[1.0]] + @property + def metrics_store(self) -> "MetricsStore": ... - ModelFactory.register_embedding("custom_embedding", CustomEmbeddingModel) - llm = ModelManager().get_or_create_embedding_model("custom", "custom_embedding") - assert isinstance(llm, CustomEmbeddingModel) - response = await llm.aembed("text") - assert response == [1.0] + @property + def tokenizer(self) -> "Tokenizer": ... - response = llm.embed("text") - assert response == [1.0] + register_embedding("custom_embedding", CustomEmbeddingModel) - response = await llm.aembed_batch(["text"]) - assert response == [[1.0]] + model = create_embedding( + ModelConfig( + type="custom_embedding", + model_provider="custom_provider", + model="custom_embedding_model", + ) + ) - response = llm.embed_batch(["text"]) - assert response == [[1.0]] + assert isinstance(model, CustomEmbeddingModel) diff --git a/tests/unit/litellm_services/test_rate_limiter.py b/tests/integration/language_model/test_rate_limiter.py similarity index 77% rename from tests/unit/litellm_services/test_rate_limiter.py rename to tests/integration/language_model/test_rate_limiter.py index ffe144212d..1bc541b16a 100644 --- a/tests/unit/litellm_services/test_rate_limiter.py +++ b/tests/integration/language_model/test_rate_limiter.py @@ -8,22 +8,15 @@ from math import ceil from queue import Queue -import pytest +from graphrag_llm.config import RateLimitConfig, RateLimitType +from graphrag_llm.rate_limit import RateLimiter, create_rate_limiter -from graphrag.language_model.providers.litellm.services.rate_limiter.rate_limiter import ( - RateLimiter, -) -from graphrag.language_model.providers.litellm.services.rate_limiter.rate_limiter_factory import ( - RateLimiterFactory, -) -from tests.unit.litellm_services.utils import ( +from tests.integration.language_model.utils import ( assert_max_num_values_per_period, assert_stagger, bin_time_intervals, ) -rate_limiter_factory = RateLimiterFactory() - _period_in_seconds = 1 _rpm = 4 _tpm = 75 @@ -46,54 +39,14 @@ def test_binning(): ] -def test_rate_limiter_validation(): - """Test that the rate limiter can be created with valid parameters.""" - - # Valid parameters - rate_limiter = rate_limiter_factory.create( - strategy="static", rpm=60, tpm=10000, period_in_seconds=60 - ) - assert rate_limiter is not None - - # Invalid strategy - with pytest.raises( - ValueError, - match=r"Strategy 'invalid_strategy' is not registered.", - ): - rate_limiter_factory.create(strategy="invalid_strategy", rpm=60, tpm=10000) - - # Both rpm and tpm are None - with pytest.raises( - ValueError, - match=r"Both TPM and RPM cannot be None \(disabled\), one or both must be set to a positive integer.", - ): - rate_limiter_factory.create(strategy="static") - - # Invalid rpm - with pytest.raises( - ValueError, - match=r"RPM and TPM must be either None \(disabled\) or positive integers.", - ): - rate_limiter_factory.create(strategy="static", rpm=-10) - - # Invalid tpm - with pytest.raises( - ValueError, - match=r"RPM and TPM must be either None \(disabled\) or positive integers.", - ): - rate_limiter_factory.create(strategy="static", tpm=-10) - - # Invalid period_in_seconds - with pytest.raises( - ValueError, match=r"Period in seconds must be a positive integer." - ): - rate_limiter_factory.create(strategy="static", rpm=10, period_in_seconds=-10) - - def test_rpm(): """Test that the rate limiter enforces RPM limits.""" - rate_limiter = rate_limiter_factory.create( - strategy="static", rpm=_rpm, period_in_seconds=_period_in_seconds + rate_limiter = create_rate_limiter( + RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=_period_in_seconds, + requests_per_period=_rpm, + ) ) time_values: list[float] = [] @@ -120,8 +73,12 @@ def test_rpm(): def test_tpm(): """Test that the rate limiter enforces TPM limits.""" - rate_limiter = rate_limiter_factory.create( - strategy="static", tpm=_tpm, period_in_seconds=_period_in_seconds + rate_limiter = create_rate_limiter( + RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=_period_in_seconds, + tokens_per_period=_tpm, + ) ) time_values: list[float] = [] @@ -153,8 +110,12 @@ def test_token_in_request_exceeds_tpm(): greater than the tpm limit but still below the context window limit of the underlying model. In this case, the request should still be allowed to proceed but may take up its own rate limit bin. """ - rate_limiter = rate_limiter_factory.create( - strategy="static", tpm=_tpm, period_in_seconds=_period_in_seconds + rate_limiter = create_rate_limiter( + RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=_period_in_seconds, + tokens_per_period=_tpm, + ) ) time_values: list[float] = [] @@ -177,8 +138,13 @@ def test_token_in_request_exceeds_tpm(): def test_rpm_and_tpm_with_rpm_as_limiting_factor(): """Test that the rate limiter enforces RPM and TPM limits.""" - rate_limiter = rate_limiter_factory.create( - strategy="static", rpm=_rpm, tpm=_tpm, period_in_seconds=_period_in_seconds + rate_limiter = create_rate_limiter( + RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=_period_in_seconds, + requests_per_period=_rpm, + tokens_per_period=_tpm, + ) ) time_values: list[float] = [] @@ -206,8 +172,13 @@ def test_rpm_and_tpm_with_rpm_as_limiting_factor(): def test_rpm_and_tpm_with_tpm_as_limiting_factor(): """Test that the rate limiter enforces TPM limits.""" - rate_limiter = rate_limiter_factory.create( - strategy="static", rpm=_rpm, tpm=_tpm, period_in_seconds=_period_in_seconds + rate_limiter = create_rate_limiter( + RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=_period_in_seconds, + requests_per_period=_rpm, + tokens_per_period=_tpm, + ) ) time_values: list[float] = [] @@ -250,8 +221,13 @@ def _run_rate_limiter( def test_rpm_threaded(): """Test that the rate limiter enforces RPM limits in a threaded environment.""" - rate_limiter = rate_limiter_factory.create( - strategy="static", rpm=_rpm, tpm=_tpm, period_in_seconds=_period_in_seconds + rate_limiter = create_rate_limiter( + RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=_period_in_seconds, + requests_per_period=_rpm, + tokens_per_period=_tpm, + ) ) input_queue: Queue[int | None] = Queue() @@ -310,8 +286,13 @@ def test_rpm_threaded(): def test_tpm_threaded(): """Test that the rate limiter enforces TPM limits in a threaded environment.""" - rate_limiter = rate_limiter_factory.create( - strategy="static", rpm=_rpm, tpm=_tpm, period_in_seconds=_period_in_seconds + rate_limiter = create_rate_limiter( + RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=_period_in_seconds, + requests_per_period=_rpm, + tokens_per_period=_tpm, + ) ) input_queue: Queue[int | None] = Queue() diff --git a/tests/integration/language_model/test_retries.py b/tests/integration/language_model/test_retries.py new file mode 100644 index 0000000000..98e25b209e --- /dev/null +++ b/tests/integration/language_model/test_retries.py @@ -0,0 +1,243 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Test LiteLLM Retries.""" + +import time +from typing import Any + +import httpx +import litellm.exceptions as exceptions +import pytest +from graphrag_llm.config import RetryConfig, RetryType +from graphrag_llm.retry import create_retry + + +@pytest.mark.parametrize( + ("config", "max_retries", "expected_time"), + [ + ( + RetryConfig( + type=RetryType.ExponentialBackoff, + max_retries=3, + base_delay=2.0, + jitter=False, + ), + 3, + 2 + 4 + 8, # No jitter, so exact times + ), + ( + RetryConfig( + type=RetryType.Immediate, + max_retries=3, + ), + 3, + 0, # Immediate retry, so no delay + ), + ], +) +def test_retries(config: RetryConfig, max_retries: int, expected_time: float) -> None: + """ + Test various retry strategies with various configurations. + """ + retry_service = create_retry(config) + + # start at -1 because the first call is not a retry + retries = -1 + + def mock_func(): + nonlocal retries + retries += 1 + msg = "Mock error for testing retries" + raise ValueError(msg) + + start_time = time.time() + with pytest.raises(ValueError, match="Mock error for testing retries"): + retry_service.retry(func=mock_func, input_args={}) + elapsed_time = time.time() - start_time + + assert retries == max_retries, f"Expected {max_retries} retries, got {retries}" + assert elapsed_time >= expected_time, ( + f"Expected elapsed time >= {expected_time}, got {elapsed_time}" + ) + + +@pytest.mark.parametrize( + ("config", "max_retries", "expected_time"), + [ + ( + RetryConfig( + type=RetryType.ExponentialBackoff, + max_retries=3, + base_delay=2.0, + jitter=False, + ), + 3, + 2 + 4 + 8, # No jitter, so exact times + ), + ( + RetryConfig( + type=RetryType.Immediate, + max_retries=3, + ), + 3, + 0, # Immediate retry, so no delay + ), + ], +) +async def test_retries_async( + config: RetryConfig, max_retries: int, expected_time: float +) -> None: + """ + Test various retry strategies with various configurations. + """ + retry_service = create_retry(config) + + # start at -1 because the first call is not a retry + retries = -1 + + def mock_func(): + nonlocal retries + retries += 1 + msg = "Mock error for testing retries" + raise ValueError(msg) + + start_time = time.time() + with pytest.raises(ValueError, match="Mock error for testing retries"): + await retry_service.retry_async(func=mock_func, input_args={}) + elapsed_time = time.time() - start_time + + assert retries == max_retries, f"Expected {max_retries} retries, got {retries}" + assert elapsed_time >= expected_time, ( + f"Expected elapsed time >= {expected_time}, got {elapsed_time}" + ) + + +@pytest.mark.parametrize( + "config", + [ + ( + RetryConfig( + type=RetryType.ExponentialBackoff, + max_retries=3, + base_delay=2.0, + jitter=False, + ) + ), + ( + RetryConfig( + type=RetryType.Immediate, + max_retries=3, + ) + ), + ], +) +@pytest.mark.parametrize( + ("exception", "exception_args"), + [ + ( + "BadRequestError", + ["Oh no!", "", ""], + ), + ( + "UnsupportedParamsError", + ["Oh no!", "", ""], + ), + ( + "ContextWindowExceededError", + ["Oh no!", "", ""], + ), + ( + "ContentPolicyViolationError", + ["Oh no!", "", ""], + ), + ( + "ImageFetchError", + ["Oh no!", "", ""], + ), + ( + "InvalidRequestError", + ["Oh no!", "", ""], + ), + ( + "AuthenticationError", + ["Oh no!", "", ""], + ), + ( + "PermissionDeniedError", + [ + "Oh no!", + "", + "", + httpx.Response( + status_code=403, + request=httpx.Request( + method="GET", url="https://litellm.ai" + ), # mock request object + ), + ], + ), + ( + "NotFoundError", + ["Oh no!", "", ""], + ), + ( + "UnprocessableEntityError", + [ + "Oh no!", + "", + "", + httpx.Response( + status_code=403, + request=httpx.Request( + method="GET", url="https://litellm.ai" + ), # mock request object + ), + ], + ), + ( + "APIConnectionError", + ["Oh no!", "", ""], + ), + ( + "APIError", + [500, "Oh no!", "", ""], + ), + ( + "ServiceUnavailableError", + ["Oh no!", "", ""], + ), + ( + "APIResponseValidationError", + ["Oh no!", "", ""], + ), + ( + "BudgetExceededError", + ["Oh no!", "", ""], + ), + ], +) +def test_exponential_backoff_skipping_exceptions( + config: RetryConfig, exception: str, exception_args: list[Any] +) -> None: + """ + Test skipping retries for exceptions that should not cause a retry. + """ + retry_service = create_retry(config) + + # start at -1 because the first call is not a retry + retries = -1 + exception_cls = exceptions.__dict__[exception] + + def mock_func(): + nonlocal retries + retries += 1 + raise exception_cls(*exception_args) + + with pytest.raises(exception_cls, match="Oh no!"): + retry_service.retry(func=mock_func, input_args={}) + + # subtract 1 from retries because the first call is not a retry + assert retries == 0, ( + f"Expected not to retry for '{exception}' exception. Got {retries} retries." + ) diff --git a/tests/unit/litellm_services/utils.py b/tests/integration/language_model/utils.py similarity index 100% rename from tests/unit/litellm_services/utils.py rename to tests/integration/language_model/utils.py diff --git a/tests/integration/logging/test_factory.py b/tests/integration/logging/test_factory.py index bacb4f0712..3da8f7965b 100644 --- a/tests/integration/logging/test_factory.py +++ b/tests/integration/logging/test_factory.py @@ -8,7 +8,6 @@ import logging import pytest - from graphrag.config.enums import ReportingType from graphrag.logger.blob_workflow_logger import BlobWorkflowLogger from graphrag.logger.factory import LoggerFactory @@ -27,7 +26,7 @@ def test_create_blob_logger(): "base_dir": "testbasedir", "container_name": "testcontainer", } - logger = LoggerFactory.create_logger(ReportingType.blob.value, kwargs) + logger = LoggerFactory().create(ReportingType.blob.value, kwargs) assert isinstance(logger, BlobWorkflowLogger) @@ -40,8 +39,8 @@ def test_register_and_create_custom_logger(): instance.initialized = True custom_logger_class.return_value = instance - LoggerFactory.register("custom", lambda **kwargs: custom_logger_class(**kwargs)) - logger = LoggerFactory.create_logger("custom", {}) + LoggerFactory().register("custom", lambda **kwargs: custom_logger_class(**kwargs)) + logger = LoggerFactory().create("custom") assert custom_logger_class.called assert logger is instance @@ -49,17 +48,15 @@ def test_register_and_create_custom_logger(): assert logger.initialized is True # type: ignore # Attribute only exists on our mock # Check if it's in the list of registered logger types - assert "custom" in LoggerFactory.get_logger_types() - assert LoggerFactory.is_supported_type("custom") + assert "custom" in LoggerFactory() def test_get_logger_types(): - logger_types = LoggerFactory.get_logger_types() # Check that built-in types are registered - assert ReportingType.file.value in logger_types - assert ReportingType.blob.value in logger_types + assert ReportingType.file.value in LoggerFactory() + assert ReportingType.blob.value in LoggerFactory() def test_create_unknown_logger(): - with pytest.raises(ValueError, match="Unknown reporting type: unknown"): - LoggerFactory.create_logger("unknown", {}) + with pytest.raises(ValueError, match="Strategy 'unknown' is not registered\\."): + LoggerFactory().create("unknown") diff --git a/tests/integration/logging/test_standard_logging.py b/tests/integration/logging/test_standard_logging.py index c0b485b5f2..6bb28a7343 100644 --- a/tests/integration/logging/test_standard_logging.py +++ b/tests/integration/logging/test_standard_logging.py @@ -4,10 +4,12 @@ """Tests for standard logging functionality.""" import logging +import os import tempfile from pathlib import Path from graphrag.logger.standard_logging import DEFAULT_LOG_FILENAME, init_loggers + from tests.unit.config.utils import get_default_graphrag_config @@ -37,7 +39,11 @@ def test_logger_hierarchy(): def test_init_loggers_file_config(): """Test that init_loggers works with file configuration.""" with tempfile.TemporaryDirectory() as temp_dir: - config = get_default_graphrag_config(root_dir=temp_dir) + # Need to manually change cwd since we are not using load_config + # to create graphrag config. + cwd = Path.cwd() + os.chdir(temp_dir) + config = get_default_graphrag_config() # call init_loggers with file config init_loggers(config=config) @@ -67,12 +73,17 @@ def test_init_loggers_file_config(): if isinstance(handler, logging.FileHandler): handler.close() logger.handlers.clear() + os.chdir(cwd) def test_init_loggers_file_verbose(): """Test that init_loggers works with verbose flag.""" with tempfile.TemporaryDirectory() as temp_dir: - config = get_default_graphrag_config(root_dir=temp_dir) + # Need to manually change cwd since we are not using load_config + # to create graphrag config. + cwd = Path.cwd() + os.chdir(temp_dir) + config = get_default_graphrag_config() # call init_loggers with file config init_loggers(config=config, verbose=True) @@ -95,12 +106,17 @@ def test_init_loggers_file_verbose(): if isinstance(handler, logging.FileHandler): handler.close() logger.handlers.clear() + os.chdir(cwd) def test_init_loggers_custom_filename(): """Test that init_loggers works with custom filename.""" with tempfile.TemporaryDirectory() as temp_dir: - config = get_default_graphrag_config(root_dir=temp_dir) + # Need to manually change cwd since we are not using load_config + # to create graphrag config. + cwd = Path.cwd() + os.chdir(temp_dir) + config = get_default_graphrag_config() # call init_loggers with file config init_loggers(config=config, filename="custom-log.log") @@ -116,3 +132,4 @@ def test_init_loggers_custom_filename(): if isinstance(handler, logging.FileHandler): handler.close() logger.handlers.clear() + os.chdir(cwd) diff --git a/tests/integration/storage/test_blob_pipeline_storage.py b/tests/integration/storage/test_blob_storage.py similarity index 70% rename from tests/integration/storage/test_blob_pipeline_storage.py rename to tests/integration/storage/test_blob_storage.py index 24e380d20e..ec996e91a2 100644 --- a/tests/integration/storage/test_blob_pipeline_storage.py +++ b/tests/integration/storage/test_blob_storage.py @@ -5,37 +5,30 @@ import re from datetime import datetime -from graphrag.storage.blob_pipeline_storage import BlobPipelineStorage +from graphrag_storage.azure_blob_storage import AzureBlobStorage # cspell:disable-next-line well-known-key WELL_KNOWN_BLOB_STORAGE_KEY = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" async def test_find(): - storage = BlobPipelineStorage( + storage = AzureBlobStorage( connection_string=WELL_KNOWN_BLOB_STORAGE_KEY, container_name="testfind", ) try: try: - items = list( - storage.find(base_dir="input", file_pattern=re.compile(r".*\.txt$")) - ) - items = [item[0] for item in items] + items = list(storage.find(file_pattern=re.compile(r".*\.txt$"))) assert items == [] await storage.set( "input/christmas.txt", "Merry Christmas!", encoding="utf-8" ) - items = list( - storage.find(base_dir="input", file_pattern=re.compile(r".*\.txt$")) - ) - items = [item[0] for item in items] + items = list(storage.find(file_pattern=re.compile(r".*\.txt$"))) assert items == ["input/christmas.txt"] await storage.set("test.txt", "Hello, World!", encoding="utf-8") items = list(storage.find(file_pattern=re.compile(r".*\.txt$"))) - items = [item[0] for item in items] assert items == ["input/christmas.txt", "test.txt"] output = await storage.get("test.txt") @@ -48,26 +41,10 @@ async def test_find(): storage._delete_container() # noqa: SLF001 -async def test_dotprefix(): - storage = BlobPipelineStorage( - connection_string=WELL_KNOWN_BLOB_STORAGE_KEY, - container_name="testfind", - path_prefix=".", - ) - try: - await storage.set("input/christmas.txt", "Merry Christmas!", encoding="utf-8") - items = list(storage.find(file_pattern=re.compile(r".*\.txt$"))) - items = [item[0] for item in items] - assert items == ["input/christmas.txt"] - finally: - storage._delete_container() # noqa: SLF001 - - async def test_get_creation_date(): - storage = BlobPipelineStorage( + storage = AzureBlobStorage( connection_string=WELL_KNOWN_BLOB_STORAGE_KEY, container_name="testfind", - path_prefix=".", ) try: await storage.set("input/christmas.txt", "Merry Christmas!", encoding="utf-8") @@ -82,7 +59,7 @@ async def test_get_creation_date(): async def test_child(): - parent = BlobPipelineStorage( + parent = AzureBlobStorage( connection_string=WELL_KNOWN_BLOB_STORAGE_KEY, container_name="testchild", ) @@ -91,12 +68,10 @@ async def test_child(): storage = parent.child("input") await storage.set("christmas.txt", "Merry Christmas!", encoding="utf-8") items = list(storage.find(re.compile(r".*\.txt$"))) - items = [item[0] for item in items] assert items == ["christmas.txt"] await storage.set("test.txt", "Hello, World!", encoding="utf-8") items = list(storage.find(re.compile(r".*\.txt$"))) - items = [item[0] for item in items] print("FOUND", items) assert items == ["christmas.txt", "test.txt"] @@ -104,7 +79,6 @@ async def test_child(): assert output == "Hello, World!" items = list(parent.find(re.compile(r".*\.txt$"))) - items = [item[0] for item in items] print("FOUND ITEMS", items) assert items == ["input/christmas.txt", "input/test.txt"] finally: diff --git a/tests/integration/storage/test_cosmosdb_storage.py b/tests/integration/storage/test_cosmosdb_storage.py index 529be52256..a044cc754f 100644 --- a/tests/integration/storage/test_cosmosdb_storage.py +++ b/tests/integration/storage/test_cosmosdb_storage.py @@ -8,8 +8,7 @@ from datetime import datetime import pytest - -from graphrag.storage.cosmosdb_pipeline_storage import CosmosDBPipelineStorage +from graphrag_storage.azure_cosmos_storage import AzureCosmosStorage # cspell:disable-next-line well-known-key WELL_KNOWN_COSMOS_CONNECTION_STRING = "AccountEndpoint=https://127.0.0.1:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" @@ -22,15 +21,14 @@ async def test_find(): - storage = CosmosDBPipelineStorage( + storage = AzureCosmosStorage( connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, - base_dir="testfind", + database_name="testfind", container_name="testfindcontainer", ) try: try: items = list(storage.find(file_pattern=re.compile(r".*\.json$"))) - items = [item[0] for item in items] assert items == [] json_content = { @@ -40,7 +38,6 @@ async def test_find(): "christmas.json", json.dumps(json_content), encoding="utf-8" ) items = list(storage.find(file_pattern=re.compile(r".*\.json$"))) - items = [item[0] for item in items] assert items == ["christmas.json"] json_content = { @@ -48,7 +45,6 @@ async def test_find(): } await storage.set("test.json", json.dumps(json_content), encoding="utf-8") items = list(storage.find(file_pattern=re.compile(r".*\.json$"))) - items = [item[0] for item in items] assert items == ["christmas.json", "test.json"] output = await storage.get("test.json") @@ -68,22 +64,22 @@ async def test_find(): async def test_child(): - storage = CosmosDBPipelineStorage( + storage = AzureCosmosStorage( connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, - base_dir="testchild", + database_name="testchild", container_name="testchildcontainer", ) try: child_storage = storage.child("child") - assert type(child_storage) is CosmosDBPipelineStorage + assert type(child_storage) is AzureCosmosStorage finally: await storage.clear() async def test_clear(): - storage = CosmosDBPipelineStorage( + storage = AzureCosmosStorage( connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, - base_dir="testclear", + database_name="testclear", container_name="testclearcontainer", ) try: @@ -111,9 +107,9 @@ async def test_clear(): async def test_get_creation_date(): - storage = CosmosDBPipelineStorage( + storage = AzureCosmosStorage( connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, - base_dir="testclear", + database_name="testclear", container_name="testclearcontainer", ) try: diff --git a/tests/integration/storage/test_factory.py b/tests/integration/storage/test_factory.py index 3d21e50e9f..f4d3092e78 100644 --- a/tests/integration/storage/test_factory.py +++ b/tests/integration/storage/test_factory.py @@ -8,14 +8,17 @@ import sys import pytest - -from graphrag.config.enums import StorageType -from graphrag.storage.blob_pipeline_storage import BlobPipelineStorage -from graphrag.storage.cosmosdb_pipeline_storage import CosmosDBPipelineStorage -from graphrag.storage.factory import StorageFactory -from graphrag.storage.file_pipeline_storage import FilePipelineStorage -from graphrag.storage.memory_pipeline_storage import MemoryPipelineStorage -from graphrag.storage.pipeline_storage import PipelineStorage +from graphrag_storage import ( + Storage, + StorageConfig, + StorageType, + create_storage, + register_storage, +) +from graphrag_storage.azure_blob_storage import AzureBlobStorage +from graphrag_storage.azure_cosmos_storage import AzureCosmosStorage +from graphrag_storage.file_storage import FileStorage +from graphrag_storage.memory_storage import MemoryStorage # cspell:disable-next-line well-known-key WELL_KNOWN_BLOB_STORAGE_KEY = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" @@ -25,14 +28,14 @@ @pytest.mark.skip(reason="Blob storage emulator is not available in this environment") def test_create_blob_storage(): - kwargs = { - "type": "blob", - "connection_string": WELL_KNOWN_BLOB_STORAGE_KEY, - "base_dir": "testbasedir", - "container_name": "testcontainer", - } - storage = StorageFactory.create_storage(StorageType.blob.value, kwargs) - assert isinstance(storage, BlobPipelineStorage) + config = StorageConfig( + type=StorageType.AzureBlob, + connection_string=WELL_KNOWN_BLOB_STORAGE_KEY, + base_dir="testbasedir", + container_name="testcontainer", + ) + storage = create_storage(config) + assert isinstance(storage, AzureBlobStorage) @pytest.mark.skipif( @@ -40,65 +43,61 @@ def test_create_blob_storage(): reason="cosmosdb emulator is only available on windows runners at this time", ) def test_create_cosmosdb_storage(): - kwargs = { - "type": "cosmosdb", - "connection_string": WELL_KNOWN_COSMOS_CONNECTION_STRING, - "base_dir": "testdatabase", - "container_name": "testcontainer", - } - storage = StorageFactory.create_storage(StorageType.cosmosdb.value, kwargs) - assert isinstance(storage, CosmosDBPipelineStorage) + config = StorageConfig( + type=StorageType.AzureCosmos, + connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, + database_name="testdatabase", + container_name="testcontainer", + ) + storage = create_storage(config) + assert isinstance(storage, AzureCosmosStorage) -def test_create_file_storage(): - kwargs = {"type": "file", "base_dir": "/tmp/teststorage"} - storage = StorageFactory.create_storage(StorageType.file.value, kwargs) - assert isinstance(storage, FilePipelineStorage) +def test_create_file(): + config = StorageConfig( + type=StorageType.File, + base_dir="/tmp/teststorage", + ) + storage = create_storage(config) + assert isinstance(storage, FileStorage) def test_create_memory_storage(): - kwargs = {} # MemoryPipelineStorage doesn't accept any constructor parameters - storage = StorageFactory.create_storage(StorageType.memory.value, kwargs) - assert isinstance(storage, MemoryPipelineStorage) + config = StorageConfig( + base_dir="", + type=StorageType.Memory, + ) + storage = create_storage(config) + assert isinstance(storage, MemoryStorage) def test_register_and_create_custom_storage(): """Test registering and creating a custom storage type.""" from unittest.mock import MagicMock - # Create a mock that satisfies the PipelineStorage interface - custom_storage_class = MagicMock(spec=PipelineStorage) + # Create a mock that satisfies the Storage interface + custom_storage_class = MagicMock(spec=Storage) # Make the mock return a mock instance when instantiated instance = MagicMock() - # We can set attributes on the mock instance, even if they don't exist on PipelineStorage + # We can set attributes on the mock instance, even if they don't exist on Storage instance.initialized = True custom_storage_class.return_value = instance - StorageFactory.register("custom", lambda **kwargs: custom_storage_class(**kwargs)) - storage = StorageFactory.create_storage("custom", {}) + register_storage("custom", lambda **kwargs: custom_storage_class(**kwargs)) + storage = create_storage(StorageConfig(type="custom")) assert custom_storage_class.called assert storage is instance # Access the attribute we set on our mock assert storage.initialized is True # type: ignore # Attribute only exists on our mock - # Check if it's in the list of registered storage types - assert "custom" in StorageFactory.get_storage_types() - assert StorageFactory.is_supported_type("custom") - - -def test_get_storage_types(): - storage_types = StorageFactory.get_storage_types() - # Check that built-in types are registered - assert StorageType.file.value in storage_types - assert StorageType.memory.value in storage_types - assert StorageType.blob.value in storage_types - assert StorageType.cosmosdb.value in storage_types - def test_create_unknown_storage(): - with pytest.raises(ValueError, match="Unknown storage type: unknown"): - StorageFactory.create_storage("unknown", {}) + with pytest.raises( + ValueError, + match="StorageConfig\\.type 'unknown' is not registered in the StorageFactory\\.", + ): + create_storage(StorageConfig(type="unknown")) def test_register_class_directly_works(): @@ -107,19 +106,14 @@ def test_register_class_directly_works(): from collections.abc import Iterator from typing import Any - from graphrag.storage.pipeline_storage import PipelineStorage - - class CustomStorage(PipelineStorage): + class CustomStorage(Storage): def __init__(self, **kwargs): pass def find( self, file_pattern: re.Pattern[str], - base_dir: str | None = None, - file_filter: dict[str, Any] | None = None, - max_count=-1, - ) -> Iterator[tuple[str, dict[str, Any]]]: + ) -> Iterator[str]: return iter([]) async def get( @@ -139,7 +133,7 @@ async def has(self, key: str) -> bool: async def clear(self) -> None: pass - def child(self, name: str | None) -> "PipelineStorage": + def child(self, name: str | None) -> "Storage": return self def keys(self) -> list[str]: @@ -149,12 +143,8 @@ async def get_creation_date(self, key: str) -> str: return "2024-01-01 00:00:00 +0000" # StorageFactory allows registering classes directly (no TypeError) - StorageFactory.register("custom_class", CustomStorage) - - # Verify it was registered - assert "custom_class" in StorageFactory.get_storage_types() - assert StorageFactory.is_supported_type("custom_class") + register_storage("custom_class", CustomStorage) # Test creating an instance - storage = StorageFactory.create_storage("custom_class", {}) + storage = create_storage(StorageConfig(type="custom_class")) assert isinstance(storage, CustomStorage) diff --git a/tests/integration/storage/test_file_pipeline_storage.py b/tests/integration/storage/test_file_storage.py similarity index 65% rename from tests/integration/storage/test_file_pipeline_storage.py rename to tests/integration/storage/test_file_storage.py index be58476480..0852cd99fb 100644 --- a/tests/integration/storage/test_file_pipeline_storage.py +++ b/tests/integration/storage/test_file_storage.py @@ -7,24 +7,18 @@ from datetime import datetime from pathlib import Path -from graphrag.storage.file_pipeline_storage import ( - FilePipelineStorage, +from graphrag_storage.file_storage import ( + FileStorage, ) __dirname__ = os.path.dirname(__file__) async def test_find(): - storage = FilePipelineStorage() - items = list( - storage.find( - base_dir="tests/fixtures/text/input", - file_pattern=re.compile(r".*\.txt$"), - file_filter=None, - ) - ) - assert items == [(str(Path("tests/fixtures/text/input/dulce.txt")), {})] - output = await storage.get("tests/fixtures/text/input/dulce.txt") + storage = FileStorage(base_dir="tests/fixtures/text/input") + items = list(storage.find(file_pattern=re.compile(r".*\.txt$"))) + assert items == [str(Path("dulce.txt"))] + output = await storage.get("dulce.txt") assert len(output) > 0 await storage.set("test.txt", "Hello, World!", encoding="utf-8") @@ -36,12 +30,12 @@ async def test_find(): async def test_get_creation_date(): - storage = FilePipelineStorage() - - creation_date = await storage.get_creation_date( - "tests/fixtures/text/input/dulce.txt" + storage = FileStorage( + base_dir="tests/fixtures/text/input", ) + creation_date = await storage.get_creation_date("dulce.txt") + datetime_format = "%Y-%m-%d %H:%M:%S %z" parsed_datetime = datetime.strptime(creation_date, datetime_format).astimezone() @@ -49,10 +43,10 @@ async def test_get_creation_date(): async def test_child(): - storage = FilePipelineStorage() + storage = FileStorage(base_dir="") storage = storage.child("tests/fixtures/text/input") items = list(storage.find(re.compile(r".*\.txt$"))) - assert items == [(str(Path("dulce.txt")), {})] + assert items == [str(Path("dulce.txt"))] output = await storage.get("dulce.txt") assert len(output) > 0 diff --git a/tests/integration/vector_stores/test_azure_ai_search.py b/tests/integration/vector_stores/test_azure_ai_search.py index 2ff3347444..ffd445508c 100644 --- a/tests/integration/vector_stores/test_azure_ai_search.py +++ b/tests/integration/vector_stores/test_azure_ai_search.py @@ -7,10 +7,10 @@ from unittest.mock import MagicMock, patch import pytest - -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig -from graphrag.vector_stores.azure_ai_search import AzureAISearchVectorStore -from graphrag.vector_stores.base import VectorStoreDocument +from graphrag_vectors import ( + VectorStoreDocument, +) +from graphrag_vectors.azure_ai_search import AzureAISearchVectorStore TEST_AZURE_AI_SEARCH_URL = os.environ.get( "TEST_AZURE_AI_SEARCH_URL", "https://test-url.search.windows.net" @@ -24,60 +24,49 @@ class TestAzureAISearchVectorStore: @pytest.fixture def mock_search_client(self): """Create a mock Azure AI Search client.""" - with patch( - "graphrag.vector_stores.azure_ai_search.SearchClient" - ) as mock_client: + with patch("graphrag_vectors.azure_ai_search.SearchClient") as mock_client: yield mock_client.return_value @pytest.fixture def mock_index_client(self): """Create a mock Azure AI Search index client.""" - with patch( - "graphrag.vector_stores.azure_ai_search.SearchIndexClient" - ) as mock_client: + with patch("graphrag_vectors.azure_ai_search.SearchIndexClient") as mock_client: yield mock_client.return_value @pytest.fixture def vector_store(self, mock_search_client, mock_index_client): """Create an Azure AI Search vector store instance.""" vector_store = AzureAISearchVectorStore( - vector_store_schema_config=VectorStoreSchemaConfig( - index_name="test_vectors", vector_size=5 - ), + url=TEST_AZURE_AI_SEARCH_URL, + api_key=TEST_AZURE_AI_SEARCH_KEY, + index_name="test_vectors", + vector_size=5, ) # Create the necessary mocks first vector_store.db_connection = mock_search_client vector_store.index_client = mock_index_client - vector_store.connect( - url=TEST_AZURE_AI_SEARCH_URL, - api_key=TEST_AZURE_AI_SEARCH_KEY, - ) + vector_store.connect() return vector_store @pytest.fixture def vector_store_custom(self, mock_search_client, mock_index_client): """Create an Azure AI Search vector store instance.""" vector_store = AzureAISearchVectorStore( - vector_store_schema_config=VectorStoreSchemaConfig( - index_name="test_vectors", - id_field="id_custom", - text_field="text_custom", - attributes_field="attributes_custom", - vector_field="vector_custom", - vector_size=5, - ), + url=TEST_AZURE_AI_SEARCH_URL, + api_key=TEST_AZURE_AI_SEARCH_KEY, + index_name="test_vectors", + id_field="id_custom", + vector_field="vector_custom", + vector_size=5, ) # Create the necessary mocks first vector_store.db_connection = mock_search_client vector_store.index_client = mock_index_client - vector_store.connect( - url=TEST_AZURE_AI_SEARCH_URL, - api_key=TEST_AZURE_AI_SEARCH_KEY, - ) + vector_store.connect() return vector_store @pytest.fixture @@ -86,15 +75,11 @@ def sample_documents(self): return [ VectorStoreDocument( id="doc1", - text="This is document 1", vector=[0.1, 0.2, 0.3, 0.4, 0.5], - attributes={"title": "Doc 1", "category": "test"}, ), VectorStoreDocument( id="doc2", - text="This is document 2", vector=[0.2, 0.3, 0.4, 0.5, 0.6], - attributes={"title": "Doc 2", "category": "test"}, ), ] @@ -110,16 +95,12 @@ async def test_vector_store_operations( search_results = [ { "id": "doc1", - "text": "This is document 1", "vector": [0.1, 0.2, 0.3, 0.4, 0.5], - "attributes": '{"title": "Doc 1", "category": "test"}', "@search.score": 0.9, }, { "id": "doc2", - "text": "This is document 2", "vector": [0.2, 0.3, 0.4, 0.5, 0.6], - "attributes": '{"title": "Doc 2", "category": "test"}', "@search.score": 0.8, }, ] @@ -127,18 +108,14 @@ async def test_vector_store_operations( mock_search_client.get_document.return_value = { "id": "doc1", - "text": "This is document 1", "vector": [0.1, 0.2, 0.3, 0.4, 0.5], - "attributes": '{"title": "Doc 1", "category": "test"}', } + vector_store.create_index() vector_store.load_documents(sample_documents) assert mock_index_client.create_or_update_index.called assert mock_search_client.upload_documents.called - filter_query = vector_store.filter_by_id(["doc1", "doc2"]) - assert filter_query == "search.in(id, 'doc1,doc2', ',')" - vector_results = vector_store.similarity_search_by_vector( [0.1, 0.2, 0.3, 0.4, 0.5], k=2 ) @@ -157,8 +134,6 @@ def mock_embedder(text: str) -> list[float]: doc = vector_store.search_by_id("doc1") assert doc.id == "doc1" - assert doc.text == "This is document 1" - assert doc.attributes["title"] == "Doc 1" async def test_empty_embedding(self, vector_store, mock_search_client): """Test similarity search by text with empty embedding.""" @@ -189,16 +164,12 @@ async def test_vector_store_customization( search_results = [ { vector_store_custom.id_field: "doc1", - vector_store_custom.text_field: "This is document 1", vector_store_custom.vector_field: [0.1, 0.2, 0.3, 0.4, 0.5], - vector_store_custom.attributes_field: '{"title": "Doc 1", "category": "test"}', "@search.score": 0.9, }, { vector_store_custom.id_field: "doc2", - vector_store_custom.text_field: "This is document 2", vector_store_custom.vector_field: [0.2, 0.3, 0.4, 0.5, 0.6], - vector_store_custom.attributes_field: '{"title": "Doc 2", "category": "test"}', "@search.score": 0.8, }, ] @@ -206,21 +177,14 @@ async def test_vector_store_customization( mock_search_client.get_document.return_value = { vector_store_custom.id_field: "doc1", - vector_store_custom.text_field: "This is document 1", vector_store_custom.vector_field: [0.1, 0.2, 0.3, 0.4, 0.5], - vector_store_custom.attributes_field: '{"title": "Doc 1", "category": "test"}', } + vector_store_custom.create_index() vector_store_custom.load_documents(sample_documents) assert mock_index_client.create_or_update_index.called assert mock_search_client.upload_documents.called - filter_query = vector_store_custom.filter_by_id(["doc1", "doc2"]) - assert ( - filter_query - == f"search.in({vector_store_custom.id_field}, 'doc1,doc2', ',')" - ) - vector_results = vector_store_custom.similarity_search_by_vector( [0.1, 0.2, 0.3, 0.4, 0.5], k=2 ) @@ -239,5 +203,3 @@ def mock_embedder(text: str) -> list[float]: doc = vector_store_custom.search_by_id("doc1") assert doc.id == "doc1" - assert doc.text == "This is document 1" - assert doc.attributes["title"] == "Doc 1" diff --git a/tests/integration/vector_stores/test_cosmosdb.py b/tests/integration/vector_stores/test_cosmosdb.py index 768858d578..de30bb6c50 100644 --- a/tests/integration/vector_stores/test_cosmosdb.py +++ b/tests/integration/vector_stores/test_cosmosdb.py @@ -7,10 +7,10 @@ import numpy as np import pytest - -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig -from graphrag.vector_stores.base import VectorStoreDocument -from graphrag.vector_stores.cosmosdb import CosmosDBVectorStore +from graphrag_vectors import ( + VectorStoreDocument, +) +from graphrag_vectors.cosmosdb import CosmosDBVectorStore # cspell:disable-next-line well-known-key WELL_KNOWN_COSMOS_CONNECTION_STRING = "AccountEndpoint=https://127.0.0.1:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" @@ -25,39 +25,32 @@ def test_vector_store_operations(): """Test basic vector store operations with CosmosDB.""" vector_store = CosmosDBVectorStore( - vector_store_schema_config=VectorStoreSchemaConfig(index_name="testvector"), + connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, + database_name="test_db", + index_name="testvector", ) try: - vector_store.connect( - connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, - database_name="test_db", - ) + vector_store.connect() docs = [ VectorStoreDocument( id="doc1", - text="This is document 1", vector=[0.1, 0.2, 0.3, 0.4, 0.5], - attributes={"title": "Doc 1", "category": "test"}, ), VectorStoreDocument( id="doc2", - text="This is document 2", vector=[0.2, 0.3, 0.4, 0.5, 0.6], - attributes={"title": "Doc 2", "category": "test"}, ), ] - vector_store.load_documents(docs) - vector_store.filter_by_id(["doc1"]) + vector_store.create_index() + vector_store.load_documents(docs) doc = vector_store.search_by_id("doc1") assert doc.id == "doc1" - assert doc.text == "This is document 1" assert doc.vector is not None assert np.allclose(doc.vector, [0.1, 0.2, 0.3, 0.4, 0.5]) - assert doc.attributes["title"] == "Doc 1" # Define a simple text embedder function for testing def mock_embedder(text: str) -> list[float]: @@ -79,21 +72,19 @@ def mock_embedder(text: str) -> list[float]: def test_clear(): """Test clearing the vector store.""" vector_store = CosmosDBVectorStore( - vector_store_schema_config=VectorStoreSchemaConfig(index_name="testclear"), + connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, + database_name="testclear", + index_name="testclear", ) try: - vector_store.connect( - connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, - database_name="testclear", - ) + vector_store.connect() doc = VectorStoreDocument( id="test", - text="Test document", vector=[0.1, 0.2, 0.3, 0.4, 0.5], - attributes={"title": "Test Doc"}, ) + vector_store.create_index() vector_store.load_documents([doc]) result = vector_store.search_by_id("test") assert result.id == "test" @@ -108,46 +99,35 @@ def test_clear(): def test_vector_store_customization(): """Test vector store customization with CosmosDB.""" vector_store = CosmosDBVectorStore( - vector_store_schema_config=VectorStoreSchemaConfig( - index_name="text-embeddings", - id_field="id", - text_field="text_custom", - vector_field="vector_custom", - attributes_field="attributes_custom", - vector_size=5, - ), + connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, + database_name="test_db", + index_name="text-embeddings", + id_field="id", + vector_field="vector_custom", + vector_size=5, ) try: - vector_store.connect( - connection_string=WELL_KNOWN_COSMOS_CONNECTION_STRING, - database_name="test_db", - ) + vector_store.connect() docs = [ VectorStoreDocument( id="doc1", - text="This is document 1", vector=[0.1, 0.2, 0.3, 0.4, 0.5], - attributes={"title": "Doc 1", "category": "test"}, ), VectorStoreDocument( id="doc2", - text="This is document 2", vector=[0.2, 0.3, 0.4, 0.5, 0.6], - attributes={"title": "Doc 2", "category": "test"}, ), ] - vector_store.load_documents(docs) - vector_store.filter_by_id(["doc1"]) + vector_store.create_index() + vector_store.load_documents(docs) doc = vector_store.search_by_id("doc1") assert doc.id == "doc1" - assert doc.text == "This is document 1" assert doc.vector is not None assert np.allclose(doc.vector, [0.1, 0.2, 0.3, 0.4, 0.5]) - assert doc.attributes["title"] == "Doc 1" # Define a simple text embedder function for testing def mock_embedder(text: str) -> list[float]: diff --git a/tests/integration/vector_stores/test_factory.py b/tests/integration/vector_stores/test_factory.py index 724c1f8e2d..ceb2a84c84 100644 --- a/tests/integration/vector_stores/test_factory.py +++ b/tests/integration/vector_stores/test_factory.py @@ -6,29 +6,28 @@ """ import pytest +from graphrag_vectors import ( + VectorStore, + VectorStoreFactory, + VectorStoreType, +) +from graphrag_vectors.azure_ai_search import AzureAISearchVectorStore +from graphrag_vectors.cosmosdb import CosmosDBVectorStore +from graphrag_vectors.lancedb import LanceDBVectorStore -from graphrag.config.enums import VectorStoreType -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig -from graphrag.vector_stores.azure_ai_search import AzureAISearchVectorStore -from graphrag.vector_stores.base import BaseVectorStore -from graphrag.vector_stores.cosmosdb import CosmosDBVectorStore -from graphrag.vector_stores.factory import VectorStoreFactory -from graphrag.vector_stores.lancedb import LanceDBVectorStore +# register the defaults, since they are lazily registered +VectorStoreFactory().register(VectorStoreType.LanceDB, LanceDBVectorStore) +VectorStoreFactory().register(VectorStoreType.AzureAISearch, AzureAISearchVectorStore) +VectorStoreFactory().register(VectorStoreType.CosmosDB, CosmosDBVectorStore) def test_create_lancedb_vector_store(): kwargs = { "db_uri": "/tmp/lancedb", } - vector_store = VectorStoreFactory.create_vector_store( - vector_store_type=VectorStoreType.LanceDB.value, - vector_store_schema_config=VectorStoreSchemaConfig( - index_name="test_collection" - ), - kwargs=kwargs, - ) + vector_store = VectorStoreFactory().create(VectorStoreType.LanceDB, kwargs) assert isinstance(vector_store, LanceDBVectorStore) - assert vector_store.index_name == "test_collection" + assert vector_store.index_name == "vector_index" @pytest.mark.skip(reason="Azure AI Search requires credentials and setup") @@ -36,13 +35,11 @@ def test_create_azure_ai_search_vector_store(): kwargs = { "url": "https://test.search.windows.net", "api_key": "test_key", + "index_name": "test_collection", } - vector_store = VectorStoreFactory.create_vector_store( - vector_store_type=VectorStoreType.AzureAISearch.value, - vector_store_schema_config=VectorStoreSchemaConfig( - index_name="test_collection" - ), - kwargs=kwargs, + vector_store = VectorStoreFactory().create( + VectorStoreType.AzureAISearch, + kwargs, ) assert isinstance(vector_store, AzureAISearchVectorStore) @@ -52,14 +49,12 @@ def test_create_cosmosdb_vector_store(): kwargs = { "connection_string": "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test_key==", "database_name": "test_db", + "index_name": "test_collection", } - vector_store = VectorStoreFactory.create_vector_store( - vector_store_type=VectorStoreType.CosmosDB.value, - vector_store_schema_config=VectorStoreSchemaConfig( - index_name="test_collection" - ), - kwargs=kwargs, + vector_store = VectorStoreFactory().create( + VectorStoreType.CosmosDB, + kwargs, ) assert isinstance(vector_store, CosmosDBVectorStore) @@ -69,20 +64,18 @@ def test_register_and_create_custom_vector_store(): """Test registering and creating a custom vector store type.""" from unittest.mock import MagicMock - # Create a mock that satisfies the BaseVectorStore interface - custom_vector_store_class = MagicMock(spec=BaseVectorStore) + # Create a mock that satisfies the VectorStore interface + custom_vector_store_class = MagicMock(spec=VectorStore) # Make the mock return a mock instance when instantiated instance = MagicMock() instance.initialized = True custom_vector_store_class.return_value = instance - VectorStoreFactory.register( + VectorStoreFactory().register( "custom", lambda **kwargs: custom_vector_store_class(**kwargs) ) - vector_store = VectorStoreFactory.create_vector_store( - vector_store_type="custom", vector_store_schema_config=VectorStoreSchemaConfig() - ) + vector_store = VectorStoreFactory().create("custom", {}) assert custom_vector_store_class.called assert vector_store is instance @@ -90,48 +83,39 @@ def test_register_and_create_custom_vector_store(): assert vector_store.initialized is True # type: ignore # Attribute only exists on our mock # Check if it's in the list of registered vector store types - assert "custom" in VectorStoreFactory.get_vector_store_types() - assert VectorStoreFactory.is_supported_type("custom") - - -def test_get_vector_store_types(): - vector_store_types = VectorStoreFactory.get_vector_store_types() - # Check that built-in types are registered - assert VectorStoreType.LanceDB.value in vector_store_types - assert VectorStoreType.AzureAISearch.value in vector_store_types - assert VectorStoreType.CosmosDB.value in vector_store_types + assert "custom" in VectorStoreFactory() def test_create_unknown_vector_store(): - with pytest.raises(ValueError, match="Unknown vector store type: unknown"): - VectorStoreFactory.create_vector_store( - vector_store_type="unknown", - vector_store_schema_config=VectorStoreSchemaConfig(), - ) + with pytest.raises(ValueError, match="Strategy 'unknown' is not registered\\."): + VectorStoreFactory().create("unknown") def test_is_supported_type(): # Test built-in types - assert VectorStoreFactory.is_supported_type(VectorStoreType.LanceDB.value) - assert VectorStoreFactory.is_supported_type(VectorStoreType.AzureAISearch.value) - assert VectorStoreFactory.is_supported_type(VectorStoreType.CosmosDB.value) + assert VectorStoreType.LanceDB in VectorStoreFactory() + assert VectorStoreType.AzureAISearch in VectorStoreFactory() + assert VectorStoreType.CosmosDB in VectorStoreFactory() # Test unknown type - assert not VectorStoreFactory.is_supported_type("unknown") + assert "unknown" not in VectorStoreFactory() def test_register_class_directly_works(): - """Test that registering a class directly works (VectorStoreFactory allows this).""" - from graphrag.vector_stores.base import BaseVectorStore + """Test that registering a class directly works.""" + from graphrag_vectors import VectorStore - class CustomVectorStore(BaseVectorStore): + class CustomVectorStore(VectorStore): def __init__(self, **kwargs): super().__init__(**kwargs) def connect(self, **kwargs): pass - def load_documents(self, documents, overwrite=True): + def create_index(self, **kwargs): + pass + + def load_documents(self, documents): pass def similarity_search_by_vector(self, query_embedding, k=10, **kwargs): @@ -140,25 +124,21 @@ def similarity_search_by_vector(self, query_embedding, k=10, **kwargs): def similarity_search_by_text(self, text, text_embedder, k=10, **kwargs): return [] - def filter_by_id(self, include_ids): - return {} - def search_by_id(self, id): - from graphrag.vector_stores.base import VectorStoreDocument + from graphrag_vectors import VectorStoreDocument - return VectorStoreDocument(id=id, text="test", vector=None) + return VectorStoreDocument(id=id, vector=None) - # VectorStoreFactory allows registering classes directly (no TypeError) - VectorStoreFactory.register("custom_class", CustomVectorStore) + # VectorStoreFactory() allows registering classes directly (no TypeError) + VectorStoreFactory().register("custom_class", CustomVectorStore) # Verify it was registered - assert "custom_class" in VectorStoreFactory.get_vector_store_types() - assert VectorStoreFactory.is_supported_type("custom_class") + assert "custom_class" in VectorStoreFactory() # Test creating an instance - vector_store = VectorStoreFactory.create_vector_store( - vector_store_type="custom_class", - vector_store_schema_config=VectorStoreSchemaConfig(), + vector_store = VectorStoreFactory().create( + "custom_class", + {}, ) assert isinstance(vector_store, CustomVectorStore) diff --git a/tests/integration/vector_stores/test_lancedb.py b/tests/integration/vector_stores/test_lancedb.py index a3fe4e3a70..dbd1198556 100644 --- a/tests/integration/vector_stores/test_lancedb.py +++ b/tests/integration/vector_stores/test_lancedb.py @@ -8,10 +8,10 @@ import numpy as np import pytest - -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig -from graphrag.vector_stores.base import VectorStoreDocument -from graphrag.vector_stores.lancedb import LanceDBVectorStore +from graphrag_vectors import ( + VectorStoreDocument, +) +from graphrag_vectors.lancedb import LanceDBVectorStore class TestLanceDBVectorStore: @@ -23,21 +23,15 @@ def sample_documents(self): return [ VectorStoreDocument( id="1", - text="This is document 1", vector=[0.1, 0.2, 0.3, 0.4, 0.5], - attributes={"title": "Doc 1", "category": "test"}, ), VectorStoreDocument( id="2", - text="This is document 2", vector=[0.2, 0.3, 0.4, 0.5, 0.6], - attributes={"title": "Doc 2", "category": "test"}, ), VectorStoreDocument( id="3", - text="This is document 3", vector=[0.3, 0.4, 0.5, 0.6, 0.7], - attributes={"title": "Doc 3", "category": "test"}, ), ] @@ -47,21 +41,15 @@ def sample_documents_categories(self): return [ VectorStoreDocument( id="1", - text="Document about cats", vector=[0.1, 0.2, 0.3, 0.4, 0.5], - attributes={"category": "animals"}, ), VectorStoreDocument( id="2", - text="Document about dogs", vector=[0.2, 0.3, 0.4, 0.5, 0.6], - attributes={"category": "animals"}, ), VectorStoreDocument( id="3", - text="Document about cars", vector=[0.3, 0.4, 0.5, 0.6, 0.7], - attributes={"category": "vehicles"}, ), ] @@ -71,11 +59,10 @@ def test_vector_store_operations(self, sample_documents): temp_dir = tempfile.mkdtemp() try: vector_store = LanceDBVectorStore( - vector_store_schema_config=VectorStoreSchemaConfig( - index_name="test_collection", vector_size=5 - ) + db_uri=temp_dir, index_name="test_collection", vector_size=5 ) - vector_store.connect(db_uri=temp_dir) + vector_store.connect() + vector_store.create_index() vector_store.load_documents(sample_documents[:2]) if vector_store.index_name: @@ -85,14 +72,8 @@ def test_vector_store_operations(self, sample_documents): doc = vector_store.search_by_id("1") assert doc.id == "1" - assert doc.text == "This is document 1" - assert doc.vector is not None assert np.allclose(doc.vector, [0.1, 0.2, 0.3, 0.4, 0.5]) - assert doc.attributes["title"] == "Doc 1" - - filter_query = vector_store.filter_by_id(["1"]) - assert filter_query == "id in ('1')" results = vector_store.similarity_search_by_vector( [0.1, 0.2, 0.3, 0.4, 0.5], k=2 @@ -101,10 +82,10 @@ def test_vector_store_operations(self, sample_documents): assert isinstance(results[0].score, float) # Test append mode - vector_store.load_documents([sample_documents[2]], overwrite=False) + vector_store.create_index() + vector_store.load_documents([sample_documents[2]]) result = vector_store.search_by_id("3") assert result.id == "3" - assert result.text == "This is document 3" # Define a simple text embedder function for testing def mock_embedder(text: str) -> list[float]: @@ -119,7 +100,6 @@ def mock_embedder(text: str) -> list[float]: # Test non-existent document non_existent = vector_store.search_by_id("nonexistent") assert non_existent.id == "nonexistent" - assert non_existent.text is None assert non_existent.vector is None finally: shutil.rmtree(temp_dir) @@ -130,19 +110,16 @@ def test_empty_collection(self): temp_dir = tempfile.mkdtemp() try: vector_store = LanceDBVectorStore( - vector_store_schema_config=VectorStoreSchemaConfig( - index_name="empty_collection", vector_size=5 - ) + db_uri=temp_dir, index_name="empty_collection", vector_size=5 ) - vector_store.connect(db_uri=temp_dir) + vector_store.connect() # Load the vector store with a document, then delete it sample_doc = VectorStoreDocument( id="tmp", - text="Temporary document to create schema", vector=[0.1, 0.2, 0.3, 0.4, 0.5], - attributes={"title": "Tmp"}, ) + vector_store.create_index() vector_store.load_documents([sample_doc]) vector_store.db_connection.open_table( vector_store.index_name if vector_store.index_name else "" @@ -157,15 +134,13 @@ def test_empty_collection(self): # Add a document after creating an empty collection doc = VectorStoreDocument( id="1", - text="This is document 1", vector=[0.1, 0.2, 0.3, 0.4, 0.5], - attributes={"title": "Doc 1"}, ) - vector_store.load_documents([doc], overwrite=False) + vector_store.create_index() + vector_store.load_documents([doc]) result = vector_store.search_by_id("1") assert result.id == "1" - assert result.text == "This is document 1" finally: # Clean up - remove the temporary directory shutil.rmtree(temp_dir) @@ -176,26 +151,22 @@ def test_filter_search(self, sample_documents_categories): temp_dir = tempfile.mkdtemp() try: vector_store = LanceDBVectorStore( - vector_store_schema_config=VectorStoreSchemaConfig( - index_name="filter_collection", vector_size=5 - ) + db_uri=temp_dir, index_name="filter_collection", vector_size=5 ) - vector_store.connect(db_uri=temp_dir) - + vector_store.connect() + vector_store.create_index() vector_store.load_documents(sample_documents_categories) # Filter to include only documents about animals - vector_store.filter_by_id(["1", "2"]) results = vector_store.similarity_search_by_vector( [0.1, 0.2, 0.3, 0.4, 0.5], k=3 ) - # Should return at most 2 documents (the filtered ones) - assert len(results) <= 2 + # Should return at most 3 documents (the filtered ones) + assert len(results) <= 3 ids = [result.document.id for result in results] - assert "3" not in ids - assert set(ids).issubset({"1", "2"}) + assert set(ids).issubset({"1", "2", "3"}) finally: shutil.rmtree(temp_dir) @@ -205,16 +176,14 @@ def test_vector_store_customization(self, sample_documents): temp_dir = tempfile.mkdtemp() try: vector_store = LanceDBVectorStore( - vector_store_schema_config=VectorStoreSchemaConfig( - index_name="text-embeddings", - id_field="id_custom", - text_field="text_custom", - vector_field="vector_custom", - attributes_field="attributes_custom", - vector_size=5, - ), + db_uri=temp_dir, + index_name="text-embeddings", + id_field="id_custom", + vector_field="vector_custom", + vector_size=5, ) - vector_store.connect(db_uri=temp_dir) + vector_store.connect() + vector_store.create_index() vector_store.load_documents(sample_documents[:2]) if vector_store.index_name: @@ -224,14 +193,8 @@ def test_vector_store_customization(self, sample_documents): doc = vector_store.search_by_id("1") assert doc.id == "1" - assert doc.text == "This is document 1" - assert doc.vector is not None assert np.allclose(doc.vector, [0.1, 0.2, 0.3, 0.4, 0.5]) - assert doc.attributes["title"] == "Doc 1" - - filter_query = vector_store.filter_by_id(["1"]) - assert filter_query == f"{vector_store.id_field} in ('1')" results = vector_store.similarity_search_by_vector( [0.1, 0.2, 0.3, 0.4, 0.5], k=2 @@ -240,10 +203,10 @@ def test_vector_store_customization(self, sample_documents): assert isinstance(results[0].score, float) # Test append mode - vector_store.load_documents([sample_documents[2]], overwrite=False) + vector_store.create_index() + vector_store.load_documents([sample_documents[2]]) result = vector_store.search_by_id("3") assert result.id == "3" - assert result.text == "This is document 3" # Define a simple text embedder function for testing def mock_embedder(text: str) -> list[float]: @@ -258,7 +221,6 @@ def mock_embedder(text: str) -> list[float]: # Test non-existent document non_existent = vector_store.search_by_id("nonexistent") assert non_existent.id == "nonexistent" - assert non_existent.text is None assert non_existent.vector is None finally: shutil.rmtree(temp_dir) diff --git a/tests/mock_provider.py b/tests/mock_provider.py deleted file mode 100644 index d68fd762df..0000000000 --- a/tests/mock_provider.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright (c) 2025 Microsoft Corporation. -# Licensed under the MIT License - -"""A module containing mock model provider definitions.""" - -from collections.abc import AsyncGenerator, Generator -from typing import Any - -from pydantic import BaseModel - -from graphrag.config.enums import ModelType -from graphrag.config.models.language_model_config import LanguageModelConfig -from graphrag.language_model.response.base import ( - BaseModelOutput, - BaseModelResponse, - ModelResponse, -) - - -class MockChatLLM: - """A mock chat LLM provider.""" - - def __init__( - self, - responses: list[str | BaseModel] | None = None, - config: LanguageModelConfig | None = None, - json: bool = False, - **kwargs: Any, - ): - self.responses = config.responses if config and config.responses else responses - self.response_index = 0 - self.config = config or LanguageModelConfig( - type=ModelType.MockChat, model="gpt-4o", api_key="mock" - ) - - async def achat( - self, - prompt: str, - history: list | None = None, - **kwargs, - ) -> ModelResponse: - """Return the next response in the list.""" - return self.chat(prompt, history, **kwargs) - - async def achat_stream( - self, - prompt: str, - history: list | None = None, - **kwargs, - ) -> AsyncGenerator[str, None]: - """Return the next response in the list.""" - if not self.responses: - return - - for response in self.responses: - response = ( - response.model_dump_json() - if isinstance(response, BaseModel) - else response - ) - - yield response - - def chat( - self, - prompt: str, - history: list | None = None, - **kwargs, - ) -> ModelResponse: - """Return the next response in the list.""" - if not self.responses: - return BaseModelResponse(output=BaseModelOutput(content="")) - - response = self.responses[self.response_index % len(self.responses)] - self.response_index += 1 - - parsed_json = response if isinstance(response, BaseModel) else None - response = ( - response.model_dump_json() if isinstance(response, BaseModel) else response - ) - - return BaseModelResponse( - output=BaseModelOutput(content=response), - parsed_response=parsed_json, - ) - - def chat_stream( - self, - prompt: str, - history: list | None = None, - **kwargs, - ) -> Generator[str, None]: - """Return the next response in the list.""" - raise NotImplementedError - - -class MockEmbeddingLLM: - """A mock embedding LLM provider.""" - - def __init__(self, **kwargs: Any): - self.config = LanguageModelConfig( - type=ModelType.MockEmbedding, model="text-embedding-ada-002", api_key="mock" - ) - - def embed_batch(self, text_list: list[str], **kwargs: Any) -> list[list[float]]: - """Generate an embedding for the input text.""" - if isinstance(text_list, str): - return [[1.0, 1.0, 1.0]] - return [[1.0, 1.0, 1.0] for _ in text_list] - - def embed(self, text: str, **kwargs: Any) -> list[float]: - """Generate an embedding for the input text.""" - return [1.0, 1.0, 1.0] - - async def aembed(self, text: str, **kwargs: Any) -> list[float]: - """Generate an embedding for the input text.""" - return [1.0, 1.0, 1.0] - - async def aembed_batch( - self, text_list: list[str], **kwargs: Any - ) -> list[list[float]]: - """Generate an embedding for the input text.""" - if isinstance(text_list, str): - return [[1.0, 1.0, 1.0]] - return [[1.0, 1.0, 1.0] for _ in text_list] diff --git a/tests/notebook/test_notebooks.py b/tests/notebook/test_notebooks.py index 9f9d9b1222..47d759fcea 100644 --- a/tests/notebook/test_notebooks.py +++ b/tests/notebook/test_notebooks.py @@ -1,18 +1,36 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License import subprocess +from dataclasses import dataclass from pathlib import Path import nbformat import pytest -NOTEBOOKS_PATH = Path("examples_notebooks") -EXCLUDED_PATH = NOTEBOOKS_PATH / "community_contrib" + +@dataclass +class NotebookDetails: + dir: Path + excluded_filenames: list[str] + + +NOTEBOOKS: list[NotebookDetails] = [ + NotebookDetails( + dir=Path("packages/graphrag-llm/notebooks"), + excluded_filenames=[], + ), + # Was in previous test but not actually pointing at a notebooks location + # NotebookDetails( + # dir=Path("examples_notebooks"), # noqa: ERA001 + # excluded_filenames=["community_contrib"], # noqa: ERA001 + # ), +] notebooks_list = [ - notebook - for notebook in NOTEBOOKS_PATH.rglob("*.ipynb") - if EXCLUDED_PATH not in notebook.parents + nb + for details in NOTEBOOKS + for nb in details.dir.rglob("*.ipynb") + if nb.stem not in details.excluded_filenames ] @@ -21,6 +39,8 @@ def _notebook_run(filepath: Path): :returns execution errors """ args = [ + "uv", + "run", "jupyter", "nbconvert", "--to", @@ -29,7 +49,7 @@ def _notebook_run(filepath: Path): "-y", "--no-prompt", "--stdout", - str(filepath.absolute().resolve()), + str(filepath.resolve()), ] notebook = subprocess.check_output(args) nb = nbformat.reads(notebook, nbformat.current_nbformat) @@ -43,6 +63,18 @@ def _notebook_run(filepath: Path): ] +def clear_cache(): + cache_dir = Path("packages/graphrag-llm/notebooks/cache") + if cache_dir.exists(): + for file in cache_dir.iterdir(): + if file.is_file(): + file.unlink() + cache_dir.rmdir() + + +clear_cache() + + @pytest.mark.parametrize("notebook_path", notebooks_list) def test_notebook(notebook_path: Path): assert _notebook_run(notebook_path) == [] diff --git a/tests/smoke/test_fixtures.py b/tests/smoke/test_fixtures.py index 7a81bf2d16..7d43b0140a 100644 --- a/tests/smoke/test_fixtures.py +++ b/tests/smoke/test_fixtures.py @@ -14,11 +14,10 @@ import pandas as pd import pytest - from graphrag.query.context_builder.community_context import ( NO_COMMUNITY_RECORDS_WARNING, ) -from graphrag.storage.blob_pipeline_storage import BlobPipelineStorage +from graphrag_storage.azure_blob_storage import AzureBlobStorage logger = logging.getLogger(__name__) @@ -95,7 +94,7 @@ async def prepare_azurite_data(input_path: str, azure: dict) -> Callable[[], Non input_base_dir = azure.get("input_base_dir") root = Path(input_path) - input_storage = BlobPipelineStorage( + input_storage = AzureBlobStorage( connection_string=WELL_KNOWN_AZURITE_CONNECTION_STRING, container_name=input_container, ) @@ -127,7 +126,8 @@ class TestIndexer: def __run_indexer( self, root: Path, - input_file_type: str, + input_type: str, + index_method: str, ): command = [ "uv", @@ -138,7 +138,7 @@ def __run_indexer( "--root", root.resolve().as_posix(), "--method", - "standard", + index_method, ] command = [arg for arg in command if arg] logger.info("running command ", " ".join(command)) @@ -204,14 +204,13 @@ def __run_query(self, root: Path, query_config: dict[str, str]): "run", "poe", "query", + query_config["query"], "--root", root.resolve().as_posix(), "--method", query_config["method"], "--community-level", str(query_config.get("community_level", 2)), - "--query", - query_config["query"], ] logger.info("running command ", " ".join(command)) @@ -233,7 +232,8 @@ def __run_query(self, root: Path, query_config: dict[str, str]): def test_fixture( self, input_path: str, - input_file_type: str, + input_type: str, + index_method: str, workflow_config: dict[str, dict[str, Any]], query_config: list[dict[str, str]], ): @@ -248,7 +248,7 @@ def test_fixture( dispose = asyncio.run(prepare_azurite_data(input_path, azure)) print("running indexer") - self.__run_indexer(root, input_file_type) + self.__run_indexer(root, input_type, index_method) print("indexer complete") if dispose is not None: diff --git a/tests/unit/indexing/operations/__init__.py b/tests/unit/chunking/__init__.py similarity index 100% rename from tests/unit/indexing/operations/__init__.py rename to tests/unit/chunking/__init__.py diff --git a/tests/unit/chunking/test_chunker.py b/tests/unit/chunking/test_chunker.py new file mode 100644 index 0000000000..5addee34d2 --- /dev/null +++ b/tests/unit/chunking/test_chunker.py @@ -0,0 +1,190 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +from typing import Any +from unittest.mock import Mock, patch + +from graphrag.tokenizer.get_tokenizer import get_tokenizer +from graphrag_chunking.bootstrap_nltk import bootstrap +from graphrag_chunking.chunk_strategy_type import ChunkerType +from graphrag_chunking.chunker_factory import create_chunker +from graphrag_chunking.chunking_config import ChunkingConfig +from graphrag_chunking.token_chunker import ( + split_text_on_tokens, +) +from graphrag_llm.tokenizer import Tokenizer + + +class MockTokenizer(Tokenizer): + def __init__(self, **kwargs: Any) -> None: + """Initialize the LiteLLM Tokenizer.""" + + def encode(self, text) -> list[int]: + return [ord(char) for char in text] + + def decode(self, tokens) -> str: + return "".join(chr(id) for id in tokens) + + +class TestRunSentences: + def setup_method(self, method): + bootstrap() + + def test_basic_functionality(self): + """Test basic sentence splitting""" + input = "This is a test. Another sentence. And a third one!" + chunker = create_chunker(ChunkingConfig(type=ChunkerType.Sentence)) + chunks = chunker.chunk(input) + + assert len(chunks) == 3 + assert chunks[0].text == "This is a test." + assert chunks[0].index == 0 + assert chunks[0].start_char == 0 + assert chunks[0].end_char == 14 + + assert chunks[1].text == "Another sentence." + assert chunks[1].index == 1 + assert chunks[1].start_char == 16 + assert chunks[1].end_char == 32 + + assert chunks[2].text == "And a third one!" + assert chunks[2].index == 2 + assert chunks[2].start_char == 34 + assert chunks[2].end_char == 49 + + def test_mixed_whitespace_handling(self): + """Test input with irregular whitespace""" + input = " Sentence with spaces. Another one! " + chunker = create_chunker(ChunkingConfig(type=ChunkerType.Sentence)) + chunks = chunker.chunk(input) + + assert len(chunks) == 2 + assert chunks[0].text == "Sentence with spaces." + assert chunks[0].index == 0 + assert chunks[0].start_char == 3 + assert chunks[0].end_char == 23 + + assert chunks[1].text == "Another one!" + assert chunks[1].index == 1 + assert chunks[1].start_char == 25 + assert chunks[1].end_char == 36 + + +class TestRunTokens: + @patch("tiktoken.get_encoding") + def test_basic_functionality(self, mock_get_encoding): + mock_encoder = Mock() + mock_encoder.encode.side_effect = lambda x: list(x.encode()) + mock_encoder.decode.side_effect = lambda x: bytes(x).decode() + mock_get_encoding.return_value = mock_encoder + + input = "Marley was dead: to begin with. There is no doubt whatever about that. The register of his burial was signed by the clergyman, the clerk, the undertaker, and the chief mourner. Scrooge signed it. And Scrooge's name was good upon 'Change, for anything he chose to put his hand to." + + config = ChunkingConfig( + size=5, + overlap=1, + encoding_model="fake-encoding", + type=ChunkerType.Tokens, + ) + + chunker = create_chunker(config, mock_encoder.encode, mock_encoder.decode) + chunks = chunker.chunk(input) + + assert len(chunks) > 0 + + +def test_split_text_str_empty(): + tokenizer = get_tokenizer() + result = split_text_on_tokens( + "", + chunk_size=5, + chunk_overlap=2, + encode=tokenizer.encode, + decode=tokenizer.decode, + ) + assert result == [] + + +def test_split_text_on_tokens(): + text = "This is a test text, meaning to be taken seriously by this test only." + mocked_tokenizer = MockTokenizer() + expected_splits = [ + "This is a ", + "is a test ", + "test text,", + "text, mean", + " meaning t", + "ing to be ", + "o be taken", + "taken seri", # cspell:disable-line + " seriously", + "ously by t", # cspell:disable-line + " by this t", + "his test o", + "est only.", + ] + + result = split_text_on_tokens( + text=text, + chunk_overlap=5, + chunk_size=10, + decode=mocked_tokenizer.decode, + encode=lambda text: mocked_tokenizer.encode(text), + ) + assert result == expected_splits + + +def test_split_text_on_tokens_one_overlap(): + text = "This is a test text, meaning to be taken seriously by this test only." + tokenizer = get_tokenizer(encoding_model="o200k_base") + + expected_splits = [ + "This is", + " is a", + " a test", + " test text", + " text,", + ", meaning", + " meaning to", + " to be", + " be taken", + " taken seriously", + " seriously by", + " by this", + " this test", + " test only", + " only.", + ] + + result = split_text_on_tokens( + text=text, + chunk_size=2, + chunk_overlap=1, + decode=tokenizer.decode, + encode=tokenizer.encode, + ) + assert result == expected_splits + + +def test_split_text_on_tokens_no_overlap(): + text = "This is a test text, meaning to be taken seriously by this test only." + tokenizer = get_tokenizer(encoding_model="o200k_base") + + expected_splits = [ + "This is a", + " test text,", + " meaning to be", + " taken seriously by", + " this test only", + ".", + ] + + result = split_text_on_tokens( + text=text, + chunk_size=3, + chunk_overlap=0, + decode=tokenizer.decode, + encode=tokenizer.encode, + ) + + assert result == expected_splits diff --git a/tests/unit/chunking/test_prepend_metadata.py b/tests/unit/chunking/test_prepend_metadata.py new file mode 100644 index 0000000000..74f5a698e5 --- /dev/null +++ b/tests/unit/chunking/test_prepend_metadata.py @@ -0,0 +1,44 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +from graphrag_chunking.transformers import add_metadata + + +def test_add_metadata_one_row(): + """Test prepending metadata to chunks""" + chunks = ["This is a test.", "Another sentence."] + metadata = {"message": "hello"} + transformer = add_metadata(metadata) + results = [transformer(chunk) for chunk in chunks] + assert results[0] == "message: hello\nThis is a test." + assert results[1] == "message: hello\nAnother sentence." + + +def test_add_metadata_one_row_append(): + """Test prepending metadata to chunks""" + chunks = ["This is a test.", "Another sentence."] + metadata = {"message": "hello"} + transformer = add_metadata(metadata, append=True) + results = [transformer(chunk) for chunk in chunks] + assert results[0] == "This is a test.message: hello\n" + assert results[1] == "Another sentence.message: hello\n" + + +def test_add_metadata_multiple_rows(): + """Test prepending metadata to chunks""" + chunks = ["This is a test.", "Another sentence."] + metadata = {"message": "hello", "tag": "first"} + transformer = add_metadata(metadata) + results = [transformer(chunk) for chunk in chunks] + assert results[0] == "message: hello\ntag: first\nThis is a test." + assert results[1] == "message: hello\ntag: first\nAnother sentence." + + +def test_add_metadata_custom_delimiters(): + """Test prepending metadata to chunks""" + chunks = ["This is a test.", "Another sentence."] + metadata = {"message": "hello", "tag": "first"} + transformer = add_metadata(metadata, delimiter="-", line_delimiter="_") + results = [transformer(chunk) for chunk in chunks] + assert results[0] == "message-hello_tag-first_This is a test." + assert results[1] == "message-hello_tag-first_Another sentence." diff --git a/tests/unit/config/fixtures/minimal_config/settings.yaml b/tests/unit/config/fixtures/minimal_config/settings.yaml index 82183a4552..049c2fa131 100644 --- a/tests/unit/config/fixtures/minimal_config/settings.yaml +++ b/tests/unit/config/fixtures/minimal_config/settings.yaml @@ -1,11 +1,11 @@ -models: - default_chat_model: +completion_models: + default_completion_model: api_key: ${CUSTOM_API_KEY} - type: chat model_provider: openai - model: gpt-4-turbo-preview + model: gpt-4.1 + +embedding_models: default_embedding_model: api_key: ${CUSTOM_API_KEY} - type: embedding model_provider: openai - model: text-embedding-3-small \ No newline at end of file + model: text-embedding-3-large diff --git a/tests/unit/config/fixtures/minimal_config_missing_env_var/settings.yaml b/tests/unit/config/fixtures/minimal_config_missing_env_var/settings.yaml index 651b997a5c..ab370e4ab6 100644 --- a/tests/unit/config/fixtures/minimal_config_missing_env_var/settings.yaml +++ b/tests/unit/config/fixtures/minimal_config_missing_env_var/settings.yaml @@ -1,11 +1,11 @@ -models: - default_chat_model: +completion_models: + default_completion_model: api_key: ${SOME_NON_EXISTENT_ENV_VAR} - type: chat model_provider: openai - model: gpt-4-turbo-preview + model: gpt-4.1 + +embedding_models: default_embedding_model: api_key: ${SOME_NON_EXISTENT_ENV_VAR} - type: embedding model_provider: openai - model: text-embedding-3-small \ No newline at end of file + model: text-embedding-3-large diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index a741de7c40..75a472f023 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -5,166 +5,56 @@ from pathlib import Path from unittest import mock -import pytest -from pydantic import ValidationError - -import graphrag.config.defaults as defs -from graphrag.config.create_graphrag_config import create_graphrag_config -from graphrag.config.enums import AuthType, ModelType from graphrag.config.load_config import load_config +from graphrag.config.models.graph_rag_config import GraphRagConfig + from tests.unit.config.utils import ( - DEFAULT_EMBEDDING_MODEL_CONFIG, - DEFAULT_MODEL_CONFIG, + DEFAULT_COMPLETION_MODELS, + DEFAULT_EMBEDDING_MODELS, FAKE_API_KEY, assert_graphrag_configs, get_default_graphrag_config, ) -def test_missing_openai_required_api_key() -> None: - model_config_missing_api_key = { - defs.DEFAULT_CHAT_MODEL_ID: { - "type": ModelType.OpenAIChat, - "model": defs.DEFAULT_CHAT_MODEL, - }, - defs.DEFAULT_EMBEDDING_MODEL_ID: DEFAULT_EMBEDDING_MODEL_CONFIG, - } - - # API Key required for OpenAIChat - with pytest.raises(ValidationError): - create_graphrag_config({"models": model_config_missing_api_key}) - - # API Key required for OpenAIEmbedding - model_config_missing_api_key[defs.DEFAULT_CHAT_MODEL_ID]["type"] = ( - ModelType.OpenAIEmbedding - ) - with pytest.raises(ValidationError): - create_graphrag_config({"models": model_config_missing_api_key}) - - -def test_missing_azure_api_key() -> None: - model_config_missing_api_key = { - defs.DEFAULT_CHAT_MODEL_ID: { - "type": ModelType.AzureOpenAIChat, - "auth_type": AuthType.APIKey, - "model": defs.DEFAULT_CHAT_MODEL, - "api_base": "some_api_base", - "api_version": "some_api_version", - "deployment_name": "some_deployment_name", - }, - defs.DEFAULT_EMBEDDING_MODEL_ID: DEFAULT_EMBEDDING_MODEL_CONFIG, - } - - with pytest.raises(ValidationError): - create_graphrag_config({"models": model_config_missing_api_key}) - - # API Key not required for managed identity - model_config_missing_api_key[defs.DEFAULT_CHAT_MODEL_ID]["auth_type"] = ( - AuthType.AzureManagedIdentity - ) - create_graphrag_config({"models": model_config_missing_api_key}) - - -def test_conflicting_auth_type() -> None: - model_config_invalid_auth_type = { - defs.DEFAULT_CHAT_MODEL_ID: { - "auth_type": AuthType.AzureManagedIdentity, - "type": ModelType.OpenAIChat, - "model": defs.DEFAULT_CHAT_MODEL, - }, - defs.DEFAULT_EMBEDDING_MODEL_ID: DEFAULT_EMBEDDING_MODEL_CONFIG, - } - - with pytest.raises(ValidationError): - create_graphrag_config({"models": model_config_invalid_auth_type}) - - -def test_conflicting_azure_api_key() -> None: - model_config_conflicting_api_key = { - defs.DEFAULT_CHAT_MODEL_ID: { - "type": ModelType.AzureOpenAIChat, - "auth_type": AuthType.AzureManagedIdentity, - "model": defs.DEFAULT_CHAT_MODEL, - "api_base": "some_api_base", - "api_version": "some_api_version", - "deployment_name": "some_deployment_name", - "api_key": "THIS_SHOULD_NOT_BE_SET_WHEN_USING_MANAGED_IDENTITY", - }, - defs.DEFAULT_EMBEDDING_MODEL_ID: DEFAULT_EMBEDDING_MODEL_CONFIG, - } - - with pytest.raises(ValidationError): - create_graphrag_config({"models": model_config_conflicting_api_key}) - - -base_azure_model_config = { - "type": ModelType.AzureOpenAIChat, - "auth_type": AuthType.AzureManagedIdentity, - "model": defs.DEFAULT_CHAT_MODEL, - "api_base": "some_api_base", - "api_version": "some_api_version", - "deployment_name": "some_deployment_name", -} - - -def test_missing_azure_api_base() -> None: - missing_api_base_config = base_azure_model_config.copy() - del missing_api_base_config["api_base"] - - with pytest.raises(ValidationError): - create_graphrag_config({ - "models": { - defs.DEFAULT_CHAT_MODEL_ID: missing_api_base_config, - defs.DEFAULT_EMBEDDING_MODEL_ID: DEFAULT_EMBEDDING_MODEL_CONFIG, - } - }) - - -def test_missing_azure_api_version() -> None: - missing_api_version_config = base_azure_model_config.copy() - del missing_api_version_config["api_version"] - - with pytest.raises(ValidationError): - create_graphrag_config({ - "models": { - defs.DEFAULT_CHAT_MODEL_ID: missing_api_version_config, - defs.DEFAULT_EMBEDDING_MODEL_ID: DEFAULT_EMBEDDING_MODEL_CONFIG, - } - }) - - def test_default_config() -> None: expected = get_default_graphrag_config() - actual = create_graphrag_config({"models": DEFAULT_MODEL_CONFIG}) + actual = GraphRagConfig( + completion_models=DEFAULT_COMPLETION_MODELS, # type: ignore + embedding_models=DEFAULT_EMBEDDING_MODELS, # type: ignore + ) assert_graphrag_configs(actual, expected) @mock.patch.dict(os.environ, {"CUSTOM_API_KEY": FAKE_API_KEY}, clear=True) def test_load_minimal_config() -> None: - cwd = Path(__file__).parent - root_dir = (cwd / "fixtures" / "minimal_config").resolve() - expected = get_default_graphrag_config(str(root_dir)) - actual = load_config(root_dir=root_dir) + cwd = Path.cwd() + root_dir = (Path(__file__).parent / "fixtures" / "minimal_config").resolve() + os.chdir(root_dir) + expected = get_default_graphrag_config() + + actual = load_config( + root_dir=root_dir, + ) assert_graphrag_configs(actual, expected) + # Need to reset cwd after test + os.chdir(cwd) @mock.patch.dict(os.environ, {"CUSTOM_API_KEY": FAKE_API_KEY}, clear=True) def test_load_config_with_cli_overrides() -> None: - cwd = Path(__file__).parent - root_dir = (cwd / "fixtures" / "minimal_config").resolve() + cwd = Path.cwd() + root_dir = (Path(__file__).parent / "fixtures" / "minimal_config").resolve() + os.chdir(root_dir) output_dir = "some_output_dir" expected_output_base_dir = root_dir / output_dir - expected = get_default_graphrag_config(str(root_dir)) - expected.output.base_dir = str(expected_output_base_dir) + expected = get_default_graphrag_config() + expected.output_storage.base_dir = str(expected_output_base_dir) + actual = load_config( root_dir=root_dir, - cli_overrides={"output.base_dir": output_dir}, + cli_overrides={"output_storage": {"base_dir": output_dir}}, ) assert_graphrag_configs(actual, expected) - - -def test_load_config_missing_env_vars() -> None: - cwd = Path(__file__).parent - root_dir = (cwd / "fixtures" / "minimal_config_missing_env_var").resolve() - with pytest.raises(KeyError): - load_config(root_dir=root_dir) + # Need to reset cwd after test + os.chdir(cwd) diff --git a/tests/unit/config/test_metrics_config.py b/tests/unit/config/test_metrics_config.py new file mode 100644 index 0000000000..967389c252 --- /dev/null +++ b/tests/unit/config/test_metrics_config.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Test metrics configuration loading.""" + +import pytest +from graphrag_llm.config import ( + MetricsConfig, + MetricsWriterType, +) + + +def test_file_metrics_writer_validation() -> None: + """Test that missing required parameters raise validation errors.""" + + with pytest.raises( + ValueError, + match="base_dir must be specified for file-based metrics writer\\.", + ): + _ = MetricsConfig( + writer=MetricsWriterType.File, + base_dir=" ", + ) + + # passes validation + _ = MetricsConfig( + writer=MetricsWriterType.File, + base_dir="./metrics", + ) diff --git a/tests/unit/config/test_model_config.py b/tests/unit/config/test_model_config.py new file mode 100644 index 0000000000..f2e7aec8e4 --- /dev/null +++ b/tests/unit/config/test_model_config.py @@ -0,0 +1,110 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Test model configuration loading.""" + +import pytest +from graphrag_llm.config import AuthMethod, LLMProviderType, ModelConfig +from pydantic import ValidationError + + +def test_litellm_provider_validation() -> None: + """Test that missing required parameters raise validation errors.""" + + with pytest.raises(ValidationError): + _ = ModelConfig( + type=LLMProviderType.LiteLLM, + model_provider="openai", + model="", + ) + + with pytest.raises(ValidationError): + _ = ModelConfig( + type=LLMProviderType.LiteLLM, + model_provider="", + model="gpt-4o", + ) + + with pytest.raises( + ValueError, + match="api_key must be set when auth_method=api_key\\.", + ): + _ = ModelConfig( + type=LLMProviderType.LiteLLM, + model_provider="openai", + model="gpt-4o", + ) + + with pytest.raises( + ValueError, + match="azure_deployment_name should not be specified for non-Azure model providers\\.", + ): + _ = ModelConfig( + type=LLMProviderType.LiteLLM, + model_provider="openai", + model="gpt-4o", + azure_deployment_name="some-deployment", + ) + + with pytest.raises( + ValueError, + match="api_base must be specified with the 'azure' model provider\\.", + ): + _ = ModelConfig( + type=LLMProviderType.LiteLLM, + model_provider="azure", + model="gpt-4o", + ) + + with pytest.raises( + ValueError, + match="api_key should not be set when using Azure Managed Identity\\.", + ): + _ = ModelConfig( + type=LLMProviderType.LiteLLM, + model_provider="azure", + model="gpt-4o", + azure_deployment_name="gpt-4o", + api_base="https://my-azure-endpoint/", + api_version="2024-06-01", + auth_method=AuthMethod.AzureManagedIdentity, + api_key="some-api-key", + ) + + with pytest.raises( + ValueError, + match="api_key must be set when auth_method=api_key\\.", + ): + _ = ModelConfig( + type=LLMProviderType.LiteLLM, + model_provider="azure", + azure_deployment_name="gpt-4o", + api_base="https://my-azure-endpoint/", + api_version="2024-06-01", + model="gpt-4o", + ) + + # pass validation + _ = ModelConfig( + type=LLMProviderType.LiteLLM, + model_provider="openai", + model="gpt-4o", + api_key="NOT_A_REAL_API_KEY", + ) + _ = ModelConfig( + type=LLMProviderType.LiteLLM, + model_provider="azure", + model="gpt-4o", + azure_deployment_name="gpt-4o", + api_base="https://my-azure-endpoint/", + api_key="NOT_A_REAL_API_KEY", + ) + _ = ModelConfig( + type=LLMProviderType.LiteLLM, + model_provider="azure", + model="gpt-4o", + azure_deployment_name="gpt-4o", + api_base="https://my-azure-endpoint/", + api_version="2024-06-01", + auth_method=AuthMethod.AzureManagedIdentity, + ) diff --git a/tests/unit/config/test_rate_limit_config.py b/tests/unit/config/test_rate_limit_config.py new file mode 100644 index 0000000000..938e1d4d4a --- /dev/null +++ b/tests/unit/config/test_rate_limit_config.py @@ -0,0 +1,66 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Test rate limit configuration loading.""" + +import pytest +from graphrag_llm.config import RateLimitConfig, RateLimitType + + +def test_sliding_window_validation() -> None: + """Test that missing required parameters raise validation errors.""" + + with pytest.raises( + ValueError, + match="period_in_seconds must be a positive integer for Sliding Window rate limit\\.", + ): + _ = RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=0, + requests_per_period=100, + tokens_per_period=1000, + ) + + with pytest.raises( + ValueError, + match="At least one of requests_per_period or tokens_per_period must be specified for Sliding Window rate limit\\.", + ): + _ = RateLimitConfig( + type=RateLimitType.SlidingWindow, + ) + + with pytest.raises( + ValueError, + match="requests_per_period must be a positive integer for Sliding Window rate limit\\.", + ): + _ = RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=60, + requests_per_period=-10, + ) + + with pytest.raises( + ValueError, + match="tokens_per_period must be a positive integer for Sliding Window rate limit\\.", + ): + _ = RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=60, + tokens_per_period=-10, + ) + + # passes validation + _ = RateLimitConfig( + type=RateLimitType.SlidingWindow, + requests_per_period=100, + ) + _ = RateLimitConfig( + type=RateLimitType.SlidingWindow, + tokens_per_period=1000, + ) + _ = RateLimitConfig( + type=RateLimitType.SlidingWindow, + period_in_seconds=60, + requests_per_period=100, + tokens_per_period=1000, + ) diff --git a/tests/unit/config/test_retry_config.py b/tests/unit/config/test_retry_config.py new file mode 100644 index 0000000000..2c3aff8375 --- /dev/null +++ b/tests/unit/config/test_retry_config.py @@ -0,0 +1,67 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Test retry configuration loading.""" + +import pytest +from graphrag_llm.config import RetryConfig, RetryType + + +def test_exponential_backoff_validation() -> None: + """Test that missing required parameters raise validation errors.""" + + with pytest.raises( + ValueError, + match="max_retries must be greater than 1 for Exponential Backoff retry\\.", + ): + _ = RetryConfig( + type=RetryType.ExponentialBackoff, + max_retries=0, + ) + + with pytest.raises( + ValueError, + match="base_delay must be greater than 1\\.0 for Exponential Backoff retry\\.", + ): + _ = RetryConfig( + type=RetryType.ExponentialBackoff, + base_delay=0.5, + ) + + with pytest.raises( + ValueError, + match="max_delay must be greater than 1 for Exponential Backoff retry\\.", + ): + _ = RetryConfig( + type=RetryType.ExponentialBackoff, + max_delay=0.5, + ) + + # passes validation + _ = RetryConfig(type=RetryType.ExponentialBackoff) + _ = RetryConfig( + type=RetryType.ExponentialBackoff, + max_retries=5, + base_delay=2.0, + max_delay=30, + ) + + +def test_immediate_validation() -> None: + """Test that missing required parameters raise validation errors.""" + + with pytest.raises( + ValueError, + match="max_retries must be greater than 1 for Immediate retry\\.", + ): + _ = RetryConfig( + type=RetryType.Immediate, + max_retries=0, + ) + + # passes validation + _ = RetryConfig(type=RetryType.Immediate) + _ = RetryConfig( + type=RetryType.Immediate, + max_retries=3, + ) diff --git a/tests/unit/config/test_template_engine_config.py b/tests/unit/config/test_template_engine_config.py new file mode 100644 index 0000000000..26aa01b895 --- /dev/null +++ b/tests/unit/config/test_template_engine_config.py @@ -0,0 +1,44 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Test metrics configuration loading.""" + +import pytest +from graphrag_llm.config import ( + TemplateEngineConfig, + TemplateEngineType, + TemplateManagerType, +) + + +def test_template_engine_config_validation() -> None: + """Test that missing required parameters raise validation errors.""" + + with pytest.raises( + ValueError, + match="base_dir must be specified for file-based template managers\\.", + ): + _ = TemplateEngineConfig( + type=TemplateEngineType.Jinja, + template_manager=TemplateManagerType.File, + base_dir=" ", + ) + + with pytest.raises( + ValueError, + match="template_extension cannot be an empty string for file-based template managers\\.", + ): + _ = TemplateEngineConfig( + type=TemplateEngineType.Jinja, + template_manager=TemplateManagerType.File, + base_dir="./templates", + template_extension=" ", + ) + + # passes validation + _ = TemplateEngineConfig( + type=TemplateEngineType.Jinja, + template_manager=TemplateManagerType.File, + base_dir="./templates", + template_extension=".jinja", + ) diff --git a/tests/unit/config/test_tokenizer_config.py b/tests/unit/config/test_tokenizer_config.py new file mode 100644 index 0000000000..62b7f6ebf6 --- /dev/null +++ b/tests/unit/config/test_tokenizer_config.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Test tokenizer configuration loading.""" + +import pytest +from graphrag_llm.config import TokenizerConfig, TokenizerType + + +def test_litellm_tokenizer_validation() -> None: + """Test that missing required parameters raise validation errors.""" + + with pytest.raises( + ValueError, + match="model_id must be specified for LiteLLM tokenizer\\.", + ): + _ = TokenizerConfig( + type=TokenizerType.LiteLLM, + model_id="", + ) + + with pytest.raises( + ValueError, + match="encoding_name must be specified for TikToken tokenizer\\.", + ): + _ = TokenizerConfig( + type=TokenizerType.Tiktoken, + encoding_name="", + ) + + # passes validation + _ = TokenizerConfig( + type=TokenizerType.LiteLLM, + model_id="openai/gpt-4o", + ) + _ = TokenizerConfig( + type=TokenizerType.Tiktoken, + encoding_name="o200k-base", + ) diff --git a/tests/unit/config/utils.py b/tests/unit/config/utils.py index ab988a50ae..ae898d4568 100644 --- a/tests/unit/config/utils.py +++ b/tests/unit/config/utils.py @@ -3,17 +3,13 @@ from dataclasses import asdict -from pydantic import BaseModel - import graphrag.config.defaults as defs from graphrag.config.models.basic_search_config import BasicSearchConfig -from graphrag.config.models.cache_config import CacheConfig -from graphrag.config.models.chunking_config import ChunkingConfig from graphrag.config.models.cluster_graph_config import ClusterGraphConfig from graphrag.config.models.community_reports_config import CommunityReportsConfig from graphrag.config.models.drift_search_config import DRIFTSearchConfig -from graphrag.config.models.embed_graph_config import EmbedGraphConfig -from graphrag.config.models.extract_claims_config import ClaimExtractionConfig +from graphrag.config.models.embed_text_config import EmbedTextConfig +from graphrag.config.models.extract_claims_config import ExtractClaimsConfig from graphrag.config.models.extract_graph_config import ExtractGraphConfig from graphrag.config.models.extract_graph_nlp_config import ( ExtractGraphNLPConfig, @@ -21,109 +17,112 @@ ) from graphrag.config.models.global_search_config import GlobalSearchConfig from graphrag.config.models.graph_rag_config import GraphRagConfig -from graphrag.config.models.input_config import InputConfig -from graphrag.config.models.language_model_config import LanguageModelConfig from graphrag.config.models.local_search_config import LocalSearchConfig from graphrag.config.models.prune_graph_config import PruneGraphConfig from graphrag.config.models.reporting_config import ReportingConfig from graphrag.config.models.snapshots_config import SnapshotsConfig -from graphrag.config.models.storage_config import StorageConfig from graphrag.config.models.summarize_descriptions_config import ( SummarizeDescriptionsConfig, ) -from graphrag.config.models.text_embedding_config import TextEmbeddingConfig -from graphrag.config.models.umap_config import UmapConfig -from graphrag.config.models.vector_store_config import VectorStoreConfig +from graphrag_cache import CacheConfig +from graphrag_chunking.chunking_config import ChunkingConfig +from graphrag_input import InputConfig +from graphrag_llm.config import MetricsConfig, ModelConfig, RateLimitConfig, RetryConfig +from graphrag_storage import StorageConfig +from graphrag_vectors import VectorStoreConfig FAKE_API_KEY = "NOT_AN_API_KEY" -DEFAULT_CHAT_MODEL_CONFIG = { +DEFAULT_COMPLETION_MODEL_CONFIG = { "api_key": FAKE_API_KEY, - "type": defs.DEFAULT_CHAT_MODEL_TYPE.value, - "model": defs.DEFAULT_CHAT_MODEL, + "model": defs.DEFAULT_COMPLETION_MODEL, "model_provider": defs.DEFAULT_MODEL_PROVIDER, } DEFAULT_EMBEDDING_MODEL_CONFIG = { "api_key": FAKE_API_KEY, - "type": defs.DEFAULT_EMBEDDING_MODEL_TYPE.value, "model": defs.DEFAULT_EMBEDDING_MODEL, "model_provider": defs.DEFAULT_MODEL_PROVIDER, } -DEFAULT_MODEL_CONFIG = { - defs.DEFAULT_CHAT_MODEL_ID: DEFAULT_CHAT_MODEL_CONFIG, +DEFAULT_COMPLETION_MODELS = { + defs.DEFAULT_COMPLETION_MODEL_ID: DEFAULT_COMPLETION_MODEL_CONFIG, +} + +DEFAULT_EMBEDDING_MODELS = { defs.DEFAULT_EMBEDDING_MODEL_ID: DEFAULT_EMBEDDING_MODEL_CONFIG, } -def get_default_graphrag_config(root_dir: str | None = None) -> GraphRagConfig: +def get_default_graphrag_config() -> GraphRagConfig: return GraphRagConfig(**{ **asdict(defs.graphrag_config_defaults), - "models": DEFAULT_MODEL_CONFIG, - **({"root_dir": root_dir} if root_dir else {}), + "completion_models": DEFAULT_COMPLETION_MODELS, + "embedding_models": DEFAULT_EMBEDDING_MODELS, }) -def assert_language_model_configs( - actual: LanguageModelConfig, expected: LanguageModelConfig +def assert_retry_configs(actual: RetryConfig, expected: RetryConfig) -> None: + assert actual.type == expected.type + assert actual.max_retries == expected.max_retries + assert actual.base_delay == expected.base_delay + assert actual.jitter == expected.jitter + assert actual.max_delay == expected.max_delay + + +def assert_rate_limit_configs( + actual: RateLimitConfig, expected: RateLimitConfig ) -> None: - assert actual.api_key == expected.api_key - assert actual.auth_type == expected.auth_type assert actual.type == expected.type + assert actual.period_in_seconds == expected.period_in_seconds + assert actual.requests_per_period == expected.requests_per_period + assert actual.tokens_per_period == expected.tokens_per_period + + +def assert_metrics_configs(actual: MetricsConfig, expected: MetricsConfig) -> None: + assert actual.type == expected.type + assert actual.store == expected.store + assert actual.writer == expected.writer + assert actual.log_level == expected.log_level + assert actual.base_dir == expected.base_dir + + +def assert_model_configs(actual: ModelConfig, expected: ModelConfig) -> None: + assert actual.type == expected.type + assert actual.model_provider == expected.model_provider assert actual.model == expected.model - assert actual.encoding_model == expected.encoding_model - assert actual.max_tokens == expected.max_tokens - assert actual.temperature == expected.temperature - assert actual.max_completion_tokens == expected.max_completion_tokens - assert actual.top_p == expected.top_p - assert actual.n == expected.n - assert actual.frequency_penalty == expected.frequency_penalty - assert actual.presence_penalty == expected.presence_penalty - assert actual.request_timeout == expected.request_timeout + assert actual.call_args == expected.call_args assert actual.api_base == expected.api_base assert actual.api_version == expected.api_version - assert actual.deployment_name == expected.deployment_name - assert actual.organization == expected.organization - assert actual.proxy == expected.proxy - assert actual.audience == expected.audience - assert actual.model_supports_json == expected.model_supports_json - assert actual.tokens_per_minute == expected.tokens_per_minute - assert actual.requests_per_minute == expected.requests_per_minute - assert actual.retry_strategy == expected.retry_strategy - assert actual.max_retries == expected.max_retries - assert actual.max_retry_wait == expected.max_retry_wait - assert actual.concurrent_requests == expected.concurrent_requests - assert actual.async_mode == expected.async_mode - if actual.responses is not None: - assert expected.responses is not None - assert len(actual.responses) == len(expected.responses) - for e, a in zip(actual.responses, expected.responses, strict=True): - assert isinstance(e, BaseModel) - assert isinstance(a, BaseModel) - assert e.model_dump() == a.model_dump() + assert actual.api_key == expected.api_key + assert actual.auth_method == expected.auth_method + assert actual.azure_deployment_name == expected.azure_deployment_name + if actual.retry and expected.retry: + assert_retry_configs(actual.retry, expected.retry) else: - assert expected.responses is None + assert actual.retry == expected.retry + if actual.rate_limit and expected.rate_limit: + assert_rate_limit_configs(actual.rate_limit, expected.rate_limit) + else: + assert actual.rate_limit == expected.rate_limit + if actual.metrics and expected.metrics: + assert_metrics_configs(actual.metrics, expected.metrics) + else: + assert actual.metrics == expected.metrics + assert actual.mock_responses == expected.mock_responses def assert_vector_store_configs( - actual: dict[str, VectorStoreConfig], - expected: dict[str, VectorStoreConfig], + actual: VectorStoreConfig, + expected: VectorStoreConfig, ): assert type(actual) is type(expected) - assert len(actual) == len(expected) - for (index_a, store_a), (index_e, store_e) in zip( - actual.items(), expected.items(), strict=True - ): - assert index_a == index_e - assert store_a.type == store_e.type - assert store_a.db_uri == store_e.db_uri - assert store_a.url == store_e.url - assert store_a.api_key == store_e.api_key - assert store_a.audience == store_e.audience - assert store_a.container_name == store_e.container_name - assert store_a.overwrite == store_e.overwrite - assert store_a.database_name == store_e.database_name + assert actual.type == expected.type + assert actual.db_uri == expected.db_uri + assert actual.url == expected.url + assert actual.api_key == expected.api_key + assert actual.audience == expected.audience + assert actual.database_name == expected.database_name def assert_reporting_configs( @@ -133,88 +132,48 @@ def assert_reporting_configs( assert actual.base_dir == expected.base_dir assert actual.connection_string == expected.connection_string assert actual.container_name == expected.container_name - assert actual.storage_account_blob_url == expected.storage_account_blob_url - + assert actual.account_url == expected.account_url -def assert_output_configs(actual: StorageConfig, expected: StorageConfig) -> None: - assert expected.type == actual.type - assert expected.base_dir == actual.base_dir - assert expected.connection_string == actual.connection_string - assert expected.container_name == actual.container_name - assert expected.storage_account_blob_url == actual.storage_account_blob_url - assert expected.cosmosdb_account_url == actual.cosmosdb_account_url - -def assert_update_output_configs( - actual: StorageConfig, expected: StorageConfig -) -> None: +def assert_storage_config(actual: StorageConfig, expected: StorageConfig) -> None: assert expected.type == actual.type assert expected.base_dir == actual.base_dir assert expected.connection_string == actual.connection_string assert expected.container_name == actual.container_name - assert expected.storage_account_blob_url == actual.storage_account_blob_url - assert expected.cosmosdb_account_url == actual.cosmosdb_account_url + assert expected.account_url == actual.account_url + assert expected.encoding == actual.encoding + assert expected.database_name == actual.database_name def assert_cache_configs(actual: CacheConfig, expected: CacheConfig) -> None: assert actual.type == expected.type - assert actual.base_dir == expected.base_dir - assert actual.connection_string == expected.connection_string - assert actual.container_name == expected.container_name - assert actual.storage_account_blob_url == expected.storage_account_blob_url - assert actual.cosmosdb_account_url == expected.cosmosdb_account_url + if actual.storage and expected.storage: + assert_storage_config(actual.storage, expected.storage) def assert_input_configs(actual: InputConfig, expected: InputConfig) -> None: - assert actual.storage.type == expected.storage.type - assert actual.file_type == expected.file_type - assert actual.storage.base_dir == expected.storage.base_dir - assert actual.storage.connection_string == expected.storage.connection_string - assert ( - actual.storage.storage_account_blob_url - == expected.storage.storage_account_blob_url - ) - assert actual.storage.container_name == expected.storage.container_name + assert actual.type == expected.type assert actual.encoding == expected.encoding assert actual.file_pattern == expected.file_pattern - assert actual.file_filter == expected.file_filter assert actual.text_column == expected.text_column assert actual.title_column == expected.title_column - assert actual.metadata == expected.metadata - - -def assert_embed_graph_configs( - actual: EmbedGraphConfig, expected: EmbedGraphConfig -) -> None: - assert actual.enabled == expected.enabled - assert actual.dimensions == expected.dimensions - assert actual.num_walks == expected.num_walks - assert actual.walk_length == expected.walk_length - assert actual.window_size == expected.window_size - assert actual.iterations == expected.iterations - assert actual.random_seed == expected.random_seed - assert actual.use_lcc == expected.use_lcc def assert_text_embedding_configs( - actual: TextEmbeddingConfig, expected: TextEmbeddingConfig + actual: EmbedTextConfig, expected: EmbedTextConfig ) -> None: assert actual.batch_size == expected.batch_size assert actual.batch_max_tokens == expected.batch_max_tokens assert actual.names == expected.names - assert actual.strategy == expected.strategy - assert actual.model_id == expected.model_id - assert actual.vector_store_id == expected.vector_store_id + assert actual.embedding_model_id == expected.embedding_model_id def assert_chunking_configs(actual: ChunkingConfig, expected: ChunkingConfig) -> None: assert actual.size == expected.size assert actual.overlap == expected.overlap - assert actual.group_by_columns == expected.group_by_columns - assert actual.strategy == expected.strategy + assert actual.type == expected.type assert actual.encoding_model == expected.encoding_model assert actual.prepend_metadata == expected.prepend_metadata - assert actual.chunk_size_includes_metadata == expected.chunk_size_includes_metadata def assert_snapshots_configs( @@ -230,8 +189,7 @@ def assert_extract_graph_configs( assert actual.prompt == expected.prompt assert actual.entity_types == expected.entity_types assert actual.max_gleanings == expected.max_gleanings - assert actual.strategy == expected.strategy - assert actual.model_id == expected.model_id + assert actual.completion_model_id == expected.completion_model_id def assert_text_analyzer_configs( @@ -274,8 +232,7 @@ def assert_summarize_descriptions_configs( ) -> None: assert actual.prompt == expected.prompt assert actual.max_length == expected.max_length - assert actual.strategy == expected.strategy - assert actual.model_id == expected.model_id + assert actual.completion_model_id == expected.completion_model_id def assert_community_reports_configs( @@ -285,19 +242,17 @@ def assert_community_reports_configs( assert actual.text_prompt == expected.text_prompt assert actual.max_length == expected.max_length assert actual.max_input_length == expected.max_input_length - assert actual.strategy == expected.strategy - assert actual.model_id == expected.model_id + assert actual.completion_model_id == expected.completion_model_id def assert_extract_claims_configs( - actual: ClaimExtractionConfig, expected: ClaimExtractionConfig + actual: ExtractClaimsConfig, expected: ExtractClaimsConfig ) -> None: assert actual.enabled == expected.enabled assert actual.prompt == expected.prompt assert actual.description == expected.description assert actual.max_gleanings == expected.max_gleanings - assert actual.strategy == expected.strategy - assert actual.model_id == expected.model_id + assert actual.completion_model_id == expected.completion_model_id def assert_cluster_graph_configs( @@ -308,10 +263,6 @@ def assert_cluster_graph_configs( assert actual.seed == expected.seed -def assert_umap_configs(actual: UmapConfig, expected: UmapConfig) -> None: - assert actual.enabled == expected.enabled - - def assert_local_search_configs( actual: LocalSearchConfig, expected: LocalSearchConfig ) -> None: @@ -384,36 +335,30 @@ def assert_basic_search_configs( def assert_graphrag_configs(actual: GraphRagConfig, expected: GraphRagConfig) -> None: - assert actual.root_dir == expected.root_dir + completion_keys = sorted(actual.completion_models.keys()) + expected_completion_keys = sorted(expected.completion_models.keys()) + assert len(completion_keys) == len(expected_completion_keys) + for a, e in zip(completion_keys, expected_completion_keys, strict=False): + assert a == e + assert_model_configs(actual.completion_models[a], expected.completion_models[e]) - a_keys = sorted(actual.models.keys()) - e_keys = sorted(expected.models.keys()) - assert len(a_keys) == len(e_keys) - for a, e in zip(a_keys, e_keys, strict=False): + embedding_keys = sorted(actual.embedding_models.keys()) + expected_embedding_keys = sorted(expected.embedding_models.keys()) + assert len(embedding_keys) == len(expected_embedding_keys) + for a, e in zip(embedding_keys, expected_embedding_keys, strict=False): assert a == e - assert_language_model_configs(actual.models[a], expected.models[e]) + assert_model_configs(actual.embedding_models[a], expected.embedding_models[e]) assert_vector_store_configs(actual.vector_store, expected.vector_store) assert_reporting_configs(actual.reporting, expected.reporting) - assert_output_configs(actual.output, expected.output) - - if expected.outputs is not None: - assert actual.outputs is not None - assert len(actual.outputs) == len(expected.outputs) - for a, e in zip(actual.outputs.keys(), expected.outputs.keys(), strict=True): - assert_output_configs(actual.outputs[a], expected.outputs[e]) - else: - assert actual.outputs is None - - assert_update_output_configs( - actual.update_index_output, expected.update_index_output - ) + assert_storage_config(actual.output_storage, expected.output_storage) + assert_storage_config(actual.input_storage, expected.input_storage) + assert_storage_config(actual.update_output_storage, expected.update_output_storage) assert_cache_configs(actual.cache, expected.cache) assert_input_configs(actual.input, expected.input) - assert_embed_graph_configs(actual.embed_graph, expected.embed_graph) assert_text_embedding_configs(actual.embed_text, expected.embed_text) - assert_chunking_configs(actual.chunks, expected.chunks) + assert_chunking_configs(actual.chunking, expected.chunking) assert_snapshots_configs(actual.snapshots, expected.snapshots) assert_extract_graph_configs(actual.extract_graph, expected.extract_graph) assert_extract_graph_nlp_configs( @@ -428,7 +373,6 @@ def assert_graphrag_configs(actual: GraphRagConfig, expected: GraphRagConfig) -> assert_extract_claims_configs(actual.extract_claims, expected.extract_claims) assert_prune_graph_configs(actual.prune_graph, expected.prune_graph) assert_cluster_graph_configs(actual.cluster_graph, expected.cluster_graph) - assert_umap_configs(actual.umap, expected.umap) assert_local_search_configs(actual.local_search, expected.local_search) assert_global_search_configs(actual.global_search, expected.global_search) assert_drift_search_configs(actual.drift_search, expected.drift_search) diff --git a/tests/unit/indexing/operations/chunk_text/__init__.py b/tests/unit/graphrag_factory/__init__.py similarity index 100% rename from tests/unit/indexing/operations/chunk_text/__init__.py rename to tests/unit/graphrag_factory/__init__.py diff --git a/tests/unit/graphrag_factory/test_factory.py b/tests/unit/graphrag_factory/test_factory.py new file mode 100644 index 0000000000..94e59b9c02 --- /dev/null +++ b/tests/unit/graphrag_factory/test_factory.py @@ -0,0 +1,66 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Unit tests for graphrag_factory package.""" + +from abc import ABC, abstractmethod + +from graphrag_common.factory import Factory + + +class TestABC(ABC): + """Test abstract base class.""" + + @abstractmethod + def get_value(self) -> str: + """ + Get a string value. + + Returns + ------- + str: A string value. + """ + msg = "Subclasses must implement the get_value method." + raise NotImplementedError(msg) + + +class ConcreteTestClass(TestABC): + """Concrete implementation of TestABC.""" + + def __init__(self, value: str): + """Initialize with a string value.""" + self._value = value + + def get_value(self) -> str: + """Get a string value. + + Returns + ------- + str: A string value. + """ + return self._value + + +def test_factory() -> None: + """Test the factory behavior.""" + + class TestFactory(Factory[TestABC]): + """Test factory for TestABC implementations.""" + + factory = TestFactory() + factory.register("transient_strategy", ConcreteTestClass) + factory.register("singleton_strategy", ConcreteTestClass, scope="singleton") + + trans1 = factory.create("transient_strategy", {"value": "test1"}) + trans2 = factory.create("transient_strategy", {"value": "test2"}) + + assert trans1 is not trans2 + assert trans1.get_value() == "test1" + assert trans2.get_value() == "test2" + + single1 = factory.create("singleton_strategy", {"value": "singleton"}) + single2 = factory.create("singleton_strategy", {"value": "singleton"}) + + assert single1 is single2 + assert single1.get_value() == "singleton" + assert single2.get_value() == "singleton" diff --git a/tests/unit/indexing/text_splitting/__init__.py b/tests/unit/hasher/__init__.py similarity index 100% rename from tests/unit/indexing/text_splitting/__init__.py rename to tests/unit/hasher/__init__.py diff --git a/tests/unit/hasher/test_hasher.py b/tests/unit/hasher/test_hasher.py new file mode 100644 index 0000000000..4b13d0dd18 --- /dev/null +++ b/tests/unit/hasher/test_hasher.py @@ -0,0 +1,104 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Test hasher""" + +from graphrag_common.hasher import hash_data + + +def test_hash_data() -> None: + """Test hash data function.""" + # Test different types of data + + class TestClass: # noqa: B903 + """Test hasher class.""" + + def __init__(self, value: str) -> None: + self.value = value + + def _test_func(): + pass + + # All should work and not raise exceptions + _ = hash_data("test string") + _ = hash_data(12345) + _ = hash_data(12.345) + _ = hash_data([1, 2, 3, 4, 5]) + _ = hash_data({"key": "value", "number": 42}) + _ = hash_data((1, "two", 3.0)) + _ = hash_data({1, 2, 3, 4, 5}) + _ = hash_data(None) + _ = hash_data(True) + _ = hash_data(b"bytes data") + _ = hash_data({"nested": {"list": [1, 2, 3], "dict": {"a": "b"}}}) + _ = hash_data(range(10)) + _ = hash_data(frozenset([1, 2, 3])) + _ = hash_data(complex(1, 2)) + _ = hash_data(bytearray(b"byte array data")) + _ = hash_data(memoryview(b"memory view data")) + _ = hash_data(Exception("test exception")) + _ = hash_data(TestClass) + _ = hash_data(TestClass("instance value")) + _ = hash_data(lambda x: x * 2) + _ = hash_data(_test_func) + + # Test that equivalent data structures produce the same hash + data1 = { + "bool": True, + "int": 42, + "float": 3.14, + "str": "hello, world", + "list": [1, 2, 3], + "dict": {"key": "value"}, + "nested": { + "list_of_dicts": [{"a": 1}, {"b": 2}], + "dict_of_lists": {"numbers": [1, 2, 3]}, + }, + "tuple": (1, 2, 3), + "set": {1, 2, 3}, + "class": TestClass, + "function": _test_func, + "instance": TestClass("instance value"), + } + # Same data but different order + data2 = { + "bool": True, + "list": [1, 2, 3], + "float": 3.14, + "str": "hello, world", + "int": 42, + "nested": { + "dict_of_lists": {"numbers": [1, 2, 3]}, + "list_of_dicts": [{"a": 1}, {"b": 2}], + }, + "dict": {"key": "value"}, + "tuple": (1, 2, 3), + "class": TestClass, + "set": {1, 3, 2}, + "instance": TestClass("instance value"), + "function": _test_func, + } + + hash1 = hash_data(data1) + hash2 = hash_data(data2) + + assert hash1 == hash2, "Hashes should be the same for equivalent data structures" + + data3 = {"key1": "value1", "key2": 124, "key3": [1, 2, 3]} # Different value + hash3 = hash_data(data3) + + assert hash1 != hash3, "Hashes should be different for different data structures" + + # Test classes + instance1 = TestClass("value1") + instance2 = TestClass("value1") + instance3 = TestClass("value2") + hash_instance1 = hash_data(instance1) + hash_instance2 = hash_data(instance2) + hash_instance3 = hash_data(instance3) + assert hash_instance1 == hash_instance2, ( + "Hashes should be the same for equivalent class instances" + ) + assert hash_instance1 != hash_instance3, ( + "Hashes should be different for different class instances" + ) diff --git a/tests/unit/indexing/cache/test_file_pipeline_cache.py b/tests/unit/indexing/cache/test_file_pipeline_cache.py index c392b4e08e..ae38e5be4e 100644 --- a/tests/unit/indexing/cache/test_file_pipeline_cache.py +++ b/tests/unit/indexing/cache/test_file_pipeline_cache.py @@ -4,17 +4,23 @@ import os import unittest -from graphrag.cache.json_pipeline_cache import JsonPipelineCache -from graphrag.storage.file_pipeline_storage import ( - FilePipelineStorage, -) +from graphrag_cache import CacheConfig, CacheType +from graphrag_cache import create_cache as cc +from graphrag_storage import StorageConfig, StorageType TEMP_DIR = "./.tmp" def create_cache(): - storage = FilePipelineStorage(base_dir=os.path.join(os.getcwd(), ".tmp")) - return JsonPipelineCache(storage) + return cc( + CacheConfig( + type=CacheType.Json, + storage=StorageConfig( + type=StorageType.File, + base_dir=os.path.join(os.getcwd(), ".tmp"), + ), + ), + ) class TestFilePipelineCache(unittest.IsolatedAsyncioTestCase): diff --git a/tests/unit/indexing/graph/extractors/community_reports/test_sort_context.py b/tests/unit/indexing/graph/extractors/community_reports/test_sort_context.py index c6b07f8cbd..2b2fd4d12a 100644 --- a/tests/unit/indexing/graph/extractors/community_reports/test_sort_context.py +++ b/tests/unit/indexing/graph/extractors/community_reports/test_sort_context.py @@ -208,8 +208,8 @@ def test_sort_context(): ctx = sort_context(context, tokenizer=tokenizer) assert ctx is not None, "Context is none" num = tokenizer.num_tokens(ctx) - assert num == 828 if platform.system() == "Windows" else 826, ( - f"num_tokens is not matched for platform (win = 827, else 826): {num}" + assert num == 825 if platform.system() == "Windows" else 826, ( + f"num_tokens is not matched for platform (win = 825, else 826): {num}" ) diff --git a/tests/unit/indexing/graph/utils/test_stable_lcc.py b/tests/unit/indexing/graph/utils/test_stable_lcc.py index 244f3b905d..c4e17e54ee 100644 --- a/tests/unit/indexing/graph/utils/test_stable_lcc.py +++ b/tests/unit/indexing/graph/utils/test_stable_lcc.py @@ -3,7 +3,6 @@ import unittest import networkx as nx - from graphrag.index.utils.stable_lcc import stable_largest_connected_component @@ -20,32 +19,6 @@ def test_undirected_graph_run_twice_produces_same_graph(self): nx.generate_graphml(graph_out_2) ) - def test_directed_graph_keeps_source_target_intact(self): - # create the test graph as a directed graph - graph_in = self._create_strongly_connected_graph_with_edges_flipped( - digraph=True - ) - graph_out = stable_largest_connected_component(graph_in.copy()) - - # Make sure edges are the same and the direction is preserved - edges_1 = [f"{edge[0]} -> {edge[1]}" for edge in graph_in.edges(data=True)] - edges_2 = [f"{edge[0]} -> {edge[1]}" for edge in graph_out.edges(data=True)] - - assert edges_1 == edges_2 - - def test_directed_graph_run_twice_produces_same_graph(self): - # create the test graph as a directed graph - graph_in = self._create_strongly_connected_graph_with_edges_flipped( - digraph=True - ) - graph_out_1 = stable_largest_connected_component(graph_in.copy()) - graph_out_2 = stable_largest_connected_component(graph_in.copy()) - - # Make sure the output is identical when run multiple times - assert "".join(nx.generate_graphml(graph_out_1)) == "".join( - nx.generate_graphml(graph_out_2) - ) - def _create_strongly_connected_graph(self, digraph=False): graph = nx.Graph() if not digraph else nx.DiGraph() graph.add_node("1", node_name=1) diff --git a/tests/unit/indexing/input/data/one-html/input.html b/tests/unit/indexing/input/data/one-html/input.html new file mode 100644 index 0000000000..dbf8badc70 --- /dev/null +++ b/tests/unit/indexing/input/data/one-html/input.html @@ -0,0 +1,8 @@ + + +Test + + +Hi how are you today? + + \ No newline at end of file diff --git a/tests/unit/indexing/input/data/one-jsonl/input.jsonl b/tests/unit/indexing/input/data/one-jsonl/input.jsonl new file mode 100644 index 0000000000..2866aed408 --- /dev/null +++ b/tests/unit/indexing/input/data/one-jsonl/input.jsonl @@ -0,0 +1,3 @@ +{ "title": "Hello", "text": "Hi how are you today?"} +{ "title": "Goodbye", "text": "I'm outta here"} +{ "title": "Adios", "text": "See you later"} \ No newline at end of file diff --git a/tests/unit/indexing/input/test_csv_loader.py b/tests/unit/indexing/input/test_csv_loader.py index 965f836676..1a84d82676 100644 --- a/tests/unit/indexing/input/test_csv_loader.py +++ b/tests/unit/indexing/input/test_csv_loader.py @@ -1,66 +1,56 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -from graphrag.config.enums import InputFileType -from graphrag.config.models.input_config import InputConfig -from graphrag.config.models.storage_config import StorageConfig -from graphrag.index.input.factory import create_input -from graphrag.utils.api import create_storage_from_config +from graphrag_input import InputConfig, InputType, create_input_reader +from graphrag_storage import StorageConfig, create_storage async def test_csv_loader_one_file(): config = InputConfig( - storage=StorageConfig( - base_dir="tests/unit/indexing/input/data/one-csv", - ), - file_type=InputFileType.csv, + type=InputType.Csv, file_pattern=".*\\.csv$", ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (2, 4) - assert documents["title"].iloc[0] == "input.csv" + storage = create_storage( + StorageConfig( + base_dir="tests/unit/indexing/input/data/one-csv", + ) + ) + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 2 + assert documents[0].title == "input.csv (0)" + assert documents[0].raw_data == { + "title": "Hello", + "text": "Hi how are you today?", + } + assert documents[1].title == "input.csv (1)" async def test_csv_loader_one_file_with_title(): config = InputConfig( - storage=StorageConfig( - base_dir="tests/unit/indexing/input/data/one-csv", - ), - file_type=InputFileType.csv, - file_pattern=".*\\.csv$", + type=InputType.Csv, title_column="title", ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (2, 4) - assert documents["title"].iloc[0] == "Hello" - - -async def test_csv_loader_one_file_with_metadata(): - config = InputConfig( - storage=StorageConfig( + storage = create_storage( + StorageConfig( base_dir="tests/unit/indexing/input/data/one-csv", - ), - file_type=InputFileType.csv, - file_pattern=".*\\.csv$", - title_column="title", - metadata=["title"], + ) ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (2, 5) - assert documents["metadata"][0] == {"title": "Hello"} + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 2 + assert documents[0].title == "Hello" async def test_csv_loader_multiple_files(): config = InputConfig( - storage=StorageConfig( + type=InputType.Csv, + ) + storage = create_storage( + StorageConfig( base_dir="tests/unit/indexing/input/data/multiple-csvs", - ), - file_type=InputFileType.csv, - file_pattern=".*\\.csv$", + ) ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (4, 4) + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 4 diff --git a/tests/unit/indexing/input/test_json_loader.py b/tests/unit/indexing/input/test_json_loader.py index c97d38d4a0..d20d64ad2d 100644 --- a/tests/unit/indexing/input/test_json_loader.py +++ b/tests/unit/indexing/input/test_json_loader.py @@ -1,81 +1,71 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -from graphrag.config.enums import InputFileType -from graphrag.config.models.input_config import InputConfig -from graphrag.config.models.storage_config import StorageConfig -from graphrag.index.input.factory import create_input -from graphrag.utils.api import create_storage_from_config +from graphrag_input import InputConfig, InputType, create_input_reader +from graphrag_storage import StorageConfig, create_storage async def test_json_loader_one_file_one_object(): config = InputConfig( - storage=StorageConfig( - base_dir="tests/unit/indexing/input/data/one-json-one-object", - ), - file_type=InputFileType.json, + type=InputType.Json, file_pattern=".*\\.json$", ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (1, 4) - assert documents["title"].iloc[0] == "input.json" + storage = create_storage( + StorageConfig( + base_dir="tests/unit/indexing/input/data/one-json-one-object", + ) + ) + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 1 + assert documents[0].title == "input.json" + assert documents[0].raw_data == { + "title": "Hello", + "text": "Hi how are you today?", + } async def test_json_loader_one_file_multiple_objects(): config = InputConfig( - storage=StorageConfig( + type=InputType.Json, + ) + storage = create_storage( + StorageConfig( base_dir="tests/unit/indexing/input/data/one-json-multiple-objects", - ), - file_type=InputFileType.json, - file_pattern=".*\\.json$", + ) ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - print(documents) - assert documents.shape == (3, 4) - assert documents["title"].iloc[0] == "input.json" + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 3 + assert documents[0].title == "input.json (0)" + assert documents[1].title == "input.json (1)" async def test_json_loader_one_file_with_title(): config = InputConfig( - storage=StorageConfig( - base_dir="tests/unit/indexing/input/data/one-json-one-object", - ), - file_type=InputFileType.json, - file_pattern=".*\\.json$", + type=InputType.Json, title_column="title", ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (1, 4) - assert documents["title"].iloc[0] == "Hello" - - -async def test_json_loader_one_file_with_metadata(): - config = InputConfig( - storage=StorageConfig( + storage = create_storage( + StorageConfig( base_dir="tests/unit/indexing/input/data/one-json-one-object", - ), - file_type=InputFileType.json, - file_pattern=".*\\.json$", - title_column="title", - metadata=["title"], + ) ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (1, 5) - assert documents["metadata"][0] == {"title": "Hello"} + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 1 + assert documents[0].title == "Hello" async def test_json_loader_multiple_files(): config = InputConfig( - storage=StorageConfig( + type=InputType.Json, + ) + storage = create_storage( + StorageConfig( base_dir="tests/unit/indexing/input/data/multiple-jsons", - ), - file_type=InputFileType.json, - file_pattern=".*\\.json$", + ) ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (4, 4) + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 4 diff --git a/tests/unit/indexing/input/test_jsonl_loader.py b/tests/unit/indexing/input/test_jsonl_loader.py new file mode 100644 index 0000000000..dd56094290 --- /dev/null +++ b/tests/unit/indexing/input/test_jsonl_loader.py @@ -0,0 +1,42 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +from graphrag_input import InputConfig, InputType, create_input_reader +from graphrag_storage import StorageConfig, create_storage + + +async def test_jsonl_loader_one_file_multiple_objects(): + config = InputConfig( + type=InputType.JsonLines, + file_pattern=".*\\.jsonl$", + ) + storage = create_storage( + StorageConfig( + base_dir="tests/unit/indexing/input/data/one-jsonl", + ) + ) + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 3 + assert documents[0].title == "input.jsonl (0)" + assert documents[0].raw_data == { + "title": "Hello", + "text": "Hi how are you today?", + } + assert documents[1].title == "input.jsonl (1)" + + +async def test_jsonl_loader_one_file_with_title(): + config = InputConfig( + type=InputType.JsonLines, + title_column="title", + ) + storage = create_storage( + StorageConfig( + base_dir="tests/unit/indexing/input/data/one-jsonl", + ) + ) + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 3 + assert documents[0].title == "Hello" diff --git a/tests/unit/indexing/input/test_markitdown_loader.py b/tests/unit/indexing/input/test_markitdown_loader.py new file mode 100644 index 0000000000..c32e36dbb7 --- /dev/null +++ b/tests/unit/indexing/input/test_markitdown_loader.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +from graphrag_input import InputConfig, InputType, create_input_reader +from graphrag_storage import StorageConfig, create_storage + + +# these tests just confirm we can load files with MarkItDown, +# and use html specifically because it requires no additional dependency installation +async def test_markitdown_loader_one_file(): + config = InputConfig( + type=InputType.MarkItDown, + file_pattern=".*\\.html$", + ) + storage = create_storage( + StorageConfig( + base_dir="tests/unit/indexing/input/data/one-html", + ) + ) + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 1 + # markitdown will extract the title and body from the HTML if present and clean them + assert documents[0].title == "Test" + assert documents[0].text == "Hi how are you today?" + assert documents[0].raw_data is None diff --git a/tests/unit/indexing/input/test_text_document.py b/tests/unit/indexing/input/test_text_document.py new file mode 100644 index 0000000000..d71c515bf5 --- /dev/null +++ b/tests/unit/indexing/input/test_text_document.py @@ -0,0 +1,76 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +import pytest +from graphrag_input import get_property + + +def test_get_property_single_level(): + data = {"foo": "bar"} + assert get_property(data, "foo") == "bar" + + +def test_get_property_two_levels(): + data = {"foo": {"bar": "baz"}} + assert get_property(data, "foo.bar") == "baz" + + +def test_get_property_three_levels(): + data = {"a": {"b": {"c": "value"}}} + assert get_property(data, "a.b.c") == "value" + + +def test_get_property_returns_dict(): + data = {"foo": {"bar": {"baz": "qux"}}} + result = get_property(data, "foo.bar") + assert result == {"baz": "qux"} + + +def test_get_property_missing_key_raises(): + data = {"foo": "bar"} + with pytest.raises(KeyError): + get_property(data, "missing") + + +def test_get_property_missing_nested_key_raises(): + data = {"foo": {"bar": "baz"}} + with pytest.raises(KeyError): + get_property(data, "foo.missing") + + +def test_get_property_non_dict_intermediate_raises(): + data = {"foo": "bar"} + with pytest.raises(KeyError): + get_property(data, "foo.bar") + + +def test_get_property_empty_dict_raises(): + data = {} + with pytest.raises(KeyError): + get_property(data, "foo") + + +def test_get_property_with_none_value(): + data = {"foo": None} + assert get_property(data, "foo") is None + + +def test_get_property_with_list_value(): + data = {"foo": [1, 2, 3]} + assert get_property(data, "foo") == [1, 2, 3] + + +def test_get_property_list_intermediate_raises(): + data = {"foo": [{"bar": "baz"}]} + with pytest.raises(KeyError): + get_property(data, "foo.bar") + + +def test_get_property_numeric_value(): + data = {"count": 42} + assert get_property(data, "count") == 42 + + +def test_get_property_boolean_value(): + data = {"enabled": True} + assert get_property(data, "enabled") is True diff --git a/tests/unit/indexing/input/test_text_loader.py b/tests/unit/indexing/input/test_text_loader.py new file mode 100644 index 0000000000..482593a270 --- /dev/null +++ b/tests/unit/indexing/input/test_text_loader.py @@ -0,0 +1,36 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +from graphrag_input import InputConfig, InputType, create_input_reader +from graphrag_storage import StorageConfig, create_storage + + +async def test_text_loader_one_file(): + config = InputConfig( + type=InputType.Text, + file_pattern=".*\\.txt$", + ) + storage = create_storage( + StorageConfig( + base_dir="tests/unit/indexing/input/data/one-txt", + ) + ) + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 1 + assert documents[0].title == "input.txt" + assert documents[0].raw_data is None + + +async def test_text_loader_multiple_files(): + config = InputConfig( + type=InputType.Text, + ) + storage = create_storage( + StorageConfig( + base_dir="tests/unit/indexing/input/data/multiple-txts", + ) + ) + reader = create_input_reader(config, storage) + documents = await reader.read_files() + assert len(documents) == 2 diff --git a/tests/unit/indexing/input/test_txt_loader.py b/tests/unit/indexing/input/test_txt_loader.py deleted file mode 100644 index 6b82a408fb..0000000000 --- a/tests/unit/indexing/input/test_txt_loader.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -from graphrag.config.enums import InputFileType -from graphrag.config.models.input_config import InputConfig -from graphrag.config.models.storage_config import StorageConfig -from graphrag.index.input.factory import create_input -from graphrag.utils.api import create_storage_from_config - - -async def test_txt_loader_one_file(): - config = InputConfig( - storage=StorageConfig( - base_dir="tests/unit/indexing/input/data/one-txt", - ), - file_type=InputFileType.text, - file_pattern=".*\\.txt$", - ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (1, 4) - assert documents["title"].iloc[0] == "input.txt" - - -async def test_txt_loader_one_file_with_metadata(): - config = InputConfig( - storage=StorageConfig( - base_dir="tests/unit/indexing/input/data/one-txt", - ), - file_type=InputFileType.text, - file_pattern=".*\\.txt$", - metadata=["title"], - ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (1, 5) - # unlike csv, we cannot set the title to anything other than the filename - assert documents["metadata"][0] == {"title": "input.txt"} - - -async def test_txt_loader_multiple_files(): - config = InputConfig( - storage=StorageConfig( - base_dir="tests/unit/indexing/input/data/multiple-txts", - ), - file_type=InputFileType.text, - file_pattern=".*\\.txt$", - ) - storage = create_storage_from_config(config.storage) - documents = await create_input(config=config, storage=storage) - assert documents.shape == (2, 4) diff --git a/tests/unit/indexing/operations/chunk_text/test_chunk_text.py b/tests/unit/indexing/operations/chunk_text/test_chunk_text.py deleted file mode 100644 index 7b6c54923f..0000000000 --- a/tests/unit/indexing/operations/chunk_text/test_chunk_text.py +++ /dev/null @@ -1,181 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - - -from unittest import mock -from unittest.mock import ANY, Mock - -import pandas as pd -import pytest - -from graphrag.config.enums import ChunkStrategyType -from graphrag.index.operations.chunk_text.chunk_text import ( - _get_num_total, - chunk_text, - load_strategy, - run_strategy, -) -from graphrag.index.operations.chunk_text.typing import ( - TextChunk, -) - - -def test_get_num_total_default(): - output = pd.DataFrame({"column": ["a", "b", "c"]}) - - total = _get_num_total(output, "column") - assert total == 3 - - -def test_get_num_total_array(): - output = pd.DataFrame({"column": [["a", "b", "c"], ["x", "y"]]}) - - total = _get_num_total(output, "column") - assert total == 5 - - -def test_load_strategy_tokens(): - strategy_type = ChunkStrategyType.tokens - - strategy_loaded = load_strategy(strategy_type) - - assert strategy_loaded.__name__ == "run_tokens" - - -def test_load_strategy_sentence(): - strategy_type = ChunkStrategyType.sentence - - strategy_loaded = load_strategy(strategy_type) - - assert strategy_loaded.__name__ == "run_sentences" - - -def test_load_strategy_none(): - strategy_type = ChunkStrategyType - - with pytest.raises( - ValueError, match="Unknown strategy: " - ): - load_strategy(strategy_type) # type: ignore - - -def test_run_strategy_str(): - input = "text test for run strategy" - config = Mock() - tick = Mock() - strategy_mocked = Mock() - - strategy_mocked.return_value = [ - TextChunk( - text_chunk="text test for run strategy", - source_doc_indices=[0], - ) - ] - - runned = run_strategy(strategy_mocked, input, config, tick) - assert runned == ["text test for run strategy"] - - -def test_run_strategy_arr_str(): - input = ["text test for run strategy", "use for strategy"] - config = Mock() - tick = Mock() - strategy_mocked = Mock() - - strategy_mocked.return_value = [ - TextChunk( - text_chunk="text test for run strategy", source_doc_indices=[0], n_tokens=5 - ), - TextChunk(text_chunk="use for strategy", source_doc_indices=[1], n_tokens=3), - ] - - expected = [ - "text test for run strategy", - "use for strategy", - ] - - runned = run_strategy(strategy_mocked, input, config, tick) - assert runned == expected - - -def test_run_strategy_arr_tuple(): - input = [("text test for run strategy", "3"), ("use for strategy", "5")] - config = Mock() - tick = Mock() - strategy_mocked = Mock() - - strategy_mocked.return_value = [ - TextChunk( - text_chunk="text test for run strategy", source_doc_indices=[0], n_tokens=5 - ), - TextChunk(text_chunk="use for strategy", source_doc_indices=[1], n_tokens=3), - ] - - expected = [ - ( - ["text test for run strategy"], - "text test for run strategy", - 5, - ), - ( - ["use for strategy"], - "use for strategy", - 3, - ), - ] - - runned = run_strategy(strategy_mocked, input, config, tick) - assert runned == expected - - -def test_run_strategy_arr_tuple_same_doc(): - input = [("text test for run strategy", "3"), ("use for strategy", "5")] - config = Mock() - tick = Mock() - strategy_mocked = Mock() - - strategy_mocked.return_value = [ - TextChunk( - text_chunk="text test for run strategy", source_doc_indices=[0], n_tokens=5 - ), - TextChunk(text_chunk="use for strategy", source_doc_indices=[0], n_tokens=3), - ] - - expected = [ - ( - ["text test for run strategy"], - "text test for run strategy", - 5, - ), - ( - ["text test for run strategy"], - "use for strategy", - 3, - ), - ] - - runned = run_strategy(strategy_mocked, input, config, tick) - assert runned == expected - - -@mock.patch("graphrag.index.operations.chunk_text.chunk_text.load_strategy") -@mock.patch("graphrag.index.operations.chunk_text.chunk_text.run_strategy") -@mock.patch("graphrag.index.operations.chunk_text.chunk_text.progress_ticker") -def test_chunk_text(mock_progress_ticker, mock_run_strategy, mock_load_strategy): - input_data = pd.DataFrame({"name": ["The Shining"]}) - column = "name" - size = 10 - overlap = 2 - encoding_model = "model" - strategy = ChunkStrategyType.sentence - callbacks = Mock() - callbacks.progress = Mock() - - mock_load_strategy.return_value = Mock() - mock_progress_ticker.return_value = Mock() - - chunk_text(input_data, column, size, overlap, encoding_model, strategy, callbacks) - - mock_run_strategy.assert_called_with( - mock_load_strategy(), "The Shining", ANY, mock_progress_ticker.return_value - ) diff --git a/tests/unit/indexing/operations/chunk_text/test_strategies.py b/tests/unit/indexing/operations/chunk_text/test_strategies.py deleted file mode 100644 index ecc95da0c0..0000000000 --- a/tests/unit/indexing/operations/chunk_text/test_strategies.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -from unittest.mock import Mock, patch - -from graphrag.config.models.chunking_config import ChunkingConfig -from graphrag.index.operations.chunk_text.bootstrap import bootstrap -from graphrag.index.operations.chunk_text.strategies import ( - get_encoding_fn, - run_sentences, - run_tokens, -) -from graphrag.index.operations.chunk_text.typing import TextChunk - - -class TestRunSentences: - def setup_method(self, method): - bootstrap() - - def test_basic_functionality(self): - """Test basic sentence splitting without metadata""" - input = ["This is a test. Another sentence."] - tick = Mock() - chunks = list(run_sentences(input, ChunkingConfig(), tick)) - - assert len(chunks) == 2 - assert chunks[0].text_chunk == "This is a test." - assert chunks[1].text_chunk == "Another sentence." - assert all(c.source_doc_indices == [0] for c in chunks) - tick.assert_called_once_with(1) - - def test_multiple_documents(self): - """Test processing multiple input documents""" - input = ["First. Document.", "Second. Doc."] - tick = Mock() - chunks = list(run_sentences(input, ChunkingConfig(), tick)) - - assert len(chunks) == 4 - assert chunks[0].source_doc_indices == [0] - assert chunks[2].source_doc_indices == [1] - assert tick.call_count == 2 - - def test_mixed_whitespace_handling(self): - """Test input with irregular whitespace""" - input = [" Sentence with spaces. Another one! "] - chunks = list(run_sentences(input, ChunkingConfig(), Mock())) - assert chunks[0].text_chunk == " Sentence with spaces." - assert chunks[1].text_chunk == "Another one!" - - -class TestRunTokens: - @patch("tiktoken.get_encoding") - def test_basic_functionality(self, mock_get_encoding): - mock_encoder = Mock() - mock_encoder.encode.side_effect = lambda x: list(x.encode()) - mock_encoder.decode.side_effect = lambda x: bytes(x).decode() - mock_get_encoding.return_value = mock_encoder - - # Input and config - input = [ - "Marley was dead: to begin with. There is no doubt whatever about that. The register of his burial was signed by the clergyman, the clerk, the undertaker, and the chief mourner. Scrooge signed it. And Scrooge's name was good upon 'Change, for anything he chose to put his hand to." - ] - config = ChunkingConfig(size=5, overlap=1, encoding_model="fake-encoding") - tick = Mock() - - # Run the function - chunks = list(run_tokens(input, config, tick)) - - # Verify output - assert len(chunks) > 0 - assert all(isinstance(chunk, TextChunk) for chunk in chunks) - tick.assert_called_once_with(1) - - @patch("tiktoken.get_encoding") - def test_non_string_input(self, mock_get_encoding): - """Test handling of non-string input (e.g., numbers).""" - mock_encoder = Mock() - mock_encoder.encode.side_effect = lambda x: list(str(x).encode()) - mock_encoder.decode.side_effect = lambda x: bytes(x).decode() - mock_get_encoding.return_value = mock_encoder - - input = [123] # Non-string input - config = ChunkingConfig(size=5, overlap=1, encoding_model="fake-encoding") - tick = Mock() - - chunks = list(run_tokens(input, config, tick)) # type: ignore - - # Verify non-string input is handled - assert len(chunks) > 0 - assert "123" in chunks[0].text_chunk - - -@patch("tiktoken.get_encoding") -def test_get_encoding_fn_encode(mock_get_encoding): - # Create a mock encoding object with encode and decode methods - mock_encoding = Mock() - mock_encoding.encode = Mock(return_value=[1, 2, 3]) - mock_encoding.decode = Mock(return_value="decoded text") - - # Configure the mock_get_encoding to return the mock encoding object - mock_get_encoding.return_value = mock_encoding - - # Call the function to get encode and decode functions - encode, _ = get_encoding_fn("mock_encoding") - - # Test the encode function - encoded_text = encode("test text") - assert encoded_text == [1, 2, 3] - mock_encoding.encode.assert_called_once_with("test text") - - -@patch("tiktoken.get_encoding") -def test_get_encoding_fn_decode(mock_get_encoding): - # Create a mock encoding object with encode and decode methods - mock_encoding = Mock() - mock_encoding.encode = Mock(return_value=[1, 2, 3]) - mock_encoding.decode = Mock(return_value="decoded text") - - # Configure the mock_get_encoding to return the mock encoding object - mock_get_encoding.return_value = mock_encoding - - # Call the function to get encode and decode functions - _, decode = get_encoding_fn("mock_encoding") - - decoded_text = decode([1, 2, 3]) - assert decoded_text == "decoded text" - mock_encoding.decode.assert_called_once_with([1, 2, 3]) diff --git a/tests/unit/indexing/test_init_content.py b/tests/unit/indexing/test_init_content.py index 58791a905c..61f2d11fa6 100644 --- a/tests/unit/indexing/test_init_content.py +++ b/tests/unit/indexing/test_init_content.py @@ -5,15 +5,13 @@ from typing import Any, cast import yaml - -from graphrag.config.create_graphrag_config import create_graphrag_config from graphrag.config.init_content import INIT_YAML from graphrag.config.models.graph_rag_config import GraphRagConfig def test_init_yaml(): data = yaml.load(INIT_YAML, Loader=yaml.FullLoader) - config = create_graphrag_config(data) + config = GraphRagConfig(**data) GraphRagConfig.model_validate(config, strict=True) @@ -27,5 +25,5 @@ def uncomment_line(line: str) -> str: content = "\n".join([uncomment_line(line) for line in lines]) data = yaml.load(content, Loader=yaml.FullLoader) - config = create_graphrag_config(data) + config = GraphRagConfig(**data) GraphRagConfig.model_validate(config, strict=True) diff --git a/tests/unit/indexing/text_splitting/test_text_splitting.py b/tests/unit/indexing/text_splitting/test_text_splitting.py deleted file mode 100644 index 4ea8b25e5d..0000000000 --- a/tests/unit/indexing/text_splitting/test_text_splitting.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -from unittest import mock -from unittest.mock import MagicMock - -import pytest -import tiktoken - -from graphrag.index.text_splitting.text_splitting import ( - NoopTextSplitter, - TokenChunkerOptions, - TokenTextSplitter, - split_multiple_texts_on_tokens, - split_single_text_on_tokens, -) - - -def test_noop_text_splitter() -> None: - splitter = NoopTextSplitter() - - assert list(splitter.split_text("some text")) == ["some text"] - assert list(splitter.split_text(["some", "text"])) == ["some", "text"] - - -class MockTokenizer: - def encode(self, text): - return [ord(char) for char in text] - - def decode(self, token_ids): - return "".join(chr(id) for id in token_ids) - - -def test_split_text_str_empty(): - splitter = TokenTextSplitter(chunk_size=5, chunk_overlap=2) - result = splitter.split_text("") - - assert result == [] - - -def test_split_text_str_bool(): - splitter = TokenTextSplitter(chunk_size=5, chunk_overlap=2) - result = splitter.split_text(None) # type: ignore - - assert result == [] - - -def test_split_text_str_int(): - splitter = TokenTextSplitter(chunk_size=5, chunk_overlap=2) - with pytest.raises(TypeError): - splitter.split_text(123) # type: ignore - - -@mock.patch("graphrag.index.text_splitting.text_splitting.split_single_text_on_tokens") -def test_split_text_large_input(mock_split): - large_text = "a" * 10_000 - mock_split.return_value = ["chunk"] * 2_000 - splitter = TokenTextSplitter(chunk_size=5, chunk_overlap=2) - - result = splitter.split_text(large_text) - - assert len(result) == 2_000, "Large input was not split correctly" - mock_split.assert_called_once() - - -@mock.patch("graphrag.index.text_splitting.text_splitting.split_single_text_on_tokens") -@mock.patch("graphrag.index.text_splitting.text_splitting.TokenChunkerOptions") -def test_token_text_splitter(mock_tokenizer, mock_split_text): - text = "chunk1 chunk2 chunk3" - expected_chunks = ["chunk1", "chunk2", "chunk3"] - - mocked_tokenizer = MagicMock() - mock_tokenizer.return_value = mocked_tokenizer - mock_split_text.return_value = expected_chunks - - splitter = TokenTextSplitter() - - splitter.split_text(["chunk1", "chunk2", "chunk3"]) - - mock_split_text.assert_called_once_with(text=text, tokenizer=mocked_tokenizer) - - -def test_split_single_text_on_tokens(): - text = "This is a test text, meaning to be taken seriously by this test only." - mocked_tokenizer = MockTokenizer() - tokenizer = TokenChunkerOptions( - chunk_overlap=5, - tokens_per_chunk=10, - decode=mocked_tokenizer.decode, - encode=lambda text: mocked_tokenizer.encode(text), - ) - - expected_splits = [ - "This is a ", - "is a test ", - "test text,", - "text, mean", - " meaning t", - "ing to be ", - "o be taken", - "taken seri", # cspell:disable-line - " seriously", - "ously by t", # cspell:disable-line - " by this t", - "his test o", - "est only.", - ] - - result = split_single_text_on_tokens(text=text, tokenizer=tokenizer) - assert result == expected_splits - - -def test_split_multiple_texts_on_tokens(): - texts = [ - "This is a test text, meaning to be taken seriously by this test only.", - "This is th second text, meaning to be taken seriously by this test only.", - ] - - mocked_tokenizer = MockTokenizer() - mock_tick = MagicMock() - tokenizer = TokenChunkerOptions( - chunk_overlap=5, - tokens_per_chunk=10, - decode=mocked_tokenizer.decode, - encode=lambda text: mocked_tokenizer.encode(text), - ) - - split_multiple_texts_on_tokens(texts, tokenizer, tick=mock_tick) - mock_tick.assert_called() - - -def test_split_single_text_on_tokens_no_overlap(): - text = "This is a test text, meaning to be taken seriously by this test only." - enc = tiktoken.get_encoding("cl100k_base") - - def encode(text: str) -> list[int]: - if not isinstance(text, str): - text = f"{text}" - return enc.encode(text) - - def decode(tokens: list[int]) -> str: - return enc.decode(tokens) - - tokenizer = TokenChunkerOptions( - chunk_overlap=1, - tokens_per_chunk=2, - decode=decode, - encode=lambda text: encode(text), - ) - - expected_splits = [ - "This is", - " is a", - " a test", - " test text", - " text,", - ", meaning", - " meaning to", - " to be", - " be taken", # cspell:disable-line - " taken seriously", # cspell:disable-line - " seriously by", - " by this", # cspell:disable-line - " this test", - " test only", - " only.", - ] - - result = split_single_text_on_tokens(text=text, tokenizer=tokenizer) - assert result == expected_splits diff --git a/tests/unit/indexing/verbs/entities/extraction/strategies/graph_intelligence/test_gi_entity_extraction.py b/tests/unit/indexing/verbs/entities/extraction/strategies/graph_intelligence/test_gi_entity_extraction.py index 13e676e9fb..486bf5fcdf 100644 --- a/tests/unit/indexing/verbs/entities/extraction/strategies/graph_intelligence/test_gi_entity_extraction.py +++ b/tests/unit/indexing/verbs/entities/extraction/strategies/graph_intelligence/test_gi_entity_extraction.py @@ -2,217 +2,78 @@ # Licensed under the MIT License import unittest -from graphrag.index.operations.extract_graph.graph_intelligence_strategy import ( - run_extract_graph, +from graphrag.index.operations.extract_graph.extract_graph import _run_extract_graph +from graphrag.prompts.index.extract_graph import GRAPH_EXTRACTION_PROMPT +from graphrag_llm.completion import create_completion +from graphrag_llm.config import LLMProviderType, ModelConfig + +SIMPLE_EXTRACTION_RESPONSE = """ +("entity"<|>TEST_ENTITY_1<|>COMPANY<|>TEST_ENTITY_1 is a test company) +## +("entity"<|>TEST_ENTITY_2<|>COMPANY<|>TEST_ENTITY_2 owns TEST_ENTITY_1 and also shares an address with TEST_ENTITY_1) +## +("entity"<|>TEST_ENTITY_3<|>PERSON<|>TEST_ENTITY_3 is director of TEST_ENTITY_1) +## +("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_2<|>TEST_ENTITY_1 and TEST_ENTITY_2 are related because TEST_ENTITY_1 is 100% owned by TEST_ENTITY_2 and the two companies also share the same address)<|>2) +## +("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_3<|>TEST_ENTITY_1 and TEST_ENTITY_3 are related because TEST_ENTITY_3 is director of TEST_ENTITY_1<|>1)) +""".strip() + + +model = create_completion( + ModelConfig( + type=LLMProviderType.MockLLM, + model_provider="openai", + model="gpt-4o", + mock_responses=[SIMPLE_EXTRACTION_RESPONSE], + ) ) -from graphrag.index.operations.extract_graph.typing import ( - Document, -) -from tests.unit.indexing.verbs.helpers.mock_llm import create_mock_llm class TestRunChain(unittest.IsolatedAsyncioTestCase): async def test_run_extract_graph_single_document_correct_entities_returned(self): - results = await run_extract_graph( - docs=[Document("test_text", "1")], - entity_types=["person"], - args={ - "max_gleanings": 0, - "summarize_descriptions": False, - }, - model=create_mock_llm( - responses=[ - """ - ("entity"<|>TEST_ENTITY_1<|>COMPANY<|>TEST_ENTITY_1 is a test company) - ## - ("entity"<|>TEST_ENTITY_2<|>COMPANY<|>TEST_ENTITY_2 owns TEST_ENTITY_1 and also shares an address with TEST_ENTITY_1) - ## - ("entity"<|>TEST_ENTITY_3<|>PERSON<|>TEST_ENTITY_3 is director of TEST_ENTITY_1) - ## - ("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_2<|>TEST_ENTITY_1 and TEST_ENTITY_2 are related because TEST_ENTITY_1 is 100% owned by TEST_ENTITY_2 and the two companies also share the same address)<|>2) - ## - ("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_3<|>TEST_ENTITY_1 and TEST_ENTITY_3 are related because TEST_ENTITY_3 is director of TEST_ENTITY_1<|>1)) - """.strip() - ], - name="test_run_extract_graph_single_document_correct_entities_returned", - ), - ) - - # self.assertItemsEqual isn't available yet, or I am just silly - # so we sort the lists and compare them - assert sorted(["TEST_ENTITY_1", "TEST_ENTITY_2", "TEST_ENTITY_3"]) == sorted([ - entity["title"] for entity in results.entities - ]) - - async def test_run_extract_graph_multiple_documents_correct_entities_returned( - self, - ): - results = await run_extract_graph( - docs=[Document("text_1", "1"), Document("text_2", "2")], + entities_df, _ = await _run_extract_graph( + text="test_text", + source_id="1", entity_types=["person"], - args={ - "max_gleanings": 0, - "summarize_descriptions": False, - }, - model=create_mock_llm( - responses=[ - """ - ("entity"<|>TEST_ENTITY_1<|>COMPANY<|>TEST_ENTITY_1 is a test company) - ## - ("entity"<|>TEST_ENTITY_2<|>COMPANY<|>TEST_ENTITY_2 owns TEST_ENTITY_1 and also shares an address with TEST_ENTITY_1) - ## - ("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_2<|>TEST_ENTITY_1 and TEST_ENTITY_2 are related because TEST_ENTITY_1 is 100% owned by TEST_ENTITY_2 and the two companies also share the same address)<|>2) - ## - """.strip(), - """ - ("entity"<|>TEST_ENTITY_1<|>COMPANY<|>TEST_ENTITY_1 is a test company) - ## - ("entity"<|>TEST_ENTITY_3<|>PERSON<|>TEST_ENTITY_3 is director of TEST_ENTITY_1) - ## - ("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_3<|>TEST_ENTITY_1 and TEST_ENTITY_3 are related because TEST_ENTITY_3 is director of TEST_ENTITY_1<|>1)) - """.strip(), - ], - name="test_run_extract_graph_multiple_documents_correct_entities_returned", - ), + max_gleanings=0, + model=model, + prompt=GRAPH_EXTRACTION_PROMPT, ) - # self.assertItemsEqual isn't available yet, or I am just silly - # so we sort the lists and compare them - assert sorted(["TEST_ENTITY_1", "TEST_ENTITY_2", "TEST_ENTITY_3"]) == sorted([ - entity["title"] for entity in results.entities - ]) - - async def test_run_extract_graph_multiple_documents_correct_edges_returned(self): - results = await run_extract_graph( - docs=[Document("text_1", "1"), Document("text_2", "2")], - entity_types=["person"], - args={ - "max_gleanings": 0, - "summarize_descriptions": False, - }, - model=create_mock_llm( - responses=[ - """ - ("entity"<|>TEST_ENTITY_1<|>COMPANY<|>TEST_ENTITY_1 is a test company) - ## - ("entity"<|>TEST_ENTITY_2<|>COMPANY<|>TEST_ENTITY_2 owns TEST_ENTITY_1 and also shares an address with TEST_ENTITY_1) - ## - ("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_2<|>TEST_ENTITY_1 and TEST_ENTITY_2 are related because TEST_ENTITY_1 is 100% owned by TEST_ENTITY_2 and the two companies also share the same address)<|>2) - ## - """.strip(), - """ - ("entity"<|>TEST_ENTITY_1<|>COMPANY<|>TEST_ENTITY_1 is a test company) - ## - ("entity"<|>TEST_ENTITY_3<|>PERSON<|>TEST_ENTITY_3 is director of TEST_ENTITY_1) - ## - ("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_3<|>TEST_ENTITY_1 and TEST_ENTITY_3 are related because TEST_ENTITY_3 is director of TEST_ENTITY_1<|>1)) - """.strip(), - ], - name="test_run_extract_graph_multiple_documents_correct_edges_returned", - ), + assert sorted(["TEST_ENTITY_1", "TEST_ENTITY_2", "TEST_ENTITY_3"]) == sorted( + entities_df["title"].tolist() ) - # self.assertItemsEqual isn't available yet, or I am just silly - # so we sort the lists and compare them - graph = results.graph - assert graph is not None, "No graph returned!" - - # convert to strings for more visual comparison - edges_str = sorted([f"{edge[0]} -> {edge[1]}" for edge in graph.edges]) - assert edges_str == sorted([ - "TEST_ENTITY_1 -> TEST_ENTITY_2", - "TEST_ENTITY_1 -> TEST_ENTITY_3", - ]) - - async def test_run_extract_graph_multiple_documents_correct_entity_source_ids_mapped( - self, - ): - results = await run_extract_graph( - docs=[Document("text_1", "1"), Document("text_2", "2")], + async def test_run_extract_graph_single_document_correct_edges_returned(self): + _, relationships_df = await _run_extract_graph( + text="test_text", + source_id="1", entity_types=["person"], - args={ - "max_gleanings": 0, - "summarize_descriptions": False, - }, - model=create_mock_llm( - responses=[ - """ - ("entity"<|>TEST_ENTITY_1<|>COMPANY<|>TEST_ENTITY_1 is a test company) - ## - ("entity"<|>TEST_ENTITY_2<|>COMPANY<|>TEST_ENTITY_2 owns TEST_ENTITY_1 and also shares an address with TEST_ENTITY_1) - ## - ("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_2<|>TEST_ENTITY_1 and TEST_ENTITY_2 are related because TEST_ENTITY_1 is 100% owned by TEST_ENTITY_2 and the two companies also share the same address)<|>2) - ## - """.strip(), - """ - ("entity"<|>TEST_ENTITY_1<|>COMPANY<|>TEST_ENTITY_1 is a test company) - ## - ("entity"<|>TEST_ENTITY_3<|>PERSON<|>TEST_ENTITY_3 is director of TEST_ENTITY_1) - ## - ("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_3<|>TEST_ENTITY_1 and TEST_ENTITY_3 are related because TEST_ENTITY_3 is director of TEST_ENTITY_1<|>1)) - """.strip(), - ], - name="test_run_extract_graph_multiple_documents_correct_entity_source_ids_mapped", - ), + max_gleanings=0, + model=model, + prompt=GRAPH_EXTRACTION_PROMPT, ) - graph = results.graph - assert graph is not None, "No graph returned!" + edges = relationships_df.to_dict("records") + assert len(edges) == 2 - # TODO: The edges might come back in any order, but we're assuming they're coming - # back in the order that we passed in the docs, that might not be true - assert ( - graph.nodes["TEST_ENTITY_3"].get("source_id") == "2" - ) # TEST_ENTITY_3 should be in just 2 - assert ( - graph.nodes["TEST_ENTITY_2"].get("source_id") == "1" - ) # TEST_ENTITY_2 should be in just 1 - ids_str = graph.nodes["TEST_ENTITY_1"].get("source_id") or "" - assert sorted(ids_str.split(",")) == sorted([ - "1", - "2", - ]) # TEST_ENTITY_1 should be 1 and 2 + relationship_pairs = {(edge["source"], edge["target"]) for edge in edges} + assert relationship_pairs == { + ("TEST_ENTITY_1", "TEST_ENTITY_2"), + ("TEST_ENTITY_1", "TEST_ENTITY_3"), + } - async def test_run_extract_graph_multiple_documents_correct_edge_source_ids_mapped( - self, - ): - results = await run_extract_graph( - docs=[Document("text_1", "1"), Document("text_2", "2")], + async def test_run_extract_graph_single_document_source_ids_mapped(self): + entities_df, relationships_df = await _run_extract_graph( + text="test_text", + source_id="1", entity_types=["person"], - args={ - "max_gleanings": 0, - "summarize_descriptions": False, - }, - model=create_mock_llm( - responses=[ - """ - ("entity"<|>TEST_ENTITY_1<|>COMPANY<|>TEST_ENTITY_1 is a test company) - ## - ("entity"<|>TEST_ENTITY_2<|>COMPANY<|>TEST_ENTITY_2 owns TEST_ENTITY_1 and also shares an address with TEST_ENTITY_1) - ## - ("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_2<|>TEST_ENTITY_1 and TEST_ENTITY_2 are related because TEST_ENTITY_1 is 100% owned by TEST_ENTITY_2 and the two companies also share the same address)<|>2) - ## - """.strip(), - """ - ("entity"<|>TEST_ENTITY_1<|>COMPANY<|>TEST_ENTITY_1 is a test company) - ## - ("entity"<|>TEST_ENTITY_3<|>PERSON<|>TEST_ENTITY_3 is director of TEST_ENTITY_1) - ## - ("relationship"<|>TEST_ENTITY_1<|>TEST_ENTITY_3<|>TEST_ENTITY_1 and TEST_ENTITY_3 are related because TEST_ENTITY_3 is director of TEST_ENTITY_1<|>1)) - """.strip(), - ], - name="test_run_extract_graph_multiple_documents_correct_edge_source_ids_mapped", - ), + max_gleanings=0, + model=model, + prompt=GRAPH_EXTRACTION_PROMPT, ) - graph = results.graph - assert graph is not None, "No graph returned!" - edges = list(graph.edges(data=True)) - - # should only have 2 edges - assert len(edges) == 2 + assert all(source_id == "1" for source_id in entities_df["source_id"]) - # Sort by source_id for consistent ordering - edge_source_ids = sorted([edge[2].get("source_id", "") for edge in edges]) - assert edge_source_ids[0].split(",") == ["1"] - assert edge_source_ids[1].split(",") == ["2"] + assert all(source_id == "1" for source_id in relationships_df["source_id"]) diff --git a/tests/unit/indexing/verbs/helpers/mock_llm.py b/tests/unit/indexing/verbs/helpers/mock_llm.py deleted file mode 100644 index 161d5d56df..0000000000 --- a/tests/unit/indexing/verbs/helpers/mock_llm.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License -from pydantic import BaseModel - -from graphrag.language_model.manager import ModelManager -from graphrag.language_model.protocol.base import ChatModel - - -def create_mock_llm(responses: list[str | BaseModel], name: str = "mock") -> ChatModel: - """Creates a mock LLM that returns the given responses.""" - return ModelManager().get_or_create_chat_model( - name, "mock_chat", responses=responses - ) diff --git a/tests/unit/litellm_services/test_retries.py b/tests/unit/litellm_services/test_retries.py deleted file mode 100644 index 3db2a39027..0000000000 --- a/tests/unit/litellm_services/test_retries.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Test LiteLLM Retries.""" - -import time - -import pytest - -from graphrag.language_model.providers.litellm.services.retry.retry_factory import ( - RetryFactory, -) - -retry_factory = RetryFactory() - - -@pytest.mark.parametrize( - ("strategy", "max_retries", "max_retry_wait", "expected_time"), - [ - ( - "native", - 3, # 3 retries - 0, # native retry does not adhere to max_retry_wait - 0, # immediate retry, expect 0 seconds elapsed time - ), - ( - "exponential_backoff", - 3, # 3 retries - 0, # exponential retry does not adhere to max_retry_wait - 14, # (2^1 + jitter) + (2^2 + jitter) + (2^3 + jitter) = 2 + 4 + 8 + 3*jitter = 14 seconds min total runtime - ), - ( - "random_wait", - 3, # 3 retries - 2, # random wait [0, 2] seconds - 0, # unpredictable, don't know what the total runtime will be - ), - ( - "incremental_wait", - 3, # 3 retries - 3, # wait for a max of 3 seconds on a single retry. - 6, # Wait 3/3 * 1 on first retry, 3/3 * 2 on second, 3/3 * 3 on third, 1 + 2 + 3 = 6 seconds total runtime. - ), - ], -) -def test_retries( - strategy: str, max_retries: int, max_retry_wait: int, expected_time: float -) -> None: - """ - Test various retry strategies with various configurations. - - Args - ---- - strategy: The retry strategy to use. - max_retries: The maximum number of retry attempts. - max_retry_wait: The maximum wait time between retries. - """ - retry_service = retry_factory.create( - strategy=strategy, - max_retries=max_retries, - max_retry_wait=max_retry_wait, - ) - - retries = 0 - - def mock_func(): - nonlocal retries - retries += 1 - msg = "Mock error for testing retries" - raise ValueError(msg) - - start_time = time.time() - with pytest.raises(ValueError, match="Mock error for testing retries"): - retry_service.retry(func=mock_func) - elapsed_time = time.time() - start_time - - # subtract 1 from retries because the first call is not a retry - assert retries - 1 == max_retries, f"Expected {max_retries} retries, got {retries}" - assert elapsed_time >= expected_time, ( - f"Expected elapsed time >= {expected_time}, got {elapsed_time}" - ) - - -@pytest.mark.parametrize( - ("strategy", "max_retries", "max_retry_wait", "expected_time"), - [ - ( - "native", - 3, # 3 retries - 0, # native retry does not adhere to max_retry_wait - 0, # immediate retry, expect 0 seconds elapsed time - ), - ( - "exponential_backoff", - 3, # 3 retries - 0, # exponential retry does not adhere to max_retry_wait - 14, # (2^1 + jitter) + (2^2 + jitter) + (2^3 + jitter) = 2 + 4 + 8 + 3*jitter = 14 seconds min total runtime - ), - ( - "random_wait", - 3, # 3 retries - 2, # random wait [0, 2] seconds - 0, # unpredictable, don't know what the total runtime will be - ), - ( - "incremental_wait", - 3, # 3 retries - 3, # wait for a max of 3 seconds on a single retry. - 6, # Wait 3/3 * 1 on first retry, 3/3 * 2 on second, 3/3 * 3 on third, 1 + 2 + 3 = 6 seconds total runtime. - ), - ], -) -async def test_retries_async( - strategy: str, max_retries: int, max_retry_wait: int, expected_time: float -) -> None: - """ - Test various retry strategies with various configurations. - - Args - ---- - strategy: The retry strategy to use. - max_retries: The maximum number of retry attempts. - max_retry_wait: The maximum wait time between retries. - """ - retry_service = retry_factory.create( - strategy=strategy, - max_retries=max_retries, - max_retry_wait=max_retry_wait, - ) - - retries = 0 - - async def mock_func(): # noqa: RUF029 - nonlocal retries - retries += 1 - msg = "Mock error for testing retries" - raise ValueError(msg) - - start_time = time.time() - with pytest.raises(ValueError, match="Mock error for testing retries"): - await retry_service.aretry(func=mock_func) - elapsed_time = time.time() - start_time - - # subtract 1 from retries because the first call is not a retry - assert retries - 1 == max_retries, f"Expected {max_retries} retries, got {retries}" - assert elapsed_time >= expected_time, ( - f"Expected elapsed time >= {expected_time}, got {elapsed_time}" - ) diff --git a/tests/unit/indexing/verbs/helpers/__init__.py b/tests/unit/load_config/__init__.py similarity index 100% rename from tests/unit/indexing/verbs/helpers/__init__.py rename to tests/unit/load_config/__init__.py diff --git a/tests/unit/load_config/config.py b/tests/unit/load_config/config.py new file mode 100644 index 0000000000..f3b77feb65 --- /dev/null +++ b/tests/unit/load_config/config.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Config models for load_config unit tests.""" + +from pydantic import BaseModel, ConfigDict, Field + + +class TestNestedModel(BaseModel): + """Test nested model.""" + + model_config = ConfigDict(extra="forbid") + + nested_str: str = Field(description="A nested field.") + nested_int: int = Field(description="Another nested field.") + + +class TestConfigModel(BaseModel): + """Test configuration model.""" + + model_config = ConfigDict(extra="forbid") + __test__ = False # type: ignore + + name: str = Field(description="Name field.") + value: int = Field(description="Value field.") + nested: TestNestedModel = Field(description="Nested model field.") + nested_list: list[TestNestedModel] = Field(description="List of nested models.") diff --git a/tests/unit/load_config/fixtures/config.yaml b/tests/unit/load_config/fixtures/config.yaml new file mode 100644 index 0000000000..a54919d1eb --- /dev/null +++ b/tests/unit/load_config/fixtures/config.yaml @@ -0,0 +1,10 @@ +name: test_name +value: 100 +nested: + nested_str: nested_value + nested_int: 42 +nested_list: + - nested_str: list_value_1 + nested_int: 7 + - nested_str: list_value_2 + nested_int: 8 \ No newline at end of file diff --git a/tests/unit/load_config/fixtures/config_with_env.yaml b/tests/unit/load_config/fixtures/config_with_env.yaml new file mode 100644 index 0000000000..ecefbbc457 --- /dev/null +++ b/tests/unit/load_config/fixtures/config_with_env.yaml @@ -0,0 +1,10 @@ +name: ${LOAD_CONFIG_NAME} +value: 100 +nested: + nested_str: nested_value + nested_int: 42 +nested_list: + - nested_str: list_value_1 + nested_int: 7 + - nested_str: list_value_2 + nested_int: 8 \ No newline at end of file diff --git a/tests/unit/load_config/fixtures/invalid_config.yaml b/tests/unit/load_config/fixtures/invalid_config.yaml new file mode 100644 index 0000000000..d2da11d0eb --- /dev/null +++ b/tests/unit/load_config/fixtures/invalid_config.yaml @@ -0,0 +1 @@ +name: test_name \ No newline at end of file diff --git a/tests/unit/load_config/fixtures/invalid_config_format.yaml b/tests/unit/load_config/fixtures/invalid_config_format.yaml new file mode 100644 index 0000000000..b851bf08c7 --- /dev/null +++ b/tests/unit/load_config/fixtures/invalid_config_format.yaml @@ -0,0 +1,8 @@ +{ + "key": "value", + "invalid_yaml": true +} +{ + "key": "value", + "invalid_yaml": true +} \ No newline at end of file diff --git a/tests/unit/load_config/fixtures/settings.yaml b/tests/unit/load_config/fixtures/settings.yaml new file mode 100644 index 0000000000..a54919d1eb --- /dev/null +++ b/tests/unit/load_config/fixtures/settings.yaml @@ -0,0 +1,10 @@ +name: test_name +value: 100 +nested: + nested_str: nested_value + nested_int: 42 +nested_list: + - nested_str: list_value_1 + nested_int: 7 + - nested_str: list_value_2 + nested_int: 8 \ No newline at end of file diff --git a/tests/unit/load_config/fixtures/test.env b/tests/unit/load_config/fixtures/test.env new file mode 100644 index 0000000000..0ca30592c0 --- /dev/null +++ b/tests/unit/load_config/fixtures/test.env @@ -0,0 +1 @@ +LOAD_CONFIG_NAME=env_name \ No newline at end of file diff --git a/tests/unit/load_config/test_load_config.py b/tests/unit/load_config/test_load_config.py new file mode 100644 index 0000000000..0945cf214f --- /dev/null +++ b/tests/unit/load_config/test_load_config.py @@ -0,0 +1,157 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Unit tests for graphrag-config.load_config.""" + +import os +from pathlib import Path + +import pytest +from graphrag_common.config import ConfigParsingError, load_config +from pydantic import ValidationError + +from .config import TestConfigModel + + +def test_load_config_validation(): + """Test loading config validation.""" + + with pytest.raises( + FileNotFoundError, + ): + _ = load_config(TestConfigModel, "non_existent_config.yaml") + + config_directory = Path(__file__).parent / "fixtures" + invalid_config_formatting_path = config_directory / "invalid_config_format.yaml" + + with pytest.raises( + FileNotFoundError, + ): + _ = load_config( + config_initializer=TestConfigModel, + config_path=invalid_config_formatting_path, + dot_env_path="non_existent.env", + ) + + # Using yaml to parse invalid json formatting + with pytest.raises( + ConfigParsingError, + ): + _ = load_config(TestConfigModel, invalid_config_formatting_path) + + invalid_config_path = config_directory / "invalid_config.yaml" + + # Test validation error from config model + with pytest.raises( + ValidationError, + ): + _ = load_config( + config_initializer=TestConfigModel, + config_path=invalid_config_path, + set_cwd=False, + ) + + +def test_load_config(): + """Test loading configuration.""" + + config_directory = Path(__file__).parent / "fixtures" + config_path = config_directory / "settings.yaml" + + # Load from dir + config = load_config( + config_initializer=TestConfigModel, config_path=config_directory, set_cwd=False + ) + + assert config.name == "test_name" + assert config.value == 100 + assert config.nested.nested_str == "nested_value" + assert config.nested.nested_int == 42 + assert len(config.nested_list) == 2 + assert config.nested_list[0].nested_str == "list_value_1" + assert config.nested_list[0].nested_int == 7 + assert config.nested_list[1].nested_str == "list_value_2" + assert config.nested_list[1].nested_int == 8 + + # Should not have changed directories + root_repo_dir = Path(__file__).parent.parent.parent.parent.resolve() + assert Path.cwd().resolve() == root_repo_dir + + config = load_config( + config_initializer=TestConfigModel, + config_path=config_path, + set_cwd=False, + ) + + assert config.name == "test_name" + assert config.value == 100 + assert config.nested.nested_str == "nested_value" + assert config.nested.nested_int == 42 + assert len(config.nested_list) == 2 + assert config.nested_list[0].nested_str == "list_value_1" + assert config.nested_list[0].nested_int == 7 + assert config.nested_list[1].nested_str == "list_value_2" + assert config.nested_list[1].nested_int == 8 + + overrides = { + "value": 65537, + "nested": {"nested_int": 84}, + "nested_list": [ + {"nested_str": "overridden_list_value_1", "nested_int": 23}, + ], + } + + cwd = Path.cwd() + config_with_overrides = load_config( + config_initializer=TestConfigModel, + config_path=config_path, + overrides=overrides, + ) + + # Should have changed directories to the config file location + assert Path.cwd() == config_directory + assert ( + Path("some/new/path").resolve() + == (config_directory / "some/new/path").resolve() + ) + # Reset cwd + os.chdir(cwd) + + assert config_with_overrides.name == "test_name" + assert config_with_overrides.value == 65537 + assert config_with_overrides.nested.nested_str == "nested_value" + assert config_with_overrides.nested.nested_int == 84 + assert len(config_with_overrides.nested_list) == 1 + assert config_with_overrides.nested_list[0].nested_str == "overridden_list_value_1" + assert config_with_overrides.nested_list[0].nested_int == 23 + + config_with_env_vars_path = config_directory / "config_with_env.yaml" + + # Config contains env vars that do not exist + # and no .env file is provided + with pytest.raises( + ConfigParsingError, + ): + _ = load_config( + config_initializer=TestConfigModel, + config_path=config_with_env_vars_path, + load_dot_env_file=False, + set_cwd=False, + ) + + env_path = config_directory / "test.env" + config_with_env_vars = load_config( + config_initializer=TestConfigModel, + config_path=config_with_env_vars_path, + dot_env_path=env_path, + ) + + assert config_with_env_vars.name == "env_name" + assert config_with_env_vars.value == 100 + assert config_with_env_vars.nested.nested_str == "nested_value" + assert config_with_env_vars.nested.nested_int == 42 + assert len(config_with_env_vars.nested_list) == 2 + assert config_with_env_vars.nested_list[0].nested_str == "list_value_1" + assert config_with_env_vars.nested_list[0].nested_int == 7 + assert config_with_env_vars.nested_list[1].nested_str == "list_value_2" + assert config_with_env_vars.nested_list[1].nested_int == 8 diff --git a/tests/unit/query/context_builder/dynamic_community_selection.py b/tests/unit/query/context_builder/dynamic_community_selection.py new file mode 100644 index 0000000000..ba63f0c774 --- /dev/null +++ b/tests/unit/query/context_builder/dynamic_community_selection.py @@ -0,0 +1,205 @@ +# Copyright (c) 2024 Microsoft Corporation. +# Licensed under the MIT License + +"""Tests for dynamic community selection with type handling.""" + +from unittest.mock import MagicMock + +from graphrag.data_model.community import Community +from graphrag.data_model.community_report import CommunityReport +from graphrag.query.context_builder.dynamic_community_selection import ( + DynamicCommunitySelection, +) + + +def create_mock_tokenizer() -> MagicMock: + """Create a mock tokenizer.""" + tokenizer = MagicMock() + tokenizer.encode.return_value = [1, 2, 3] + return tokenizer + + +def create_mock_model() -> MagicMock: + """Create a mock chat model.""" + return MagicMock() + + +def test_dynamic_community_selection_handles_int_children(): + """Test that DynamicCommunitySelection correctly handles children IDs as integers. + + This tests the fix for issue #2004 where children IDs could be integers + while self.reports keys are strings, causing child communities to be skipped. + """ + # Create communities with integer children (simulating the bug scenario) + # Note: Even though the type annotation says list[str], actual data may have ints + communities = [ + Community( + id="comm-0", + short_id="0", + title="Root Community", + level="0", + parent="", + children=[1, 2], # type: ignore[list-item] # Integer children - testing bug fix + ), + Community( + id="comm-1", + short_id="1", + title="Child Community 1", + level="1", + parent="0", + children=[], + ), + Community( + id="comm-2", + short_id="2", + title="Child Community 2", + level="1", + parent="0", + children=[], + ), + ] + + # Create community reports with string community_id + reports = [ + CommunityReport( + id="report-0", + short_id="0", + title="Report 0", + community_id="0", + summary="Root community summary", + full_content="Root community full content", + rank=1.0, + ), + CommunityReport( + id="report-1", + short_id="1", + title="Report 1", + community_id="1", + summary="Child 1 summary", + full_content="Child 1 full content", + rank=1.0, + ), + CommunityReport( + id="report-2", + short_id="2", + title="Report 2", + community_id="2", + summary="Child 2 summary", + full_content="Child 2 full content", + rank=1.0, + ), + ] + + model = create_mock_model() + tokenizer = create_mock_tokenizer() + + selector = DynamicCommunitySelection( + community_reports=reports, + communities=communities, + model=model, + tokenizer=tokenizer, + threshold=1, + keep_parent=False, + max_level=2, + ) + + # Verify that reports are keyed by string + assert "0" in selector.reports + assert "1" in selector.reports + assert "2" in selector.reports + + # Verify that communities are keyed by string short_id + assert "0" in selector.communities + assert "1" in selector.communities + assert "2" in selector.communities + + # Verify that the children are properly accessible + # Before the fix, int children would fail the `in self.reports` check + root_community = selector.communities["0"] + for child in root_community.children: + child_id = str(child) + # This should now work with the fix + assert child_id in selector.reports, ( + f"Child {child} (as '{child_id}') should be found in reports" + ) + + +def test_dynamic_community_selection_handles_str_children(): + """Test that DynamicCommunitySelection works correctly with string children IDs.""" + communities = [ + Community( + id="comm-0", + short_id="0", + title="Root Community", + level="0", + parent="", + children=["1", "2"], # String children - expected type + ), + Community( + id="comm-1", + short_id="1", + title="Child Community 1", + level="1", + parent="0", + children=[], + ), + Community( + id="comm-2", + short_id="2", + title="Child Community 2", + level="1", + parent="0", + children=[], + ), + ] + + reports = [ + CommunityReport( + id="report-0", + short_id="0", + title="Report 0", + community_id="0", + summary="Root community summary", + full_content="Root community full content", + rank=1.0, + ), + CommunityReport( + id="report-1", + short_id="1", + title="Report 1", + community_id="1", + summary="Child 1 summary", + full_content="Child 1 full content", + rank=1.0, + ), + CommunityReport( + id="report-2", + short_id="2", + title="Report 2", + community_id="2", + summary="Child 2 summary", + full_content="Child 2 full content", + rank=1.0, + ), + ] + + model = create_mock_model() + tokenizer = create_mock_tokenizer() + + selector = DynamicCommunitySelection( + community_reports=reports, + communities=communities, + model=model, + tokenizer=tokenizer, + threshold=1, + keep_parent=False, + max_level=2, + ) + + # Verify that children can be found in reports + root_community = selector.communities["0"] + for child in root_community.children: + child_id = str(child) + assert child_id in selector.reports, ( + f"Child {child} (as '{child_id}') should be found in reports" + ) diff --git a/tests/unit/query/context_builder/test_entity_extraction.py b/tests/unit/query/context_builder/test_entity_extraction.py index 8a15e8e929..c20c34c395 100644 --- a/tests/unit/query/context_builder/test_entity_extraction.py +++ b/tests/unit/query/context_builder/test_entity_extraction.py @@ -3,34 +3,42 @@ from typing import Any -from graphrag.config.models.vector_store_schema_config import VectorStoreSchemaConfig from graphrag.data_model.entity import Entity -from graphrag.data_model.types import TextEmbedder -from graphrag.language_model.manager import ModelManager from graphrag.query.context_builder.entity_extraction import ( EntityVectorStoreKey, map_query_to_entities, ) -from graphrag.vector_stores.base import ( - BaseVectorStore, +from graphrag_llm.config import LLMProviderType, ModelConfig +from graphrag_llm.embedding import create_embedding +from graphrag_vectors import ( + TextEmbedder, + VectorStore, VectorStoreDocument, VectorStoreSearchResult, ) +embedding_model = create_embedding( + ModelConfig( + type=LLMProviderType.MockLLM, + model_provider="openai", + model="text-embedding-3-small", + mock_responses=[1.0, 1.0, 1.0], + ) +) + -class MockBaseVectorStore(BaseVectorStore): +class MockVectorStore(VectorStore): def __init__(self, documents: list[VectorStoreDocument]) -> None: - super().__init__( - vector_store_schema_config=VectorStoreSchemaConfig(index_name="mock") - ) + super().__init__(index_name="mock") self.documents = documents def connect(self, **kwargs: Any) -> None: raise NotImplementedError - def load_documents( - self, documents: list[VectorStoreDocument], overwrite: bool = True - ) -> None: + def create_index(self) -> None: + raise NotImplementedError + + def load_documents(self, documents: list[VectorStoreDocument]) -> None: raise NotImplementedError def similarity_search_by_vector( @@ -47,16 +55,14 @@ def similarity_search_by_text( return sorted( [ VectorStoreSearchResult( - document=document, score=abs(len(text) - len(document.text or "")) + document=document, + score=abs(len(text) - len(str(document.id) or "")), ) for document in self.documents ], key=lambda x: x.score, )[:k] - def filter_by_id(self, include_ids: list[str] | list[int]) -> Any: - return [document for document in self.documents if document.id in include_ids] - def search_by_id(self, id: str) -> VectorStoreDocument: result = self.documents[0] result.id = id @@ -93,35 +99,10 @@ def test_map_query_to_entities(): assert map_query_to_entities( query="t22", - text_embedding_vectorstore=MockBaseVectorStore([ - VectorStoreDocument(id=entity.id, text=entity.title, vector=None) - for entity in entities + text_embedding_vectorstore=MockVectorStore([ + VectorStoreDocument(id=entity.title, vector=None) for entity in entities ]), - text_embedder=ModelManager().get_or_create_embedding_model( - model_type="mock_embedding", name="mock" - ), - all_entities_dict={entity.id: entity for entity in entities}, - embedding_vectorstore_key=EntityVectorStoreKey.ID, - k=1, - oversample_scaler=1, - ) == [ - Entity( - id="c4f93564-4507-4ee4-b102-98add401a965", - short_id="sid2", - title="t22", - rank=4, - ) - ] - - assert map_query_to_entities( - query="t22", - text_embedding_vectorstore=MockBaseVectorStore([ - VectorStoreDocument(id=entity.title, text=entity.title, vector=None) - for entity in entities - ]), - text_embedder=ModelManager().get_or_create_embedding_model( - model_type="mock_embedding", name="mock" - ), + text_embedder=embedding_model, all_entities_dict={entity.id: entity for entity in entities}, embedding_vectorstore_key=EntityVectorStoreKey.TITLE, k=1, @@ -137,40 +118,10 @@ def test_map_query_to_entities(): assert map_query_to_entities( query="", - text_embedding_vectorstore=MockBaseVectorStore([ - VectorStoreDocument(id=entity.id, text=entity.title, vector=None) - for entity in entities + text_embedding_vectorstore=MockVectorStore([ + VectorStoreDocument(id=entity.id, vector=None) for entity in entities ]), - text_embedder=ModelManager().get_or_create_embedding_model( - model_type="mock_embedding", name="mock" - ), - all_entities_dict={entity.id: entity for entity in entities}, - embedding_vectorstore_key=EntityVectorStoreKey.ID, - k=2, - ) == [ - Entity( - id="c4f93564-4507-4ee4-b102-98add401a965", - short_id="sid2", - title="t22", - rank=4, - ), - Entity( - id="8fd6d72a-8e9d-4183-8a97-c38bcc971c83", - short_id="sid4", - title="t4444", - rank=3, - ), - ] - - assert map_query_to_entities( - query="", - text_embedding_vectorstore=MockBaseVectorStore([ - VectorStoreDocument(id=entity.id, text=entity.title, vector=None) - for entity in entities - ]), - text_embedder=ModelManager().get_or_create_embedding_model( - model_type="mock_embedding", name="mock" - ), + text_embedder=embedding_model, all_entities_dict={entity.id: entity for entity in entities}, embedding_vectorstore_key=EntityVectorStoreKey.TITLE, k=2, diff --git a/tests/unit/utils/test_embeddings.py b/tests/unit/utils/test_embeddings.py deleted file mode 100644 index 9349f0c813..0000000000 --- a/tests/unit/utils/test_embeddings.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -import pytest - -from graphrag.config.embeddings import create_index_name - - -def test_create_index_name(): - collection = create_index_name("default", "entity.title") - assert collection == "default-entity-title" - - -def test_create_index_name_invalid_embedding_throws(): - with pytest.raises(KeyError): - create_index_name("default", "invalid.name") - - -def test_create_index_name_invalid_embedding_does_not_throw(): - collection = create_index_name("default", "invalid.name", validate=False) - assert collection == "default-invalid-name" diff --git a/tests/unit/utils/test_encoding.py b/tests/unit/utils/test_encoding.py index aca5575b2e..7ad83b5d7f 100644 --- a/tests/unit/utils/test_encoding.py +++ b/tests/unit/utils/test_encoding.py @@ -8,7 +8,9 @@ def test_encode_basic(): tokenizer = get_tokenizer() result = tokenizer.encode("abc def") - assert result == [13997, 711], "Encoding failed to return expected tokens" + assert result == [26682, 1056], ( + f"Encoding failed to return expected tokens, sent {result}" + ) def test_num_tokens_empty_input(): diff --git a/tests/verbs/data/communities.parquet b/tests/verbs/data/communities.parquet index d8a5c82dec..76f255886c 100644 Binary files a/tests/verbs/data/communities.parquet and b/tests/verbs/data/communities.parquet differ diff --git a/tests/verbs/data/community_reports.parquet b/tests/verbs/data/community_reports.parquet index 7600bb66c9..3998d1f203 100644 Binary files a/tests/verbs/data/community_reports.parquet and b/tests/verbs/data/community_reports.parquet differ diff --git a/tests/verbs/data/covariates.parquet b/tests/verbs/data/covariates.parquet index 92c2a1a753..0160267dd2 100644 Binary files a/tests/verbs/data/covariates.parquet and b/tests/verbs/data/covariates.parquet differ diff --git a/tests/verbs/data/documents.parquet b/tests/verbs/data/documents.parquet index 654c5c7f5e..8176b28f34 100644 Binary files a/tests/verbs/data/documents.parquet and b/tests/verbs/data/documents.parquet differ diff --git a/tests/verbs/data/entities.parquet b/tests/verbs/data/entities.parquet index 8de7e6da15..0d7d047d05 100644 Binary files a/tests/verbs/data/entities.parquet and b/tests/verbs/data/entities.parquet differ diff --git a/tests/verbs/data/relationships.parquet b/tests/verbs/data/relationships.parquet index f926d44091..b96143352a 100644 Binary files a/tests/verbs/data/relationships.parquet and b/tests/verbs/data/relationships.parquet differ diff --git a/tests/verbs/data/text_units.parquet b/tests/verbs/data/text_units.parquet index b47de34e9b..88368a9f05 100644 Binary files a/tests/verbs/data/text_units.parquet and b/tests/verbs/data/text_units.parquet differ diff --git a/tests/verbs/data/text_units_metadata.parquet b/tests/verbs/data/text_units_metadata.parquet deleted file mode 100644 index 71ec620e2a..0000000000 Binary files a/tests/verbs/data/text_units_metadata.parquet and /dev/null differ diff --git a/tests/verbs/data/text_units_metadata_included_chunk.parquet b/tests/verbs/data/text_units_metadata_included_chunk.parquet deleted file mode 100644 index ccf4b3903e..0000000000 Binary files a/tests/verbs/data/text_units_metadata_included_chunk.parquet and /dev/null differ diff --git a/tests/verbs/test_create_base_text_units.py b/tests/verbs/test_create_base_text_units.py index ea34ae8b9c..34bad99dc7 100644 --- a/tests/verbs/test_create_base_text_units.py +++ b/tests/verbs/test_create_base_text_units.py @@ -1,16 +1,15 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -from graphrag.config.create_graphrag_config import create_graphrag_config from graphrag.index.workflows.create_base_text_units import run_workflow from graphrag.utils.storage import load_table_from_storage +from tests.unit.config.utils import get_default_graphrag_config + from .util import ( - DEFAULT_MODEL_CONFIG, compare_outputs, create_test_context, load_test_table, - update_document_metadata, ) @@ -19,50 +18,19 @@ async def test_create_base_text_units(): context = await create_test_context() - config = create_graphrag_config({"models": DEFAULT_MODEL_CONFIG}) + config = get_default_graphrag_config() + config.chunking.prepend_metadata = ["title"] await run_workflow(config, context) actual = await load_table_from_storage("text_units", context.output_storage) - compare_outputs(actual, expected, columns=["text", "document_ids", "n_tokens"]) - - -async def test_create_base_text_units_metadata(): - expected = load_test_table("text_units_metadata") - - context = await create_test_context() - - config = create_graphrag_config({"models": DEFAULT_MODEL_CONFIG}) - # test data was created with 4o, so we need to match the encoding for chunks to be identical - config.chunks.encoding_model = "o200k_base" - config.input.metadata = ["title"] - config.chunks.prepend_metadata = True - - await update_document_metadata(config.input.metadata, context) + print("EXPECTED") + print(expected.columns) + print(expected) - await run_workflow(config, context) - - actual = await load_table_from_storage("text_units", context.output_storage) - compare_outputs(actual, expected) + print("ACTUAL") + print(actual.columns) + print(actual) - -async def test_create_base_text_units_metadata_included_in_chunk(): - expected = load_test_table("text_units_metadata_included_chunk") - - context = await create_test_context() - - config = create_graphrag_config({"models": DEFAULT_MODEL_CONFIG}) - # test data was created with 4o, so we need to match the encoding for chunks to be identical - config.chunks.encoding_model = "o200k_base" - config.input.metadata = ["title"] - config.chunks.prepend_metadata = True - config.chunks.chunk_size_includes_metadata = True - - await update_document_metadata(config.input.metadata, context) - - await run_workflow(config, context) - - actual = await load_table_from_storage("text_units", context.output_storage) - # only check the columns from the base workflow - our expected table is the final and will have more - compare_outputs(actual, expected, columns=["text", "document_ids", "n_tokens"]) + compare_outputs(actual, expected, columns=["text", "document_id", "n_tokens"]) diff --git a/tests/verbs/test_create_communities.py b/tests/verbs/test_create_communities.py index 1f51667cf3..d5505d7a31 100644 --- a/tests/verbs/test_create_communities.py +++ b/tests/verbs/test_create_communities.py @@ -1,15 +1,15 @@ # Copyright (c) 2024 Microsoft Corporation. # Licensed under the MIT License -from graphrag.config.create_graphrag_config import create_graphrag_config from graphrag.data_model.schemas import COMMUNITIES_FINAL_COLUMNS from graphrag.index.workflows.create_communities import ( run_workflow, ) from graphrag.utils.storage import load_table_from_storage +from tests.unit.config.utils import get_default_graphrag_config + from .util import ( - DEFAULT_MODEL_CONFIG, compare_outputs, create_test_context, load_test_table, @@ -26,7 +26,7 @@ async def test_create_communities(): ], ) - config = create_graphrag_config({"models": DEFAULT_MODEL_CONFIG}) + config = get_default_graphrag_config() await run_workflow( config, diff --git a/tests/verbs/test_create_community_reports.py b/tests/verbs/test_create_community_reports.py index 56fe4a6221..a36b6c7a66 100644 --- a/tests/verbs/test_create_community_reports.py +++ b/tests/verbs/test_create_community_reports.py @@ -2,8 +2,6 @@ # Licensed under the MIT License -from graphrag.config.create_graphrag_config import create_graphrag_config -from graphrag.config.enums import ModelType from graphrag.data_model.schemas import COMMUNITY_REPORTS_FINAL_COLUMNS from graphrag.index.operations.summarize_communities.community_reports_extractor import ( CommunityReportResponse, @@ -14,8 +12,9 @@ ) from graphrag.utils.storage import load_table_from_storage +from tests.unit.config.utils import get_default_graphrag_config + from .util import ( - DEFAULT_MODEL_CONFIG, compare_outputs, create_test_context, load_test_table, @@ -35,7 +34,7 @@ summary="", explanation=" PipelineRunContext: """Create a test context with tables loaded into storage storage.""" @@ -68,7 +45,10 @@ def compare_outputs( ) for column in cols: - assert column in actual.columns + try: + assert column in actual.columns + except AssertionError: + print(f"Column '{column}' not found in actual output.") try: # dtypes can differ since the test data is read from parquet and our workflow runs in memory if column != "id": # don't check uuids @@ -79,17 +59,9 @@ def compare_outputs( check_index=False, ) except AssertionError: + print(f"Column '{column}' does not match.") print("Expected:") print(expected[column]) print("Actual:") print(actual[column]) raise - - -async def update_document_metadata(metadata: list[str], context: PipelineRunContext): - """Takes the default documents and adds the configured metadata columns for later parsing by the text units and final documents workflows.""" - documents = await load_table_from_storage("documents", context.output_storage) - documents["metadata"] = documents[metadata].apply(lambda row: row.to_dict(), axis=1) - await write_table_to_storage( - documents, "documents", context.output_storage - ) # write to the runtime context storage only diff --git a/unified-search-app/README.md b/unified-search-app/README.md index 0f74a8c67e..6e7ccc82f0 100644 --- a/unified-search-app/README.md +++ b/unified-search-app/README.md @@ -15,14 +15,7 @@ We recommend always using a virtual environment: - `source .venv/bin/activate` ## Run index -Use GraphRAG to index your dataset before running Unified Search. We recommend starting with the [Getting Started guide](https://microsoft.github.io/graphrag/get_started/). You need to run GraphRAG indexing with graph embedding umap enabled to use the functionalities of Unified Search. -``` yaml -embed_graph: - enabled: true - -umap: - enabled: true -``` +Use GraphRAG to index your dataset before running Unified Search. We recommend starting with the [Getting Started guide](https://microsoft.github.io/graphrag/get_started/). ## Datasets Unified Search supports multiple GraphRAG indexes by using a directory listing file. Create a `listing.json` file in the root folder where all your datasets are stored (locally or in blob storage), with the following format (one entry per dataset): @@ -46,8 +39,8 @@ Unified Search supports multiple GraphRAG indexes by using a directory listing f For example, if you have a folder of GraphRAG indexes called "projects" and inside that you ran the Getting Started instructions, your listing.json in the projects folder could look like: ```json [{ - "key": "ragtest-demo", - "path": "ragtest", + "key": "christmas-demo", + "path": "christmas", "name": "A Christmas Carol", "description": "Getting Started index of the novel A Christmas Carol", "community_level": 2 @@ -110,9 +103,9 @@ In the right panel you have several functionalities. 1. At the top you can see general information related to the chosen dataset (name and description). 2. Below the dataset information there is a button labeled "Suggest some questions" which analyzes the dataset using global search and generates the most important questions (the number of questions generated is the amount set in the configuration panel). If you want to select a question generated you have to click the checkbox at the left side of the question to select it. 3. A textbox that it is labeled as "Ask a question to compare the results" where you can type the question that you want to send. -4. Two tabs called Search and Graph Explorer: +4. Two tabs called Search and Community Explorer: 1. Search: Here all the searches results are displayed with their citations. - 2. Graph Explorer: This tab is divided in three sections: Community Reports, Entity Graph and Selected Report. + 2. Community Explorer: This tab is divided in two sections: Community Reports List, and Selected Report. ##### Suggest some question clicked ![Suggest some question clicked](images/image-2.png) @@ -120,8 +113,8 @@ In the right panel you have several functionalities. ##### Selected question clicked ![Selected question clicked](images/image-3.png) -##### Graph Explorer tab -![Graph Explorer tab](images/image-4.png) +##### Community Explorer tab +![Community Explorer tab](images/image-4.png) diff --git a/unified-search-app/app/app_logic.py b/unified-search-app/app/app_logic.py index a573b9daa5..dc64e0e77c 100644 --- a/unified-search-app/app/app_logic.py +++ b/unified-search-app/app/app_logic.py @@ -7,6 +7,7 @@ import logging from typing import TYPE_CHECKING +import graphrag.api as api import streamlit as st from knowledge_loader.data_sources.loader import ( create_datasource, @@ -17,8 +18,6 @@ from state.session_variables import SessionVariables from ui.search import display_search_result -import graphrag.api as api - if TYPE_CHECKING: import pandas as pd diff --git a/unified-search-app/app/home_page.py b/unified-search-app/app/home_page.py index 47aa070e6e..6a249f0e5c 100644 --- a/unified-search-app/app/home_page.py +++ b/unified-search-app/app/home_page.py @@ -10,7 +10,6 @@ from rag.typing import SearchType from st_tabs import TabBar from state.session_variables import SessionVariables -from ui.full_graph import create_full_graph_ui from ui.questions_list import create_questions_list_ui from ui.report_details import create_report_details_ui from ui.report_list import create_report_list_ui @@ -78,7 +77,7 @@ def on_change(sv: SessionVariables): st.button(label="Reset", on_click=on_click_reset, kwargs={"sv": sv}) tab_id = TabBar( - tabs=["Search", "Graph Explorer"], + tabs=["Search", "Community Explorer"], color="#fc9e9e", activeColor="#ff4b4b", default=0, @@ -237,20 +236,12 @@ def on_change(sv: SessionVariables): st.write(e) if tab_id == 1: - report_list, graph, report_content = st.columns([0.20, 0.55, 0.25]) + report_list, report_content = st.columns([0.33, 0.67]) with report_list: st.markdown("##### Community Reports") create_report_list_ui(sv) - with graph: - title, dropdown = st.columns([0.80, 0.20]) - title.markdown("##### Entity Graph (All entities)") - dropdown.selectbox( - "Community level", options=[0, 1], key=sv.graph_community_level.key - ) - create_full_graph_ui(sv) - with report_content: st.markdown("##### Selected Report") create_report_details_ui(sv) diff --git a/unified-search-app/app/knowledge_loader/data_sources/blob_source.py b/unified-search-app/app/knowledge_loader/data_sources/blob_source.py index 8f86b8dfa7..63774d79f7 100644 --- a/unified-search-app/app/knowledge_loader/data_sources/blob_source.py +++ b/unified-search-app/app/knowledge_loader/data_sources/blob_source.py @@ -13,10 +13,8 @@ import yaml from azure.identity import DefaultAzureCredential from azure.storage.blob import BlobServiceClient, ContainerClient -from knowledge_loader.data_sources.typing import Datasource - -from graphrag.config.create_graphrag_config import create_graphrag_config from graphrag.config.models.graph_rag_config import GraphRagConfig +from knowledge_loader.data_sources.typing import Datasource from .default import blob_account_name, blob_container_name @@ -115,7 +113,7 @@ def read_settings( str_settings = settings.read().decode("utf-8") config = os.path.expandvars(str_settings) settings_yaml = yaml.safe_load(config) - graphrag_config = create_graphrag_config(values=settings_yaml) + graphrag_config = GraphRagConfig(**settings_yaml) except Exception as err: if throw_on_missing: error_msg = f"File {file} does not exist" diff --git a/unified-search-app/app/knowledge_loader/data_sources/local_source.py b/unified-search-app/app/knowledge_loader/data_sources/local_source.py index f68f616858..bd6370ba23 100644 --- a/unified-search-app/app/knowledge_loader/data_sources/local_source.py +++ b/unified-search-app/app/knowledge_loader/data_sources/local_source.py @@ -8,10 +8,9 @@ from pathlib import Path import pandas as pd -from knowledge_loader.data_sources.typing import Datasource - from graphrag.config.load_config import load_config from graphrag.config.models.graph_rag_config import GraphRagConfig +from knowledge_loader.data_sources.typing import Datasource logging.basicConfig(level=logging.INFO) logging.getLogger("azure").setLevel(logging.WARNING) diff --git a/unified-search-app/app/knowledge_loader/data_sources/typing.py b/unified-search-app/app/knowledge_loader/data_sources/typing.py index 95fc2f120b..2f595f00ec 100644 --- a/unified-search-app/app/knowledge_loader/data_sources/typing.py +++ b/unified-search-app/app/knowledge_loader/data_sources/typing.py @@ -8,7 +8,6 @@ from enum import Enum import pandas as pd - from graphrag.config.models.graph_rag_config import GraphRagConfig diff --git a/unified-search-app/app/ui/full_graph.py b/unified-search-app/app/ui/full_graph.py deleted file mode 100644 index e2017c9ea8..0000000000 --- a/unified-search-app/app/ui/full_graph.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2024 Microsoft Corporation. -# Licensed under the MIT License - -"""Full graph module.""" - -import altair as alt -import pandas as pd -import streamlit as st -from state.session_variables import SessionVariables - - -def create_full_graph_ui(sv: SessionVariables): - """Return graph UI object.""" - entities = sv.entities.value.copy() - communities = sv.communities.value.copy() - - if not communities.empty and not entities.empty: - communities_entities = ( - communities.explode("entity_ids") - .merge( - entities, - left_on="entity_ids", - right_on="id", - suffixes=("_entities", "_communities"), - ) - .dropna(subset=["x", "y"]) - ) - else: - communities_entities = pd.DataFrame() - - level = sv.graph_community_level.value - communities_entities_filtered = communities_entities[ - communities_entities["level"] == level - ] - - graph = ( - alt.Chart(communities_entities_filtered) - .mark_circle() - .encode( - x="x", - y="y", - color=alt.Color( - "community", - scale=alt.Scale( - domain=communities_entities_filtered["community"].unique(), - scheme="category10", - ), - ), - size=alt.Size("degree", scale=alt.Scale(range=[50, 1000]), legend=None), - tooltip=["id_entities", "type", "description", "community"], - ) - .properties(height=1000) - .configure_axis(disable=True) - ) - st.altair_chart(graph, use_container_width=True) - return graph diff --git a/unified-search-app/app/ui/report_details.py b/unified-search-app/app/ui/report_details.py index 3b5b7a41d7..606be50aee 100644 --- a/unified-search-app/app/ui/report_details.py +++ b/unified-search-app/app/ui/report_details.py @@ -58,7 +58,8 @@ def create_report_details_ui(sv: SessionVariables): st.write("Error parsing report.") st.write(sv.selected_report.value.full_content_json) text_replacement = ( - text.replace("Entity_Relationships", "Relationships") + text + .replace("Entity_Relationships", "Relationships") .replace("Entity_Claims", "Claims") .replace("Entity_Details", "Entities") ) diff --git a/unified-search-app/app/ui/report_list.py b/unified-search-app/app/ui/report_list.py index edd221566a..b7f7e1df23 100644 --- a/unified-search-app/app/ui/report_list.py +++ b/unified-search-app/app/ui/report_list.py @@ -13,7 +13,7 @@ def create_report_list_ui(sv: SessionVariables): sv.community_reports.value, height=1000, hide_index=True, - column_order=["id", "title"], + column_order=["human_readable_id", "title"], selection_mode="single-row", on_select="rerun", ) diff --git a/unified-search-app/app/ui/search.py b/unified-search-app/app/ui/search.py index fa5c062f38..5e9d0c61c3 100644 --- a/unified-search-app/app/ui/search.py +++ b/unified-search-app/app/ui/search.py @@ -247,7 +247,8 @@ def render_html_table(df: pd.DataFrame, search_type: str, key: str): else value ) title_value = ( - title_value.replace('"', """) + title_value + .replace('"', """) .replace("'", "'") .replace("\n", " ") .replace("\n\n", " ") diff --git a/unified-search-app/pyproject.toml b/unified-search-app/pyproject.toml index 4dc2cf6519..2b55568f63 100644 --- a/unified-search-app/pyproject.toml +++ b/unified-search-app/pyproject.toml @@ -6,26 +6,26 @@ authors = [ {name = "GraphRAG team"}, ] readme = "README.md" -requires-python = ">=3.10,<3.12" +requires-python = ">=3.11,<3.14" dependencies = [ "streamlit==1.43.0", - "azure-search-documents>=11.4.0", - "azure-storage-blob>=12.20.0", - "azure-identity>=1.16.0", - "graphrag==2.0.0", - "altair>=5.3.0", - "streamlit-agraph>=0.0.45", - "st-tabs>=0.1.1", - "spacy>=3.8.4,<4.0.0", + "azure-search-documents~=11.4", + "azure-storage-blob~=12.20", + "azure-identity~=1.16", + "graphrag==2.5.0", + "altair~=5.3", + "streamlit-agraph~=0.0.45", + "st-tabs~=0.1", + "spacy~=3.8", ] [project.optional-dependencies] dev = [ - "poethepoet>=0.26.1", - "ipykernel>=6.29.4", - "pyright>=1.1.349", - "ruff>=0.4.7", + "poethepoet~=0.26", + "ipykernel~=6.29", + "pyright~=1.1", + "ruff~=0.8", ] [build-system] diff --git a/unified-search-app/uv.lock b/unified-search-app/uv.lock index 0fdb3dbf47..c200f831cd 100644 --- a/unified-search-app/uv.lock +++ b/unified-search-app/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 1 requires-python = ">=3.10, <3.12" resolution-markers = [ "python_full_version >= '3.11'", @@ -10,18 +10,18 @@ resolution-markers = [ name = "aiofiles" version = "24.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, ] [[package]] name = "aiolimiter" version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/23/b52debf471f7a1e42e362d959a3982bdcb4fe13a5d46e63d28868807a79c/aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9", size = 7185, upload-time = "2024-12-08T15:31:51.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/23/b52debf471f7a1e42e362d959a3982bdcb4fe13a5d46e63d28868807a79c/aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9", size = 7185 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/ba/df6e8e1045aebc4778d19b8a3a9bc1808adb1619ba94ca354d9ba17d86c3/aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7", size = 6711, upload-time = "2024-12-08T15:31:49.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ba/df6e8e1045aebc4778d19b8a3a9bc1808adb1619ba94ca354d9ba17d86c3/aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7", size = 6711 }, ] [[package]] @@ -35,18 +35,18 @@ dependencies = [ { name = "packaging" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305, upload-time = "2024-11-23T23:39:58.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200, upload-time = "2024-11-23T23:39:56.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 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" }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] @@ -59,27 +59,27 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, ] [[package]] name = "anytree" version = "2.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/a8/eb55fab589c56f9b6be2b3fd6997aa04bb6f3da93b01154ce6fc8e799db2/anytree-2.13.0.tar.gz", hash = "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714", size = 48389, upload-time = "2025-04-08T21:06:30.662Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/a8/eb55fab589c56f9b6be2b3fd6997aa04bb6f3da93b01154ce6fc8e799db2/anytree-2.13.0.tar.gz", hash = "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714", size = 48389 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/98/f6aa7fe0783e42be3093d8ef1b0ecdc22c34c0d69640dfb37f56925cb141/anytree-2.13.0-py3-none-any.whl", hash = "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff", size = 45077, upload-time = "2025-04-08T21:06:29.494Z" }, + { url = "https://files.pythonhosted.org/packages/7b/98/f6aa7fe0783e42be3093d8ef1b0ecdc22c34c0d69640dfb37f56925cb141/anytree-2.13.0-py3-none-any.whl", hash = "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff", size = 45077 }, ] [[package]] name = "appnope" version = "0.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170 } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, ] [[package]] @@ -89,18 +89,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/1d/f03bcb60c4a3212e15f99a56085d93093a497718adf828d050b9d675da81/asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0", size = 62284, upload-time = "2023-10-26T10:03:05.06Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/1d/f03bcb60c4a3212e15f99a56085d93093a497718adf828d050b9d675da81/asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0", size = 62284 } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764, upload-time = "2023-10-26T10:03:01.789Z" }, + { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764 }, ] [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, ] [[package]] @@ -110,18 +110,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/1c/3c24ec03c8ba4decc742b1df5a10c52f98c84ca8797757f313e7bdcdf276/autograd-1.8.0.tar.gz", hash = "sha256:107374ded5b09fc8643ac925348c0369e7b0e73bbed9565ffd61b8fd04425683", size = 2562146, upload-time = "2025-05-05T12:49:02.502Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1c/3c24ec03c8ba4decc742b1df5a10c52f98c84ca8797757f313e7bdcdf276/autograd-1.8.0.tar.gz", hash = "sha256:107374ded5b09fc8643ac925348c0369e7b0e73bbed9565ffd61b8fd04425683", size = 2562146 } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ea/e16f0c423f7d83cf8b79cae9452040fb7b2e020c7439a167ee7c317de448/autograd-1.8.0-py3-none-any.whl", hash = "sha256:4ab9084294f814cf56c280adbe19612546a35574d67c574b04933c7d2ecb7d78", size = 51478, upload-time = "2025-05-05T12:49:00.585Z" }, + { url = "https://files.pythonhosted.org/packages/84/ea/e16f0c423f7d83cf8b79cae9452040fb7b2e020c7439a167ee7c317de448/autograd-1.8.0-py3-none-any.whl", hash = "sha256:4ab9084294f814cf56c280adbe19612546a35574d67c574b04933c7d2ecb7d78", size = 51478 }, ] [[package]] name = "azure-common" version = "1.1.28" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914, upload-time = "2022-02-03T19:39:44.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914 } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462, upload-time = "2022-02-03T19:39:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462 }, ] [[package]] @@ -133,9 +133,9 @@ dependencies = [ { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/89/f53968635b1b2e53e4aad2dd641488929fef4ca9dfb0b97927fa7697ddf3/azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c", size = 339689, upload-time = "2025-07-03T00:55:23.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/89/f53968635b1b2e53e4aad2dd641488929fef4ca9dfb0b97927fa7697ddf3/azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c", size = 339689 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/78/bf94897361fdd650850f0f2e405b2293e2f12808239046232bdedf554301/azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1", size = 210708, upload-time = "2025-07-03T00:55:25.238Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/bf94897361fdd650850f0f2e405b2293e2f12808239046232bdedf554301/azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1", size = 210708 }, ] [[package]] @@ -146,9 +146,9 @@ dependencies = [ { name = "azure-core" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/7c/a4e7810f85e7f83d94265ef5ff0fb1efad55a768de737d940151ea2eec45/azure_cosmos-4.9.0.tar.gz", hash = "sha256:c70db4cbf55b0ff261ed7bb8aa325a5dfa565d3c6eaa43d75d26ae5e2ad6d74f", size = 1824155, upload-time = "2024-11-19T04:09:30.195Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/7c/a4e7810f85e7f83d94265ef5ff0fb1efad55a768de737d940151ea2eec45/azure_cosmos-4.9.0.tar.gz", hash = "sha256:c70db4cbf55b0ff261ed7bb8aa325a5dfa565d3c6eaa43d75d26ae5e2ad6d74f", size = 1824155 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/dc/380f843744535497acd0b85aacb59565c84fc28bf938c8d6e897a858cd95/azure_cosmos-4.9.0-py3-none-any.whl", hash = "sha256:3b60eaa01a16a857d0faf0cec304bac6fa8620a81bc268ce760339032ef617fe", size = 303157, upload-time = "2024-11-19T04:09:32.148Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/380f843744535497acd0b85aacb59565c84fc28bf938c8d6e897a858cd95/azure_cosmos-4.9.0-py3-none-any.whl", hash = "sha256:3b60eaa01a16a857d0faf0cec304bac6fa8620a81bc268ce760339032ef617fe", size = 303157 }, ] [[package]] @@ -162,9 +162,9 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/29/1201ffbb6a57a16524dd91f3e741b4c828a70aaba436578bdcb3fbcb438c/azure_identity-1.23.1.tar.gz", hash = "sha256:226c1ef982a9f8d5dcf6e0f9ed35eaef2a4d971e7dd86317e9b9d52e70a035e4", size = 266185, upload-time = "2025-07-15T19:16:38.077Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/29/1201ffbb6a57a16524dd91f3e741b4c828a70aaba436578bdcb3fbcb438c/azure_identity-1.23.1.tar.gz", hash = "sha256:226c1ef982a9f8d5dcf6e0f9ed35eaef2a4d971e7dd86317e9b9d52e70a035e4", size = 266185 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/b3/e2d7ab810eb68575a5c7569b03c0228b8f4ce927ffa6211471b526f270c9/azure_identity-1.23.1-py3-none-any.whl", hash = "sha256:7eed28baa0097a47e3fb53bd35a63b769e6b085bb3cb616dfce2b67f28a004a1", size = 186810, upload-time = "2025-07-15T19:16:40.184Z" }, + { url = "https://files.pythonhosted.org/packages/99/b3/e2d7ab810eb68575a5c7569b03c0228b8f4ce927ffa6211471b526f270c9/azure_identity-1.23.1-py3-none-any.whl", hash = "sha256:7eed28baa0097a47e3fb53bd35a63b769e6b085bb3cb616dfce2b67f28a004a1", size = 186810 }, ] [[package]] @@ -177,9 +177,9 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/11/9ecde2bd9e6c00cc0e3f312ab096a33d333f8ba40c847f01f94d524895fe/azure_search_documents-11.5.3.tar.gz", hash = "sha256:6931149ec0db90485d78648407f18ea4271420473c7cb646bf87790374439989", size = 300353, upload-time = "2025-06-25T16:48:58.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/11/9ecde2bd9e6c00cc0e3f312ab096a33d333f8ba40c847f01f94d524895fe/azure_search_documents-11.5.3.tar.gz", hash = "sha256:6931149ec0db90485d78648407f18ea4271420473c7cb646bf87790374439989", size = 300353 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/f5/0f6b52567cbb33f1efba13060514ed7088a86de84d74b77cda17d278bcd9/azure_search_documents-11.5.3-py3-none-any.whl", hash = "sha256:110617751c6c8bd50b1f0af2b00a478bd4fbaf4e2f0387e3454c26ec3eb433d6", size = 298772, upload-time = "2025-06-25T16:49:00.764Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f5/0f6b52567cbb33f1efba13060514ed7088a86de84d74b77cda17d278bcd9/azure_search_documents-11.5.3-py3-none-any.whl", hash = "sha256:110617751c6c8bd50b1f0af2b00a478bd4fbaf4e2f0387e3454c26ec3eb433d6", size = 298772 }, ] [[package]] @@ -192,53 +192,53 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/95/3e3414491ce45025a1cde107b6ae72bf72049e6021597c201cd6a3029b9a/azure_storage_blob-12.26.0.tar.gz", hash = "sha256:5dd7d7824224f7de00bfeb032753601c982655173061e242f13be6e26d78d71f", size = 583332, upload-time = "2025-07-16T21:34:07.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/95/3e3414491ce45025a1cde107b6ae72bf72049e6021597c201cd6a3029b9a/azure_storage_blob-12.26.0.tar.gz", hash = "sha256:5dd7d7824224f7de00bfeb032753601c982655173061e242f13be6e26d78d71f", size = 583332 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/64/63dbfdd83b31200ac58820a7951ddfdeed1fbee9285b0f3eae12d1357155/azure_storage_blob-12.26.0-py3-none-any.whl", hash = "sha256:8c5631b8b22b4f53ec5fff2f3bededf34cfef111e2af613ad42c9e6de00a77fe", size = 412907, upload-time = "2025-07-16T21:34:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/5b/64/63dbfdd83b31200ac58820a7951ddfdeed1fbee9285b0f3eae12d1357155/azure_storage_blob-12.26.0-py3-none-any.whl", hash = "sha256:8c5631b8b22b4f53ec5fff2f3bededf34cfef111e2af613ad42c9e6de00a77fe", size = 412907 }, ] [[package]] name = "backports-datetime-fromisoformat" version = "2.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/81/eff3184acb1d9dc3ce95a98b6f3c81a49b4be296e664db8e1c2eeabef3d9/backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d", size = 23588, upload-time = "2024-12-28T20:18:15.017Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/4b/d6b051ca4b3d76f23c2c436a9669f3be616b8cf6461a7e8061c7c4269642/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4", size = 27561, upload-time = "2024-12-28T20:16:47.974Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/e39b0d471e55eb1b5c7c81edab605c02f71c786d59fb875f0a6f23318747/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c", size = 34448, upload-time = "2024-12-28T20:16:50.712Z" }, - { url = "https://files.pythonhosted.org/packages/f2/28/7a5c87c5561d14f1c9af979231fdf85d8f9fad7a95ff94e56d2205e2520a/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b", size = 27093, upload-time = "2024-12-28T20:16:52.994Z" }, - { url = "https://files.pythonhosted.org/packages/80/ba/f00296c5c4536967c7d1136107fdb91c48404fe769a4a6fd5ab045629af8/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4", size = 52836, upload-time = "2024-12-28T20:16:55.283Z" }, - { url = "https://files.pythonhosted.org/packages/e3/92/bb1da57a069ddd601aee352a87262c7ae93467e66721d5762f59df5021a6/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22", size = 52798, upload-time = "2024-12-28T20:16:56.64Z" }, - { url = "https://files.pythonhosted.org/packages/df/ef/b6cfd355982e817ccdb8d8d109f720cab6e06f900784b034b30efa8fa832/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf", size = 52891, upload-time = "2024-12-28T20:16:58.887Z" }, - { url = "https://files.pythonhosted.org/packages/37/39/b13e3ae8a7c5d88b68a6e9248ffe7066534b0cfe504bf521963e61b6282d/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58", size = 52955, upload-time = "2024-12-28T20:17:00.028Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e4/70cffa3ce1eb4f2ff0c0d6f5d56285aacead6bd3879b27a2ba57ab261172/backports_datetime_fromisoformat-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc", size = 29323, upload-time = "2024-12-28T20:17:01.125Z" }, - { url = "https://files.pythonhosted.org/packages/62/f5/5bc92030deadf34c365d908d4533709341fb05d0082db318774fdf1b2bcb/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443", size = 27626, upload-time = "2024-12-28T20:17:03.448Z" }, - { url = "https://files.pythonhosted.org/packages/28/45/5885737d51f81dfcd0911dd5c16b510b249d4c4cf6f4a991176e0358a42a/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0", size = 34588, upload-time = "2024-12-28T20:17:04.459Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6d/bd74de70953f5dd3e768c8fc774af942af0ce9f211e7c38dd478fa7ea910/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005", size = 27162, upload-time = "2024-12-28T20:17:06.752Z" }, - { url = "https://files.pythonhosted.org/packages/47/ba/1d14b097f13cce45b2b35db9898957578b7fcc984e79af3b35189e0d332f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75", size = 54482, upload-time = "2024-12-28T20:17:08.15Z" }, - { url = "https://files.pythonhosted.org/packages/25/e9/a2a7927d053b6fa148b64b5e13ca741ca254c13edca99d8251e9a8a09cfe/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45", size = 54362, upload-time = "2024-12-28T20:17:10.605Z" }, - { url = "https://files.pythonhosted.org/packages/c1/99/394fb5e80131a7d58c49b89e78a61733a9994885804a0bb582416dd10c6f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01", size = 54162, upload-time = "2024-12-28T20:17:12.301Z" }, - { url = "https://files.pythonhosted.org/packages/88/25/1940369de573c752889646d70b3fe8645e77b9e17984e72a554b9b51ffc4/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6", size = 54118, upload-time = "2024-12-28T20:17:13.609Z" }, - { url = "https://files.pythonhosted.org/packages/b7/46/f275bf6c61683414acaf42b2df7286d68cfef03e98b45c168323d7707778/backports_datetime_fromisoformat-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061", size = 29329, upload-time = "2024-12-28T20:17:16.124Z" }, - { url = "https://files.pythonhosted.org/packages/be/03/7eaa9f9bf290395d57fd30d7f1f2f9dff60c06a31c237dc2beb477e8f899/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039", size = 28980, upload-time = "2024-12-28T20:18:06.554Z" }, - { url = "https://files.pythonhosted.org/packages/47/80/a0ecf33446c7349e79f54cc532933780341d20cff0ee12b5bfdcaa47067e/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06", size = 28449, upload-time = "2024-12-28T20:18:07.77Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/71/81/eff3184acb1d9dc3ce95a98b6f3c81a49b4be296e664db8e1c2eeabef3d9/backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d", size = 23588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/4b/d6b051ca4b3d76f23c2c436a9669f3be616b8cf6461a7e8061c7c4269642/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4", size = 27561 }, + { url = "https://files.pythonhosted.org/packages/6d/40/e39b0d471e55eb1b5c7c81edab605c02f71c786d59fb875f0a6f23318747/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c", size = 34448 }, + { url = "https://files.pythonhosted.org/packages/f2/28/7a5c87c5561d14f1c9af979231fdf85d8f9fad7a95ff94e56d2205e2520a/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b", size = 27093 }, + { url = "https://files.pythonhosted.org/packages/80/ba/f00296c5c4536967c7d1136107fdb91c48404fe769a4a6fd5ab045629af8/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4", size = 52836 }, + { url = "https://files.pythonhosted.org/packages/e3/92/bb1da57a069ddd601aee352a87262c7ae93467e66721d5762f59df5021a6/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22", size = 52798 }, + { url = "https://files.pythonhosted.org/packages/df/ef/b6cfd355982e817ccdb8d8d109f720cab6e06f900784b034b30efa8fa832/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf", size = 52891 }, + { url = "https://files.pythonhosted.org/packages/37/39/b13e3ae8a7c5d88b68a6e9248ffe7066534b0cfe504bf521963e61b6282d/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58", size = 52955 }, + { url = "https://files.pythonhosted.org/packages/1e/e4/70cffa3ce1eb4f2ff0c0d6f5d56285aacead6bd3879b27a2ba57ab261172/backports_datetime_fromisoformat-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc", size = 29323 }, + { url = "https://files.pythonhosted.org/packages/62/f5/5bc92030deadf34c365d908d4533709341fb05d0082db318774fdf1b2bcb/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443", size = 27626 }, + { url = "https://files.pythonhosted.org/packages/28/45/5885737d51f81dfcd0911dd5c16b510b249d4c4cf6f4a991176e0358a42a/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0", size = 34588 }, + { url = "https://files.pythonhosted.org/packages/bc/6d/bd74de70953f5dd3e768c8fc774af942af0ce9f211e7c38dd478fa7ea910/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005", size = 27162 }, + { url = "https://files.pythonhosted.org/packages/47/ba/1d14b097f13cce45b2b35db9898957578b7fcc984e79af3b35189e0d332f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75", size = 54482 }, + { url = "https://files.pythonhosted.org/packages/25/e9/a2a7927d053b6fa148b64b5e13ca741ca254c13edca99d8251e9a8a09cfe/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45", size = 54362 }, + { url = "https://files.pythonhosted.org/packages/c1/99/394fb5e80131a7d58c49b89e78a61733a9994885804a0bb582416dd10c6f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01", size = 54162 }, + { url = "https://files.pythonhosted.org/packages/88/25/1940369de573c752889646d70b3fe8645e77b9e17984e72a554b9b51ffc4/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6", size = 54118 }, + { url = "https://files.pythonhosted.org/packages/b7/46/f275bf6c61683414acaf42b2df7286d68cfef03e98b45c168323d7707778/backports_datetime_fromisoformat-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061", size = 29329 }, + { url = "https://files.pythonhosted.org/packages/be/03/7eaa9f9bf290395d57fd30d7f1f2f9dff60c06a31c237dc2beb477e8f899/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039", size = 28980 }, + { url = "https://files.pythonhosted.org/packages/47/80/a0ecf33446c7349e79f54cc532933780341d20cff0ee12b5bfdcaa47067e/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06", size = 28449 }, ] [[package]] name = "beartype" version = "0.18.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/15/4e623478a9628ad4cee2391f19aba0b16c1dd6fedcb2a399f0928097b597/beartype-0.18.5.tar.gz", hash = "sha256:264ddc2f1da9ec94ff639141fbe33d22e12a9f75aa863b83b7046ffff1381927", size = 1193506, upload-time = "2024-04-21T07:25:58.64Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/15/4e623478a9628ad4cee2391f19aba0b16c1dd6fedcb2a399f0928097b597/beartype-0.18.5.tar.gz", hash = "sha256:264ddc2f1da9ec94ff639141fbe33d22e12a9f75aa863b83b7046ffff1381927", size = 1193506 } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/43/7a1259741bd989723272ac7d381a43be932422abcff09a1d9f7ba212cb74/beartype-0.18.5-py3-none-any.whl", hash = "sha256:5301a14f2a9a5540fe47ec6d34d758e9cd8331d36c4760fc7a5499ab86310089", size = 917762, upload-time = "2024-04-21T07:25:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/64/43/7a1259741bd989723272ac7d381a43be932422abcff09a1d9f7ba212cb74/beartype-0.18.5-py3-none-any.whl", hash = "sha256:5301a14f2a9a5540fe47ec6d34d758e9cd8331d36c4760fc7a5499ab86310089", size = 917762 }, ] [[package]] name = "blinker" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, ] [[package]] @@ -248,53 +248,53 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/aa/0743c994884de83472c854bb534c9edab8d711e1880d4fa194e6d876bb60/blis-1.2.1.tar.gz", hash = "sha256:1066beedbedc2143c22bd28742658de05694afebacde8d8c2d14dd4b5a96765a", size = 2510297, upload-time = "2025-04-01T12:01:56.849Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/83/9f74b0f768628ddc213502446900dbe133dd2d7aa12f2b462e119ce61952/blis-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:112443b90698158ada38f71e74c079c3561e802554a51e9850d487c39db25de0", size = 6973893, upload-time = "2025-04-01T12:01:03.13Z" }, - { url = "https://files.pythonhosted.org/packages/a7/47/b503681ddd77c6cabcf192c566a476b09f3dbecf10652abb3e6c1c11df0b/blis-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b9f8c4fbc303f47778d1fd47916cae785b6f3beaa2031502112a8c0aa5eb29f6", size = 1280909, upload-time = "2025-04-01T12:01:05.576Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/7bf08ee499938b0237b206e11578e37d0086558bdc063bfff39d6bdf8247/blis-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0260ecbbaa890f11d8c88e9ce37d4fc9a91839adc34ba1763ba89424362e54c9", size = 2982233, upload-time = "2025-04-01T12:01:07.711Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b3/37a90ff44d51aada91a33e9e64a35d6424ccfaac49cd5d090e2f1ac46ba2/blis-1.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b70e0693564444b608d765727ab31618de3b92c5f203b9dc6b6a108170a8cea", size = 3187098, upload-time = "2025-04-01T12:01:09.624Z" }, - { url = "https://files.pythonhosted.org/packages/30/f2/b52d4c18b116dc3feda9269e3f944defe1e9d12ec157b1ae5ec823191834/blis-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67ae48f73828cf38f65f24b6c6d8ec16f22c99820e0d13e7d97370682fdb023d", size = 11526282, upload-time = "2025-04-01T12:01:11.322Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3a/3979ebe9629fe0040cc8768c9b02791bc6995aa3518ad29dcbd452b05555/blis-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9eff1af9b142fd156a7b83f513061f2e464c4409afb37080fde436e969951703", size = 3000400, upload-time = "2025-04-01T12:01:13.339Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bb/c102d2583cd51d541e4785989d6025d40372661e2aa40e908d5bf073a17f/blis-1.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d05f07fd37b407edb294322d3b2991b0950a61123076cc380d3e9c3deba77c83", size = 4226030, upload-time = "2025-04-01T12:01:15.264Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/cd57b4bd0d2a207e36fd8f5625bc63541129258666f267804c661ca0e12f/blis-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8d5abc324180918a4d7ef81f31c37907d13e85f2831317cba3edacd4ef9b7d39", size = 14694442, upload-time = "2025-04-01T12:01:17.453Z" }, - { url = "https://files.pythonhosted.org/packages/84/50/fd53ebc7eb911f1db0e802b35d1247b44df1cfdad550eea565dba74c0eb4/blis-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:8de9a1e536202064b57c60d09ff0886275b50c5878df6d58fb49c731eaf535a7", size = 6249165, upload-time = "2025-04-01T12:01:19.535Z" }, - { url = "https://files.pythonhosted.org/packages/67/57/ae6596b1e27859886e0b81fb99497bcfff139895585a9e2284681c8a8846/blis-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:778c4f72b71f97187e3304acfbd30eab98c9ba1a5b03b65128bc3875400ae604", size = 6976808, upload-time = "2025-04-01T12:01:21.175Z" }, - { url = "https://files.pythonhosted.org/packages/ce/35/6225e6ad2bccf23ac124448d59112c098d63a8917462e9f73967bc217168/blis-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c5f2ffb0ae9c1f5aaa95b9681bcdd9a777d007c501fa220796329b939ca2790", size = 1281913, upload-time = "2025-04-01T12:01:23.202Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/c6a6d1c0a8a00799d2ec5db05d676bd9a9b0472cac4d3eff2e2fd1953521/blis-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4dc5d2d57106bb411633603a5c7d178a0845267c3efc7e5ea4fa7a44772976", size = 3104139, upload-time = "2025-04-01T12:01:24.781Z" }, - { url = "https://files.pythonhosted.org/packages/a5/6c/c5fab7ed1fe6e8bdcda732017400d1adc53db5b6dd2c2a6046acab91f4fa/blis-1.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c621271c2843101927407e052b35a67f853da59d5c74e9e070e982c7f82e2e04", size = 3304143, upload-time = "2025-04-01T12:01:27.363Z" }, - { url = "https://files.pythonhosted.org/packages/22/d1/85f03269886253758546fcfdbeddee7e717d843ea134596b60db9c2648c4/blis-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43f65f882250b817566d7543abd1f6da297f1662e5dd9936e14c04b88285a497", size = 11660080, upload-time = "2025-04-01T12:01:29.478Z" }, - { url = "https://files.pythonhosted.org/packages/78/c8/c81ed3036e8ce0d6ce0d19a032c7f3d69247f221c5357e18548dea9380d3/blis-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78a0613d559ccc426c101c67e8f84e1f93491e29d722c370872c538ee652bd07", size = 3133133, upload-time = "2025-04-01T12:01:31.537Z" }, - { url = "https://files.pythonhosted.org/packages/b8/42/7c296e04b979204777ecae2fe9287ac7b0255d8c4c2111d2a735c439b9d7/blis-1.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2f5e32e5e5635fc7087b724b53120dbcd86201f56c0405882ce254bc0e493392", size = 4360695, upload-time = "2025-04-01T12:01:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/aa5c8dfd0068d2cc976830797dd092779259860f964286db05739154e3a7/blis-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d339c97cc83f53e39c1013d0dcd7d5278c853dc102d931132eeb05b226e28429", size = 14828081, upload-time = "2025-04-01T12:01:35.129Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c0/047fef3ac4a531903c52ba7c108fd608556627723bfef7554f040b10e556/blis-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:8d284323cc994e9b818c32046f1aa3e57bcc41c74e02daebdf0d3bc3e14355cb", size = 6232639, upload-time = "2025-04-01T12:01:37.268Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/61/aa/0743c994884de83472c854bb534c9edab8d711e1880d4fa194e6d876bb60/blis-1.2.1.tar.gz", hash = "sha256:1066beedbedc2143c22bd28742658de05694afebacde8d8c2d14dd4b5a96765a", size = 2510297 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/83/9f74b0f768628ddc213502446900dbe133dd2d7aa12f2b462e119ce61952/blis-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:112443b90698158ada38f71e74c079c3561e802554a51e9850d487c39db25de0", size = 6973893 }, + { url = "https://files.pythonhosted.org/packages/a7/47/b503681ddd77c6cabcf192c566a476b09f3dbecf10652abb3e6c1c11df0b/blis-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b9f8c4fbc303f47778d1fd47916cae785b6f3beaa2031502112a8c0aa5eb29f6", size = 1280909 }, + { url = "https://files.pythonhosted.org/packages/ee/9e/7bf08ee499938b0237b206e11578e37d0086558bdc063bfff39d6bdf8247/blis-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0260ecbbaa890f11d8c88e9ce37d4fc9a91839adc34ba1763ba89424362e54c9", size = 2982233 }, + { url = "https://files.pythonhosted.org/packages/c6/b3/37a90ff44d51aada91a33e9e64a35d6424ccfaac49cd5d090e2f1ac46ba2/blis-1.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b70e0693564444b608d765727ab31618de3b92c5f203b9dc6b6a108170a8cea", size = 3187098 }, + { url = "https://files.pythonhosted.org/packages/30/f2/b52d4c18b116dc3feda9269e3f944defe1e9d12ec157b1ae5ec823191834/blis-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67ae48f73828cf38f65f24b6c6d8ec16f22c99820e0d13e7d97370682fdb023d", size = 11526282 }, + { url = "https://files.pythonhosted.org/packages/a9/3a/3979ebe9629fe0040cc8768c9b02791bc6995aa3518ad29dcbd452b05555/blis-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9eff1af9b142fd156a7b83f513061f2e464c4409afb37080fde436e969951703", size = 3000400 }, + { url = "https://files.pythonhosted.org/packages/d3/bb/c102d2583cd51d541e4785989d6025d40372661e2aa40e908d5bf073a17f/blis-1.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d05f07fd37b407edb294322d3b2991b0950a61123076cc380d3e9c3deba77c83", size = 4226030 }, + { url = "https://files.pythonhosted.org/packages/6b/16/cd57b4bd0d2a207e36fd8f5625bc63541129258666f267804c661ca0e12f/blis-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8d5abc324180918a4d7ef81f31c37907d13e85f2831317cba3edacd4ef9b7d39", size = 14694442 }, + { url = "https://files.pythonhosted.org/packages/84/50/fd53ebc7eb911f1db0e802b35d1247b44df1cfdad550eea565dba74c0eb4/blis-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:8de9a1e536202064b57c60d09ff0886275b50c5878df6d58fb49c731eaf535a7", size = 6249165 }, + { url = "https://files.pythonhosted.org/packages/67/57/ae6596b1e27859886e0b81fb99497bcfff139895585a9e2284681c8a8846/blis-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:778c4f72b71f97187e3304acfbd30eab98c9ba1a5b03b65128bc3875400ae604", size = 6976808 }, + { url = "https://files.pythonhosted.org/packages/ce/35/6225e6ad2bccf23ac124448d59112c098d63a8917462e9f73967bc217168/blis-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c5f2ffb0ae9c1f5aaa95b9681bcdd9a777d007c501fa220796329b939ca2790", size = 1281913 }, + { url = "https://files.pythonhosted.org/packages/7a/84/c6a6d1c0a8a00799d2ec5db05d676bd9a9b0472cac4d3eff2e2fd1953521/blis-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4dc5d2d57106bb411633603a5c7d178a0845267c3efc7e5ea4fa7a44772976", size = 3104139 }, + { url = "https://files.pythonhosted.org/packages/a5/6c/c5fab7ed1fe6e8bdcda732017400d1adc53db5b6dd2c2a6046acab91f4fa/blis-1.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c621271c2843101927407e052b35a67f853da59d5c74e9e070e982c7f82e2e04", size = 3304143 }, + { url = "https://files.pythonhosted.org/packages/22/d1/85f03269886253758546fcfdbeddee7e717d843ea134596b60db9c2648c4/blis-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43f65f882250b817566d7543abd1f6da297f1662e5dd9936e14c04b88285a497", size = 11660080 }, + { url = "https://files.pythonhosted.org/packages/78/c8/c81ed3036e8ce0d6ce0d19a032c7f3d69247f221c5357e18548dea9380d3/blis-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78a0613d559ccc426c101c67e8f84e1f93491e29d722c370872c538ee652bd07", size = 3133133 }, + { url = "https://files.pythonhosted.org/packages/b8/42/7c296e04b979204777ecae2fe9287ac7b0255d8c4c2111d2a735c439b9d7/blis-1.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2f5e32e5e5635fc7087b724b53120dbcd86201f56c0405882ce254bc0e493392", size = 4360695 }, + { url = "https://files.pythonhosted.org/packages/0c/ab/aa5c8dfd0068d2cc976830797dd092779259860f964286db05739154e3a7/blis-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d339c97cc83f53e39c1013d0dcd7d5278c853dc102d931132eeb05b226e28429", size = 14828081 }, + { url = "https://files.pythonhosted.org/packages/7c/c0/047fef3ac4a531903c52ba7c108fd608556627723bfef7554f040b10e556/blis-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:8d284323cc994e9b818c32046f1aa3e57bcc41c74e02daebdf0d3bc3e14355cb", size = 6232639 }, ] [[package]] name = "cachetools" version = "5.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, ] [[package]] name = "catalogue" version = "2.0.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/b4/244d58127e1cdf04cf2dc7d9566f0d24ef01d5ce21811bab088ecc62b5ea/catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15", size = 19561, upload-time = "2023-09-25T06:29:24.962Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b4/244d58127e1cdf04cf2dc7d9566f0d24ef01d5ce21811bab088ecc62b5ea/catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15", size = 19561 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/96/d32b941a501ab566a16358d68b6eb4e4acc373fab3c3c4d7d9e649f7b4bb/catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f", size = 17325, upload-time = "2023-09-25T06:29:23.337Z" }, + { url = "https://files.pythonhosted.org/packages/9e/96/d32b941a501ab566a16358d68b6eb4e4acc373fab3c3c4d7d9e649f7b4bb/catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f", size = 17325 }, ] [[package]] name = "certifi" version = "2025.7.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722 }, ] [[package]] @@ -304,67 +304,67 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, ] [[package]] @@ -374,9 +374,9 @@ 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" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 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" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] [[package]] @@ -386,27 +386,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/15/ae3256348834b92b9594d73eb7230538bae2bf726c2d721b920a668017c5/cloudpathlib-0.21.1.tar.gz", hash = "sha256:f26a855abf34d98f267aafd15efdb2db3c9665913dbabe5fad079df92837a431", size = 45295, upload-time = "2025-05-15T02:32:05.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/15/ae3256348834b92b9594d73eb7230538bae2bf726c2d721b920a668017c5/cloudpathlib-0.21.1.tar.gz", hash = "sha256:f26a855abf34d98f267aafd15efdb2db3c9665913dbabe5fad079df92837a431", size = 45295 } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/e7/6fea57b887f8e367c1e4a496ba03bfaf57824b766f777723ce1faf28834b/cloudpathlib-0.21.1-py3-none-any.whl", hash = "sha256:bfe580ad72ec030472ec233cd7380701b2d3227da7b2898387bd170aa70c803c", size = 52776, upload-time = "2025-05-15T02:32:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/6fea57b887f8e367c1e4a496ba03bfaf57824b766f777723ce1faf28834b/cloudpathlib-0.21.1-py3-none-any.whl", hash = "sha256:bfe580ad72ec030472ec233cd7380701b2d3227da7b2898387bd170aa70c803c", size = 52776 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 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" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "comm" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319 } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294 }, ] [[package]] @@ -417,9 +417,9 @@ dependencies = [ { name = "pydantic" }, { name = "srsly" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/d3/57c6631159a1b48d273b40865c315cf51f89df7a9d1101094ef12e3a37c2/confection-0.1.5.tar.gz", hash = "sha256:8e72dd3ca6bd4f48913cd220f10b8275978e740411654b6e8ca6d7008c590f0e", size = 38924, upload-time = "2024-05-31T16:17:01.559Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/d3/57c6631159a1b48d273b40865c315cf51f89df7a9d1101094ef12e3a37c2/confection-0.1.5.tar.gz", hash = "sha256:8e72dd3ca6bd4f48913cd220f10b8275978e740411654b6e8ca6d7008c590f0e", size = 38924 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/00/3106b1854b45bd0474ced037dfe6b73b90fe68a68968cef47c23de3d43d2/confection-0.1.5-py3-none-any.whl", hash = "sha256:e29d3c3f8eac06b3f77eb9dfb4bf2fc6bcc9622a98ca00a698e3d019c6430b14", size = 35451, upload-time = "2024-05-31T16:16:59.075Z" }, + { url = "https://files.pythonhosted.org/packages/0c/00/3106b1854b45bd0474ced037dfe6b73b90fe68a68968cef47c23de3d43d2/confection-0.1.5-py3-none-any.whl", hash = "sha256:e29d3c3f8eac06b3f77eb9dfb4bf2fc6bcc9622a98ca00a698e3d019c6430b14", size = 35451 }, ] [[package]] @@ -432,34 +432,34 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, - { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, - { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, - { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, - { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, - { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, - { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, - { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, - { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551 }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399 }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061 }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956 }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872 }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027 }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641 }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075 }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534 }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188 }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636 }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636 }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053 }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985 }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750 }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246 }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728 }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762 }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196 }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017 }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681 }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101 }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599 }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807 }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729 }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791 }, ] [[package]] @@ -472,24 +472,24 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, - { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773 }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149 }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222 }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234 }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555 }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238 }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218 }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867 }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677 }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234 }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123 }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809 }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593 }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202 }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207 }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315 }, ] [[package]] @@ -499,101 +499,101 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" }, - { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, - { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, - { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, - { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" }, - { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" }, - { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, - { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, - { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, - { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, - { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, - { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, - { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8b/34394337abe4566848a2bd49b26bcd4b07fd466afd3e8cce4cb79a390869/cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd", size = 3575762, upload-time = "2025-07-02T13:05:53.166Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5d/a19441c1e89afb0f173ac13178606ca6fab0d3bd3ebc29e9ed1318b507fc/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097", size = 4140906, upload-time = "2025-07-02T13:05:55.914Z" }, - { url = "https://files.pythonhosted.org/packages/4b/db/daceb259982a3c2da4e619f45b5bfdec0e922a23de213b2636e78ef0919b/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e", size = 4374411, upload-time = "2025-07-02T13:05:57.814Z" }, - { url = "https://files.pythonhosted.org/packages/6a/35/5d06ad06402fc522c8bf7eab73422d05e789b4e38fe3206a85e3d6966c11/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30", size = 4140942, upload-time = "2025-07-02T13:06:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/65/79/020a5413347e44c382ef1f7f7e7a66817cd6273e3e6b5a72d18177b08b2f/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e", size = 4374079, upload-time = "2025-07-02T13:06:02.043Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c5/c0e07d84a9a2a8a0ed4f865e58f37c71af3eab7d5e094ff1b21f3f3af3bc/cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d", size = 3321362, upload-time = "2025-07-02T13:06:04.463Z" }, - { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878, upload-time = "2025-07-02T13:06:06.339Z" }, - { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447, upload-time = "2025-07-02T13:06:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778, upload-time = "2025-07-02T13:06:10.263Z" }, - { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627, upload-time = "2025-07-02T13:06:13.097Z" }, - { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593, upload-time = "2025-07-02T13:06:15.689Z" }, - { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106, upload-time = "2025-07-02T13:06:18.058Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092 }, + { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926 }, + { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235 }, + { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785 }, + { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050 }, + { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379 }, + { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355 }, + { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087 }, + { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873 }, + { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651 }, + { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050 }, + { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224 }, + { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143 }, + { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780 }, + { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091 }, + { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711 }, + { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299 }, + { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558 }, + { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020 }, + { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759 }, + { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991 }, + { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189 }, + { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769 }, + { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016 }, + { url = "https://files.pythonhosted.org/packages/f8/8b/34394337abe4566848a2bd49b26bcd4b07fd466afd3e8cce4cb79a390869/cryptography-45.0.5-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:206210d03c1193f4e1ff681d22885181d47efa1ab3018766a7b32a7b3d6e6afd", size = 3575762 }, + { url = "https://files.pythonhosted.org/packages/8b/5d/a19441c1e89afb0f173ac13178606ca6fab0d3bd3ebc29e9ed1318b507fc/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c648025b6840fe62e57107e0a25f604db740e728bd67da4f6f060f03017d5097", size = 4140906 }, + { url = "https://files.pythonhosted.org/packages/4b/db/daceb259982a3c2da4e619f45b5bfdec0e922a23de213b2636e78ef0919b/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b8fa8b0a35a9982a3c60ec79905ba5bb090fc0b9addcfd3dc2dd04267e45f25e", size = 4374411 }, + { url = "https://files.pythonhosted.org/packages/6a/35/5d06ad06402fc522c8bf7eab73422d05e789b4e38fe3206a85e3d6966c11/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:14d96584701a887763384f3c47f0ca7c1cce322aa1c31172680eb596b890ec30", size = 4140942 }, + { url = "https://files.pythonhosted.org/packages/65/79/020a5413347e44c382ef1f7f7e7a66817cd6273e3e6b5a72d18177b08b2f/cryptography-45.0.5-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57c816dfbd1659a367831baca4b775b2a5b43c003daf52e9d57e1d30bc2e1b0e", size = 4374079 }, + { url = "https://files.pythonhosted.org/packages/9b/c5/c0e07d84a9a2a8a0ed4f865e58f37c71af3eab7d5e094ff1b21f3f3af3bc/cryptography-45.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b9e38e0a83cd51e07f5a48ff9691cae95a79bea28fe4ded168a8e5c6c77e819d", size = 3321362 }, + { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878 }, + { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447 }, + { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778 }, + { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627 }, + { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593 }, + { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106 }, ] [[package]] name = "cycler" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, ] [[package]] name = "cymem" version = "2.0.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4a/1acd761fb6ac4c560e823ce40536a62f886f2d59b2763b5c3fc7e9d92101/cymem-2.0.11.tar.gz", hash = "sha256:efe49a349d4a518be6b6c6b255d4a80f740a341544bde1a807707c058b88d0bd", size = 10346, upload-time = "2025-01-16T21:50:41.045Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4a/1acd761fb6ac4c560e823ce40536a62f886f2d59b2763b5c3fc7e9d92101/cymem-2.0.11.tar.gz", hash = "sha256:efe49a349d4a518be6b6c6b255d4a80f740a341544bde1a807707c058b88d0bd", size = 10346 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/55/f453f2b2f560e057f20eb2acdaafbf6488d72a6e8a36a4aef30f6053a51c/cymem-2.0.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1b4dd8f8c2475c7c9948eefa89c790d83134600858d8d43b90276efd8df3882e", size = 41886, upload-time = "2025-01-16T21:49:17.183Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9d/03299eff35bd4fd80db33e4fd516661b82bb7b898cb677829acf22391ede/cymem-2.0.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d46ba0d2e0f749195297d16f2286b55af7d7c084db2b853fdfccece2c000c5dc", size = 41696, upload-time = "2025-01-16T21:49:18.788Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0c/90aa41f258a67ea210886c5c73f88dc9f120b7a20e6b5d92c5ce73a68276/cymem-2.0.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739c4336b9d04ce9761851e9260ef77508d4a86ee3060e41302bfb6fa82c37de", size = 203719, upload-time = "2025-01-16T21:49:23.13Z" }, - { url = "https://files.pythonhosted.org/packages/52/d1/dc4a72aa2049c34a53a220290b1a59fadae61929dff3a6e1a830a22971fe/cymem-2.0.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a69c470c2fb118161f49761f9137384f46723c77078b659bba33858e19e46b49", size = 204763, upload-time = "2025-01-16T21:49:26.164Z" }, - { url = "https://files.pythonhosted.org/packages/69/51/86ed323585530558bcdda1324c570abe032db2c1d5afd1c5e8e3e8fde63a/cymem-2.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40159f6c92627438de970fd761916e745d70dfd84a7dcc28c1627eb49cee00d8", size = 193964, upload-time = "2025-01-16T21:49:28.057Z" }, - { url = "https://files.pythonhosted.org/packages/ed/0c/aee4ad2996a4e24342228ccf44d7835c7784042f0ee0c47ad33be1443f18/cymem-2.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f503f98e6aa333fffbe657a6854f13a9c3de68860795ae21171284213b9c5c09", size = 195002, upload-time = "2025-01-16T21:49:31.329Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d5/eda823d639258d2ed1db83403c991a9a57d5a4ddea3bf08e59060809a9aa/cymem-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:7f05ed5920cc92d6b958ec5da55bd820d326fe9332b90660e6fa67e3b476ceb1", size = 39079, upload-time = "2025-01-16T21:49:33.777Z" }, - { url = "https://files.pythonhosted.org/packages/03/e3/d98e3976f4ffa99cddebc1ce379d4d62e3eb1da22285267f902c99cc3395/cymem-2.0.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ee54039aad3ef65de82d66c40516bf54586287b46d32c91ea0530c34e8a2745", size = 42005, upload-time = "2025-01-16T21:49:34.977Z" }, - { url = "https://files.pythonhosted.org/packages/41/b4/7546faf2ab63e59befc95972316d62276cec153f7d4d60e7b0d5e08f0602/cymem-2.0.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c05ef75b5db217be820604e43a47ccbbafea98ab6659d07cea92fa3c864ea58", size = 41747, upload-time = "2025-01-16T21:49:36.108Z" }, - { url = "https://files.pythonhosted.org/packages/7d/4e/042f372e5b3eb7f5f3dd7677161771d301de2b6fa3f7c74e1cebcd502552/cymem-2.0.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d5381e5793ce531bac0dbc00829c8381f18605bb67e4b61d34f8850463da40", size = 217647, upload-time = "2025-01-16T21:49:37.433Z" }, - { url = "https://files.pythonhosted.org/packages/48/cb/2207679e4b92701f78cf141e1ab4f81f55247dbe154eb426b842a0a993de/cymem-2.0.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2b9d3f42d7249ac81802135cad51d707def058001a32f73fc7fbf3de7045ac7", size = 218857, upload-time = "2025-01-16T21:49:40.09Z" }, - { url = "https://files.pythonhosted.org/packages/31/7a/76ae3b7a39ab2531029d281e43fcfcaad728c2341b150a81a3a1f5587cf3/cymem-2.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:39b78f2195d20b75c2d465732f6b8e8721c5d4eb012777c2cb89bdb45a043185", size = 206148, upload-time = "2025-01-16T21:49:41.383Z" }, - { url = "https://files.pythonhosted.org/packages/25/f9/d0fc0191ac79f15638ddb59237aa76f234691374d7d7950e10f384bd8a25/cymem-2.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2203bd6525a80d8fd0c94654a263af21c0387ae1d5062cceaebb652bf9bad7bc", size = 207112, upload-time = "2025-01-16T21:49:43.986Z" }, - { url = "https://files.pythonhosted.org/packages/56/c8/75f75889401b20f4c3a7c5965dda09df42913e904ddc2ffe7ef3bdf25061/cymem-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:aa54af7314de400634448da1f935b61323da80a49484074688d344fb2036681b", size = 39360, upload-time = "2025-01-16T21:49:45.479Z" }, + { url = "https://files.pythonhosted.org/packages/6d/55/f453f2b2f560e057f20eb2acdaafbf6488d72a6e8a36a4aef30f6053a51c/cymem-2.0.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1b4dd8f8c2475c7c9948eefa89c790d83134600858d8d43b90276efd8df3882e", size = 41886 }, + { url = "https://files.pythonhosted.org/packages/a6/9d/03299eff35bd4fd80db33e4fd516661b82bb7b898cb677829acf22391ede/cymem-2.0.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d46ba0d2e0f749195297d16f2286b55af7d7c084db2b853fdfccece2c000c5dc", size = 41696 }, + { url = "https://files.pythonhosted.org/packages/d3/0c/90aa41f258a67ea210886c5c73f88dc9f120b7a20e6b5d92c5ce73a68276/cymem-2.0.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739c4336b9d04ce9761851e9260ef77508d4a86ee3060e41302bfb6fa82c37de", size = 203719 }, + { url = "https://files.pythonhosted.org/packages/52/d1/dc4a72aa2049c34a53a220290b1a59fadae61929dff3a6e1a830a22971fe/cymem-2.0.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a69c470c2fb118161f49761f9137384f46723c77078b659bba33858e19e46b49", size = 204763 }, + { url = "https://files.pythonhosted.org/packages/69/51/86ed323585530558bcdda1324c570abe032db2c1d5afd1c5e8e3e8fde63a/cymem-2.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40159f6c92627438de970fd761916e745d70dfd84a7dcc28c1627eb49cee00d8", size = 193964 }, + { url = "https://files.pythonhosted.org/packages/ed/0c/aee4ad2996a4e24342228ccf44d7835c7784042f0ee0c47ad33be1443f18/cymem-2.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f503f98e6aa333fffbe657a6854f13a9c3de68860795ae21171284213b9c5c09", size = 195002 }, + { url = "https://files.pythonhosted.org/packages/eb/d5/eda823d639258d2ed1db83403c991a9a57d5a4ddea3bf08e59060809a9aa/cymem-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:7f05ed5920cc92d6b958ec5da55bd820d326fe9332b90660e6fa67e3b476ceb1", size = 39079 }, + { url = "https://files.pythonhosted.org/packages/03/e3/d98e3976f4ffa99cddebc1ce379d4d62e3eb1da22285267f902c99cc3395/cymem-2.0.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ee54039aad3ef65de82d66c40516bf54586287b46d32c91ea0530c34e8a2745", size = 42005 }, + { url = "https://files.pythonhosted.org/packages/41/b4/7546faf2ab63e59befc95972316d62276cec153f7d4d60e7b0d5e08f0602/cymem-2.0.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c05ef75b5db217be820604e43a47ccbbafea98ab6659d07cea92fa3c864ea58", size = 41747 }, + { url = "https://files.pythonhosted.org/packages/7d/4e/042f372e5b3eb7f5f3dd7677161771d301de2b6fa3f7c74e1cebcd502552/cymem-2.0.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d5381e5793ce531bac0dbc00829c8381f18605bb67e4b61d34f8850463da40", size = 217647 }, + { url = "https://files.pythonhosted.org/packages/48/cb/2207679e4b92701f78cf141e1ab4f81f55247dbe154eb426b842a0a993de/cymem-2.0.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2b9d3f42d7249ac81802135cad51d707def058001a32f73fc7fbf3de7045ac7", size = 218857 }, + { url = "https://files.pythonhosted.org/packages/31/7a/76ae3b7a39ab2531029d281e43fcfcaad728c2341b150a81a3a1f5587cf3/cymem-2.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:39b78f2195d20b75c2d465732f6b8e8721c5d4eb012777c2cb89bdb45a043185", size = 206148 }, + { url = "https://files.pythonhosted.org/packages/25/f9/d0fc0191ac79f15638ddb59237aa76f234691374d7d7950e10f384bd8a25/cymem-2.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2203bd6525a80d8fd0c94654a263af21c0387ae1d5062cceaebb652bf9bad7bc", size = 207112 }, + { url = "https://files.pythonhosted.org/packages/56/c8/75f75889401b20f4c3a7c5965dda09df42913e904ddc2ffe7ef3bdf25061/cymem-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:aa54af7314de400634448da1f935b61323da80a49484074688d344fb2036681b", size = 39360 }, ] [[package]] name = "debugpy" version = "1.8.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/3a9a28ddb750a76eaec445c7f4d3147ea2c579a97dbd9e25d39001b92b21/debugpy-1.8.15.tar.gz", hash = "sha256:58d7a20b7773ab5ee6bdfb2e6cf622fdf1e40c9d5aef2857d85391526719ac00", size = 1643279, upload-time = "2025-07-15T16:43:29.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/3a9a28ddb750a76eaec445c7f4d3147ea2c579a97dbd9e25d39001b92b21/debugpy-1.8.15.tar.gz", hash = "sha256:58d7a20b7773ab5ee6bdfb2e6cf622fdf1e40c9d5aef2857d85391526719ac00", size = 1643279 } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/51/0b4315169f0d945271db037ae6b98c0548a2d48cc036335cd1b2f5516c1b/debugpy-1.8.15-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e9a8125c85172e3ec30985012e7a81ea5e70bbb836637f8a4104f454f9b06c97", size = 2084890, upload-time = "2025-07-15T16:43:31.239Z" }, - { url = "https://files.pythonhosted.org/packages/36/cc/a5391dedb079280d7b72418022e00ba8227ae0b5bc8b2e3d1ecffc5d6b01/debugpy-1.8.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd0b6b5eccaa745c214fd240ea82f46049d99ef74b185a3517dad3ea1ec55d9", size = 3561470, upload-time = "2025-07-15T16:43:32.515Z" }, - { url = "https://files.pythonhosted.org/packages/e8/92/acf64b92010c66b33c077dee3862c733798a2c90e7d14b25c01d771e2a0d/debugpy-1.8.15-cp310-cp310-win32.whl", hash = "sha256:8181cce4d344010f6bfe94a531c351a46a96b0f7987750932b2908e7a1e14a55", size = 5229194, upload-time = "2025-07-15T16:43:33.997Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f5/c58c015c9ff78de35901bea3ab4dbf7946d7a4aa867ee73875df06ba6468/debugpy-1.8.15-cp310-cp310-win_amd64.whl", hash = "sha256:af2dcae4e4cd6e8b35f982ccab29fe65f7e8766e10720a717bc80c464584ee21", size = 5260900, upload-time = "2025-07-15T16:43:35.413Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b3/1c44a2ed311199ab11c2299c9474a6c7cd80d19278defd333aeb7c287995/debugpy-1.8.15-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:babc4fb1962dd6a37e94d611280e3d0d11a1f5e6c72ac9b3d87a08212c4b6dd3", size = 2183442, upload-time = "2025-07-15T16:43:36.733Z" }, - { url = "https://files.pythonhosted.org/packages/f6/69/e2dcb721491e1c294d348681227c9b44fb95218f379aa88e12a19d85528d/debugpy-1.8.15-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f778e68f2986a58479d0ac4f643e0b8c82fdd97c2e200d4d61e7c2d13838eb53", size = 3134215, upload-time = "2025-07-15T16:43:38.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/76/4ce63b95d8294dcf2fd1820860b300a420d077df4e93afcaa25a984c2ca7/debugpy-1.8.15-cp311-cp311-win32.whl", hash = "sha256:f9d1b5abd75cd965e2deabb1a06b0e93a1546f31f9f621d2705e78104377c702", size = 5154037, upload-time = "2025-07-15T16:43:39.471Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a7/e5a7c784465eb9c976d84408873d597dc7ce74a0fc69ed009548a1a94813/debugpy-1.8.15-cp311-cp311-win_amd64.whl", hash = "sha256:62954fb904bec463e2b5a415777f6d1926c97febb08ef1694da0e5d1463c5c3b", size = 5178133, upload-time = "2025-07-15T16:43:40.969Z" }, - { url = "https://files.pythonhosted.org/packages/07/d5/98748d9860e767a1248b5e31ffa7ce8cb7006e97bf8abbf3d891d0a8ba4e/debugpy-1.8.15-py2.py3-none-any.whl", hash = "sha256:bce2e6c5ff4f2e00b98d45e7e01a49c7b489ff6df5f12d881c67d2f1ac635f3d", size = 5282697, upload-time = "2025-07-15T16:44:07.996Z" }, + { url = "https://files.pythonhosted.org/packages/69/51/0b4315169f0d945271db037ae6b98c0548a2d48cc036335cd1b2f5516c1b/debugpy-1.8.15-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e9a8125c85172e3ec30985012e7a81ea5e70bbb836637f8a4104f454f9b06c97", size = 2084890 }, + { url = "https://files.pythonhosted.org/packages/36/cc/a5391dedb079280d7b72418022e00ba8227ae0b5bc8b2e3d1ecffc5d6b01/debugpy-1.8.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd0b6b5eccaa745c214fd240ea82f46049d99ef74b185a3517dad3ea1ec55d9", size = 3561470 }, + { url = "https://files.pythonhosted.org/packages/e8/92/acf64b92010c66b33c077dee3862c733798a2c90e7d14b25c01d771e2a0d/debugpy-1.8.15-cp310-cp310-win32.whl", hash = "sha256:8181cce4d344010f6bfe94a531c351a46a96b0f7987750932b2908e7a1e14a55", size = 5229194 }, + { url = "https://files.pythonhosted.org/packages/3f/f5/c58c015c9ff78de35901bea3ab4dbf7946d7a4aa867ee73875df06ba6468/debugpy-1.8.15-cp310-cp310-win_amd64.whl", hash = "sha256:af2dcae4e4cd6e8b35f982ccab29fe65f7e8766e10720a717bc80c464584ee21", size = 5260900 }, + { url = "https://files.pythonhosted.org/packages/d2/b3/1c44a2ed311199ab11c2299c9474a6c7cd80d19278defd333aeb7c287995/debugpy-1.8.15-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:babc4fb1962dd6a37e94d611280e3d0d11a1f5e6c72ac9b3d87a08212c4b6dd3", size = 2183442 }, + { url = "https://files.pythonhosted.org/packages/f6/69/e2dcb721491e1c294d348681227c9b44fb95218f379aa88e12a19d85528d/debugpy-1.8.15-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f778e68f2986a58479d0ac4f643e0b8c82fdd97c2e200d4d61e7c2d13838eb53", size = 3134215 }, + { url = "https://files.pythonhosted.org/packages/17/76/4ce63b95d8294dcf2fd1820860b300a420d077df4e93afcaa25a984c2ca7/debugpy-1.8.15-cp311-cp311-win32.whl", hash = "sha256:f9d1b5abd75cd965e2deabb1a06b0e93a1546f31f9f621d2705e78104377c702", size = 5154037 }, + { url = "https://files.pythonhosted.org/packages/c2/a7/e5a7c784465eb9c976d84408873d597dc7ce74a0fc69ed009548a1a94813/debugpy-1.8.15-cp311-cp311-win_amd64.whl", hash = "sha256:62954fb904bec463e2b5a415777f6d1926c97febb08ef1694da0e5d1463c5c3b", size = 5178133 }, + { url = "https://files.pythonhosted.org/packages/07/d5/98748d9860e767a1248b5e31ffa7ce8cb7006e97bf8abbf3d891d0a8ba4e/debugpy-1.8.15-py2.py3-none-any.whl", hash = "sha256:bce2e6c5ff4f2e00b98d45e7e01a49c7b489ff6df5f12d881c67d2f1ac635f3d", size = 5282697 }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, ] [[package]] @@ -603,9 +603,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788 } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178 }, ] [[package]] @@ -617,18 +617,18 @@ dependencies = [ { name = "executing" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/75/b78198620640d394bc435c17bb49db18419afdd6cfa3ed8bcfe14034ec80/devtools-0.12.2.tar.gz", hash = "sha256:efceab184cb35e3a11fa8e602cc4fadacaa2e859e920fc6f87bf130b69885507", size = 75005, upload-time = "2023-09-03T16:57:00.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/75/b78198620640d394bc435c17bb49db18419afdd6cfa3ed8bcfe14034ec80/devtools-0.12.2.tar.gz", hash = "sha256:efceab184cb35e3a11fa8e602cc4fadacaa2e859e920fc6f87bf130b69885507", size = 75005 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/ae/afb1487556e2dc827a17097aac8158a25b433a345386f0e249f6d2694ccb/devtools-0.12.2-py3-none-any.whl", hash = "sha256:c366e3de1df4cdd635f1ad8cbcd3af01a384d7abda71900e68d43b04eb6aaca7", size = 19411, upload-time = "2023-09-03T16:56:59.049Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ae/afb1487556e2dc827a17097aac8158a25b433a345386f0e249f6d2694ccb/devtools-0.12.2-py3-none-any.whl", hash = "sha256:c366e3de1df4cdd635f1ad8cbcd3af01a384d7abda71900e68d43b04eb6aaca7", size = 19411 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } 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" }, + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, ] [[package]] @@ -639,9 +639,9 @@ dependencies = [ { name = "marshmallow" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/08/2b7d9cacf2b27482c9218ee6762336aa47bdb9d07ee26a136d072a328297/environs-11.2.1.tar.gz", hash = "sha256:e068ae3174cef52ba4b95ead22e639056a02465f616e62323e04ae08e86a75a4", size = 27485, upload-time = "2024-11-20T17:38:40.795Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/08/2b7d9cacf2b27482c9218ee6762336aa47bdb9d07ee26a136d072a328297/environs-11.2.1.tar.gz", hash = "sha256:e068ae3174cef52ba4b95ead22e639056a02465f616e62323e04ae08e86a75a4", size = 27485 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/21/1e0d8de234e9d0c675ea8fd50f9e7ad66fae32c207bc982f1d14f7c0835b/environs-11.2.1-py3-none-any.whl", hash = "sha256:9d2080cf25807a26fc0d4301e2d7b62c64fbf547540f21e3a30cc02bc5fbe948", size = 12923, upload-time = "2024-11-20T17:38:39.013Z" }, + { url = "https://files.pythonhosted.org/packages/1a/21/1e0d8de234e9d0c675ea8fd50f9e7ad66fae32c207bc982f1d14f7c0835b/environs-11.2.1-py3-none-any.whl", hash = "sha256:9d2080cf25807a26fc0d4301e2d7b62c64fbf547540f21e3a30cc02bc5fbe948", size = 12923 }, ] [[package]] @@ -651,23 +651,23 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, ] [[package]] name = "executing" version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, ] [[package]] name = "fnllm" -version = "0.2.9" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiolimiter" }, @@ -676,9 +676,9 @@ dependencies = [ { name = "pydantic" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/37/3cd445ea54eac04239e5a1e351b0c2970b0bc38edc31467eb2ad557165de/fnllm-0.2.9.tar.gz", hash = "sha256:6760094565741492154bc7e076a5e90e1f013b350419876d3fa426179b97933c", size = 88545, upload-time = "2025-04-07T19:32:33.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/84/bc3d02134a46dd267afbed66a47dc281b252bd8171c94ad22bcc8f924f8b/fnllm-0.4.1.tar.gz", hash = "sha256:80a7450693691bf0832e12a2d70420647bfea35a43cb91c4a9cb5e2f39172b50", size = 93566 } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/6a/dac596ffc5b4b395773ed72abfb4d6b16ca117adec397a0649a80ef41ebc/fnllm-0.2.9-py3-none-any.whl", hash = "sha256:ad09074902cff32c0bf9f36a8e1f327a950b2186613de6198ed8e46e7a941da9", size = 74092, upload-time = "2025-04-07T19:32:31.844Z" }, + { url = "https://files.pythonhosted.org/packages/ac/6a/04db92a7e8d9cf9b73d3c29c38e16d5728069ec1be06a4723f74579499fa/fnllm-0.4.1-py3-none-any.whl", hash = "sha256:22f1b3316a90f29fde94bfe651e0e4963ff68cddb438035ef7c2161e39789ccf", size = 79273 }, ] [package.optional-dependencies] @@ -695,34 +695,34 @@ openai = [ name = "fonttools" version = "4.59.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521, upload-time = "2025-07-16T12:04:54.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/1f/3dcae710b7c4b56e79442b03db64f6c9f10c3348f7af40339dffcefb581e/fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96", size = 2761846, upload-time = "2025-07-16T12:03:33.267Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0e/ae3a1884fa1549acac1191cc9ec039142f6ac0e9cbc139c2e6a3dab967da/fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df", size = 2332060, upload-time = "2025-07-16T12:03:36.472Z" }, - { url = "https://files.pythonhosted.org/packages/75/46/58bff92a7216829159ac7bdb1d05a48ad1b8ab8c539555f12d29fdecfdd4/fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482", size = 4852354, upload-time = "2025-07-16T12:03:39.102Z" }, - { url = "https://files.pythonhosted.org/packages/05/57/767e31e48861045d89691128bd81fd4c62b62150f9a17a666f731ce4f197/fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64", size = 4781132, upload-time = "2025-07-16T12:03:41.415Z" }, - { url = "https://files.pythonhosted.org/packages/d7/78/adb5e9b0af5c6ce469e8b0e112f144eaa84b30dd72a486e9c778a9b03b31/fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db", size = 4832901, upload-time = "2025-07-16T12:03:43.115Z" }, - { url = "https://files.pythonhosted.org/packages/ac/92/bc3881097fbf3d56d112bec308c863c058e5d4c9c65f534e8ae58450ab8a/fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d", size = 4940140, upload-time = "2025-07-16T12:03:44.781Z" }, - { url = "https://files.pythonhosted.org/packages/4a/54/39cdb23f0eeda2e07ae9cb189f2b6f41da89aabc682d3a387b3ff4a4ed29/fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f", size = 2215890, upload-time = "2025-07-16T12:03:46.961Z" }, - { url = "https://files.pythonhosted.org/packages/d8/eb/f8388d9e19f95d8df2449febe9b1a38ddd758cfdb7d6de3a05198d785d61/fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e", size = 2260191, upload-time = "2025-07-16T12:03:48.908Z" }, - { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387, upload-time = "2025-07-16T12:03:51.424Z" }, - { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194, upload-time = "2025-07-16T12:03:53.295Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333, upload-time = "2025-07-16T12:03:55.177Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422, upload-time = "2025-07-16T12:03:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631, upload-time = "2025-07-16T12:03:59.449Z" }, - { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198, upload-time = "2025-07-16T12:04:01.542Z" }, - { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216, upload-time = "2025-07-16T12:04:03.515Z" }, - { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879, upload-time = "2025-07-16T12:04:05.015Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1f/3dcae710b7c4b56e79442b03db64f6c9f10c3348f7af40339dffcefb581e/fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96", size = 2761846 }, + { url = "https://files.pythonhosted.org/packages/eb/0e/ae3a1884fa1549acac1191cc9ec039142f6ac0e9cbc139c2e6a3dab967da/fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df", size = 2332060 }, + { url = "https://files.pythonhosted.org/packages/75/46/58bff92a7216829159ac7bdb1d05a48ad1b8ab8c539555f12d29fdecfdd4/fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482", size = 4852354 }, + { url = "https://files.pythonhosted.org/packages/05/57/767e31e48861045d89691128bd81fd4c62b62150f9a17a666f731ce4f197/fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64", size = 4781132 }, + { url = "https://files.pythonhosted.org/packages/d7/78/adb5e9b0af5c6ce469e8b0e112f144eaa84b30dd72a486e9c778a9b03b31/fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db", size = 4832901 }, + { url = "https://files.pythonhosted.org/packages/ac/92/bc3881097fbf3d56d112bec308c863c058e5d4c9c65f534e8ae58450ab8a/fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d", size = 4940140 }, + { url = "https://files.pythonhosted.org/packages/4a/54/39cdb23f0eeda2e07ae9cb189f2b6f41da89aabc682d3a387b3ff4a4ed29/fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f", size = 2215890 }, + { url = "https://files.pythonhosted.org/packages/d8/eb/f8388d9e19f95d8df2449febe9b1a38ddd758cfdb7d6de3a05198d785d61/fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e", size = 2260191 }, + { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387 }, + { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194 }, + { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333 }, + { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422 }, + { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631 }, + { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198 }, + { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216 }, + { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879 }, + { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050 }, ] [[package]] name = "future" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490 } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326 }, ] [[package]] @@ -734,18 +734,18 @@ dependencies = [ { name = "scipy" }, { name = "smart-open" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/bc/36ce4d510085cf150f17d79bb5e88cde942aeba2a894aed5893812ea1e6d/gensim-4.3.3.tar.gz", hash = "sha256:84852076a6a3d88d7dac5be245e24c21c3b819b565e14c1b61fa3e5ee76dcf57", size = 23258708, upload-time = "2024-07-19T14:42:35.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/bc/36ce4d510085cf150f17d79bb5e88cde942aeba2a894aed5893812ea1e6d/gensim-4.3.3.tar.gz", hash = "sha256:84852076a6a3d88d7dac5be245e24c21c3b819b565e14c1b61fa3e5ee76dcf57", size = 23258708 } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/12/047dc8b6bed7c4833bcdfbafc10af0f96dc3847ce37be63b14bd6e6c7767/gensim-4.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4e72840adfbea35c5804fd559bc0cb6bc9f439926220a37d852b7ce76eb325c1", size = 24086876, upload-time = "2024-07-19T14:39:26.268Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/7c6d7dda41924b83c4b1eb096942b68b85ba305df7f0963ad0642ac0d73f/gensim-4.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4019263c9d9afae7c669f880c17e09461e77a71afce04ed4d79cf71a4cad2848", size = 24041730, upload-time = "2024-07-19T14:39:34.431Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/376290613da44ea9d11bdce3a1705ba7cc25f971edb2b460dc192092068c/gensim-4.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dea62d3e2ada547687bde6cbba37efa50b534db77e9d44fd5802676bb072c9d9", size = 26398007, upload-time = "2024-07-19T14:39:41.67Z" }, - { url = "https://files.pythonhosted.org/packages/de/63/776ee55c773f55fa9d4fc1596f2e5e15de109921a6727dfe29cc4f0baeb7/gensim-4.3.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fac93ef5e44982defef9d3c1e4cd00245506b8a29cec19ec5e00f0221b8144c", size = 26506925, upload-time = "2024-07-19T14:39:48.662Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4a/f07e2f255aedd6bb4bd0ae420a465f228a4a91bc78ac359216ea20557be6/gensim-4.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:7c3409f755fb8d62da99cea65e7a40a99d21f8fd86443a3aaf2d90eb68995021", size = 24012924, upload-time = "2024-07-19T14:39:56.224Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f4/f43fd909aa29fd92f0e6d703d90c0e6507a7c6be3d686a025b1e192afa3a/gensim-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99e7b70352aecc6c1674dde82b75f453e7a5d1cc71ac1cfbc460bf1fe20501b7", size = 24082968, upload-time = "2024-07-19T14:40:03.849Z" }, - { url = "https://files.pythonhosted.org/packages/2a/15/aca2fc3b9e97bd0e28be4a4302793c43757b04b828223c6d103c72132f19/gensim-4.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:32a4cac3f3c38af2069eab9524609fc92ebaeb2692b7280cfda365a3517a280a", size = 24036231, upload-time = "2024-07-19T14:40:10.943Z" }, - { url = "https://files.pythonhosted.org/packages/ef/84/e46049a16fa7daa26ac9e83e41b3bc3b30867da832a5d7cb0779da893255/gensim-4.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c071b4329ed1be02446eb7ef637b94c68cf0080c15c57fbcde667fce2e49c3fe", size = 26558362, upload-time = "2024-07-19T14:40:17.997Z" }, - { url = "https://files.pythonhosted.org/packages/78/4f/f6045d5d5f8e7838c42572607ce440f95dbf4de5da41ae664198c2839c05/gensim-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d662bf96e3d741b6ab61a54be842a7cbf5e45193008b2f4225c758cafd7f9cdc", size = 26662669, upload-time = "2024-07-19T14:40:26.14Z" }, - { url = "https://files.pythonhosted.org/packages/f5/57/f2e6568dbf464a4b270954e5fa3dee4a4054d163a41c0e7bf0a34eb40f0f/gensim-4.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a54bd53a0e6f991abb837f126663353657270e75be53287e8a568ada0b35b1b0", size = 24010102, upload-time = "2024-07-19T14:40:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/27/12/047dc8b6bed7c4833bcdfbafc10af0f96dc3847ce37be63b14bd6e6c7767/gensim-4.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4e72840adfbea35c5804fd559bc0cb6bc9f439926220a37d852b7ce76eb325c1", size = 24086876 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/7c6d7dda41924b83c4b1eb096942b68b85ba305df7f0963ad0642ac0d73f/gensim-4.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4019263c9d9afae7c669f880c17e09461e77a71afce04ed4d79cf71a4cad2848", size = 24041730 }, + { url = "https://files.pythonhosted.org/packages/73/f4/376290613da44ea9d11bdce3a1705ba7cc25f971edb2b460dc192092068c/gensim-4.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dea62d3e2ada547687bde6cbba37efa50b534db77e9d44fd5802676bb072c9d9", size = 26398007 }, + { url = "https://files.pythonhosted.org/packages/de/63/776ee55c773f55fa9d4fc1596f2e5e15de109921a6727dfe29cc4f0baeb7/gensim-4.3.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fac93ef5e44982defef9d3c1e4cd00245506b8a29cec19ec5e00f0221b8144c", size = 26506925 }, + { url = "https://files.pythonhosted.org/packages/cd/4a/f07e2f255aedd6bb4bd0ae420a465f228a4a91bc78ac359216ea20557be6/gensim-4.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:7c3409f755fb8d62da99cea65e7a40a99d21f8fd86443a3aaf2d90eb68995021", size = 24012924 }, + { url = "https://files.pythonhosted.org/packages/7b/f4/f43fd909aa29fd92f0e6d703d90c0e6507a7c6be3d686a025b1e192afa3a/gensim-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99e7b70352aecc6c1674dde82b75f453e7a5d1cc71ac1cfbc460bf1fe20501b7", size = 24082968 }, + { url = "https://files.pythonhosted.org/packages/2a/15/aca2fc3b9e97bd0e28be4a4302793c43757b04b828223c6d103c72132f19/gensim-4.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:32a4cac3f3c38af2069eab9524609fc92ebaeb2692b7280cfda365a3517a280a", size = 24036231 }, + { url = "https://files.pythonhosted.org/packages/ef/84/e46049a16fa7daa26ac9e83e41b3bc3b30867da832a5d7cb0779da893255/gensim-4.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c071b4329ed1be02446eb7ef637b94c68cf0080c15c57fbcde667fce2e49c3fe", size = 26558362 }, + { url = "https://files.pythonhosted.org/packages/78/4f/f6045d5d5f8e7838c42572607ce440f95dbf4de5da41ae664198c2839c05/gensim-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d662bf96e3d741b6ab61a54be842a7cbf5e45193008b2f4225c758cafd7f9cdc", size = 26662669 }, + { url = "https://files.pythonhosted.org/packages/f5/57/f2e6568dbf464a4b270954e5fa3dee4a4054d163a41c0e7bf0a34eb40f0f/gensim-4.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a54bd53a0e6f991abb837f126663353657270e75be53287e8a568ada0b35b1b0", size = 24010102 }, ] [[package]] @@ -755,9 +755,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, ] [[package]] @@ -767,14 +767,14 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076 } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168 }, ] [[package]] name = "graphrag" -version = "2.0.0" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -799,7 +799,6 @@ dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "pyyaml" }, - { name = "rich" }, { name = "spacy" }, { name = "textblob" }, { name = "tiktoken" }, @@ -808,9 +807,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "umap-learn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/71/9fed9773efb6f5b483dce39878d6821bc211c0b5be435d27668e150616bb/graphrag-2.0.0.tar.gz", hash = "sha256:7279e1535b93ffa929e5c08b8f6ebb26878df01f191d6b01dea4852a22af4863", size = 208669, upload-time = "2025-02-26T16:59:16.454Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/39/42538610cf97438e4acbcb688e7dfa84b99b7e8f34fd923959922cccc413/graphrag-2.5.0.tar.gz", hash = "sha256:a68ebc73f55693d9c6e197da60f491aa42147a70d326e4ca2bc96f95a0964645", size = 219699 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/31/638fa7a1069bbabffedb85c3fe810174b9bf124695eea8dc1851829f4b6d/graphrag-2.0.0-py3-none-any.whl", hash = "sha256:57ac39315ad49c722bf40cb5c4311bf737fd40343c10ff267d090fb29c6fafd9", size = 361145, upload-time = "2025-02-26T16:59:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/49/93/d773256100cdc1b8335928cc4971d97925764a502503d75e4d17892d029b/graphrag-2.5.0-py3-none-any.whl", hash = "sha256:23c8df37a2910d071bb93f2ced33edea54037bd4a613dde445cc5f8fe451247f", size = 370350 }, ] [[package]] @@ -836,30 +835,30 @@ dependencies = [ { name = "typing-extensions" }, { name = "umap-learn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/de/83d653cc8029dc8c5f75bc5aea68f6b1e834230f05525fb3e7ac4aeae226/graspologic-3.4.1.tar.gz", hash = "sha256:7561f0b852a2bccd351bff77e8db07d9892f9dfa35a420fdec01690e4fdc8075", size = 5134018, upload-time = "2024-05-22T22:54:42.797Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/de/83d653cc8029dc8c5f75bc5aea68f6b1e834230f05525fb3e7ac4aeae226/graspologic-3.4.1.tar.gz", hash = "sha256:7561f0b852a2bccd351bff77e8db07d9892f9dfa35a420fdec01690e4fdc8075", size = 5134018 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/0b/9a167cec9cc4555b59cd282e8669998a50cb3f929a9a503965b24fa58a20/graspologic-3.4.1-py3-none-any.whl", hash = "sha256:c6563e087eda599bad1de831d4b7321c0daa7a82f4e85a7d7737ff67e07cdda2", size = 5200768, upload-time = "2024-05-22T22:54:39.259Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0b/9a167cec9cc4555b59cd282e8669998a50cb3f929a9a503965b24fa58a20/graspologic-3.4.1-py3-none-any.whl", hash = "sha256:c6563e087eda599bad1de831d4b7321c0daa7a82f4e85a7d7737ff67e07cdda2", size = 5200768 }, ] [[package]] name = "graspologic-native" version = "1.2.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/2d/62b30d89533643ccf4778a18eb023f291b8877b5d85de3342f07b2d363a7/graspologic_native-1.2.5.tar.gz", hash = "sha256:27ea7e01fa44466c0b4cdd678d4561e5d3dc0cb400015683b7ae1386031257a0", size = 2512729, upload-time = "2025-04-02T19:34:22.961Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/2d/62b30d89533643ccf4778a18eb023f291b8877b5d85de3342f07b2d363a7/graspologic_native-1.2.5.tar.gz", hash = "sha256:27ea7e01fa44466c0b4cdd678d4561e5d3dc0cb400015683b7ae1386031257a0", size = 2512729 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/86/10748f4c474b0c8f6060dd379bb0c4da5d42779244bb13a58656ffb44a03/graspologic_native-1.2.5-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bf05f2e162ae2a2a8d6e8cfccbe3586d1faa0b808159ff950478348df557c61e", size = 648437, upload-time = "2025-04-02T19:34:16.29Z" }, - { url = "https://files.pythonhosted.org/packages/42/cc/b75ea35755340bedda29727e5388390c639ea533f55b9249f5ac3003f656/graspologic_native-1.2.5-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fff06ed49c3875cf351bb09a92ae7cbc169ce92dcc4c3439e28e801f822ae", size = 352044, upload-time = "2025-04-02T19:34:18.153Z" }, - { url = "https://files.pythonhosted.org/packages/8e/55/15e6e4f18bf249b529ac4cd1522b03f5c9ef9284a2f7bfaa1fd1f96464fe/graspologic_native-1.2.5-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e7e993e7d70fe0d860773fc62812fbb8cb4ef2d11d8661a1f06f8772593915", size = 364644, upload-time = "2025-04-02T19:34:19.486Z" }, - { url = "https://files.pythonhosted.org/packages/3b/51/21097af79f3d68626539ab829bdbf6cc42933f020e161972927d916e394c/graspologic_native-1.2.5-cp38-abi3-win_amd64.whl", hash = "sha256:c3ef2172d774083d7e2c8e77daccd218571ddeebeb2c1703cebb1a2cc4c56e07", size = 210438, upload-time = "2025-04-02T19:34:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/ae/86/10748f4c474b0c8f6060dd379bb0c4da5d42779244bb13a58656ffb44a03/graspologic_native-1.2.5-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bf05f2e162ae2a2a8d6e8cfccbe3586d1faa0b808159ff950478348df557c61e", size = 648437 }, + { url = "https://files.pythonhosted.org/packages/42/cc/b75ea35755340bedda29727e5388390c639ea533f55b9249f5ac3003f656/graspologic_native-1.2.5-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fff06ed49c3875cf351bb09a92ae7cbc169ce92dcc4c3439e28e801f822ae", size = 352044 }, + { url = "https://files.pythonhosted.org/packages/8e/55/15e6e4f18bf249b529ac4cd1522b03f5c9ef9284a2f7bfaa1fd1f96464fe/graspologic_native-1.2.5-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e7e993e7d70fe0d860773fc62812fbb8cb4ef2d11d8661a1f06f8772593915", size = 364644 }, + { url = "https://files.pythonhosted.org/packages/3b/51/21097af79f3d68626539ab829bdbf6cc42933f020e161972927d916e394c/graspologic_native-1.2.5-cp38-abi3-win_amd64.whl", hash = "sha256:c3ef2172d774083d7e2c8e77daccd218571ddeebeb2c1703cebb1a2cc4c56e07", size = 210438 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } 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" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, ] [[package]] @@ -870,9 +869,9 @@ 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" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } 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" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, ] [[package]] @@ -885,9 +884,9 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 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" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] [[package]] @@ -902,16 +901,16 @@ dependencies = [ { name = "scipy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/34/87/7940713f929d0280cff1bde207479cb588a0d3a4dd49a0e2e69bfff46363/hyppo-0.4.0-py3-none-any.whl", hash = "sha256:4e75565b8deb601485cd7bc1b5c3f44e6ddf329136fc81e65d011f9b4e95132f", size = 146607, upload-time = "2023-05-24T13:50:04.441Z" }, + { url = "https://files.pythonhosted.org/packages/34/87/7940713f929d0280cff1bde207479cb588a0d3a4dd49a0e2e69bfff46363/hyppo-0.4.0-py3-none-any.whl", hash = "sha256:4e75565b8deb601485cd7bc1b5c3f44e6ddf329136fc81e65d011f9b4e95132f", size = 146607 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] @@ -934,9 +933,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/27/9e6e30ed92f2ac53d29f70b09da8b2dc456e256148e289678fa0e825f46a/ipykernel-6.30.0.tar.gz", hash = "sha256:b7b808ddb2d261aae2df3a26ff3ff810046e6de3dfbc6f7de8c98ea0a6cb632c", size = 165125, upload-time = "2025-07-21T10:36:09.259Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/27/9e6e30ed92f2ac53d29f70b09da8b2dc456e256148e289678fa0e825f46a/ipykernel-6.30.0.tar.gz", hash = "sha256:b7b808ddb2d261aae2df3a26ff3ff810046e6de3dfbc6f7de8c98ea0a6cb632c", size = 165125 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/3d/00813c3d9b46e3dcd88bd4530e0a3c63c0509e5d8c9eff34723ea243ab04/ipykernel-6.30.0-py3-none-any.whl", hash = "sha256:fd2936e55c4a1c2ee8b1e5fa6a372b8eecc0ab1338750dee76f48fa5cca1301e", size = 117264, upload-time = "2025-07-21T10:36:06.854Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/00813c3d9b46e3dcd88bd4530e0a3c63c0509e5d8c9eff34723ea243ab04/ipykernel-6.30.0-py3-none-any.whl", hash = "sha256:fd2936e55c4a1c2ee8b1e5fa6a372b8eecc0ab1338750dee76f48fa5cca1301e", size = 117264 }, ] [[package]] @@ -959,9 +958,9 @@ dependencies = [ { name = "traitlets", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088 } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" }, + { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864 }, ] [[package]] @@ -984,9 +983,9 @@ dependencies = [ { name = "traitlets", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/80/406f9e3bde1c1fd9bf5a0be9d090f8ae623e401b7670d8f6fdf2ab679891/ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270", size = 4385338, upload-time = "2025-07-01T11:11:30.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/80/406f9e3bde1c1fd9bf5a0be9d090f8ae623e401b7670d8f6fdf2ab679891/ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270", size = 4385338 } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/f8/0031ee2b906a15a33d6bfc12dd09c3dfa966b3cb5b284ecfb7549e6ac3c4/ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066", size = 611021, upload-time = "2025-07-01T11:11:27.85Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/0031ee2b906a15a33d6bfc12dd09c3dfa966b3cb5b284ecfb7549e6ac3c4/ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066", size = 611021 }, ] [[package]] @@ -996,18 +995,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, ] [[package]] name = "isodate" version = "0.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, ] [[package]] @@ -1017,9 +1016,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, ] [[package]] @@ -1029,59 +1028,59 @@ 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" } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } 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" }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, ] [[package]] name = "jiter" version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/7e/4011b5c77bec97cb2b572f566220364e3e21b51c48c5bd9c4a9c26b41b67/jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303", size = 317215, upload-time = "2025-05-18T19:03:04.303Z" }, - { url = "https://files.pythonhosted.org/packages/8a/4f/144c1b57c39692efc7ea7d8e247acf28e47d0912800b34d0ad815f6b2824/jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e", size = 322814, upload-time = "2025-05-18T19:03:06.433Z" }, - { url = "https://files.pythonhosted.org/packages/63/1f/db977336d332a9406c0b1f0b82be6f71f72526a806cbb2281baf201d38e3/jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f", size = 345237, upload-time = "2025-05-18T19:03:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1c/aa30a4a775e8a672ad7f21532bdbfb269f0706b39c6ff14e1f86bdd9e5ff/jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224", size = 370999, upload-time = "2025-05-18T19:03:09.338Z" }, - { url = "https://files.pythonhosted.org/packages/35/df/f8257abc4207830cb18880781b5f5b716bad5b2a22fb4330cfd357407c5b/jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7", size = 491109, upload-time = "2025-05-18T19:03:11.13Z" }, - { url = "https://files.pythonhosted.org/packages/06/76/9e1516fd7b4278aa13a2cc7f159e56befbea9aa65c71586305e7afa8b0b3/jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6", size = 388608, upload-time = "2025-05-18T19:03:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/6d/64/67750672b4354ca20ca18d3d1ccf2c62a072e8a2d452ac3cf8ced73571ef/jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf", size = 352454, upload-time = "2025-05-18T19:03:14.741Z" }, - { url = "https://files.pythonhosted.org/packages/96/4d/5c4e36d48f169a54b53a305114be3efa2bbffd33b648cd1478a688f639c1/jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90", size = 391833, upload-time = "2025-05-18T19:03:16.426Z" }, - { url = "https://files.pythonhosted.org/packages/0b/de/ce4a6166a78810bd83763d2fa13f85f73cbd3743a325469a4a9289af6dae/jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0", size = 523646, upload-time = "2025-05-18T19:03:17.704Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a6/3bc9acce53466972964cf4ad85efecb94f9244539ab6da1107f7aed82934/jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee", size = 514735, upload-time = "2025-05-18T19:03:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d8/243c2ab8426a2a4dea85ba2a2ba43df379ccece2145320dfd4799b9633c5/jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4", size = 210747, upload-time = "2025-05-18T19:03:21.184Z" }, - { url = "https://files.pythonhosted.org/packages/37/7a/8021bd615ef7788b98fc76ff533eaac846322c170e93cbffa01979197a45/jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5", size = 207484, upload-time = "2025-05-18T19:03:23.046Z" }, - { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473, upload-time = "2025-05-18T19:03:25.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971, upload-time = "2025-05-18T19:03:27.255Z" }, - { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574, upload-time = "2025-05-18T19:03:28.63Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028, upload-time = "2025-05-18T19:03:30.292Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083, upload-time = "2025-05-18T19:03:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821, upload-time = "2025-05-18T19:03:33.184Z" }, - { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174, upload-time = "2025-05-18T19:03:34.965Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869, upload-time = "2025-05-18T19:03:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741, upload-time = "2025-05-18T19:03:38.168Z" }, - { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527, upload-time = "2025-05-18T19:03:39.577Z" }, - { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765, upload-time = "2025-05-18T19:03:41.271Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234, upload-time = "2025-05-18T19:03:42.918Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/7e/4011b5c77bec97cb2b572f566220364e3e21b51c48c5bd9c4a9c26b41b67/jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303", size = 317215 }, + { url = "https://files.pythonhosted.org/packages/8a/4f/144c1b57c39692efc7ea7d8e247acf28e47d0912800b34d0ad815f6b2824/jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e", size = 322814 }, + { url = "https://files.pythonhosted.org/packages/63/1f/db977336d332a9406c0b1f0b82be6f71f72526a806cbb2281baf201d38e3/jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f", size = 345237 }, + { url = "https://files.pythonhosted.org/packages/d7/1c/aa30a4a775e8a672ad7f21532bdbfb269f0706b39c6ff14e1f86bdd9e5ff/jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224", size = 370999 }, + { url = "https://files.pythonhosted.org/packages/35/df/f8257abc4207830cb18880781b5f5b716bad5b2a22fb4330cfd357407c5b/jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7", size = 491109 }, + { url = "https://files.pythonhosted.org/packages/06/76/9e1516fd7b4278aa13a2cc7f159e56befbea9aa65c71586305e7afa8b0b3/jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6", size = 388608 }, + { url = "https://files.pythonhosted.org/packages/6d/64/67750672b4354ca20ca18d3d1ccf2c62a072e8a2d452ac3cf8ced73571ef/jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf", size = 352454 }, + { url = "https://files.pythonhosted.org/packages/96/4d/5c4e36d48f169a54b53a305114be3efa2bbffd33b648cd1478a688f639c1/jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90", size = 391833 }, + { url = "https://files.pythonhosted.org/packages/0b/de/ce4a6166a78810bd83763d2fa13f85f73cbd3743a325469a4a9289af6dae/jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0", size = 523646 }, + { url = "https://files.pythonhosted.org/packages/a2/a6/3bc9acce53466972964cf4ad85efecb94f9244539ab6da1107f7aed82934/jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee", size = 514735 }, + { url = "https://files.pythonhosted.org/packages/b4/d8/243c2ab8426a2a4dea85ba2a2ba43df379ccece2145320dfd4799b9633c5/jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4", size = 210747 }, + { url = "https://files.pythonhosted.org/packages/37/7a/8021bd615ef7788b98fc76ff533eaac846322c170e93cbffa01979197a45/jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5", size = 207484 }, + { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473 }, + { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971 }, + { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574 }, + { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028 }, + { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821 }, + { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174 }, + { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869 }, + { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741 }, + { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527 }, + { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765 }, + { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234 }, ] [[package]] name = "joblib" version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746 }, ] [[package]] name = "json-repair" version = "0.30.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/7a/7745d0d908563a478421c7520649dfd6a5c551858e2233ff7caf20cb8df7/json_repair-0.30.3.tar.gz", hash = "sha256:0ac56e7ae9253ee9c507a7e1a3a26799c9b0bbe5e2bec1b2cc5053e90d5b05e3", size = 27803, upload-time = "2024-12-04T19:53:02.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/7a/7745d0d908563a478421c7520649dfd6a5c551858e2233ff7caf20cb8df7/json_repair-0.30.3.tar.gz", hash = "sha256:0ac56e7ae9253ee9c507a7e1a3a26799c9b0bbe5e2bec1b2cc5053e90d5b05e3", size = 27803 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/2d/79a46330c4b97ee90dd403fb0d267da7b25b24d7db604c5294e5c57d5f7c/json_repair-0.30.3-py3-none-any.whl", hash = "sha256:63bb588162b0958ae93d85356ecbe54c06b8c33f8a4834f93fa2719ea669804e", size = 18951, upload-time = "2024-12-04T19:53:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2d/79a46330c4b97ee90dd403fb0d267da7b25b24d7db604c5294e5c57d5f7c/json_repair-0.30.3-py3-none-any.whl", hash = "sha256:63bb588162b0958ae93d85356ecbe54c06b8c33f8a4834f93fa2719ea669804e", size = 18951 }, ] [[package]] @@ -1094,9 +1093,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184 }, ] [[package]] @@ -1106,9 +1105,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513 } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 }, ] [[package]] @@ -1122,9 +1121,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019 } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105 }, ] [[package]] @@ -1136,53 +1135,53 @@ dependencies = [ { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, + { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880 }, ] [[package]] name = "kiwisolver" version = "1.4.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623, upload-time = "2024-12-24T18:28:17.687Z" }, - { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720, upload-time = "2024-12-24T18:28:19.158Z" }, - { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413, upload-time = "2024-12-24T18:28:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826, upload-time = "2024-12-24T18:28:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231, upload-time = "2024-12-24T18:28:23.851Z" }, - { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938, upload-time = "2024-12-24T18:28:26.687Z" }, - { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799, upload-time = "2024-12-24T18:28:30.538Z" }, - { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362, upload-time = "2024-12-24T18:28:32.943Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695, upload-time = "2024-12-24T18:28:35.641Z" }, - { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802, upload-time = "2024-12-24T18:28:38.357Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646, upload-time = "2024-12-24T18:28:40.941Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260, upload-time = "2024-12-24T18:28:42.273Z" }, - { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633, upload-time = "2024-12-24T18:28:44.87Z" }, - { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885, upload-time = "2024-12-24T18:28:47.346Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175, upload-time = "2024-12-24T18:28:49.651Z" }, - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403, upload-time = "2024-12-24T18:30:41.372Z" }, - { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657, upload-time = "2024-12-24T18:30:42.392Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948, upload-time = "2024-12-24T18:30:44.703Z" }, - { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186, upload-time = "2024-12-24T18:30:45.654Z" }, - { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279, upload-time = "2024-12-24T18:30:47.951Z" }, - { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762, upload-time = "2024-12-24T18:30:48.903Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, ] [[package]] @@ -1198,12 +1197,12 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/ed/58e04eaf815acd6b75ad7db8a6a61d01eccc5cb2191d431c0d5f234cf20a/lancedb-0.17.0-cp39-abi3-macosx_10_15_x86_64.whl", hash = "sha256:40aac1583edda390e51189c4e95bdfd4768d23705234e12a7b81957f1143df42", size = 26393821, upload-time = "2024-12-06T17:57:29.699Z" }, - { url = "https://files.pythonhosted.org/packages/87/a9/14807f23f0fb415453626ba4ea7431ab62f0906bd0ef1df24680fd5ae2df/lancedb-0.17.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:895bed499dae61cac1dbfc40ad71a566e06ab5c8d538aa57873a0cba859f8a7a", size = 24846600, upload-time = "2024-12-06T17:39:43.739Z" }, - { url = "https://files.pythonhosted.org/packages/a5/46/4a5af607b9904d76344b56e62d6799ce7ae8f6c835bf05d1678313ca877f/lancedb-0.17.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea688d0f63796ee912a7cfe6667f36661e36756fa8340b94dd54d666a7db63f", size = 30443392, upload-time = "2024-12-06T17:31:41.92Z" }, - { url = "https://files.pythonhosted.org/packages/eb/03/4eb452f02a740ab1cfa334570384f10810890b2670ef6277af7abcb0039d/lancedb-0.17.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:f51a61950ead30a605b5653a81e8362e4aac6fec32705b88b9c9319e9308b2bb", size = 28242872, upload-time = "2024-12-06T17:32:03.134Z" }, - { url = "https://files.pythonhosted.org/packages/b2/11/c48248f984dfd8dfec0bb074465ca697cf64b6b71b0aa199c15ad0153597/lancedb-0.17.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:07e6f10b3fcbeb6c737996e5ebd68d04c3ca2656a9b8b970111ecf368245e7f6", size = 29925342, upload-time = "2024-12-06T17:31:37.967Z" }, - { url = "https://files.pythonhosted.org/packages/34/b9/a3d4bfdaefbc9098ef18bff2cf403c6060f70894c5022983464f9c3db367/lancedb-0.17.0-cp39-abi3-win_amd64.whl", hash = "sha256:9d7e82f83f430d906c285d3303729258b21b1cc8da634c9f7017e354bcb7318a", size = 27511050, upload-time = "2024-12-06T17:55:58.08Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ed/58e04eaf815acd6b75ad7db8a6a61d01eccc5cb2191d431c0d5f234cf20a/lancedb-0.17.0-cp39-abi3-macosx_10_15_x86_64.whl", hash = "sha256:40aac1583edda390e51189c4e95bdfd4768d23705234e12a7b81957f1143df42", size = 26393821 }, + { url = "https://files.pythonhosted.org/packages/87/a9/14807f23f0fb415453626ba4ea7431ab62f0906bd0ef1df24680fd5ae2df/lancedb-0.17.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:895bed499dae61cac1dbfc40ad71a566e06ab5c8d538aa57873a0cba859f8a7a", size = 24846600 }, + { url = "https://files.pythonhosted.org/packages/a5/46/4a5af607b9904d76344b56e62d6799ce7ae8f6c835bf05d1678313ca877f/lancedb-0.17.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea688d0f63796ee912a7cfe6667f36661e36756fa8340b94dd54d666a7db63f", size = 30443392 }, + { url = "https://files.pythonhosted.org/packages/eb/03/4eb452f02a740ab1cfa334570384f10810890b2670ef6277af7abcb0039d/lancedb-0.17.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:f51a61950ead30a605b5653a81e8362e4aac6fec32705b88b9c9319e9308b2bb", size = 28242872 }, + { url = "https://files.pythonhosted.org/packages/b2/11/c48248f984dfd8dfec0bb074465ca697cf64b6b71b0aa199c15ad0153597/lancedb-0.17.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:07e6f10b3fcbeb6c737996e5ebd68d04c3ca2656a9b8b970111ecf368245e7f6", size = 29925342 }, + { url = "https://files.pythonhosted.org/packages/34/b9/a3d4bfdaefbc9098ef18bff2cf403c6060f70894c5022983464f9c3db367/lancedb-0.17.0-cp39-abi3-win_amd64.whl", hash = "sha256:9d7e82f83f430d906c285d3303729258b21b1cc8da634c9f7017e354bcb7318a", size = 27511050 }, ] [[package]] @@ -1213,9 +1212,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "language-data" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/7a/5a97e327063409a5caa21541e6d08ae4a0f2da328447e9f2c7b39e179226/langcodes-3.5.0.tar.gz", hash = "sha256:1eef8168d07e51e131a2497ffecad4b663f6208e7c3ae3b8dc15c51734a6f801", size = 191030, upload-time = "2024-11-19T10:23:45.546Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/7a/5a97e327063409a5caa21541e6d08ae4a0f2da328447e9f2c7b39e179226/langcodes-3.5.0.tar.gz", hash = "sha256:1eef8168d07e51e131a2497ffecad4b663f6208e7c3ae3b8dc15c51734a6f801", size = 191030 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/6b/068c2ea7a712bf805c62445bd9e9c06d7340358ef2824150eceac027444b/langcodes-3.5.0-py3-none-any.whl", hash = "sha256:853c69d1a35e0e13da2f427bb68fb2fa4a8f4fb899e0c62ad8df8d073dcfed33", size = 182974, upload-time = "2024-11-19T10:23:42.824Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6b/068c2ea7a712bf805c62445bd9e9c06d7340358ef2824150eceac027444b/langcodes-3.5.0-py3-none-any.whl", hash = "sha256:853c69d1a35e0e13da2f427bb68fb2fa4a8f4fb899e0c62ad8df8d073dcfed33", size = 182974 }, ] [[package]] @@ -1225,27 +1224,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "marisa-trie" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/ce/3f144716a9f2cbf42aa86ebc8b085a184be25c80aa453eea17c294d239c1/language_data-1.3.0.tar.gz", hash = "sha256:7600ef8aa39555145d06c89f0c324bf7dab834ea0b0a439d8243762e3ebad7ec", size = 5129310, upload-time = "2024-11-19T10:21:37.912Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/ce/3f144716a9f2cbf42aa86ebc8b085a184be25c80aa453eea17c294d239c1/language_data-1.3.0.tar.gz", hash = "sha256:7600ef8aa39555145d06c89f0c324bf7dab834ea0b0a439d8243762e3ebad7ec", size = 5129310 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/e9/5a5ffd9b286db82be70d677d0a91e4d58f7912bb8dd026ddeeb4abe70679/language_data-1.3.0-py3-none-any.whl", hash = "sha256:e2ee943551b5ae5f89cd0e801d1fc3835bb0ef5b7e9c3a4e8e17b2b214548fbf", size = 5385760, upload-time = "2024-11-19T10:21:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e9/5a5ffd9b286db82be70d677d0a91e4d58f7912bb8dd026ddeeb4abe70679/language_data-1.3.0-py3-none-any.whl", hash = "sha256:e2ee943551b5ae5f89cd0e801d1fc3835bb0ef5b7e9c3a4e8e17b2b214548fbf", size = 5385760 }, ] [[package]] name = "llvmlite" version = "0.44.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306, upload-time = "2025-01-20T11:12:18.634Z" }, - { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096, upload-time = "2025-01-20T11:12:24.544Z" }, - { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859, upload-time = "2025-01-20T11:12:31.839Z" }, - { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199, upload-time = "2025-01-20T11:12:40.049Z" }, - { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381, upload-time = "2025-01-20T11:12:47.054Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305, upload-time = "2025-01-20T11:12:53.936Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090, upload-time = "2025-01-20T11:12:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858, upload-time = "2025-01-20T11:13:07.623Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200, upload-time = "2025-01-20T11:13:20.058Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193, upload-time = "2025-01-20T11:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306 }, + { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096 }, + { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859 }, + { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199 }, + { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381 }, + { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305 }, + { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090 }, + { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858 }, + { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200 }, + { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193 }, ] [[package]] @@ -1255,30 +1254,30 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/15/9d9743897e4450b2de199ee673b50cb018980c4ced477d41cf91304a85e3/marisa_trie-1.2.1.tar.gz", hash = "sha256:3a27c408e2aefc03e0f1d25b2ff2afb85aac3568f6fa2ae2a53b57a2e87ce29d", size = 416124, upload-time = "2024-10-12T11:30:15.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/83/ccf5b33f2123f3110705c608f8e0caa82002626511aafafc58f82e50d322/marisa_trie-1.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2eb41d2f9114d8b7bd66772c237111e00d2bae2260824560eaa0a1e291ce9e8", size = 362200, upload-time = "2024-10-12T11:28:25.418Z" }, - { url = "https://files.pythonhosted.org/packages/9d/74/f7ce1fc2ee480c7f8ceadd9b992caceaba442a97e5e99d6aea00d3635a0b/marisa_trie-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e956e6a46f604b17d570901e66f5214fb6f658c21e5e7665deace236793cef6", size = 192309, upload-time = "2024-10-12T11:28:27.348Z" }, - { url = "https://files.pythonhosted.org/packages/e4/52/5dbbc13e57ce54c2ef0d04962d7d8f66edc69ed34310c734a2913199a581/marisa_trie-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd45142501300e7538b2e544905580918b67b1c82abed1275fe4c682c95635fa", size = 174713, upload-time = "2024-10-12T11:28:28.912Z" }, - { url = "https://files.pythonhosted.org/packages/57/49/2580372f3f980aea95c23d05b2c1d3bbb9ee1ab8cfd441545153e44f1be7/marisa_trie-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8443d116c612cfd1961fbf76769faf0561a46d8e317315dd13f9d9639ad500c", size = 1314808, upload-time = "2024-10-12T11:28:30.705Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ba/e12a4d450f265414cc68df6a116a78beece72b95f774f04d29cd48e08d19/marisa_trie-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:875a6248e60fbb48d947b574ffa4170f34981f9e579bde960d0f9a49ea393ecc", size = 1346678, upload-time = "2024-10-12T11:28:33.106Z" }, - { url = "https://files.pythonhosted.org/packages/b2/81/8e130cb1eea741fd17694d821096f7ec9841f0e3d3c69b740257f5eeafa8/marisa_trie-1.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:746a7c60a17fccd3cfcfd4326926f02ea4fcdfc25d513411a0c4fc8e4a1ca51f", size = 1307254, upload-time = "2024-10-12T11:28:35.053Z" }, - { url = "https://files.pythonhosted.org/packages/d7/d0/3deb5ea2bf7e4d845339875dbb31f3c3f66c8d6568723db1d137fb08a91c/marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e70869737cc0e5bd903f620667da6c330d6737048d1f44db792a6af68a1d35be", size = 2194712, upload-time = "2024-10-12T11:28:36.87Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5f/b38d728dd30954816497b53425cfaddaf7b93ac0912db5911888f191b07a/marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06b099dd743676dbcd8abd8465ceac8f6d97d8bfaabe2c83b965495523b4cef2", size = 2355625, upload-time = "2024-10-12T11:28:38.206Z" }, - { url = "https://files.pythonhosted.org/packages/7e/4f/61c0faa9ae9e53600a1b7a0c367bc9db1a4fdc625402ec232c755a05e094/marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d2a82eb21afdaf22b50d9b996472305c05ca67fc4ff5a026a220320c9c961db6", size = 2290290, upload-time = "2024-10-12T11:28:40.148Z" }, - { url = "https://files.pythonhosted.org/packages/7c/7d/713b970fb3043248881ed776dbf4d54918398aa5dde843a38711d0d62c8f/marisa_trie-1.2.1-cp310-cp310-win32.whl", hash = "sha256:8951e7ce5d3167fbd085703b4cbb3f47948ed66826bef9a2173c379508776cf5", size = 130743, upload-time = "2024-10-12T11:28:41.31Z" }, - { url = "https://files.pythonhosted.org/packages/cc/94/3d619cc82c30daeacd18a88674f4e6540ebfb7b4b7752ca0552793be80cf/marisa_trie-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:5685a14b3099b1422c4f59fa38b0bf4b5342ee6cc38ae57df9666a0b28eeaad3", size = 151891, upload-time = "2024-10-12T11:28:42.279Z" }, - { url = "https://files.pythonhosted.org/packages/4a/93/ffb01dfa22b6eee918e798e0bc3487427036c608aa4c065725f31aaf4104/marisa_trie-1.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed3fb4ed7f2084597e862bcd56c56c5529e773729a426c083238682dba540e98", size = 362823, upload-time = "2024-10-12T11:28:43.983Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1d/5c36500ac350c278c9bdfd88e17fa846fa4136d75597c167141ed973cdf2/marisa_trie-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe69fb9ffb2767746181f7b3b29bbd3454d1d24717b5958e030494f3d3cddf3", size = 192741, upload-time = "2024-10-12T11:28:45.536Z" }, - { url = "https://files.pythonhosted.org/packages/e8/04/87dd0840f3f720e511eba56193c02bf64d7d96df1ca9f6d19994f55154be/marisa_trie-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4728ed3ae372d1ea2cdbd5eaa27b8f20a10e415d1f9d153314831e67d963f281", size = 174995, upload-time = "2024-10-12T11:28:46.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/51/9e903a7e13b7593e2e675d0ec4c390ca076dc5df1c1a0d5e85a513b886a3/marisa_trie-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cf4f25cf895692b232f49aa5397af6aba78bb679fb917a05fce8d3cb1ee446d", size = 1384728, upload-time = "2024-10-12T11:28:48.28Z" }, - { url = "https://files.pythonhosted.org/packages/e8/3f/7362a5ac60c2b0aad0f52cd57e7bd0c708f20d2660d8df85360f3d8f1c4b/marisa_trie-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cca7f96236ffdbf49be4b2e42c132e3df05968ac424544034767650913524de", size = 1412620, upload-time = "2024-10-12T11:28:50.427Z" }, - { url = "https://files.pythonhosted.org/packages/1f/bc/aaa3eaf6875f78a204a8da9692d56e3a36f89997dad2c388628385614576/marisa_trie-1.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7eb20bf0e8b55a58d2a9b518aabc4c18278787bdba476c551dd1c1ed109e509", size = 1361555, upload-time = "2024-10-12T11:28:51.603Z" }, - { url = "https://files.pythonhosted.org/packages/18/98/e11b5a6206c5d110f32adab37fa84a85410d684e9c731acdd5c9250e2ce4/marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b1ec93f0d1ee6d7ab680a6d8ea1a08bf264636358e92692072170032dda652ba", size = 2257717, upload-time = "2024-10-12T11:28:52.881Z" }, - { url = "https://files.pythonhosted.org/packages/d2/9d/6b4a40867875e738a67c5b29f83e2e490a66bd9067ace3dd9a5c497e2b7f/marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e2699255d7ac610dee26d4ae7bda5951d05c7d9123a22e1f7c6a6f1964e0a4e4", size = 2417044, upload-time = "2024-10-12T11:28:54.115Z" }, - { url = "https://files.pythonhosted.org/packages/fe/61/e25613c72f2931757334b8bcf6b501569ef713f5ee9c6c7688ec460bd720/marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c484410911182457a8a1a0249d0c09c01e2071b78a0a8538cd5f7fa45589b13a", size = 2351960, upload-time = "2024-10-12T11:28:55.417Z" }, - { url = "https://files.pythonhosted.org/packages/19/0a/a90ccaf3eb476d13ec261f80c6c52defaf10ebc7f35eb2bcd7dfb533aef7/marisa_trie-1.2.1-cp311-cp311-win32.whl", hash = "sha256:ad548117744b2bcf0e3d97374608be0a92d18c2af13d98b728d37cd06248e571", size = 130446, upload-time = "2024-10-12T11:28:57.294Z" }, - { url = "https://files.pythonhosted.org/packages/fc/98/574b4e143e0a2f5f71af8716b6c4a8a46220f75a6e0847ce7d11ee0ba4aa/marisa_trie-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:436f62d27714970b9cdd3b3c41bdad046f260e62ebb0daa38125ef70536fc73b", size = 152037, upload-time = "2024-10-12T11:28:58.399Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/31/15/9d9743897e4450b2de199ee673b50cb018980c4ced477d41cf91304a85e3/marisa_trie-1.2.1.tar.gz", hash = "sha256:3a27c408e2aefc03e0f1d25b2ff2afb85aac3568f6fa2ae2a53b57a2e87ce29d", size = 416124 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/83/ccf5b33f2123f3110705c608f8e0caa82002626511aafafc58f82e50d322/marisa_trie-1.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2eb41d2f9114d8b7bd66772c237111e00d2bae2260824560eaa0a1e291ce9e8", size = 362200 }, + { url = "https://files.pythonhosted.org/packages/9d/74/f7ce1fc2ee480c7f8ceadd9b992caceaba442a97e5e99d6aea00d3635a0b/marisa_trie-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9e956e6a46f604b17d570901e66f5214fb6f658c21e5e7665deace236793cef6", size = 192309 }, + { url = "https://files.pythonhosted.org/packages/e4/52/5dbbc13e57ce54c2ef0d04962d7d8f66edc69ed34310c734a2913199a581/marisa_trie-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd45142501300e7538b2e544905580918b67b1c82abed1275fe4c682c95635fa", size = 174713 }, + { url = "https://files.pythonhosted.org/packages/57/49/2580372f3f980aea95c23d05b2c1d3bbb9ee1ab8cfd441545153e44f1be7/marisa_trie-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8443d116c612cfd1961fbf76769faf0561a46d8e317315dd13f9d9639ad500c", size = 1314808 }, + { url = "https://files.pythonhosted.org/packages/5a/ba/e12a4d450f265414cc68df6a116a78beece72b95f774f04d29cd48e08d19/marisa_trie-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:875a6248e60fbb48d947b574ffa4170f34981f9e579bde960d0f9a49ea393ecc", size = 1346678 }, + { url = "https://files.pythonhosted.org/packages/b2/81/8e130cb1eea741fd17694d821096f7ec9841f0e3d3c69b740257f5eeafa8/marisa_trie-1.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:746a7c60a17fccd3cfcfd4326926f02ea4fcdfc25d513411a0c4fc8e4a1ca51f", size = 1307254 }, + { url = "https://files.pythonhosted.org/packages/d7/d0/3deb5ea2bf7e4d845339875dbb31f3c3f66c8d6568723db1d137fb08a91c/marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e70869737cc0e5bd903f620667da6c330d6737048d1f44db792a6af68a1d35be", size = 2194712 }, + { url = "https://files.pythonhosted.org/packages/9c/5f/b38d728dd30954816497b53425cfaddaf7b93ac0912db5911888f191b07a/marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06b099dd743676dbcd8abd8465ceac8f6d97d8bfaabe2c83b965495523b4cef2", size = 2355625 }, + { url = "https://files.pythonhosted.org/packages/7e/4f/61c0faa9ae9e53600a1b7a0c367bc9db1a4fdc625402ec232c755a05e094/marisa_trie-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d2a82eb21afdaf22b50d9b996472305c05ca67fc4ff5a026a220320c9c961db6", size = 2290290 }, + { url = "https://files.pythonhosted.org/packages/7c/7d/713b970fb3043248881ed776dbf4d54918398aa5dde843a38711d0d62c8f/marisa_trie-1.2.1-cp310-cp310-win32.whl", hash = "sha256:8951e7ce5d3167fbd085703b4cbb3f47948ed66826bef9a2173c379508776cf5", size = 130743 }, + { url = "https://files.pythonhosted.org/packages/cc/94/3d619cc82c30daeacd18a88674f4e6540ebfb7b4b7752ca0552793be80cf/marisa_trie-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:5685a14b3099b1422c4f59fa38b0bf4b5342ee6cc38ae57df9666a0b28eeaad3", size = 151891 }, + { url = "https://files.pythonhosted.org/packages/4a/93/ffb01dfa22b6eee918e798e0bc3487427036c608aa4c065725f31aaf4104/marisa_trie-1.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed3fb4ed7f2084597e862bcd56c56c5529e773729a426c083238682dba540e98", size = 362823 }, + { url = "https://files.pythonhosted.org/packages/6d/1d/5c36500ac350c278c9bdfd88e17fa846fa4136d75597c167141ed973cdf2/marisa_trie-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe69fb9ffb2767746181f7b3b29bbd3454d1d24717b5958e030494f3d3cddf3", size = 192741 }, + { url = "https://files.pythonhosted.org/packages/e8/04/87dd0840f3f720e511eba56193c02bf64d7d96df1ca9f6d19994f55154be/marisa_trie-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4728ed3ae372d1ea2cdbd5eaa27b8f20a10e415d1f9d153314831e67d963f281", size = 174995 }, + { url = "https://files.pythonhosted.org/packages/c9/51/9e903a7e13b7593e2e675d0ec4c390ca076dc5df1c1a0d5e85a513b886a3/marisa_trie-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cf4f25cf895692b232f49aa5397af6aba78bb679fb917a05fce8d3cb1ee446d", size = 1384728 }, + { url = "https://files.pythonhosted.org/packages/e8/3f/7362a5ac60c2b0aad0f52cd57e7bd0c708f20d2660d8df85360f3d8f1c4b/marisa_trie-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cca7f96236ffdbf49be4b2e42c132e3df05968ac424544034767650913524de", size = 1412620 }, + { url = "https://files.pythonhosted.org/packages/1f/bc/aaa3eaf6875f78a204a8da9692d56e3a36f89997dad2c388628385614576/marisa_trie-1.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7eb20bf0e8b55a58d2a9b518aabc4c18278787bdba476c551dd1c1ed109e509", size = 1361555 }, + { url = "https://files.pythonhosted.org/packages/18/98/e11b5a6206c5d110f32adab37fa84a85410d684e9c731acdd5c9250e2ce4/marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b1ec93f0d1ee6d7ab680a6d8ea1a08bf264636358e92692072170032dda652ba", size = 2257717 }, + { url = "https://files.pythonhosted.org/packages/d2/9d/6b4a40867875e738a67c5b29f83e2e490a66bd9067ace3dd9a5c497e2b7f/marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e2699255d7ac610dee26d4ae7bda5951d05c7d9123a22e1f7c6a6f1964e0a4e4", size = 2417044 }, + { url = "https://files.pythonhosted.org/packages/fe/61/e25613c72f2931757334b8bcf6b501569ef713f5ee9c6c7688ec460bd720/marisa_trie-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c484410911182457a8a1a0249d0c09c01e2071b78a0a8538cd5f7fa45589b13a", size = 2351960 }, + { url = "https://files.pythonhosted.org/packages/19/0a/a90ccaf3eb476d13ec261f80c6c52defaf10ebc7f35eb2bcd7dfb533aef7/marisa_trie-1.2.1-cp311-cp311-win32.whl", hash = "sha256:ad548117744b2bcf0e3d97374608be0a92d18c2af13d98b728d37cd06248e571", size = 130446 }, + { url = "https://files.pythonhosted.org/packages/fc/98/574b4e143e0a2f5f71af8716b6c4a8a46220f75a6e0847ce7d11ee0ba4aa/marisa_trie-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:436f62d27714970b9cdd3b3c41bdad046f260e62ebb0daa38125ef70536fc73b", size = 152037 }, ] [[package]] @@ -1288,37 +1287,37 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, ] [[package]] @@ -1329,9 +1328,9 @@ dependencies = [ { name = "backports-datetime-fromisoformat", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/ff/26df5a9f5ac57ccf693a5854916ab47243039d2aa9e0fe5f5a0331e7b74b/marshmallow-4.0.0.tar.gz", hash = "sha256:3b6e80aac299a7935cfb97ed01d1854fb90b5079430969af92118ea1b12a8d55", size = 220507, upload-time = "2025-04-17T02:25:54.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/ff/26df5a9f5ac57ccf693a5854916ab47243039d2aa9e0fe5f5a0331e7b74b/marshmallow-4.0.0.tar.gz", hash = "sha256:3b6e80aac299a7935cfb97ed01d1854fb90b5079430969af92118ea1b12a8d55", size = 220507 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/26/6cc45d156f44dbe1d5696d9e54042e4dcaf7b946c0b86df6a97d29706f32/marshmallow-4.0.0-py3-none-any.whl", hash = "sha256:e7b0528337e9990fd64950f8a6b3a1baabed09ad17a0dfb844d701151f92d203", size = 48420, upload-time = "2025-04-17T02:25:53.375Z" }, + { url = "https://files.pythonhosted.org/packages/d6/26/6cc45d156f44dbe1d5696d9e54042e4dcaf7b946c0b86df6a97d29706f32/marshmallow-4.0.0-py3-none-any.whl", hash = "sha256:e7b0528337e9990fd64950f8a6b3a1baabed09ad17a0dfb844d701151f92d203", size = 48420 }, ] [[package]] @@ -1350,23 +1349,23 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/ea/2bba25d289d389c7451f331ecd593944b3705f06ddf593fa7be75037d308/matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7", size = 8167862, upload-time = "2025-05-08T19:09:39.563Z" }, - { url = "https://files.pythonhosted.org/packages/41/81/cc70b5138c926604e8c9ed810ed4c79e8116ba72e02230852f5c12c87ba2/matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb", size = 8042149, upload-time = "2025-05-08T19:09:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/0ff45b6bfa42bb16de597e6058edf2361c298ad5ef93b327728145161bbf/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21ae75651c0231b3ba014b6d5e08fb969c40cdb5a011e33e99ed0c9ea86ecb", size = 8453719, upload-time = "2025-05-08T19:09:44.901Z" }, - { url = "https://files.pythonhosted.org/packages/85/c7/1866e972fed6d71ef136efbc980d4d1854ab7ef1ea8152bbd995ca231c81/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e39755580b08e30e3620efc659330eac5d6534ab7eae50fa5e31f53ee4e30", size = 8590801, upload-time = "2025-05-08T19:09:47.404Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b9/748f6626d534ab7e255bdc39dc22634d337cf3ce200f261b5d65742044a1/matplotlib-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf4636203e1190871d3a73664dea03d26fb019b66692cbfd642faafdad6208e8", size = 9402111, upload-time = "2025-05-08T19:09:49.474Z" }, - { url = "https://files.pythonhosted.org/packages/1f/78/8bf07bd8fb67ea5665a6af188e70b57fcb2ab67057daa06b85a08e59160a/matplotlib-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:fd5641a9bb9d55f4dd2afe897a53b537c834b9012684c8444cc105895c8c16fd", size = 8057213, upload-time = "2025-05-08T19:09:51.489Z" }, - { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873, upload-time = "2025-05-08T19:09:53.857Z" }, - { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205, upload-time = "2025-05-08T19:09:55.684Z" }, - { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823, upload-time = "2025-05-08T19:09:57.442Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464, upload-time = "2025-05-08T19:09:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103, upload-time = "2025-05-08T19:10:03.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492, upload-time = "2025-05-08T19:10:05.271Z" }, - { url = "https://files.pythonhosted.org/packages/3d/d1/f54d43e95384b312ffa4a74a4326c722f3b8187aaaa12e9a84cdf3037131/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4", size = 8162896, upload-time = "2025-05-08T19:10:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/24/a4/fbfc00c2346177c95b353dcf9b5a004106abe8730a62cb6f27e79df0a698/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751", size = 8039702, upload-time = "2025-05-08T19:10:49.634Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298, upload-time = "2025-05-08T19:10:51.738Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ea/2bba25d289d389c7451f331ecd593944b3705f06ddf593fa7be75037d308/matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7", size = 8167862 }, + { url = "https://files.pythonhosted.org/packages/41/81/cc70b5138c926604e8c9ed810ed4c79e8116ba72e02230852f5c12c87ba2/matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb", size = 8042149 }, + { url = "https://files.pythonhosted.org/packages/4a/9a/0ff45b6bfa42bb16de597e6058edf2361c298ad5ef93b327728145161bbf/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21ae75651c0231b3ba014b6d5e08fb969c40cdb5a011e33e99ed0c9ea86ecb", size = 8453719 }, + { url = "https://files.pythonhosted.org/packages/85/c7/1866e972fed6d71ef136efbc980d4d1854ab7ef1ea8152bbd995ca231c81/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e39755580b08e30e3620efc659330eac5d6534ab7eae50fa5e31f53ee4e30", size = 8590801 }, + { url = "https://files.pythonhosted.org/packages/5d/b9/748f6626d534ab7e255bdc39dc22634d337cf3ce200f261b5d65742044a1/matplotlib-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf4636203e1190871d3a73664dea03d26fb019b66692cbfd642faafdad6208e8", size = 9402111 }, + { url = "https://files.pythonhosted.org/packages/1f/78/8bf07bd8fb67ea5665a6af188e70b57fcb2ab67057daa06b85a08e59160a/matplotlib-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:fd5641a9bb9d55f4dd2afe897a53b537c834b9012684c8444cc105895c8c16fd", size = 8057213 }, + { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873 }, + { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205 }, + { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823 }, + { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464 }, + { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103 }, + { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492 }, + { url = "https://files.pythonhosted.org/packages/3d/d1/f54d43e95384b312ffa4a74a4326c722f3b8187aaaa12e9a84cdf3037131/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4", size = 8162896 }, + { url = "https://files.pythonhosted.org/packages/24/a4/fbfc00c2346177c95b353dcf9b5a004106abe8730a62cb6f27e79df0a698/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751", size = 8039702 }, + { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298 }, ] [[package]] @@ -1376,18 +1375,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 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" }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] [[package]] @@ -1399,9 +1398,9 @@ dependencies = [ { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/da/81acbe0c1fd7e9e4ec35f55dadeba9833a847b9a6ba2e2d1e4432da901dd/msal-1.33.0.tar.gz", hash = "sha256:836ad80faa3e25a7d71015c990ce61f704a87328b1e73bcbb0623a18cbf17510", size = 153801, upload-time = "2025-07-22T19:36:33.693Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/da/81acbe0c1fd7e9e4ec35f55dadeba9833a847b9a6ba2e2d1e4432da901dd/msal-1.33.0.tar.gz", hash = "sha256:836ad80faa3e25a7d71015c990ce61f704a87328b1e73bcbb0623a18cbf17510", size = 153801 } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/5b/fbc73e91f7727ae1e79b21ed833308e99dc11cc1cd3d4717f579775de5e9/msal-1.33.0-py3-none-any.whl", hash = "sha256:c0cd41cecf8eaed733ee7e3be9e040291eba53b0f262d3ae9c58f38b04244273", size = 116853, upload-time = "2025-07-22T19:36:32.403Z" }, + { url = "https://files.pythonhosted.org/packages/86/5b/fbc73e91f7727ae1e79b21ed833308e99dc11cc1cd3d4717f579775de5e9/msal-1.33.0-py3-none-any.whl", hash = "sha256:c0cd41cecf8eaed733ee7e3be9e040291eba53b0f262d3ae9c58f38b04244273", size = 116853 }, ] [[package]] @@ -1411,49 +1410,49 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "msal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, ] [[package]] name = "murmurhash" version = "1.0.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/e9/02efbc6dfc2dd2085da3daacf9a8c17e8356019eceaedbfa21555e32d2af/murmurhash-1.0.13.tar.gz", hash = "sha256:737246d41ee00ff74b07b0bd1f0888be304d203ce668e642c86aa64ede30f8b7", size = 13258, upload-time = "2025-05-22T12:35:57.019Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/e9/02efbc6dfc2dd2085da3daacf9a8c17e8356019eceaedbfa21555e32d2af/murmurhash-1.0.13.tar.gz", hash = "sha256:737246d41ee00ff74b07b0bd1f0888be304d203ce668e642c86aa64ede30f8b7", size = 13258 } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/c3/ac14ed2aff4f18eadccf7d4e80c2361cf6e9a6a350442db9987919c4a747/murmurhash-1.0.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:136c7017e7d59ef16f065c2285bf5d30557ad8260adf47714c3c2802725e3e07", size = 26278, upload-time = "2025-05-22T12:35:10.16Z" }, - { url = "https://files.pythonhosted.org/packages/62/38/87e5f72aa96a0a816b90cd66209cda713e168d4d23b52af62fdba3c8b33c/murmurhash-1.0.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d0292f6fcd99361157fafad5c86d508f367931b7699cce1e14747364596950cb", size = 26528, upload-time = "2025-05-22T12:35:12.181Z" }, - { url = "https://files.pythonhosted.org/packages/6a/df/f74b22acf2ebf04ea24b858667836c9490e677ef29c1fe7bc993ecf4bc12/murmurhash-1.0.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12265dc748257966c62041b677201b8fa74334a2548dc27f1c7a9e78dab7c2c1", size = 120045, upload-time = "2025-05-22T12:35:13.657Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/19c48d4c5ad475e144fba5b1adf45d8a189eabde503168660e1ec5d081e8/murmurhash-1.0.13-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e411d5be64d37f2ce10a5d4d74c50bb35bd06205745b9631c4d8b1cb193e540", size = 117103, upload-time = "2025-05-22T12:35:14.899Z" }, - { url = "https://files.pythonhosted.org/packages/48/0e/3d6e009c539709f0cf643679977e2dfbd5d50e1ef49928f9a92941839482/murmurhash-1.0.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:da3500ad3dbf75ac9c6bc8c5fbc677d56dfc34aec0a289269939d059f194f61d", size = 118191, upload-time = "2025-05-22T12:35:16.098Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8c/fab9d11bde62783d2aa7919e1ecbbf12dea7100ea61f63f55c9e0f199a6a/murmurhash-1.0.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b23278c5428fc14f3101f8794f38ec937da042198930073e8c86d00add0fa2f0", size = 118663, upload-time = "2025-05-22T12:35:17.847Z" }, - { url = "https://files.pythonhosted.org/packages/cf/23/322d87ab935782f2676a836ea88d92f87e58db40fb49112ba03b03d335a1/murmurhash-1.0.13-cp310-cp310-win_amd64.whl", hash = "sha256:7bc27226c0e8d9927f8e59af0dfefc93f5009e4ec3dde8da4ba7751ba19edd47", size = 24504, upload-time = "2025-05-22T12:35:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d1/9d13a02d9c8bfff10b1f68d19df206eaf2a8011defeccf7eb05ea0b8c54e/murmurhash-1.0.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b20d168370bc3ce82920121b78ab35ae244070a9b18798f4a2e8678fa03bd7e0", size = 26410, upload-time = "2025-05-22T12:35:20.786Z" }, - { url = "https://files.pythonhosted.org/packages/14/b0/3ee762e98cf9a8c2df9c8b377c326f3dd4495066d4eace9066fca46eba7a/murmurhash-1.0.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cef667d2e83bdceea3bc20c586c491fa442662ace1aea66ff5e3a18bb38268d8", size = 26679, upload-time = "2025-05-22T12:35:21.808Z" }, - { url = "https://files.pythonhosted.org/packages/39/06/24618f79cd5aac48490932e50263bddfd1ea90f7123d49bfe806a5982675/murmurhash-1.0.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:507148e50929ba1fce36898808573b9f81c763d5676f3fc6e4e832ff56b66992", size = 125970, upload-time = "2025-05-22T12:35:23.222Z" }, - { url = "https://files.pythonhosted.org/packages/e8/09/0e7afce0a422692506c85474a26fb3a03c1971b2b5f7e7745276c4b3de7f/murmurhash-1.0.13-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d50f6173d266ad165beb8bca6101d824217fc9279f9e9981f4c0245c1e7ee6", size = 123390, upload-time = "2025-05-22T12:35:24.303Z" }, - { url = "https://files.pythonhosted.org/packages/22/4c/c98f579b1a951b2bcc722a35270a2eec105c1e21585c9b314a02079e3c4d/murmurhash-1.0.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0f272e15a84a8ae5f8b4bc0a68f9f47be38518ddffc72405791178058e9d019a", size = 124007, upload-time = "2025-05-22T12:35:25.446Z" }, - { url = "https://files.pythonhosted.org/packages/df/f8/1b0dcebc8df8e091341617102b5b3b97deb6435f345b84f75382c290ec2c/murmurhash-1.0.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9423e0b0964ed1013a06c970199538c7ef9ca28c0be54798c0f1473a6591761", size = 123705, upload-time = "2025-05-22T12:35:26.709Z" }, - { url = "https://files.pythonhosted.org/packages/79/17/f2a38558e150a0669d843f75e128afb83c1a67af41885ea2acb940e18e2a/murmurhash-1.0.13-cp311-cp311-win_amd64.whl", hash = "sha256:83b81e7084b696df3d853f2c78e0c9bda6b285d643f923f1a6fa9ab145d705c5", size = 24572, upload-time = "2025-05-22T12:35:30.38Z" }, + { url = "https://files.pythonhosted.org/packages/32/c3/ac14ed2aff4f18eadccf7d4e80c2361cf6e9a6a350442db9987919c4a747/murmurhash-1.0.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:136c7017e7d59ef16f065c2285bf5d30557ad8260adf47714c3c2802725e3e07", size = 26278 }, + { url = "https://files.pythonhosted.org/packages/62/38/87e5f72aa96a0a816b90cd66209cda713e168d4d23b52af62fdba3c8b33c/murmurhash-1.0.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d0292f6fcd99361157fafad5c86d508f367931b7699cce1e14747364596950cb", size = 26528 }, + { url = "https://files.pythonhosted.org/packages/6a/df/f74b22acf2ebf04ea24b858667836c9490e677ef29c1fe7bc993ecf4bc12/murmurhash-1.0.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12265dc748257966c62041b677201b8fa74334a2548dc27f1c7a9e78dab7c2c1", size = 120045 }, + { url = "https://files.pythonhosted.org/packages/f8/ed/19c48d4c5ad475e144fba5b1adf45d8a189eabde503168660e1ec5d081e8/murmurhash-1.0.13-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e411d5be64d37f2ce10a5d4d74c50bb35bd06205745b9631c4d8b1cb193e540", size = 117103 }, + { url = "https://files.pythonhosted.org/packages/48/0e/3d6e009c539709f0cf643679977e2dfbd5d50e1ef49928f9a92941839482/murmurhash-1.0.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:da3500ad3dbf75ac9c6bc8c5fbc677d56dfc34aec0a289269939d059f194f61d", size = 118191 }, + { url = "https://files.pythonhosted.org/packages/7c/8c/fab9d11bde62783d2aa7919e1ecbbf12dea7100ea61f63f55c9e0f199a6a/murmurhash-1.0.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b23278c5428fc14f3101f8794f38ec937da042198930073e8c86d00add0fa2f0", size = 118663 }, + { url = "https://files.pythonhosted.org/packages/cf/23/322d87ab935782f2676a836ea88d92f87e58db40fb49112ba03b03d335a1/murmurhash-1.0.13-cp310-cp310-win_amd64.whl", hash = "sha256:7bc27226c0e8d9927f8e59af0dfefc93f5009e4ec3dde8da4ba7751ba19edd47", size = 24504 }, + { url = "https://files.pythonhosted.org/packages/2c/d1/9d13a02d9c8bfff10b1f68d19df206eaf2a8011defeccf7eb05ea0b8c54e/murmurhash-1.0.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b20d168370bc3ce82920121b78ab35ae244070a9b18798f4a2e8678fa03bd7e0", size = 26410 }, + { url = "https://files.pythonhosted.org/packages/14/b0/3ee762e98cf9a8c2df9c8b377c326f3dd4495066d4eace9066fca46eba7a/murmurhash-1.0.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cef667d2e83bdceea3bc20c586c491fa442662ace1aea66ff5e3a18bb38268d8", size = 26679 }, + { url = "https://files.pythonhosted.org/packages/39/06/24618f79cd5aac48490932e50263bddfd1ea90f7123d49bfe806a5982675/murmurhash-1.0.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:507148e50929ba1fce36898808573b9f81c763d5676f3fc6e4e832ff56b66992", size = 125970 }, + { url = "https://files.pythonhosted.org/packages/e8/09/0e7afce0a422692506c85474a26fb3a03c1971b2b5f7e7745276c4b3de7f/murmurhash-1.0.13-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d50f6173d266ad165beb8bca6101d824217fc9279f9e9981f4c0245c1e7ee6", size = 123390 }, + { url = "https://files.pythonhosted.org/packages/22/4c/c98f579b1a951b2bcc722a35270a2eec105c1e21585c9b314a02079e3c4d/murmurhash-1.0.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0f272e15a84a8ae5f8b4bc0a68f9f47be38518ddffc72405791178058e9d019a", size = 124007 }, + { url = "https://files.pythonhosted.org/packages/df/f8/1b0dcebc8df8e091341617102b5b3b97deb6435f345b84f75382c290ec2c/murmurhash-1.0.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9423e0b0964ed1013a06c970199538c7ef9ca28c0be54798c0f1473a6591761", size = 123705 }, + { url = "https://files.pythonhosted.org/packages/79/17/f2a38558e150a0669d843f75e128afb83c1a67af41885ea2acb940e18e2a/murmurhash-1.0.13-cp311-cp311-win_amd64.whl", hash = "sha256:83b81e7084b696df3d853f2c78e0c9bda6b285d643f923f1a6fa9ab145d705c5", size = 24572 }, ] [[package]] name = "narwhals" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/58/0fbbfb13662297c8447d1872670da79f3f2a63fb68ae9aac9965cdc2d428/narwhals-2.0.0.tar.gz", hash = "sha256:d967bea54dfb6cd787abf3865ab4d72b8259d8f798c1c12c4eb693d5e9cebb24", size = 525527, upload-time = "2025-07-28T08:12:43.407Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/58/0fbbfb13662297c8447d1872670da79f3f2a63fb68ae9aac9965cdc2d428/narwhals-2.0.0.tar.gz", hash = "sha256:d967bea54dfb6cd787abf3865ab4d72b8259d8f798c1c12c4eb693d5e9cebb24", size = 525527 } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/9d/9e2afb7d3d43bfa1a1f80d2da291064753305f9871851f1cd5a60d870893/narwhals-2.0.0-py3-none-any.whl", hash = "sha256:9c9fe8a969b090d783edbcb3b58e1d0d15f5100fdf85b53f5e76d38f4ce7f19a", size = 385206, upload-time = "2025-07-28T08:12:39.545Z" }, + { url = "https://files.pythonhosted.org/packages/93/9d/9e2afb7d3d43bfa1a1f80d2da291064753305f9871851f1cd5a60d870893/narwhals-2.0.0-py3-none-any.whl", hash = "sha256:9c9fe8a969b090d783edbcb3b58e1d0d15f5100fdf85b53f5e76d38f4ce7f19a", size = 385206 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } 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" }, + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, ] [[package]] @@ -1463,9 +1462,9 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, ] [[package]] @@ -1475,9 +1474,9 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065 } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406 }, ] [[package]] @@ -1490,18 +1489,18 @@ dependencies = [ { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691, upload-time = "2024-08-18T19:48:37.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442, upload-time = "2024-08-18T19:48:21.909Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442 }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] [[package]] @@ -1512,47 +1511,47 @@ dependencies = [ { name = "llvmlite" }, { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615 } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663, upload-time = "2025-04-09T02:57:34.143Z" }, - { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344, upload-time = "2025-04-09T02:57:36.609Z" }, - { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054, upload-time = "2025-04-09T02:57:38.162Z" }, - { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531, upload-time = "2025-04-09T02:57:39.709Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612, upload-time = "2025-04-09T02:57:41.559Z" }, - { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825, upload-time = "2025-04-09T02:57:43.442Z" }, - { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695, upload-time = "2025-04-09T02:57:44.968Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227, upload-time = "2025-04-09T02:57:46.63Z" }, - { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422, upload-time = "2025-04-09T02:57:48.222Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505, upload-time = "2025-04-09T02:57:50.108Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663 }, + { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344 }, + { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054 }, + { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531 }, + { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612 }, + { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825 }, + { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695 }, + { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227 }, + { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422 }, + { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505 }, ] [[package]] name = "numpy" version = "1.26.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, - { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, - { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, - { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468 }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411 }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016 }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889 }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746 }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620 }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659 }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905 }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554 }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127 }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994 }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005 }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297 }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567 }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812 }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913 }, ] [[package]] name = "openai" -version = "1.97.1" +version = "1.107.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1564,27 +1563,27 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/57/1c471f6b3efb879d26686d31582997615e969f3bb4458111c9705e56332e/openai-1.97.1.tar.gz", hash = "sha256:a744b27ae624e3d4135225da9b1c89c107a2a7e5bc4c93e5b7b5214772ce7a4e", size = 494267, upload-time = "2025-07-22T13:10:12.607Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/67/d6498de300f83ff57a79cb7aa96ef3bef8d6f070c3ded0f1b5b45442a6bc/openai-1.107.0.tar.gz", hash = "sha256:43e04927584e57d0e9e640ee0077c78baf8150098be96ebd5c512539b6c4e9a4", size = 566056 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/35/412a0e9c3f0d37c94ed764b8ac7adae2d834dbd20e69f6aca582118e0f55/openai-1.97.1-py3-none-any.whl", hash = "sha256:4e96bbdf672ec3d44968c9ea39d2c375891db1acc1794668d8149d5fa6000606", size = 764380, upload-time = "2025-07-22T13:10:10.689Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/e8a4fd20390f2858b95227c288df8fe0c835f7c77625f7583609161684ba/openai-1.107.0-py3-none-any.whl", hash = "sha256:3dcfa3cbb116bd6924b27913b8da28c4a787379ff60049588547a1013e6d6438", size = 950968 }, ] [[package]] name = "overrides" version = "7.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] [[package]] @@ -1597,40 +1596,40 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ca/aa97b47287221fa37a49634532e520300088e290b20d690b21ce3e448143/pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9", size = 11542731, upload-time = "2025-07-07T19:18:12.619Z" }, - { url = "https://files.pythonhosted.org/packages/80/bf/7938dddc5f01e18e573dcfb0f1b8c9357d9b5fa6ffdee6e605b92efbdff2/pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1", size = 10790031, upload-time = "2025-07-07T19:18:16.611Z" }, - { url = "https://files.pythonhosted.org/packages/ee/2f/9af748366763b2a494fed477f88051dbf06f56053d5c00eba652697e3f94/pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f951fbb702dacd390561e0ea45cdd8ecfa7fb56935eb3dd78e306c19104b9b0", size = 11724083, upload-time = "2025-07-07T19:18:20.512Z" }, - { url = "https://files.pythonhosted.org/packages/2c/95/79ab37aa4c25d1e7df953dde407bb9c3e4ae47d154bc0dd1692f3a6dcf8c/pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd05b72ec02ebfb993569b4931b2e16fbb4d6ad6ce80224a3ee838387d83a191", size = 12342360, upload-time = "2025-07-07T19:18:23.194Z" }, - { url = "https://files.pythonhosted.org/packages/75/a7/d65e5d8665c12c3c6ff5edd9709d5836ec9b6f80071b7f4a718c6106e86e/pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1b916a627919a247d865aed068eb65eb91a344b13f5b57ab9f610b7716c92de1", size = 13202098, upload-time = "2025-07-07T19:18:25.558Z" }, - { url = "https://files.pythonhosted.org/packages/65/f3/4c1dbd754dbaa79dbf8b537800cb2fa1a6e534764fef50ab1f7533226c5c/pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fe67dc676818c186d5a3d5425250e40f179c2a89145df477dd82945eaea89e97", size = 13837228, upload-time = "2025-07-07T19:18:28.344Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d6/d7f5777162aa9b48ec3910bca5a58c9b5927cfd9cfde3aa64322f5ba4b9f/pandas-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eb789ae0274672acbd3c575b0598d213345660120a257b47b5dafdc618aec83", size = 11336561, upload-time = "2025-07-07T19:18:31.211Z" }, - { url = "https://files.pythonhosted.org/packages/76/1c/ccf70029e927e473a4476c00e0d5b32e623bff27f0402d0a92b7fc29bb9f/pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b", size = 11566608, upload-time = "2025-07-07T19:18:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d3/3c37cb724d76a841f14b8f5fe57e5e3645207cc67370e4f84717e8bb7657/pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f", size = 10823181, upload-time = "2025-07-07T19:18:36.151Z" }, - { url = "https://files.pythonhosted.org/packages/8a/4c/367c98854a1251940edf54a4df0826dcacfb987f9068abf3e3064081a382/pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85", size = 11793570, upload-time = "2025-07-07T19:18:38.385Z" }, - { url = "https://files.pythonhosted.org/packages/07/5f/63760ff107bcf5146eee41b38b3985f9055e710a72fdd637b791dea3495c/pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d", size = 12378887, upload-time = "2025-07-07T19:18:41.284Z" }, - { url = "https://files.pythonhosted.org/packages/15/53/f31a9b4dfe73fe4711c3a609bd8e60238022f48eacedc257cd13ae9327a7/pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678", size = 13230957, upload-time = "2025-07-07T19:18:44.187Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/6fce6bf85b5056d065e0a7933cba2616dcb48596f7ba3c6341ec4bcc529d/pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299", size = 13883883, upload-time = "2025-07-07T19:18:46.498Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7b/bdcb1ed8fccb63d04bdb7635161d0ec26596d92c9d7a6cce964e7876b6c1/pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab", size = 11340212, upload-time = "2025-07-07T19:18:49.293Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ca/aa97b47287221fa37a49634532e520300088e290b20d690b21ce3e448143/pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9", size = 11542731 }, + { url = "https://files.pythonhosted.org/packages/80/bf/7938dddc5f01e18e573dcfb0f1b8c9357d9b5fa6ffdee6e605b92efbdff2/pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1", size = 10790031 }, + { url = "https://files.pythonhosted.org/packages/ee/2f/9af748366763b2a494fed477f88051dbf06f56053d5c00eba652697e3f94/pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f951fbb702dacd390561e0ea45cdd8ecfa7fb56935eb3dd78e306c19104b9b0", size = 11724083 }, + { url = "https://files.pythonhosted.org/packages/2c/95/79ab37aa4c25d1e7df953dde407bb9c3e4ae47d154bc0dd1692f3a6dcf8c/pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd05b72ec02ebfb993569b4931b2e16fbb4d6ad6ce80224a3ee838387d83a191", size = 12342360 }, + { url = "https://files.pythonhosted.org/packages/75/a7/d65e5d8665c12c3c6ff5edd9709d5836ec9b6f80071b7f4a718c6106e86e/pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1b916a627919a247d865aed068eb65eb91a344b13f5b57ab9f610b7716c92de1", size = 13202098 }, + { url = "https://files.pythonhosted.org/packages/65/f3/4c1dbd754dbaa79dbf8b537800cb2fa1a6e534764fef50ab1f7533226c5c/pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fe67dc676818c186d5a3d5425250e40f179c2a89145df477dd82945eaea89e97", size = 13837228 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/d7f5777162aa9b48ec3910bca5a58c9b5927cfd9cfde3aa64322f5ba4b9f/pandas-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eb789ae0274672acbd3c575b0598d213345660120a257b47b5dafdc618aec83", size = 11336561 }, + { url = "https://files.pythonhosted.org/packages/76/1c/ccf70029e927e473a4476c00e0d5b32e623bff27f0402d0a92b7fc29bb9f/pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b", size = 11566608 }, + { url = "https://files.pythonhosted.org/packages/ec/d3/3c37cb724d76a841f14b8f5fe57e5e3645207cc67370e4f84717e8bb7657/pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f", size = 10823181 }, + { url = "https://files.pythonhosted.org/packages/8a/4c/367c98854a1251940edf54a4df0826dcacfb987f9068abf3e3064081a382/pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85", size = 11793570 }, + { url = "https://files.pythonhosted.org/packages/07/5f/63760ff107bcf5146eee41b38b3985f9055e710a72fdd637b791dea3495c/pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d", size = 12378887 }, + { url = "https://files.pythonhosted.org/packages/15/53/f31a9b4dfe73fe4711c3a609bd8e60238022f48eacedc257cd13ae9327a7/pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678", size = 13230957 }, + { url = "https://files.pythonhosted.org/packages/e0/94/6fce6bf85b5056d065e0a7933cba2616dcb48596f7ba3c6341ec4bcc529d/pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299", size = 13883883 }, + { url = "https://files.pythonhosted.org/packages/c8/7b/bdcb1ed8fccb63d04bdb7635161d0ec26596d92c9d7a6cce964e7876b6c1/pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab", size = 11340212 }, ] [[package]] name = "parso" version = "0.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, ] [[package]] name = "pastel" version = "0.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955 }, ] [[package]] @@ -1640,9 +1639,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/81/74f6a65b848ffd16c18f920620ce999fe45fe27f01ab3911260ce4ed85e4/patsy-1.0.1.tar.gz", hash = "sha256:e786a9391eec818c054e359b737bbce692f051aee4c661f4141cc88fb459c0c4", size = 396010, upload-time = "2024-11-12T14:10:54.642Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/81/74f6a65b848ffd16c18f920620ce999fe45fe27f01ab3911260ce4ed85e4/patsy-1.0.1.tar.gz", hash = "sha256:e786a9391eec818c054e359b737bbce692f051aee4c661f4141cc88fb459c0c4", size = 396010 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/2b/b50d3d08ea0fc419c183a84210571eba005328efa62b6b98bc28e9ead32a/patsy-1.0.1-py2.py3-none-any.whl", hash = "sha256:751fb38f9e97e62312e921a1954b81e1bb2bcda4f5eeabaf94db251ee791509c", size = 232923, upload-time = "2024-11-12T14:10:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/87/2b/b50d3d08ea0fc419c183a84210571eba005328efa62b6b98bc28e9ead32a/patsy-1.0.1-py2.py3-none-any.whl", hash = "sha256:751fb38f9e97e62312e921a1954b81e1bb2bcda4f5eeabaf94db251ee791509c", size = 232923 }, ] [[package]] @@ -1652,62 +1651,62 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, ] [[package]] name = "pillow" version = "11.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, - { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, - { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, - { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, - { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, - { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, - { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, - { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, - { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, - { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, - { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554 }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548 }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742 }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087 }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350 }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840 }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005 }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372 }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090 }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988 }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899 }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531 }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560 }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978 }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168 }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053 }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273 }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043 }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516 }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768 }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055 }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079 }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556 }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625 }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207 }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939 }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166 }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482 }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566 }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618 }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248 }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963 }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170 }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505 }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598 }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, ] [[package]] @@ -1719,9 +1718,9 @@ dependencies = [ { name = "pyyaml" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/ac/311c8a492dc887f0b7a54d0ec3324cb2f9538b7b78ea06e5f7ae1f167e52/poethepoet-0.36.0.tar.gz", hash = "sha256:2217b49cb4e4c64af0b42ff8c4814b17f02e107d38bc461542517348ede25663", size = 66854, upload-time = "2025-06-29T19:54:50.444Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/ac/311c8a492dc887f0b7a54d0ec3324cb2f9538b7b78ea06e5f7ae1f167e52/poethepoet-0.36.0.tar.gz", hash = "sha256:2217b49cb4e4c64af0b42ff8c4814b17f02e107d38bc461542517348ede25663", size = 66854 } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/29/dedb3a6b7e17ea723143b834a2da428a7d743c80d5cd4d22ed28b5e8c441/poethepoet-0.36.0-py3-none-any.whl", hash = "sha256:693e3c1eae9f6731d3613c3c0c40f747d3c5c68a375beda42e590a63c5623308", size = 88031, upload-time = "2025-06-29T19:54:48.884Z" }, + { url = "https://files.pythonhosted.org/packages/03/29/dedb3a6b7e17ea723143b834a2da428a7d743c80d5cd4d22ed28b5e8c441/poethepoet-0.36.0-py3-none-any.whl", hash = "sha256:693e3c1eae9f6731d3613c3c0c40f747d3c5c68a375beda42e590a63c5623308", size = 88031 }, ] [[package]] @@ -1732,22 +1731,22 @@ dependencies = [ { name = "numpy" }, { name = "scipy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/40/3e0c8dd88328d944f9d82b30cafd2a1c911bddff0b8bccc8dc9dd5e45b7c/pot-0.9.5.tar.gz", hash = "sha256:9644ee7ff51c3cffa3c2632b9dd9dff4f3520266f9fb771450935ffb646d6042", size = 440808, upload-time = "2024-11-07T10:05:05.567Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/40/3e0c8dd88328d944f9d82b30cafd2a1c911bddff0b8bccc8dc9dd5e45b7c/pot-0.9.5.tar.gz", hash = "sha256:9644ee7ff51c3cffa3c2632b9dd9dff4f3520266f9fb771450935ffb646d6042", size = 440808 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/53/acd66a8e50f992e6ca578181009e81d367ad738d0ac135f63d0de3ca92cd/POT-0.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:34d766c38e65a69c087b01a854fe89fbd152c3e8af93da2227b6c40aed6d37b9", size = 410989, upload-time = "2024-11-07T10:04:04.166Z" }, - { url = "https://files.pythonhosted.org/packages/24/51/43c68e7cb1dc7c40286d9e19f6cb599108cd01c2b32307296eba9cb01a05/POT-0.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5407377256de11b6fdc94bbba9b50ea5a2301570905fc9014541cc8473806d9", size = 351111, upload-time = "2024-11-07T10:04:06.604Z" }, - { url = "https://files.pythonhosted.org/packages/3f/87/17069069948e40fa0e41366e6412322c7849d4b2a0ddae0428d10b571604/POT-0.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f37039cd356198c1fb994e7d935b9bf75d44f2a40319d298bf8cc149eb360d5", size = 344289, upload-time = "2024-11-07T10:04:08.151Z" }, - { url = "https://files.pythonhosted.org/packages/21/49/7bbb5ac2989abd775ae200cdbcf1a2e023cf07e8d1d6afc7d673d4e380d3/POT-0.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00a18427c9abdd107a2285ea0a814c6b22e95a1af8f88a37c56f23cd216f7a6b", size = 858699, upload-time = "2024-11-07T10:04:10.231Z" }, - { url = "https://files.pythonhosted.org/packages/97/ad/1724a238cef180c04a3d63e8702cbe91f0abe946eb7a55c3857cd0ac1d9b/POT-0.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0dc608cea1107289a58dec33cddc1b0a3fea77ff36d66e2c8ac7aeea543969a", size = 865565, upload-time = "2024-11-07T10:04:12.421Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e9/a1901cbbf765b765ab4adace1711adc3eef01db526dc898e31fbdca653a5/POT-0.9.5-cp310-cp310-win32.whl", hash = "sha256:8312bee055389db47adab063749c8d77b5981534177ca6cd9b91e4fb68f69d00", size = 344137, upload-time = "2024-11-07T10:04:14.693Z" }, - { url = "https://files.pythonhosted.org/packages/95/00/2ef88c57c0ee5ff55a95bcb3ff62d904039bb460809d7577ec314b5e7186/POT-0.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:043706d69202ac87e140121ba32ed1b038f2b3fc4a5549586187239a583cd50d", size = 348385, upload-time = "2024-11-07T10:04:15.851Z" }, - { url = "https://files.pythonhosted.org/packages/08/81/c9eaa405d40567452d102385a2077b4d34f7961dd7ea3354b7749efd4ea7/POT-0.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b5f000da00e408ff781672a4895bfa8daacec055bd534c9e66ead479f3c6d83c", size = 410977, upload-time = "2024-11-07T10:04:17.396Z" }, - { url = "https://files.pythonhosted.org/packages/43/32/8d319ab8eee96397569115aac644b19136170966667c59b026c277e1b026/POT-0.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9eddd9ff29bdb17d4db8ba00ba18d42656c694a128591502bf59afc1369e1bb3", size = 351059, upload-time = "2024-11-07T10:04:18.821Z" }, - { url = "https://files.pythonhosted.org/packages/23/7c/ed772734847ada457af0fdb9dd7073bd3823915721bf64147a1434da5a0c/POT-0.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7eb9b88c73387a9966775a6f6d077d9d071814783701d2656dc05b5032a9662d", size = 344293, upload-time = "2024-11-07T10:04:20.193Z" }, - { url = "https://files.pythonhosted.org/packages/8d/af/a99bc77cf4f79ec04b23d415da005e83aa2a2b91d4216045c87f46d3109f/POT-0.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f44446056f5fc9d132ed8e431732c33cbe754fb1e6d73636f1b6ae811be7df", size = 891139, upload-time = "2024-11-07T10:04:22.344Z" }, - { url = "https://files.pythonhosted.org/packages/68/e8/efc53871cc5b086565702e123d62b37aa40320023b46b30923bb9055b287/POT-0.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7f5d27bc9063e01b03d906bb77e7b3428065fdd72ed64233b249584ead2e2bf", size = 897470, upload-time = "2024-11-07T10:04:23.686Z" }, - { url = "https://files.pythonhosted.org/packages/a1/dd/aab8edf448d68fa6be6454887667e04a7bf2b2a5929f2ec35c49f83ef286/POT-0.9.5-cp311-cp311-win32.whl", hash = "sha256:cd79a8b4d35b706f2124f73ebff3bb1ce3450e01cc8f610eda3b6ce13616b829", size = 343915, upload-time = "2024-11-07T10:04:24.98Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ee/9cd8b16e4e8e7254951b83fc6f871763e7e1315078b17b7008662833ed63/POT-0.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:6680aadb69df2f75a413fe9c58bd1c5cb744d017a7c8ba8841654fd0dc75433b", size = 348566, upload-time = "2024-11-07T10:04:26.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/53/acd66a8e50f992e6ca578181009e81d367ad738d0ac135f63d0de3ca92cd/POT-0.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:34d766c38e65a69c087b01a854fe89fbd152c3e8af93da2227b6c40aed6d37b9", size = 410989 }, + { url = "https://files.pythonhosted.org/packages/24/51/43c68e7cb1dc7c40286d9e19f6cb599108cd01c2b32307296eba9cb01a05/POT-0.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5407377256de11b6fdc94bbba9b50ea5a2301570905fc9014541cc8473806d9", size = 351111 }, + { url = "https://files.pythonhosted.org/packages/3f/87/17069069948e40fa0e41366e6412322c7849d4b2a0ddae0428d10b571604/POT-0.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f37039cd356198c1fb994e7d935b9bf75d44f2a40319d298bf8cc149eb360d5", size = 344289 }, + { url = "https://files.pythonhosted.org/packages/21/49/7bbb5ac2989abd775ae200cdbcf1a2e023cf07e8d1d6afc7d673d4e380d3/POT-0.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00a18427c9abdd107a2285ea0a814c6b22e95a1af8f88a37c56f23cd216f7a6b", size = 858699 }, + { url = "https://files.pythonhosted.org/packages/97/ad/1724a238cef180c04a3d63e8702cbe91f0abe946eb7a55c3857cd0ac1d9b/POT-0.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0dc608cea1107289a58dec33cddc1b0a3fea77ff36d66e2c8ac7aeea543969a", size = 865565 }, + { url = "https://files.pythonhosted.org/packages/1c/e9/a1901cbbf765b765ab4adace1711adc3eef01db526dc898e31fbdca653a5/POT-0.9.5-cp310-cp310-win32.whl", hash = "sha256:8312bee055389db47adab063749c8d77b5981534177ca6cd9b91e4fb68f69d00", size = 344137 }, + { url = "https://files.pythonhosted.org/packages/95/00/2ef88c57c0ee5ff55a95bcb3ff62d904039bb460809d7577ec314b5e7186/POT-0.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:043706d69202ac87e140121ba32ed1b038f2b3fc4a5549586187239a583cd50d", size = 348385 }, + { url = "https://files.pythonhosted.org/packages/08/81/c9eaa405d40567452d102385a2077b4d34f7961dd7ea3354b7749efd4ea7/POT-0.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b5f000da00e408ff781672a4895bfa8daacec055bd534c9e66ead479f3c6d83c", size = 410977 }, + { url = "https://files.pythonhosted.org/packages/43/32/8d319ab8eee96397569115aac644b19136170966667c59b026c277e1b026/POT-0.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9eddd9ff29bdb17d4db8ba00ba18d42656c694a128591502bf59afc1369e1bb3", size = 351059 }, + { url = "https://files.pythonhosted.org/packages/23/7c/ed772734847ada457af0fdb9dd7073bd3823915721bf64147a1434da5a0c/POT-0.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7eb9b88c73387a9966775a6f6d077d9d071814783701d2656dc05b5032a9662d", size = 344293 }, + { url = "https://files.pythonhosted.org/packages/8d/af/a99bc77cf4f79ec04b23d415da005e83aa2a2b91d4216045c87f46d3109f/POT-0.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f44446056f5fc9d132ed8e431732c33cbe754fb1e6d73636f1b6ae811be7df", size = 891139 }, + { url = "https://files.pythonhosted.org/packages/68/e8/efc53871cc5b086565702e123d62b37aa40320023b46b30923bb9055b287/POT-0.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7f5d27bc9063e01b03d906bb77e7b3428065fdd72ed64233b249584ead2e2bf", size = 897470 }, + { url = "https://files.pythonhosted.org/packages/a1/dd/aab8edf448d68fa6be6454887667e04a7bf2b2a5929f2ec35c49f83ef286/POT-0.9.5-cp311-cp311-win32.whl", hash = "sha256:cd79a8b4d35b706f2124f73ebff3bb1ce3450e01cc8f610eda3b6ce13616b829", size = 343915 }, + { url = "https://files.pythonhosted.org/packages/fe/ee/9cd8b16e4e8e7254951b83fc6f871763e7e1315078b17b7008662833ed63/POT-0.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:6680aadb69df2f75a413fe9c58bd1c5cb744d017a7c8ba8841654fd0dc75433b", size = 348566 }, ] [[package]] @@ -1758,22 +1757,22 @@ dependencies = [ { name = "cymem" }, { name = "murmurhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/3a/db814f67a05b6d7f9c15d38edef5ec9b21415710705b393883de92aee5ef/preshed-3.0.10.tar.gz", hash = "sha256:5a5c8e685e941f4ffec97f1fbf32694b8107858891a4bc34107fac981d8296ff", size = 15039, upload-time = "2025-05-26T15:18:33.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/3a/db814f67a05b6d7f9c15d38edef5ec9b21415710705b393883de92aee5ef/preshed-3.0.10.tar.gz", hash = "sha256:5a5c8e685e941f4ffec97f1fbf32694b8107858891a4bc34107fac981d8296ff", size = 15039 } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/12/3bfd7790481513d71a281a3a7194a6d7aa9a59289a109253e78d9bcedcec/preshed-3.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:14593c32e6705fda0fd54684293ca079530418bb1fb036dcbaa6c0ef0f144b7d", size = 131102, upload-time = "2025-05-26T15:17:41.762Z" }, - { url = "https://files.pythonhosted.org/packages/e4/bf/54635387524315fe40b1f3d1688a5ad369f59a4e3a377b0da6e8a3ecba30/preshed-3.0.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba1960a3996678aded882260133853e19e3a251d9f35a19c9d7d830c4238c4eb", size = 127302, upload-time = "2025-05-26T15:17:43.263Z" }, - { url = "https://files.pythonhosted.org/packages/fe/df/d057705c9c6aff877ee687f612f242006750f165c0e557f6075fe913a8e3/preshed-3.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0830c0a262015be743a01455a1da5963750afed1bde2395590b01af3b7da2741", size = 793737, upload-time = "2025-05-26T15:17:44.736Z" }, - { url = "https://files.pythonhosted.org/packages/c4/73/9206a60e59e81a259d49273f95307821f5e88c84c400533ed0cb9a8093af/preshed-3.0.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:165dda5862c28e77ee1f3feabad98d4ebb65345f458b5626596b92fd20a65275", size = 795131, upload-time = "2025-05-26T15:17:46.382Z" }, - { url = "https://files.pythonhosted.org/packages/25/18/02a40bcb13ae6c1ca3a859a709354621b45c83857994943c9c409f85f183/preshed-3.0.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e88e4c7fbbfa7c23a90d7d0cbe27e4c5fa2fd742ef1be09c153f9ccd2c600098", size = 777924, upload-time = "2025-05-26T15:17:48.184Z" }, - { url = "https://files.pythonhosted.org/packages/11/13/bb2db0f037fc659494fbe964255f80fbca7e5e4154137e9855619e3543d9/preshed-3.0.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87780ae00def0c97130c9d1652295ec8362c2e4ca553673b64fe0dc7b321a382", size = 796024, upload-time = "2025-05-26T15:17:49.568Z" }, - { url = "https://files.pythonhosted.org/packages/99/ab/7187df84a32f02d987b689f4bbb1ad77304bdc8129d8fed483b8ebde113d/preshed-3.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:32496f216255a6cbdd60965dde29ff42ed8fc2d77968c28ae875e3856c6fa01a", size = 117429, upload-time = "2025-05-26T15:17:51.091Z" }, - { url = "https://files.pythonhosted.org/packages/08/99/c3709638f687da339504d1daeca48604cadb338bf3556a1484d1f0cd95e6/preshed-3.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d96c4fe2b41c1cdcc8c4fc1fdb10f922a6095c0430a3ebe361fe62c78902d068", size = 131486, upload-time = "2025-05-26T15:17:52.231Z" }, - { url = "https://files.pythonhosted.org/packages/e0/27/0fd36b63caa8bbf57b31a121d9565d385bbd7521771d4eb93e17d326873d/preshed-3.0.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb01ea930b96f3301526a2ab26f41347d07555e4378c4144c6b7645074f2ebb0", size = 127938, upload-time = "2025-05-26T15:17:54.19Z" }, - { url = "https://files.pythonhosted.org/packages/90/54/6a876d9cc8d401a9c1fb6bb8ca5a31b3664d0bcb888a9016258a1ae17344/preshed-3.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dd1f0a7b7d150e229d073fd4fe94f72610cae992e907cee74687c4695873a98", size = 842263, upload-time = "2025-05-26T15:17:55.398Z" }, - { url = "https://files.pythonhosted.org/packages/1c/7d/ff19f74d15ee587905bafa3582883cfe2f72b574e6d691ee64dc690dc276/preshed-3.0.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fd7b350c280137f324cd447afbf6ba9a849af0e8898850046ac6f34010e08bd", size = 842913, upload-time = "2025-05-26T15:17:56.687Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3a/1c345a26463345557705b61965e1e0a732cc0e9c6dfd4787845dbfa50b4a/preshed-3.0.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cf6a5fdc89ad06079aa6ee63621e417d4f4cf2a3d8b63c72728baad35a9ff641", size = 820548, upload-time = "2025-05-26T15:17:58.057Z" }, - { url = "https://files.pythonhosted.org/packages/7f/6b/71f25e2b7a23dba168f43edfae0bb508552dbef89114ce65c73f2ea7172f/preshed-3.0.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c29a7bd66985808ad181c9ad05205a6aa7400cd0f98426acd7bc86588b93f8", size = 840379, upload-time = "2025-05-26T15:17:59.565Z" }, - { url = "https://files.pythonhosted.org/packages/3a/86/d8f32b0b31a36ee8770a9b1a95321430e364cd0ba4bfebb7348aed2f198d/preshed-3.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:1367c1fd6f44296305315d4e1c3fe3171787d4d01c1008a76bc9466bd79c3249", size = 117655, upload-time = "2025-05-26T15:18:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/66/12/3bfd7790481513d71a281a3a7194a6d7aa9a59289a109253e78d9bcedcec/preshed-3.0.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:14593c32e6705fda0fd54684293ca079530418bb1fb036dcbaa6c0ef0f144b7d", size = 131102 }, + { url = "https://files.pythonhosted.org/packages/e4/bf/54635387524315fe40b1f3d1688a5ad369f59a4e3a377b0da6e8a3ecba30/preshed-3.0.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba1960a3996678aded882260133853e19e3a251d9f35a19c9d7d830c4238c4eb", size = 127302 }, + { url = "https://files.pythonhosted.org/packages/fe/df/d057705c9c6aff877ee687f612f242006750f165c0e557f6075fe913a8e3/preshed-3.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0830c0a262015be743a01455a1da5963750afed1bde2395590b01af3b7da2741", size = 793737 }, + { url = "https://files.pythonhosted.org/packages/c4/73/9206a60e59e81a259d49273f95307821f5e88c84c400533ed0cb9a8093af/preshed-3.0.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:165dda5862c28e77ee1f3feabad98d4ebb65345f458b5626596b92fd20a65275", size = 795131 }, + { url = "https://files.pythonhosted.org/packages/25/18/02a40bcb13ae6c1ca3a859a709354621b45c83857994943c9c409f85f183/preshed-3.0.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e88e4c7fbbfa7c23a90d7d0cbe27e4c5fa2fd742ef1be09c153f9ccd2c600098", size = 777924 }, + { url = "https://files.pythonhosted.org/packages/11/13/bb2db0f037fc659494fbe964255f80fbca7e5e4154137e9855619e3543d9/preshed-3.0.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87780ae00def0c97130c9d1652295ec8362c2e4ca553673b64fe0dc7b321a382", size = 796024 }, + { url = "https://files.pythonhosted.org/packages/99/ab/7187df84a32f02d987b689f4bbb1ad77304bdc8129d8fed483b8ebde113d/preshed-3.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:32496f216255a6cbdd60965dde29ff42ed8fc2d77968c28ae875e3856c6fa01a", size = 117429 }, + { url = "https://files.pythonhosted.org/packages/08/99/c3709638f687da339504d1daeca48604cadb338bf3556a1484d1f0cd95e6/preshed-3.0.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d96c4fe2b41c1cdcc8c4fc1fdb10f922a6095c0430a3ebe361fe62c78902d068", size = 131486 }, + { url = "https://files.pythonhosted.org/packages/e0/27/0fd36b63caa8bbf57b31a121d9565d385bbd7521771d4eb93e17d326873d/preshed-3.0.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb01ea930b96f3301526a2ab26f41347d07555e4378c4144c6b7645074f2ebb0", size = 127938 }, + { url = "https://files.pythonhosted.org/packages/90/54/6a876d9cc8d401a9c1fb6bb8ca5a31b3664d0bcb888a9016258a1ae17344/preshed-3.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dd1f0a7b7d150e229d073fd4fe94f72610cae992e907cee74687c4695873a98", size = 842263 }, + { url = "https://files.pythonhosted.org/packages/1c/7d/ff19f74d15ee587905bafa3582883cfe2f72b574e6d691ee64dc690dc276/preshed-3.0.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fd7b350c280137f324cd447afbf6ba9a849af0e8898850046ac6f34010e08bd", size = 842913 }, + { url = "https://files.pythonhosted.org/packages/f1/3a/1c345a26463345557705b61965e1e0a732cc0e9c6dfd4787845dbfa50b4a/preshed-3.0.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cf6a5fdc89ad06079aa6ee63621e417d4f4cf2a3d8b63c72728baad35a9ff641", size = 820548 }, + { url = "https://files.pythonhosted.org/packages/7f/6b/71f25e2b7a23dba168f43edfae0bb508552dbef89114ce65c73f2ea7172f/preshed-3.0.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c29a7bd66985808ad181c9ad05205a6aa7400cd0f98426acd7bc86588b93f8", size = 840379 }, + { url = "https://files.pythonhosted.org/packages/3a/86/d8f32b0b31a36ee8770a9b1a95321430e364cd0ba4bfebb7348aed2f198d/preshed-3.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:1367c1fd6f44296305315d4e1c3fe3171787d4d01c1008a76bc9466bd79c3249", size = 117655 }, ] [[package]] @@ -1783,90 +1782,87 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, ] [[package]] name = "protobuf" version = "5.29.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226, upload-time = "2025-05-28T23:51:59.82Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963, upload-time = "2025-05-28T23:51:41.204Z" }, - { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818, upload-time = "2025-05-28T23:51:44.297Z" }, - { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091, upload-time = "2025-05-28T23:51:45.907Z" }, - { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824, upload-time = "2025-05-28T23:51:47.545Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942, upload-time = "2025-05-28T23:51:49.11Z" }, - { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823, upload-time = "2025-05-28T23:51:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963 }, + { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818 }, + { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091 }, + { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824 }, + { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942 }, + { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823 }, ] [[package]] name = "psutil" version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, ] [[package]] name = "pure-eval" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, ] [[package]] name = "pyarrow" -version = "15.0.2" +version = "21.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/35/a1/b7c9bacfd17a9d1d8d025db2fc39112e0b1a629ea401880e4e97632dbc4c/pyarrow-15.0.2.tar.gz", hash = "sha256:9c9bc803cb3b7bfacc1e96ffbfd923601065d9d3f911179d81e72d99fd74a3d9", size = 1064226, upload-time = "2024-03-18T16:58:06.866Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487 } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/fc/9e58e43f41d161bf3b3bcc580170b3b0bdac8c0f1603a65b967cf94b6bf4/pyarrow-15.0.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:88b340f0a1d05b5ccc3d2d986279045655b1fe8e41aba6ca44ea28da0d1455d8", size = 27150472, upload-time = "2024-03-18T16:53:30.164Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f4/d39bdce9661621df9bdb511c3f72c81817edc8bc6365672b22a5de41004a/pyarrow-15.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eaa8f96cecf32da508e6c7f69bb8401f03745c050c1dd42ec2596f2e98deecac", size = 24196261, upload-time = "2024-03-18T16:53:37.402Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b2/de978e01592192695c7449c6fa28f2269bf74808b533a177c90ee6295bdd/pyarrow-15.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23c6753ed4f6adb8461e7c383e418391b8d8453c5d67e17f416c3a5d5709afbd", size = 36153060, upload-time = "2024-03-18T16:53:46.902Z" }, - { url = "https://files.pythonhosted.org/packages/01/e0/13aada7b0af1039554e675bd8c878acb3d86bab690e5a6b05fc8547a9cf2/pyarrow-15.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f639c059035011db8c0497e541a8a45d98a58dbe34dc8fadd0ef128f2cee46e5", size = 38402930, upload-time = "2024-03-18T16:53:55.894Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f9/7f82c25c89828f38ebc2ce2f7d6b544107bc7502255ed92ac398be69cc19/pyarrow-15.0.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:290e36a59a0993e9a5224ed2fb3e53375770f07379a0ea03ee2fce2e6d30b423", size = 35655190, upload-time = "2024-03-18T16:54:04.8Z" }, - { url = "https://files.pythonhosted.org/packages/e9/0e/0d30e6fd1e0fc9cc267381520f9386a56b2b51c4066d8f9a0d4a5a2e0b44/pyarrow-15.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:06c2bb2a98bc792f040bef31ad3e9be6a63d0cb39189227c08a7d955db96816e", size = 38331501, upload-time = "2024-03-18T16:54:14.322Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/abca962d99950aad803bd755baf020a8183ca3be1319bb205f52bbbcce16/pyarrow-15.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:f7a197f3670606a960ddc12adbe8075cea5f707ad7bf0dffa09637fdbb89f76c", size = 24814742, upload-time = "2024-03-18T16:54:21.932Z" }, - { url = "https://files.pythonhosted.org/packages/34/50/93f6104e79bec6e1af4356f5164695a0b6338f230e1273706ec9eb836bea/pyarrow-15.0.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5f8bc839ea36b1f99984c78e06e7a06054693dc2af8920f6fb416b5bca9944e4", size = 27187122, upload-time = "2024-03-18T16:54:29.514Z" }, - { url = "https://files.pythonhosted.org/packages/47/cb/be17c4879e60e683761be281d955923d586a572fbc2503e08f08ca713349/pyarrow-15.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5e81dfb4e519baa6b4c80410421528c214427e77ca0ea9461eb4097c328fa33", size = 24217346, upload-time = "2024-03-18T16:54:36.41Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f6/57d67d7729643ebc80f0df18420b9fc1857ca418d1b2bb3bc5be2fd2119e/pyarrow-15.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4f240852b302a7af4646c8bfe9950c4691a419847001178662a98915fd7ee7", size = 36151795, upload-time = "2024-03-18T16:54:44.674Z" }, - { url = "https://files.pythonhosted.org/packages/ff/42/df219f3a1e06c2dd63599243384d6ba2a02a44a976801fbc9601264ff562/pyarrow-15.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e7d9cfb5a1e648e172428c7a42b744610956f3b70f524aa3a6c02a448ba853e", size = 38398065, upload-time = "2024-03-18T16:54:53.221Z" }, - { url = "https://files.pythonhosted.org/packages/4a/37/a32de321c7270df01b709f554903acf4edaaef373310ff116302224348a9/pyarrow-15.0.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2d4f905209de70c0eb5b2de6763104d5a9a37430f137678edfb9a675bac9cd98", size = 35672270, upload-time = "2024-03-18T16:55:02.175Z" }, - { url = "https://files.pythonhosted.org/packages/61/94/0b28417737ea56a4819603c0024c8b24365f85154bb938785352e09bea55/pyarrow-15.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90adb99e8ce5f36fbecbbc422e7dcbcbed07d985eed6062e459e23f9e71fd197", size = 38346410, upload-time = "2024-03-18T16:55:10.399Z" }, - { url = "https://files.pythonhosted.org/packages/96/2f/0092154f3e1ebbc814de1f8a9075543d77a7ecc691fbad407df174799abe/pyarrow-15.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:b116e7fd7889294cbd24eb90cd9bdd3850be3738d61297855a71ac3b8124ee38", size = 24799922, upload-time = "2024-03-18T16:55:17.261Z" }, + { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837 }, + { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470 }, + { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619 }, + { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488 }, + { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159 }, + { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567 }, + { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959 }, + { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234 }, + { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370 }, + { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424 }, + { url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810 }, + { url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538 }, + { url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056 }, + { url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568 }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] [[package]] @@ -1879,9 +1875,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 }, ] [[package]] @@ -1891,53 +1887,53 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, - { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, - { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, - { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, - { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, - { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, - { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, - { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, - { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817 }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357 }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011 }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730 }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178 }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462 }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652 }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306 }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720 }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915 }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884 }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496 }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982 }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412 }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749 }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527 }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225 }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490 }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525 }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446 }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678 }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, ] [[package]] @@ -1948,27 +1944,27 @@ dependencies = [ { name = "jinja2" }, { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240, upload-time = "2024-05-10T15:36:21.153Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403, upload-time = "2024-05-10T15:36:17.36Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, ] [[package]] name = "pyjwt" version = "2.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, ] [package.optional-dependencies] @@ -1985,12 +1981,12 @@ dependencies = [ { name = "pyarrow" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/d9/f2a5ee73b07df1c2c6bc06b53f67960caa5374f55118ee46fabe35396de5/pylance-0.20.0-cp39-abi3-macosx_10_15_x86_64.whl", hash = "sha256:fbb640b00567ff79d23a5994c0f0bc97587fcf74ece6ca568e77c453f70801c5", size = 31512397, upload-time = "2024-12-04T22:59:47.925Z" }, - { url = "https://files.pythonhosted.org/packages/01/dc/14c8321a08bbe110789e19aa8b9ba840f52ef8db88d0cdd9c3a29789791b/pylance-0.20.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8e30f1b6429b843429fde8f3d6fb7e715153174161e3bcf29902e2d32ee471f", size = 29266199, upload-time = "2024-12-04T22:42:09.353Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/f262507cdbed70994afc8bcc60beae2b823d10967bc632d9144806f035d4/pylance-0.20.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:032242a347ac909db81c0ade6384d82102f4ec61bc892d8caaa04b3d0a7b1613", size = 33539993, upload-time = "2024-12-04T22:41:27.379Z" }, - { url = "https://files.pythonhosted.org/packages/41/9c/88eb6eb07f1a803dec43930d28c587d9df3dc996337d399fa74bcb3cbb10/pylance-0.20.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:5320f11925524c1a67279afc4638cad60f61c36f11d3d9c2a91651489874be0d", size = 31858413, upload-time = "2024-12-04T22:41:48.2Z" }, - { url = "https://files.pythonhosted.org/packages/22/d2/acaf3328d1bd55201f9775d8b8a3f7c497966d3f3371e22aabb269cb4f0f/pylance-0.20.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fa5acd4488c574f6017145eafd5b45b178d611a5cbcd2ed492e01013fc72f5a2", size = 33465409, upload-time = "2024-12-04T22:41:44.675Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0a/c012ef957c3c99edf7a87d5f77ccf174bdf161d4ae1aac2181d750fcbcd5/pylance-0.20.0-cp39-abi3-win_amd64.whl", hash = "sha256:587850cddd0e669addd9414f378fa30527fc9020010cb73c842f026ea8a9b4ea", size = 31356456, upload-time = "2024-12-04T22:52:54.62Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d9/f2a5ee73b07df1c2c6bc06b53f67960caa5374f55118ee46fabe35396de5/pylance-0.20.0-cp39-abi3-macosx_10_15_x86_64.whl", hash = "sha256:fbb640b00567ff79d23a5994c0f0bc97587fcf74ece6ca568e77c453f70801c5", size = 31512397 }, + { url = "https://files.pythonhosted.org/packages/01/dc/14c8321a08bbe110789e19aa8b9ba840f52ef8db88d0cdd9c3a29789791b/pylance-0.20.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8e30f1b6429b843429fde8f3d6fb7e715153174161e3bcf29902e2d32ee471f", size = 29266199 }, + { url = "https://files.pythonhosted.org/packages/1e/2c/f262507cdbed70994afc8bcc60beae2b823d10967bc632d9144806f035d4/pylance-0.20.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:032242a347ac909db81c0ade6384d82102f4ec61bc892d8caaa04b3d0a7b1613", size = 33539993 }, + { url = "https://files.pythonhosted.org/packages/41/9c/88eb6eb07f1a803dec43930d28c587d9df3dc996337d399fa74bcb3cbb10/pylance-0.20.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:5320f11925524c1a67279afc4638cad60f61c36f11d3d9c2a91651489874be0d", size = 31858413 }, + { url = "https://files.pythonhosted.org/packages/22/d2/acaf3328d1bd55201f9775d8b8a3f7c497966d3f3371e22aabb269cb4f0f/pylance-0.20.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fa5acd4488c574f6017145eafd5b45b178d611a5cbcd2ed492e01013fc72f5a2", size = 33465409 }, + { url = "https://files.pythonhosted.org/packages/c7/0a/c012ef957c3c99edf7a87d5f77ccf174bdf161d4ae1aac2181d750fcbcd5/pylance-0.20.0-cp39-abi3-win_amd64.whl", hash = "sha256:587850cddd0e669addd9414f378fa30527fc9020010cb73c842f026ea8a9b4ea", size = 31356456 }, ] [[package]] @@ -2004,18 +2000,18 @@ dependencies = [ { name = "scikit-learn" }, { name = "scipy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/58/560a4db5eb3794d922fe55804b10326534ded3d971e1933c1eef91193f5e/pynndescent-0.5.13.tar.gz", hash = "sha256:d74254c0ee0a1eeec84597d5fe89fedcf778593eeabe32c2f97412934a9800fb", size = 2975955, upload-time = "2024-06-17T15:48:32.914Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/58/560a4db5eb3794d922fe55804b10326534ded3d971e1933c1eef91193f5e/pynndescent-0.5.13.tar.gz", hash = "sha256:d74254c0ee0a1eeec84597d5fe89fedcf778593eeabe32c2f97412934a9800fb", size = 2975955 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/53/d23a97e0a2c690d40b165d1062e2c4ccc796be458a1ce59f6ba030434663/pynndescent-0.5.13-py3-none-any.whl", hash = "sha256:69aabb8f394bc631b6ac475a1c7f3994c54adf3f51cd63b2730fefba5771b949", size = 56850, upload-time = "2024-06-17T15:48:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/d23a97e0a2c690d40b165d1062e2c4ccc796be458a1ce59f6ba030434663/pynndescent-0.5.13-py3-none-any.whl", hash = "sha256:69aabb8f394bc631b6ac475a1c7f3994c54adf3f51cd63b2730fefba5771b949", size = 56850 }, ] [[package]] name = "pyparsing" version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, ] [[package]] @@ -2026,9 +2022,9 @@ dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504 }, ] [[package]] @@ -2038,27 +2034,27 @@ 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" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } 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" }, + { 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 }, ] [[package]] name = "python-dotenv" version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, ] [[package]] name = "pytz" version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, ] [[package]] @@ -2066,38 +2062,38 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, - { 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/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432 }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103 }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557 }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031 }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308 }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930 }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, ] [[package]] @@ -2107,48 +2103,48 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/06/50a4e9648b3e8b992bef8eb632e457307553a89d294103213cfd47b3da69/pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf", size = 280478, upload-time = "2025-06-13T14:09:07.087Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/09/1681d4b047626d352c083770618ac29655ab1f5c20eee31dc94c000b9b7b/pyzmq-27.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:b973ee650e8f442ce482c1d99ca7ab537c69098d53a3d046676a484fd710c87a", size = 1329291, upload-time = "2025-06-13T14:06:57.945Z" }, - { url = "https://files.pythonhosted.org/packages/9d/b2/9c9385225fdd54db9506ed8accbb9ea63ca813ba59d43d7f282a6a16a30b/pyzmq-27.0.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:661942bc7cd0223d569d808f2e5696d9cc120acc73bf3e88a1f1be7ab648a7e4", size = 905952, upload-time = "2025-06-13T14:07:03.232Z" }, - { url = "https://files.pythonhosted.org/packages/41/73/333c72c7ec182cdffe25649e3da1c3b9f3cf1cede63cfdc23d1384d4a601/pyzmq-27.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50360fb2a056ffd16e5f4177eee67f1dd1017332ea53fb095fe7b5bf29c70246", size = 666165, upload-time = "2025-06-13T14:07:04.667Z" }, - { url = "https://files.pythonhosted.org/packages/a5/fe/fc7b9c1a50981928e25635a926653cb755364316db59ccd6e79cfb9a0b4f/pyzmq-27.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf209a6dc4b420ed32a7093642843cbf8703ed0a7d86c16c0b98af46762ebefb", size = 853755, upload-time = "2025-06-13T14:07:06.93Z" }, - { url = "https://files.pythonhosted.org/packages/8c/4c/740ed4b6e8fa160cd19dc5abec8db68f440564b2d5b79c1d697d9862a2f7/pyzmq-27.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c2dace4a7041cca2fba5357a2d7c97c5effdf52f63a1ef252cfa496875a3762d", size = 1654868, upload-time = "2025-06-13T14:07:08.224Z" }, - { url = "https://files.pythonhosted.org/packages/97/00/875b2ecfcfc78ab962a59bd384995186818524ea957dc8ad3144611fae12/pyzmq-27.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:63af72b2955fc77caf0a77444baa2431fcabb4370219da38e1a9f8d12aaebe28", size = 2033443, upload-time = "2025-06-13T14:07:09.653Z" }, - { url = "https://files.pythonhosted.org/packages/60/55/6dd9c470c42d713297c5f2a56f7903dc1ebdb4ab2edda996445c21651900/pyzmq-27.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8c4adce8e37e75c4215297d7745551b8dcfa5f728f23ce09bf4e678a9399413", size = 1891288, upload-time = "2025-06-13T14:07:11.099Z" }, - { url = "https://files.pythonhosted.org/packages/28/5d/54b0ef50d40d7c65a627f4a4b4127024ba9820f2af8acd933a4d30ae192e/pyzmq-27.0.0-cp310-cp310-win32.whl", hash = "sha256:5d5ef4718ecab24f785794e0e7536436698b459bfbc19a1650ef55280119d93b", size = 567936, upload-time = "2025-06-13T14:07:12.468Z" }, - { url = "https://files.pythonhosted.org/packages/18/ea/dedca4321de748ca48d3bcdb72274d4d54e8d84ea49088d3de174bd45d88/pyzmq-27.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:e40609380480b3d12c30f841323f42451c755b8fece84235236f5fe5ffca8c1c", size = 628686, upload-time = "2025-06-13T14:07:14.051Z" }, - { url = "https://files.pythonhosted.org/packages/d4/a7/fcdeedc306e71e94ac262cba2d02337d885f5cdb7e8efced8e5ffe327808/pyzmq-27.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6b0397b0be277b46762956f576e04dc06ced265759e8c2ff41a0ee1aa0064198", size = 559039, upload-time = "2025-06-13T14:07:15.289Z" }, - { url = "https://files.pythonhosted.org/packages/44/df/84c630654106d9bd9339cdb564aa941ed41b023a0264251d6743766bb50e/pyzmq-27.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:21457825249b2a53834fa969c69713f8b5a79583689387a5e7aed880963ac564", size = 1332718, upload-time = "2025-06-13T14:07:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8e/f6a5461a07654d9840d256476434ae0ff08340bba562a455f231969772cb/pyzmq-27.0.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1958947983fef513e6e98eff9cb487b60bf14f588dc0e6bf35fa13751d2c8251", size = 908248, upload-time = "2025-06-13T14:07:18.033Z" }, - { url = "https://files.pythonhosted.org/packages/7c/93/82863e8d695a9a3ae424b63662733ae204a295a2627d52af2f62c2cd8af9/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0dc628b5493f9a8cd9844b8bee9732ef587ab00002157c9329e4fc0ef4d3afa", size = 668647, upload-time = "2025-06-13T14:07:19.378Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/15278769b348121eacdbfcbd8c4d40f1102f32fa6af5be1ffc032ed684be/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7bbe9e1ed2c8d3da736a15694d87c12493e54cc9dc9790796f0321794bbc91f", size = 856600, upload-time = "2025-06-13T14:07:20.906Z" }, - { url = "https://files.pythonhosted.org/packages/d4/af/1c469b3d479bd095edb28e27f12eee10b8f00b356acbefa6aeb14dd295d1/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc1091f59143b471d19eb64f54bae4f54bcf2a466ffb66fe45d94d8d734eb495", size = 1657748, upload-time = "2025-06-13T14:07:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f4/17f965d0ee6380b1d6326da842a50e4b8b9699745161207945f3745e8cb5/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7011ade88c8e535cf140f8d1a59428676fbbce7c6e54fefce58bf117aefb6667", size = 2034311, upload-time = "2025-06-13T14:07:23.966Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6e/7c391d81fa3149fd759de45d298003de6cfab343fb03e92c099821c448db/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c386339d7e3f064213aede5d03d054b237937fbca6dd2197ac8cf3b25a6b14e", size = 1893630, upload-time = "2025-06-13T14:07:25.899Z" }, - { url = "https://files.pythonhosted.org/packages/0e/e0/eaffe7a86f60e556399e224229e7769b717f72fec0706b70ab2c03aa04cb/pyzmq-27.0.0-cp311-cp311-win32.whl", hash = "sha256:0546a720c1f407b2172cb04b6b094a78773491497e3644863cf5c96c42df8cff", size = 567706, upload-time = "2025-06-13T14:07:27.595Z" }, - { url = "https://files.pythonhosted.org/packages/c9/05/89354a8cffdcce6e547d48adaaf7be17007fc75572123ff4ca90a4ca04fc/pyzmq-27.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:15f39d50bd6c9091c67315ceb878a4f531957b121d2a05ebd077eb35ddc5efed", size = 630322, upload-time = "2025-06-13T14:07:28.938Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/4ab976d5e1e63976719389cc4f3bfd248a7f5f2bb2ebe727542363c61b5f/pyzmq-27.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c5817641eebb391a2268c27fecd4162448e03538387093cdbd8bf3510c316b38", size = 558435, upload-time = "2025-06-13T14:07:30.256Z" }, - { url = "https://files.pythonhosted.org/packages/93/a7/9ad68f55b8834ede477842214feba6a4c786d936c022a67625497aacf61d/pyzmq-27.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cbabc59dcfaac66655c040dfcb8118f133fb5dde185e5fc152628354c1598e52", size = 1305438, upload-time = "2025-06-13T14:07:31.676Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ee/26aa0f98665a22bc90ebe12dced1de5f3eaca05363b717f6fb229b3421b3/pyzmq-27.0.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cb0ac5179cba4b2f94f1aa208fbb77b62c4c9bf24dd446278b8b602cf85fcda3", size = 895095, upload-time = "2025-06-13T14:07:33.104Z" }, - { url = "https://files.pythonhosted.org/packages/cf/85/c57e7ab216ecd8aa4cc7e3b83b06cc4e9cf45c87b0afc095f10cd5ce87c1/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a48f0228eab6cbf69fde3aa3c03cbe04e50e623ef92ae395fce47ef8a76152", size = 651826, upload-time = "2025-06-13T14:07:34.831Z" }, - { url = "https://files.pythonhosted.org/packages/69/9a/9ea7e230feda9400fb0ae0d61d7d6ddda635e718d941c44eeab22a179d34/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111db5f395e09f7e775f759d598f43cb815fc58e0147623c4816486e1a39dc22", size = 839750, upload-time = "2025-06-13T14:07:36.553Z" }, - { url = "https://files.pythonhosted.org/packages/08/66/4cebfbe71f3dfbd417011daca267539f62ed0fbc68105357b68bbb1a25b7/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c8878011653dcdc27cc2c57e04ff96f0471e797f5c19ac3d7813a245bcb24371", size = 1641357, upload-time = "2025-06-13T14:07:38.21Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f6/b0f62578c08d2471c791287149cb8c2aaea414ae98c6e995c7dbe008adfb/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0ed2c1f335ba55b5fdc964622254917d6b782311c50e138863eda409fbb3b6d", size = 2020281, upload-time = "2025-06-13T14:07:39.599Z" }, - { url = "https://files.pythonhosted.org/packages/37/b9/4f670b15c7498495da9159edc374ec09c88a86d9cd5a47d892f69df23450/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e918d70862d4cfd4b1c187310015646a14e1f5917922ab45b29f28f345eeb6be", size = 1877110, upload-time = "2025-06-13T14:07:41.027Z" }, - { url = "https://files.pythonhosted.org/packages/66/31/9dee25c226295b740609f0d46db2fe972b23b6f5cf786360980524a3ba92/pyzmq-27.0.0-cp312-abi3-win32.whl", hash = "sha256:88b4e43cab04c3c0f0d55df3b1eef62df2b629a1a369b5289a58f6fa8b07c4f4", size = 559297, upload-time = "2025-06-13T14:07:42.533Z" }, - { url = "https://files.pythonhosted.org/packages/9b/12/52da5509800f7ff2d287b2f2b4e636e7ea0f001181cba6964ff6c1537778/pyzmq-27.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:dce4199bf5f648a902ce37e7b3afa286f305cd2ef7a8b6ec907470ccb6c8b371", size = 619203, upload-time = "2025-06-13T14:07:43.843Z" }, - { url = "https://files.pythonhosted.org/packages/93/6d/7f2e53b19d1edb1eb4f09ec7c3a1f945ca0aac272099eab757d15699202b/pyzmq-27.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:56e46bbb85d52c1072b3f809cc1ce77251d560bc036d3a312b96db1afe76db2e", size = 551927, upload-time = "2025-06-13T14:07:45.51Z" }, - { url = "https://files.pythonhosted.org/packages/09/6f/be6523a7f3821c0b5370912ef02822c028611360e0d206dd945bdbf9eaef/pyzmq-27.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:656c1866505a5735d0660b7da6d7147174bbf59d4975fc2b7f09f43c9bc25745", size = 835950, upload-time = "2025-06-13T14:08:35Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1e/a50fdd5c15018de07ab82a61bc460841be967ee7bbe7abee3b714d66f7ac/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74175b9e12779382432dd1d1f5960ebe7465d36649b98a06c6b26be24d173fab", size = 799876, upload-time = "2025-06-13T14:08:36.849Z" }, - { url = "https://files.pythonhosted.org/packages/88/a1/89eb5b71f5a504f8f887aceb8e1eb3626e00c00aa8085381cdff475440dc/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c6de908465697a8708e4d6843a1e884f567962fc61eb1706856545141d0cbb", size = 567400, upload-time = "2025-06-13T14:08:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/56/aa/4571dbcff56cfb034bac73fde8294e123c975ce3eea89aff31bf6dc6382b/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c644aaacc01d0df5c7072826df45e67301f191c55f68d7b2916d83a9ddc1b551", size = 747031, upload-time = "2025-06-13T14:08:40.413Z" }, - { url = "https://files.pythonhosted.org/packages/46/e0/d25f30fe0991293c5b2f5ef3b070d35fa6d57c0c7428898c3ab4913d0297/pyzmq-27.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:10f70c1d9a446a85013a36871a296007f6fe4232b530aa254baf9da3f8328bc0", size = 544726, upload-time = "2025-06-13T14:08:41.997Z" }, - { url = "https://files.pythonhosted.org/packages/98/a6/92394373b8dbc1edc9d53c951e8d3989d518185174ee54492ec27711779d/pyzmq-27.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd1dc59763effd1576f8368047c9c31468fce0af89d76b5067641137506792ae", size = 835948, upload-time = "2025-06-13T14:08:43.516Z" }, - { url = "https://files.pythonhosted.org/packages/56/f3/4dc38d75d9995bfc18773df3e41f2a2ca9b740b06f1a15dbf404077e7588/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:60e8cc82d968174650c1860d7b716366caab9973787a1c060cf8043130f7d0f7", size = 799874, upload-time = "2025-06-13T14:08:45.017Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ba/64af397e0f421453dc68e31d5e0784d554bf39013a2de0872056e96e58af/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14fe7aaac86e4e93ea779a821967360c781d7ac5115b3f1a171ced77065a0174", size = 567400, upload-time = "2025-06-13T14:08:46.855Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/ec956cbe98809270b59a22891d5758edae147a258e658bf3024a8254c855/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ad0562d4e6abb785be3e4dd68599c41be821b521da38c402bc9ab2a8e7ebc7e", size = 747031, upload-time = "2025-06-13T14:08:48.419Z" }, - { url = "https://files.pythonhosted.org/packages/be/8a/4a3764a68abc02e2fbb0668d225b6fda5cd39586dd099cee8b2ed6ab0452/pyzmq-27.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9df43a2459cd3a3563404c1456b2c4c69564daa7dbaf15724c09821a3329ce46", size = 544726, upload-time = "2025-06-13T14:08:49.903Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f1/06/50a4e9648b3e8b992bef8eb632e457307553a89d294103213cfd47b3da69/pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf", size = 280478 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/09/1681d4b047626d352c083770618ac29655ab1f5c20eee31dc94c000b9b7b/pyzmq-27.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:b973ee650e8f442ce482c1d99ca7ab537c69098d53a3d046676a484fd710c87a", size = 1329291 }, + { url = "https://files.pythonhosted.org/packages/9d/b2/9c9385225fdd54db9506ed8accbb9ea63ca813ba59d43d7f282a6a16a30b/pyzmq-27.0.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:661942bc7cd0223d569d808f2e5696d9cc120acc73bf3e88a1f1be7ab648a7e4", size = 905952 }, + { url = "https://files.pythonhosted.org/packages/41/73/333c72c7ec182cdffe25649e3da1c3b9f3cf1cede63cfdc23d1384d4a601/pyzmq-27.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50360fb2a056ffd16e5f4177eee67f1dd1017332ea53fb095fe7b5bf29c70246", size = 666165 }, + { url = "https://files.pythonhosted.org/packages/a5/fe/fc7b9c1a50981928e25635a926653cb755364316db59ccd6e79cfb9a0b4f/pyzmq-27.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf209a6dc4b420ed32a7093642843cbf8703ed0a7d86c16c0b98af46762ebefb", size = 853755 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/740ed4b6e8fa160cd19dc5abec8db68f440564b2d5b79c1d697d9862a2f7/pyzmq-27.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c2dace4a7041cca2fba5357a2d7c97c5effdf52f63a1ef252cfa496875a3762d", size = 1654868 }, + { url = "https://files.pythonhosted.org/packages/97/00/875b2ecfcfc78ab962a59bd384995186818524ea957dc8ad3144611fae12/pyzmq-27.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:63af72b2955fc77caf0a77444baa2431fcabb4370219da38e1a9f8d12aaebe28", size = 2033443 }, + { url = "https://files.pythonhosted.org/packages/60/55/6dd9c470c42d713297c5f2a56f7903dc1ebdb4ab2edda996445c21651900/pyzmq-27.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8c4adce8e37e75c4215297d7745551b8dcfa5f728f23ce09bf4e678a9399413", size = 1891288 }, + { url = "https://files.pythonhosted.org/packages/28/5d/54b0ef50d40d7c65a627f4a4b4127024ba9820f2af8acd933a4d30ae192e/pyzmq-27.0.0-cp310-cp310-win32.whl", hash = "sha256:5d5ef4718ecab24f785794e0e7536436698b459bfbc19a1650ef55280119d93b", size = 567936 }, + { url = "https://files.pythonhosted.org/packages/18/ea/dedca4321de748ca48d3bcdb72274d4d54e8d84ea49088d3de174bd45d88/pyzmq-27.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:e40609380480b3d12c30f841323f42451c755b8fece84235236f5fe5ffca8c1c", size = 628686 }, + { url = "https://files.pythonhosted.org/packages/d4/a7/fcdeedc306e71e94ac262cba2d02337d885f5cdb7e8efced8e5ffe327808/pyzmq-27.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6b0397b0be277b46762956f576e04dc06ced265759e8c2ff41a0ee1aa0064198", size = 559039 }, + { url = "https://files.pythonhosted.org/packages/44/df/84c630654106d9bd9339cdb564aa941ed41b023a0264251d6743766bb50e/pyzmq-27.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:21457825249b2a53834fa969c69713f8b5a79583689387a5e7aed880963ac564", size = 1332718 }, + { url = "https://files.pythonhosted.org/packages/c1/8e/f6a5461a07654d9840d256476434ae0ff08340bba562a455f231969772cb/pyzmq-27.0.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1958947983fef513e6e98eff9cb487b60bf14f588dc0e6bf35fa13751d2c8251", size = 908248 }, + { url = "https://files.pythonhosted.org/packages/7c/93/82863e8d695a9a3ae424b63662733ae204a295a2627d52af2f62c2cd8af9/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0dc628b5493f9a8cd9844b8bee9732ef587ab00002157c9329e4fc0ef4d3afa", size = 668647 }, + { url = "https://files.pythonhosted.org/packages/f3/85/15278769b348121eacdbfcbd8c4d40f1102f32fa6af5be1ffc032ed684be/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7bbe9e1ed2c8d3da736a15694d87c12493e54cc9dc9790796f0321794bbc91f", size = 856600 }, + { url = "https://files.pythonhosted.org/packages/d4/af/1c469b3d479bd095edb28e27f12eee10b8f00b356acbefa6aeb14dd295d1/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc1091f59143b471d19eb64f54bae4f54bcf2a466ffb66fe45d94d8d734eb495", size = 1657748 }, + { url = "https://files.pythonhosted.org/packages/8c/f4/17f965d0ee6380b1d6326da842a50e4b8b9699745161207945f3745e8cb5/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7011ade88c8e535cf140f8d1a59428676fbbce7c6e54fefce58bf117aefb6667", size = 2034311 }, + { url = "https://files.pythonhosted.org/packages/e0/6e/7c391d81fa3149fd759de45d298003de6cfab343fb03e92c099821c448db/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c386339d7e3f064213aede5d03d054b237937fbca6dd2197ac8cf3b25a6b14e", size = 1893630 }, + { url = "https://files.pythonhosted.org/packages/0e/e0/eaffe7a86f60e556399e224229e7769b717f72fec0706b70ab2c03aa04cb/pyzmq-27.0.0-cp311-cp311-win32.whl", hash = "sha256:0546a720c1f407b2172cb04b6b094a78773491497e3644863cf5c96c42df8cff", size = 567706 }, + { url = "https://files.pythonhosted.org/packages/c9/05/89354a8cffdcce6e547d48adaaf7be17007fc75572123ff4ca90a4ca04fc/pyzmq-27.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:15f39d50bd6c9091c67315ceb878a4f531957b121d2a05ebd077eb35ddc5efed", size = 630322 }, + { url = "https://files.pythonhosted.org/packages/fa/07/4ab976d5e1e63976719389cc4f3bfd248a7f5f2bb2ebe727542363c61b5f/pyzmq-27.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c5817641eebb391a2268c27fecd4162448e03538387093cdbd8bf3510c316b38", size = 558435 }, + { url = "https://files.pythonhosted.org/packages/93/a7/9ad68f55b8834ede477842214feba6a4c786d936c022a67625497aacf61d/pyzmq-27.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cbabc59dcfaac66655c040dfcb8118f133fb5dde185e5fc152628354c1598e52", size = 1305438 }, + { url = "https://files.pythonhosted.org/packages/ba/ee/26aa0f98665a22bc90ebe12dced1de5f3eaca05363b717f6fb229b3421b3/pyzmq-27.0.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cb0ac5179cba4b2f94f1aa208fbb77b62c4c9bf24dd446278b8b602cf85fcda3", size = 895095 }, + { url = "https://files.pythonhosted.org/packages/cf/85/c57e7ab216ecd8aa4cc7e3b83b06cc4e9cf45c87b0afc095f10cd5ce87c1/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a48f0228eab6cbf69fde3aa3c03cbe04e50e623ef92ae395fce47ef8a76152", size = 651826 }, + { url = "https://files.pythonhosted.org/packages/69/9a/9ea7e230feda9400fb0ae0d61d7d6ddda635e718d941c44eeab22a179d34/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111db5f395e09f7e775f759d598f43cb815fc58e0147623c4816486e1a39dc22", size = 839750 }, + { url = "https://files.pythonhosted.org/packages/08/66/4cebfbe71f3dfbd417011daca267539f62ed0fbc68105357b68bbb1a25b7/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c8878011653dcdc27cc2c57e04ff96f0471e797f5c19ac3d7813a245bcb24371", size = 1641357 }, + { url = "https://files.pythonhosted.org/packages/ac/f6/b0f62578c08d2471c791287149cb8c2aaea414ae98c6e995c7dbe008adfb/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0ed2c1f335ba55b5fdc964622254917d6b782311c50e138863eda409fbb3b6d", size = 2020281 }, + { url = "https://files.pythonhosted.org/packages/37/b9/4f670b15c7498495da9159edc374ec09c88a86d9cd5a47d892f69df23450/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e918d70862d4cfd4b1c187310015646a14e1f5917922ab45b29f28f345eeb6be", size = 1877110 }, + { url = "https://files.pythonhosted.org/packages/66/31/9dee25c226295b740609f0d46db2fe972b23b6f5cf786360980524a3ba92/pyzmq-27.0.0-cp312-abi3-win32.whl", hash = "sha256:88b4e43cab04c3c0f0d55df3b1eef62df2b629a1a369b5289a58f6fa8b07c4f4", size = 559297 }, + { url = "https://files.pythonhosted.org/packages/9b/12/52da5509800f7ff2d287b2f2b4e636e7ea0f001181cba6964ff6c1537778/pyzmq-27.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:dce4199bf5f648a902ce37e7b3afa286f305cd2ef7a8b6ec907470ccb6c8b371", size = 619203 }, + { url = "https://files.pythonhosted.org/packages/93/6d/7f2e53b19d1edb1eb4f09ec7c3a1f945ca0aac272099eab757d15699202b/pyzmq-27.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:56e46bbb85d52c1072b3f809cc1ce77251d560bc036d3a312b96db1afe76db2e", size = 551927 }, + { url = "https://files.pythonhosted.org/packages/09/6f/be6523a7f3821c0b5370912ef02822c028611360e0d206dd945bdbf9eaef/pyzmq-27.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:656c1866505a5735d0660b7da6d7147174bbf59d4975fc2b7f09f43c9bc25745", size = 835950 }, + { url = "https://files.pythonhosted.org/packages/c6/1e/a50fdd5c15018de07ab82a61bc460841be967ee7bbe7abee3b714d66f7ac/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74175b9e12779382432dd1d1f5960ebe7465d36649b98a06c6b26be24d173fab", size = 799876 }, + { url = "https://files.pythonhosted.org/packages/88/a1/89eb5b71f5a504f8f887aceb8e1eb3626e00c00aa8085381cdff475440dc/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c6de908465697a8708e4d6843a1e884f567962fc61eb1706856545141d0cbb", size = 567400 }, + { url = "https://files.pythonhosted.org/packages/56/aa/4571dbcff56cfb034bac73fde8294e123c975ce3eea89aff31bf6dc6382b/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c644aaacc01d0df5c7072826df45e67301f191c55f68d7b2916d83a9ddc1b551", size = 747031 }, + { url = "https://files.pythonhosted.org/packages/46/e0/d25f30fe0991293c5b2f5ef3b070d35fa6d57c0c7428898c3ab4913d0297/pyzmq-27.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:10f70c1d9a446a85013a36871a296007f6fe4232b530aa254baf9da3f8328bc0", size = 544726 }, + { url = "https://files.pythonhosted.org/packages/98/a6/92394373b8dbc1edc9d53c951e8d3989d518185174ee54492ec27711779d/pyzmq-27.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd1dc59763effd1576f8368047c9c31468fce0af89d76b5067641137506792ae", size = 835948 }, + { url = "https://files.pythonhosted.org/packages/56/f3/4dc38d75d9995bfc18773df3e41f2a2ca9b740b06f1a15dbf404077e7588/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:60e8cc82d968174650c1860d7b716366caab9973787a1c060cf8043130f7d0f7", size = 799874 }, + { url = "https://files.pythonhosted.org/packages/ab/ba/64af397e0f421453dc68e31d5e0784d554bf39013a2de0872056e96e58af/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14fe7aaac86e4e93ea779a821967360c781d7ac5115b3f1a171ced77065a0174", size = 567400 }, + { url = "https://files.pythonhosted.org/packages/63/87/ec956cbe98809270b59a22891d5758edae147a258e658bf3024a8254c855/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ad0562d4e6abb785be3e4dd68599c41be821b521da38c402bc9ab2a8e7ebc7e", size = 747031 }, + { url = "https://files.pythonhosted.org/packages/be/8a/4a3764a68abc02e2fbb0668d225b6fda5cd39586dd099cee8b2ed6ab0452/pyzmq-27.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9df43a2459cd3a3563404c1456b2c4c69564daa7dbaf15724c09821a3329ce46", size = 544726 }, ] [[package]] @@ -2159,9 +2155,9 @@ dependencies = [ { name = "isodate", marker = "python_full_version < '3.11'" }, { name = "pyparsing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/7e/cb2d74466bd8495051ebe2d241b1cb1d4acf9740d481126aef19ef2697f5/rdflib-7.1.4.tar.gz", hash = "sha256:fed46e24f26a788e2ab8e445f7077f00edcf95abb73bcef4b86cefa8b62dd174", size = 4692745, upload-time = "2025-03-29T02:23:02.386Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/7e/cb2d74466bd8495051ebe2d241b1cb1d4acf9740d481126aef19ef2697f5/rdflib-7.1.4.tar.gz", hash = "sha256:fed46e24f26a788e2ab8e445f7077f00edcf95abb73bcef4b86cefa8b62dd174", size = 4692745 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/31/e9b6f04288dcd3fa60cb3179260d6dad81b92aef3063d679ac7d80a827ea/rdflib-7.1.4-py3-none-any.whl", hash = "sha256:72f4adb1990fa5241abd22ddaf36d7cafa5d91d9ff2ba13f3086d339b213d997", size = 565051, upload-time = "2025-03-29T02:22:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/f4/31/e9b6f04288dcd3fa60cb3179260d6dad81b92aef3063d679ac7d80a827ea/rdflib-7.1.4-py3-none-any.whl", hash = "sha256:72f4adb1990fa5241abd22ddaf36d7cafa5d91d9ff2ba13f3086d339b213d997", size = 565051 }, ] [[package]] @@ -2173,48 +2169,48 @@ dependencies = [ { name = "rpds-py" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, ] [[package]] name = "regex" version = "2024.11.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674, upload-time = "2024-11-06T20:08:57.575Z" }, - { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684, upload-time = "2024-11-06T20:08:59.787Z" }, - { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589, upload-time = "2024-11-06T20:09:01.896Z" }, - { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511, upload-time = "2024-11-06T20:09:04.062Z" }, - { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149, upload-time = "2024-11-06T20:09:06.237Z" }, - { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707, upload-time = "2024-11-06T20:09:07.715Z" }, - { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702, upload-time = "2024-11-06T20:09:10.101Z" }, - { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976, upload-time = "2024-11-06T20:09:11.566Z" }, - { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397, upload-time = "2024-11-06T20:09:13.119Z" }, - { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726, upload-time = "2024-11-06T20:09:14.85Z" }, - { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098, upload-time = "2024-11-06T20:09:16.504Z" }, - { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325, upload-time = "2024-11-06T20:09:18.698Z" }, - { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277, upload-time = "2024-11-06T20:09:21.725Z" }, - { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197, upload-time = "2024-11-06T20:09:24.092Z" }, - { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714, upload-time = "2024-11-06T20:09:26.36Z" }, - { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042, upload-time = "2024-11-06T20:09:28.762Z" }, - { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669, upload-time = "2024-11-06T20:09:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684, upload-time = "2024-11-06T20:09:32.915Z" }, - { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589, upload-time = "2024-11-06T20:09:35.504Z" }, - { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121, upload-time = "2024-11-06T20:09:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275, upload-time = "2024-11-06T20:09:40.371Z" }, - { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257, upload-time = "2024-11-06T20:09:43.059Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727, upload-time = "2024-11-06T20:09:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667, upload-time = "2024-11-06T20:09:49.828Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963, upload-time = "2024-11-06T20:09:51.819Z" }, - { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700, upload-time = "2024-11-06T20:09:53.982Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592, upload-time = "2024-11-06T20:09:56.222Z" }, - { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929, upload-time = "2024-11-06T20:09:58.642Z" }, - { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213, upload-time = "2024-11-06T20:10:00.867Z" }, - { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734, upload-time = "2024-11-06T20:10:03.361Z" }, - { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052, upload-time = "2024-11-06T20:10:05.179Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, + { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, + { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, + { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, + { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, + { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, + { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, + { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, + { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, + { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, + { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, + { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, + { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, + { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, ] [[package]] @@ -2227,9 +2223,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, ] [[package]] @@ -2241,92 +2237,92 @@ dependencies = [ { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, ] [[package]] name = "rpds-py" version = "0.26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466, upload-time = "2025-07-01T15:53:40.55Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825, upload-time = "2025-07-01T15:53:42.247Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530, upload-time = "2025-07-01T15:53:43.585Z" }, - { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933, upload-time = "2025-07-01T15:53:45.78Z" }, - { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973, upload-time = "2025-07-01T15:53:47.085Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293, upload-time = "2025-07-01T15:53:48.117Z" }, - { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787, upload-time = "2025-07-01T15:53:50.874Z" }, - { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312, upload-time = "2025-07-01T15:53:52.046Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403, upload-time = "2025-07-01T15:53:53.192Z" }, - { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323, upload-time = "2025-07-01T15:53:54.336Z" }, - { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541, upload-time = "2025-07-01T15:53:55.469Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442, upload-time = "2025-07-01T15:53:56.524Z" }, - { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314, upload-time = "2025-07-01T15:53:57.842Z" }, - { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610, upload-time = "2025-07-01T15:53:58.844Z" }, - { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032, upload-time = "2025-07-01T15:53:59.985Z" }, - { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525, upload-time = "2025-07-01T15:54:01.162Z" }, - { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089, upload-time = "2025-07-01T15:54:02.319Z" }, - { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255, upload-time = "2025-07-01T15:54:03.38Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283, upload-time = "2025-07-01T15:54:04.923Z" }, - { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881, upload-time = "2025-07-01T15:54:06.482Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822, upload-time = "2025-07-01T15:54:07.605Z" }, - { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347, upload-time = "2025-07-01T15:54:08.591Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956, upload-time = "2025-07-01T15:54:09.963Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363, upload-time = "2025-07-01T15:54:11.073Z" }, - { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123, upload-time = "2025-07-01T15:54:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732, upload-time = "2025-07-01T15:54:13.434Z" }, - { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917, upload-time = "2025-07-01T15:54:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226, upload-time = "2025-07-01T15:56:16.578Z" }, - { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230, upload-time = "2025-07-01T15:56:17.978Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363, upload-time = "2025-07-01T15:56:19.977Z" }, - { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146, upload-time = "2025-07-01T15:56:21.39Z" }, - { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804, upload-time = "2025-07-01T15:56:22.78Z" }, - { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820, upload-time = "2025-07-01T15:56:24.584Z" }, - { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567, upload-time = "2025-07-01T15:56:26.064Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520, upload-time = "2025-07-01T15:56:27.608Z" }, - { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362, upload-time = "2025-07-01T15:56:29.078Z" }, - { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113, upload-time = "2025-07-01T15:56:30.485Z" }, - { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429, upload-time = "2025-07-01T15:56:31.956Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950, upload-time = "2025-07-01T15:56:33.337Z" }, - { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505, upload-time = "2025-07-01T15:56:34.716Z" }, - { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468, upload-time = "2025-07-01T15:56:36.219Z" }, - { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680, upload-time = "2025-07-01T15:56:37.644Z" }, - { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035, upload-time = "2025-07-01T15:56:39.241Z" }, - { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922, upload-time = "2025-07-01T15:56:40.645Z" }, - { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822, upload-time = "2025-07-01T15:56:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336, upload-time = "2025-07-01T15:56:44.239Z" }, - { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871, upload-time = "2025-07-01T15:56:46.284Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439, upload-time = "2025-07-01T15:56:48.549Z" }, - { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380, upload-time = "2025-07-01T15:56:50.086Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334, upload-time = "2025-07-01T15:56:51.703Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466 }, + { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530 }, + { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933 }, + { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973 }, + { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293 }, + { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787 }, + { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312 }, + { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403 }, + { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323 }, + { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541 }, + { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442 }, + { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314 }, + { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610 }, + { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032 }, + { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525 }, + { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089 }, + { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255 }, + { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283 }, + { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881 }, + { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822 }, + { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347 }, + { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956 }, + { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363 }, + { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123 }, + { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732 }, + { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917 }, + { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226 }, + { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230 }, + { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363 }, + { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146 }, + { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804 }, + { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820 }, + { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567 }, + { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520 }, + { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362 }, + { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113 }, + { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429 }, + { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950 }, + { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505 }, + { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468 }, + { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680 }, + { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035 }, + { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922 }, + { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822 }, + { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336 }, + { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871 }, + { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439 }, + { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380 }, + { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334 }, ] [[package]] name = "ruff" version = "0.12.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" }, - { url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" }, - { url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" }, - { url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" }, - { url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" }, - { url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" }, - { url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" }, - { url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" }, - { url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" }, - { url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" }, - { url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" }, + { url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133 }, + { url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114 }, + { url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873 }, + { url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829 }, + { url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619 }, + { url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894 }, + { url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909 }, + { url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652 }, + { url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451 }, + { url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465 }, + { url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136 }, + { url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644 }, + { url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068 }, + { url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537 }, + { url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575 }, + { url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273 }, + { url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564 }, ] [[package]] @@ -2339,18 +2335,18 @@ dependencies = [ { name = "scipy" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/84/5f4af978fff619706b8961accac84780a6d298d82a8873446f72edb4ead0/scikit_learn-1.7.1.tar.gz", hash = "sha256:24b3f1e976a4665aa74ee0fcaac2b8fccc6ae77c8e07ab25da3ba6d3292b9802", size = 7190445, upload-time = "2025-07-18T08:01:54.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/84/5f4af978fff619706b8961accac84780a6d298d82a8873446f72edb4ead0/scikit_learn-1.7.1.tar.gz", hash = "sha256:24b3f1e976a4665aa74ee0fcaac2b8fccc6ae77c8e07ab25da3ba6d3292b9802", size = 7190445 } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/88/0dd5be14ef19f2d80a77780be35a33aa94e8a3b3223d80bee8892a7832b4/scikit_learn-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:406204dd4004f0517f0b23cf4b28c6245cbd51ab1b6b78153bc784def214946d", size = 9338868, upload-time = "2025-07-18T08:01:00.25Z" }, - { url = "https://files.pythonhosted.org/packages/fd/52/3056b6adb1ac58a0bc335fc2ed2fcf599974d908855e8cb0ca55f797593c/scikit_learn-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:16af2e44164f05d04337fd1fc3ae7c4ea61fd9b0d527e22665346336920fe0e1", size = 8655943, upload-time = "2025-07-18T08:01:02.974Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a4/e488acdece6d413f370a9589a7193dac79cd486b2e418d3276d6ea0b9305/scikit_learn-1.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2f2e78e56a40c7587dea9a28dc4a49500fa2ead366869418c66f0fd75b80885c", size = 9652056, upload-time = "2025-07-18T08:01:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/18/41/bceacec1285b94eb9e4659b24db46c23346d7e22cf258d63419eb5dec6f7/scikit_learn-1.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62b76ad408a821475b43b7bb90a9b1c9a4d8d125d505c2df0539f06d6e631b1", size = 9473691, upload-time = "2025-07-18T08:01:07.006Z" }, - { url = "https://files.pythonhosted.org/packages/12/7b/e1ae4b7e1dd85c4ca2694ff9cc4a9690970fd6150d81b975e6c5c6f8ee7c/scikit_learn-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:9963b065677a4ce295e8ccdee80a1dd62b37249e667095039adcd5bce6e90deb", size = 8900873, upload-time = "2025-07-18T08:01:09.332Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bd/a23177930abd81b96daffa30ef9c54ddbf544d3226b8788ce4c3ef1067b4/scikit_learn-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90c8494ea23e24c0fb371afc474618c1019dc152ce4a10e4607e62196113851b", size = 9334838, upload-time = "2025-07-18T08:01:11.239Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a1/d3a7628630a711e2ac0d1a482910da174b629f44e7dd8cfcd6924a4ef81a/scikit_learn-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:bb870c0daf3bf3be145ec51df8ac84720d9972170786601039f024bf6d61a518", size = 8651241, upload-time = "2025-07-18T08:01:13.234Z" }, - { url = "https://files.pythonhosted.org/packages/26/92/85ec172418f39474c1cd0221d611345d4f433fc4ee2fc68e01f524ccc4e4/scikit_learn-1.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40daccd1b5623f39e8943ab39735cadf0bdce80e67cdca2adcb5426e987320a8", size = 9718677, upload-time = "2025-07-18T08:01:15.649Z" }, - { url = "https://files.pythonhosted.org/packages/df/ce/abdb1dcbb1d2b66168ec43b23ee0cee356b4cc4100ddee3943934ebf1480/scikit_learn-1.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30d1f413cfc0aa5a99132a554f1d80517563c34a9d3e7c118fde2d273c6fe0f7", size = 9511189, upload-time = "2025-07-18T08:01:18.013Z" }, - { url = "https://files.pythonhosted.org/packages/b2/3b/47b5eaee01ef2b5a80ba3f7f6ecf79587cb458690857d4777bfd77371c6f/scikit_learn-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c711d652829a1805a95d7fe96654604a8f16eab5a9e9ad87b3e60173415cb650", size = 8914794, upload-time = "2025-07-18T08:01:20.357Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/0dd5be14ef19f2d80a77780be35a33aa94e8a3b3223d80bee8892a7832b4/scikit_learn-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:406204dd4004f0517f0b23cf4b28c6245cbd51ab1b6b78153bc784def214946d", size = 9338868 }, + { url = "https://files.pythonhosted.org/packages/fd/52/3056b6adb1ac58a0bc335fc2ed2fcf599974d908855e8cb0ca55f797593c/scikit_learn-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:16af2e44164f05d04337fd1fc3ae7c4ea61fd9b0d527e22665346336920fe0e1", size = 8655943 }, + { url = "https://files.pythonhosted.org/packages/fb/a4/e488acdece6d413f370a9589a7193dac79cd486b2e418d3276d6ea0b9305/scikit_learn-1.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2f2e78e56a40c7587dea9a28dc4a49500fa2ead366869418c66f0fd75b80885c", size = 9652056 }, + { url = "https://files.pythonhosted.org/packages/18/41/bceacec1285b94eb9e4659b24db46c23346d7e22cf258d63419eb5dec6f7/scikit_learn-1.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62b76ad408a821475b43b7bb90a9b1c9a4d8d125d505c2df0539f06d6e631b1", size = 9473691 }, + { url = "https://files.pythonhosted.org/packages/12/7b/e1ae4b7e1dd85c4ca2694ff9cc4a9690970fd6150d81b975e6c5c6f8ee7c/scikit_learn-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:9963b065677a4ce295e8ccdee80a1dd62b37249e667095039adcd5bce6e90deb", size = 8900873 }, + { url = "https://files.pythonhosted.org/packages/b4/bd/a23177930abd81b96daffa30ef9c54ddbf544d3226b8788ce4c3ef1067b4/scikit_learn-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90c8494ea23e24c0fb371afc474618c1019dc152ce4a10e4607e62196113851b", size = 9334838 }, + { url = "https://files.pythonhosted.org/packages/8d/a1/d3a7628630a711e2ac0d1a482910da174b629f44e7dd8cfcd6924a4ef81a/scikit_learn-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:bb870c0daf3bf3be145ec51df8ac84720d9972170786601039f024bf6d61a518", size = 8651241 }, + { url = "https://files.pythonhosted.org/packages/26/92/85ec172418f39474c1cd0221d611345d4f433fc4ee2fc68e01f524ccc4e4/scikit_learn-1.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40daccd1b5623f39e8943ab39735cadf0bdce80e67cdca2adcb5426e987320a8", size = 9718677 }, + { url = "https://files.pythonhosted.org/packages/df/ce/abdb1dcbb1d2b66168ec43b23ee0cee356b4cc4100ddee3943934ebf1480/scikit_learn-1.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30d1f413cfc0aa5a99132a554f1d80517563c34a9d3e7c118fde2d273c6fe0f7", size = 9511189 }, + { url = "https://files.pythonhosted.org/packages/b2/3b/47b5eaee01ef2b5a80ba3f7f6ecf79587cb458690857d4777bfd77371c6f/scikit_learn-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c711d652829a1805a95d7fe96654604a8f16eab5a9e9ad87b3e60173415cb650", size = 8914794 }, ] [[package]] @@ -2360,20 +2356,20 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/85/cdbf2c3c460fe5aae812917866392068a88d02f07de0fe31ce738734c477/scipy-1.12.0.tar.gz", hash = "sha256:4bf5abab8a36d20193c698b0f1fc282c1d083c94723902c447e5d2f1780936a3", size = 56811768, upload-time = "2024-01-20T21:13:43.442Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/85/cdbf2c3c460fe5aae812917866392068a88d02f07de0fe31ce738734c477/scipy-1.12.0.tar.gz", hash = "sha256:4bf5abab8a36d20193c698b0f1fc282c1d083c94723902c447e5d2f1780936a3", size = 56811768 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/d9/214971dae573bd7e9303b56d2612dae439decbfc0dae0f539a591c0562ce/scipy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78e4402e140879387187f7f25d91cc592b3501a2e51dfb320f48dfb73565f10b", size = 38900384, upload-time = "2024-01-20T21:10:31.498Z" }, - { url = "https://files.pythonhosted.org/packages/dd/14/549fd7066a112c4bdf1cc11228d11284bc784ea09124fc4d663f28815564/scipy-1.12.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5f00ebaf8de24d14b8449981a2842d404152774c1a1d880c901bf454cb8e2a1", size = 31357553, upload-time = "2024-01-20T21:10:38.509Z" }, - { url = "https://files.pythonhosted.org/packages/69/1d/0582401b6d77865e080c90f39e52f65ca2bdc94e668e0bfbed8977dae3f4/scipy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e53958531a7c695ff66c2e7bb7b79560ffdc562e2051644c5576c39ff8efb563", size = 34789974, upload-time = "2024-01-20T21:10:45.054Z" }, - { url = "https://files.pythonhosted.org/packages/f5/aa/8e6071a5e4dca4ec68b5b22e4991ee74c59c5d372112b9c236ec1faff57d/scipy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e32847e08da8d895ce09d108a494d9eb78974cf6de23063f93306a3e419960c", size = 38441046, upload-time = "2024-01-20T21:10:51.285Z" }, - { url = "https://files.pythonhosted.org/packages/65/9e/43b86ec57ecdc9931b43aaf727f9d71743bfd06bdddfd441165bd3d8c6be/scipy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c1020cad92772bf44b8e4cdabc1df5d87376cb219742549ef69fc9fd86282dd", size = 38630107, upload-time = "2024-01-20T21:10:58.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a7/5f829b100d208c85163aecba93faf01d088d944fc91585338751d812f1e4/scipy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:75ea2a144096b5e39402e2ff53a36fecfd3b960d786b7efd3c180e29c39e53f2", size = 46191228, upload-time = "2024-01-20T21:11:05.92Z" }, - { url = "https://files.pythonhosted.org/packages/c3/32/7915195ca4643508fe9730691eaed57b879646279572b10b02bdadf165c5/scipy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:408c68423f9de16cb9e602528be4ce0d6312b05001f3de61fe9ec8b1263cad08", size = 38908720, upload-time = "2024-01-20T21:11:13.467Z" }, - { url = "https://files.pythonhosted.org/packages/21/d4/e6c57acc61e59cd46acca27af1f400094d5dee218e372cc604b8162b97cb/scipy-1.12.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5adfad5dbf0163397beb4aca679187d24aec085343755fcdbdeb32b3679f254c", size = 31392892, upload-time = "2024-01-20T21:11:18.947Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c5/d40abc1a857c1c6519e1a4e096d6aee86861eddac019fb736b6af8a58d25/scipy-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3003652496f6e7c387b1cf63f4bb720951cfa18907e998ea551e6de51a04467", size = 34733860, upload-time = "2024-01-20T21:11:26.666Z" }, - { url = "https://files.pythonhosted.org/packages/d4/b8/7169935f9a2ea9e274ad8c21d6133d492079e6ebc3fc69a915c2375616b0/scipy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8066bce124ee5531d12a74b617d9ac0ea59245246410e19bca549656d9a40a", size = 38418720, upload-time = "2024-01-20T21:11:33.479Z" }, - { url = "https://files.pythonhosted.org/packages/64/e7/4dbb779d09d1cb757ddbe42cae7c4fe8270497566bb902138d637b04d88c/scipy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8bee4993817e204d761dba10dbab0774ba5a8612e57e81319ea04d84945375ba", size = 38652247, upload-time = "2024-01-20T21:11:40.229Z" }, - { url = "https://files.pythonhosted.org/packages/9a/25/5b30cb3efc9566f0ebeaeca1976150316353c17031ad7868ef46de5ab8dc/scipy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a24024d45ce9a675c1fb8494e8e5244efea1c7a09c60beb1eeb80373d0fecc70", size = 46162940, upload-time = "2024-01-20T21:11:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/214971dae573bd7e9303b56d2612dae439decbfc0dae0f539a591c0562ce/scipy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78e4402e140879387187f7f25d91cc592b3501a2e51dfb320f48dfb73565f10b", size = 38900384 }, + { url = "https://files.pythonhosted.org/packages/dd/14/549fd7066a112c4bdf1cc11228d11284bc784ea09124fc4d663f28815564/scipy-1.12.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5f00ebaf8de24d14b8449981a2842d404152774c1a1d880c901bf454cb8e2a1", size = 31357553 }, + { url = "https://files.pythonhosted.org/packages/69/1d/0582401b6d77865e080c90f39e52f65ca2bdc94e668e0bfbed8977dae3f4/scipy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e53958531a7c695ff66c2e7bb7b79560ffdc562e2051644c5576c39ff8efb563", size = 34789974 }, + { url = "https://files.pythonhosted.org/packages/f5/aa/8e6071a5e4dca4ec68b5b22e4991ee74c59c5d372112b9c236ec1faff57d/scipy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e32847e08da8d895ce09d108a494d9eb78974cf6de23063f93306a3e419960c", size = 38441046 }, + { url = "https://files.pythonhosted.org/packages/65/9e/43b86ec57ecdc9931b43aaf727f9d71743bfd06bdddfd441165bd3d8c6be/scipy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c1020cad92772bf44b8e4cdabc1df5d87376cb219742549ef69fc9fd86282dd", size = 38630107 }, + { url = "https://files.pythonhosted.org/packages/fd/a7/5f829b100d208c85163aecba93faf01d088d944fc91585338751d812f1e4/scipy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:75ea2a144096b5e39402e2ff53a36fecfd3b960d786b7efd3c180e29c39e53f2", size = 46191228 }, + { url = "https://files.pythonhosted.org/packages/c3/32/7915195ca4643508fe9730691eaed57b879646279572b10b02bdadf165c5/scipy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:408c68423f9de16cb9e602528be4ce0d6312b05001f3de61fe9ec8b1263cad08", size = 38908720 }, + { url = "https://files.pythonhosted.org/packages/21/d4/e6c57acc61e59cd46acca27af1f400094d5dee218e372cc604b8162b97cb/scipy-1.12.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5adfad5dbf0163397beb4aca679187d24aec085343755fcdbdeb32b3679f254c", size = 31392892 }, + { url = "https://files.pythonhosted.org/packages/e3/c5/d40abc1a857c1c6519e1a4e096d6aee86861eddac019fb736b6af8a58d25/scipy-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3003652496f6e7c387b1cf63f4bb720951cfa18907e998ea551e6de51a04467", size = 34733860 }, + { url = "https://files.pythonhosted.org/packages/d4/b8/7169935f9a2ea9e274ad8c21d6133d492079e6ebc3fc69a915c2375616b0/scipy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8066bce124ee5531d12a74b617d9ac0ea59245246410e19bca549656d9a40a", size = 38418720 }, + { url = "https://files.pythonhosted.org/packages/64/e7/4dbb779d09d1cb757ddbe42cae7c4fe8270497566bb902138d637b04d88c/scipy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8bee4993817e204d761dba10dbab0774ba5a8612e57e81319ea04d84945375ba", size = 38652247 }, + { url = "https://files.pythonhosted.org/packages/9a/25/5b30cb3efc9566f0ebeaeca1976150316353c17031ad7868ef46de5ab8dc/scipy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a24024d45ce9a675c1fb8494e8e5244efea1c7a09c60beb1eeb80373d0fecc70", size = 46162940 }, ] [[package]] @@ -2385,36 +2381,36 @@ dependencies = [ { name = "numpy" }, { name = "pandas" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696 } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914 }, ] [[package]] name = "setuptools" version = "80.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 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" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } 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" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] [[package]] @@ -2424,27 +2420,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/2b/5e7234c68ed5bc872ad6ae77b8a421c2ed70dcb1190b44dc1abdeed5e347/smart_open-7.3.0.post1.tar.gz", hash = "sha256:ce6a3d9bc1afbf6234ad13c010b77f8cd36d24636811e3c52c3b5160f5214d1e", size = 51557, upload-time = "2025-07-03T10:06:31.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/2b/5e7234c68ed5bc872ad6ae77b8a421c2ed70dcb1190b44dc1abdeed5e347/smart_open-7.3.0.post1.tar.gz", hash = "sha256:ce6a3d9bc1afbf6234ad13c010b77f8cd36d24636811e3c52c3b5160f5214d1e", size = 51557 } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/5b/a2a3d4514c64818925f4e886d39981f1926eeb5288a4549c6b3c17ed66bb/smart_open-7.3.0.post1-py3-none-any.whl", hash = "sha256:c73661a2c24bf045c1e04e08fffc585b59af023fe783d57896f590489db66fb4", size = 61946, upload-time = "2025-07-03T10:06:29.599Z" }, + { url = "https://files.pythonhosted.org/packages/08/5b/a2a3d4514c64818925f4e886d39981f1926eeb5288a4549c6b3c17ed66bb/smart_open-7.3.0.post1-py3-none-any.whl", hash = "sha256:c73661a2c24bf045c1e04e08fffc585b59af023fe783d57896f590489db66fb4", size = 61946 }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 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" }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] @@ -2472,40 +2468,40 @@ dependencies = [ { name = "wasabi" }, { name = "weasel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/9e/fb4e1cefe3fbd51ea6a243e5a3d2bc629baa9a28930bf4be6fe5672fa1ca/spacy-3.8.7.tar.gz", hash = "sha256:700fd174c6c552276be142c48e70bb53cae24c4dd86003c4432af9cb93e4c908", size = 1316143, upload-time = "2025-05-23T08:55:39.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/9e/fb4e1cefe3fbd51ea6a243e5a3d2bc629baa9a28930bf4be6fe5672fa1ca/spacy-3.8.7.tar.gz", hash = "sha256:700fd174c6c552276be142c48e70bb53cae24c4dd86003c4432af9cb93e4c908", size = 1316143 } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/2c/bbba614290492c169ee50777e44d3e4325a1e646272379988de8749b9dd4/spacy-3.8.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ec0368ce96cd775fb14906f04b771c912ea8393ba30f8b35f9c4dc47a420b8e", size = 6613435, upload-time = "2025-05-23T08:54:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/39/a9/c1fdecc11d8855b3df601bbfb5fc4cdb98d79b6a5d166af974354ea658eb/spacy-3.8.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5672f8a0fe7a3847e925544890be60015fbf48a60a838803425f82e849dd4f18", size = 6261550, upload-time = "2025-05-23T08:54:06.984Z" }, - { url = "https://files.pythonhosted.org/packages/39/fe/e8b5a374f2517716f510f0dd6a0b68e88637e66db7c315d4002ba80b2bfe/spacy-3.8.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60cde9fe8b15be04eb1e634c353d9c160187115d825b368cc1975452dd54f264", size = 31215973, upload-time = "2025-05-23T08:54:09.46Z" }, - { url = "https://files.pythonhosted.org/packages/bb/e7/bd1df17add98a5ec3e0d2dd73d4e5884683ffd2e34d3c0e5828f48933787/spacy-3.8.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cac8e58fb92fb1c5e06328039595fa6589a9d1403681266f8f5e454d15319c", size = 31504596, upload-time = "2025-05-23T08:54:12.684Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/5fd95749f390478a31a806500e829c5a8d97312ea18129494d255e231c00/spacy-3.8.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1456245a4ed04bc882db2d89a27ca1b6dc0b947b643bedaeaa5da11d9f7e22ec", size = 30527369, upload-time = "2025-05-23T08:54:15.467Z" }, - { url = "https://files.pythonhosted.org/packages/7a/74/f4708260fc135f8de15eb1d0ecfe00fd7b53f4b1d4927f90a33d48dff637/spacy-3.8.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bb98f85d467963d17c7c660884069ba948bde71c07280c91ee3235e554375308", size = 31357330, upload-time = "2025-05-23T08:54:18.342Z" }, - { url = "https://files.pythonhosted.org/packages/53/a6/3086859d2bfb5b6f97b17e19f51da0983eb11b07f63c24dced6506cdb370/spacy-3.8.7-cp310-cp310-win_amd64.whl", hash = "sha256:b0df50d69e6691e97eae228733b321971607dbbb799e59d8470f2e70b8b27a8e", size = 14929267, upload-time = "2025-05-23T08:54:21.365Z" }, - { url = "https://files.pythonhosted.org/packages/29/c5/5fbb3a4e694d4855a5bab87af9664377c48b89691f180ad3cde4faeaf35c/spacy-3.8.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bdff8b9b556468a6dd527af17f0ddf9fb0b0bee92ee7703339ddf542361cff98", size = 6746140, upload-time = "2025-05-23T08:54:23.483Z" }, - { url = "https://files.pythonhosted.org/packages/03/2a/43afac516eb82409ca47d7206f982beaf265d2ba06a72ca07cf06b290c20/spacy-3.8.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9194b7cf015ed9b4450ffb162da49c8a9305e76b468de036b0948abdfc748a37", size = 6392440, upload-time = "2025-05-23T08:54:25.12Z" }, - { url = "https://files.pythonhosted.org/packages/6f/83/2ea68c18e2b1b9a6f6b30ef63eb9d07e979626b9595acfdb5394f18923c4/spacy-3.8.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dc38b78d48b9c2a80a3eea95f776304993f63fc307f07cdd104441442f92f1e", size = 32699126, upload-time = "2025-05-23T08:54:27.385Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0a/bb90e9aa0b3c527876627567d82517aabab08006ccf63796c33b0242254d/spacy-3.8.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e43bd70772751b8fc7a14f338d087a3d297195d43d171832923ef66204b23ab", size = 33008865, upload-time = "2025-05-23T08:54:30.248Z" }, - { url = "https://files.pythonhosted.org/packages/39/dd/8e906ba378457107ab0394976ea9f7b12fdb2cad682ef1a2ccf473d61e5f/spacy-3.8.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c402bf5dcf345fd96d202378c54bc345219681e3531f911d99567d569328c45f", size = 31933169, upload-time = "2025-05-23T08:54:33.199Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b5/42df07eb837a923fbb42509864d5c7c2072d010de933dccdfb3c655b3a76/spacy-3.8.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4234189861e486d86f1269e50542d87e8a6391a1ee190652479cf1a793db115f", size = 32776322, upload-time = "2025-05-23T08:54:36.891Z" }, - { url = "https://files.pythonhosted.org/packages/92/e7/8176484801c67dcd814f141991fe0a3c9b5b4a3583ea30c2062e93d1aa6b/spacy-3.8.7-cp311-cp311-win_amd64.whl", hash = "sha256:e9d12e2eb7f36bc11dd9edae011032fe49ea100d63e83177290d3cbd80eaa650", size = 14938936, upload-time = "2025-05-23T08:54:40.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/2c/bbba614290492c169ee50777e44d3e4325a1e646272379988de8749b9dd4/spacy-3.8.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6ec0368ce96cd775fb14906f04b771c912ea8393ba30f8b35f9c4dc47a420b8e", size = 6613435 }, + { url = "https://files.pythonhosted.org/packages/39/a9/c1fdecc11d8855b3df601bbfb5fc4cdb98d79b6a5d166af974354ea658eb/spacy-3.8.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5672f8a0fe7a3847e925544890be60015fbf48a60a838803425f82e849dd4f18", size = 6261550 }, + { url = "https://files.pythonhosted.org/packages/39/fe/e8b5a374f2517716f510f0dd6a0b68e88637e66db7c315d4002ba80b2bfe/spacy-3.8.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60cde9fe8b15be04eb1e634c353d9c160187115d825b368cc1975452dd54f264", size = 31215973 }, + { url = "https://files.pythonhosted.org/packages/bb/e7/bd1df17add98a5ec3e0d2dd73d4e5884683ffd2e34d3c0e5828f48933787/spacy-3.8.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cac8e58fb92fb1c5e06328039595fa6589a9d1403681266f8f5e454d15319c", size = 31504596 }, + { url = "https://files.pythonhosted.org/packages/b2/fa/5fd95749f390478a31a806500e829c5a8d97312ea18129494d255e231c00/spacy-3.8.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1456245a4ed04bc882db2d89a27ca1b6dc0b947b643bedaeaa5da11d9f7e22ec", size = 30527369 }, + { url = "https://files.pythonhosted.org/packages/7a/74/f4708260fc135f8de15eb1d0ecfe00fd7b53f4b1d4927f90a33d48dff637/spacy-3.8.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bb98f85d467963d17c7c660884069ba948bde71c07280c91ee3235e554375308", size = 31357330 }, + { url = "https://files.pythonhosted.org/packages/53/a6/3086859d2bfb5b6f97b17e19f51da0983eb11b07f63c24dced6506cdb370/spacy-3.8.7-cp310-cp310-win_amd64.whl", hash = "sha256:b0df50d69e6691e97eae228733b321971607dbbb799e59d8470f2e70b8b27a8e", size = 14929267 }, + { url = "https://files.pythonhosted.org/packages/29/c5/5fbb3a4e694d4855a5bab87af9664377c48b89691f180ad3cde4faeaf35c/spacy-3.8.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bdff8b9b556468a6dd527af17f0ddf9fb0b0bee92ee7703339ddf542361cff98", size = 6746140 }, + { url = "https://files.pythonhosted.org/packages/03/2a/43afac516eb82409ca47d7206f982beaf265d2ba06a72ca07cf06b290c20/spacy-3.8.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9194b7cf015ed9b4450ffb162da49c8a9305e76b468de036b0948abdfc748a37", size = 6392440 }, + { url = "https://files.pythonhosted.org/packages/6f/83/2ea68c18e2b1b9a6f6b30ef63eb9d07e979626b9595acfdb5394f18923c4/spacy-3.8.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dc38b78d48b9c2a80a3eea95f776304993f63fc307f07cdd104441442f92f1e", size = 32699126 }, + { url = "https://files.pythonhosted.org/packages/0a/0a/bb90e9aa0b3c527876627567d82517aabab08006ccf63796c33b0242254d/spacy-3.8.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e43bd70772751b8fc7a14f338d087a3d297195d43d171832923ef66204b23ab", size = 33008865 }, + { url = "https://files.pythonhosted.org/packages/39/dd/8e906ba378457107ab0394976ea9f7b12fdb2cad682ef1a2ccf473d61e5f/spacy-3.8.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c402bf5dcf345fd96d202378c54bc345219681e3531f911d99567d569328c45f", size = 31933169 }, + { url = "https://files.pythonhosted.org/packages/c9/b5/42df07eb837a923fbb42509864d5c7c2072d010de933dccdfb3c655b3a76/spacy-3.8.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4234189861e486d86f1269e50542d87e8a6391a1ee190652479cf1a793db115f", size = 32776322 }, + { url = "https://files.pythonhosted.org/packages/92/e7/8176484801c67dcd814f141991fe0a3c9b5b4a3583ea30c2062e93d1aa6b/spacy-3.8.7-cp311-cp311-win_amd64.whl", hash = "sha256:e9d12e2eb7f36bc11dd9edae011032fe49ea100d63e83177290d3cbd80eaa650", size = 14938936 }, ] [[package]] name = "spacy-legacy" version = "3.0.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/79/91f9d7cc8db5642acad830dcc4b49ba65a7790152832c4eceb305e46d681/spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774", size = 23806, upload-time = "2023-01-23T09:04:15.104Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/79/91f9d7cc8db5642acad830dcc4b49ba65a7790152832c4eceb305e46d681/spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774", size = 23806 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/55/12e842c70ff8828e34e543a2c7176dac4da006ca6901c9e8b43efab8bc6b/spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f", size = 29971, upload-time = "2023-01-23T09:04:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/55/12e842c70ff8828e34e543a2c7176dac4da006ca6901c9e8b43efab8bc6b/spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f", size = 29971 }, ] [[package]] name = "spacy-loggers" version = "1.0.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/3d/926db774c9c98acf66cb4ed7faf6c377746f3e00b84b700d0868b95d0712/spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24", size = 20811, upload-time = "2023-09-11T12:26:52.323Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/3d/926db774c9c98acf66cb4ed7faf6c377746f3e00b84b700d0868b95d0712/spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24", size = 20811 } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/78/d1a1a026ef3af911159398c939b1509d5c36fe524c7b644f34a5146c4e16/spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645", size = 22343, upload-time = "2023-09-11T12:26:50.586Z" }, + { url = "https://files.pythonhosted.org/packages/33/78/d1a1a026ef3af911159398c939b1509d5c36fe524c7b644f34a5146c4e16/spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645", size = 22343 }, ] [[package]] @@ -2515,22 +2511,22 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "catalogue" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/e8/eb51b1349f50bac0222398af0942613fdc9d1453ae67cbe4bf9936a1a54b/srsly-2.5.1.tar.gz", hash = "sha256:ab1b4bf6cf3e29da23dae0493dd1517fb787075206512351421b89b4fc27c77e", size = 466464, upload-time = "2025-01-17T09:26:26.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/e8/eb51b1349f50bac0222398af0942613fdc9d1453ae67cbe4bf9936a1a54b/srsly-2.5.1.tar.gz", hash = "sha256:ab1b4bf6cf3e29da23dae0493dd1517fb787075206512351421b89b4fc27c77e", size = 466464 } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/08/448bcc87bb93bc19fccf70c2f0f993ac42aa41d5f44a19c60d00186aea09/srsly-2.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0cda6f65cc0dd1daf47e856b0d6c5d51db8a9343c5007723ca06903dcfe367d", size = 636045, upload-time = "2025-01-17T09:25:04.605Z" }, - { url = "https://files.pythonhosted.org/packages/03/8a/379dd9014e56460e71346cf512632fb8cbc89aa6dfebe31dff21c9eb37ba/srsly-2.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf643e6f45c266cfacea54997a1f9cfe0113fadac1ac21a1ec5b200cfe477ba0", size = 634425, upload-time = "2025-01-17T09:25:07.957Z" }, - { url = "https://files.pythonhosted.org/packages/95/69/46e672941b5f4403b0e2b14918d8e1393ca48e3338e2c01e549113261cdf/srsly-2.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:467ed25ddab09ca9404fda92519a317c803b5ea0849f846e74ba8b7843557df5", size = 1085032, upload-time = "2025-01-17T09:25:11.291Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d8/1039e663b87a06d2450148ebadc07eaf6f8b7dd7f7d5e2f4221050ce6702/srsly-2.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8113d202664b7d31025bdbe40b9d3536e8d7154d09520b6a1955818fa6d622", size = 1089469, upload-time = "2025-01-17T09:25:15.913Z" }, - { url = "https://files.pythonhosted.org/packages/e9/62/f819ac665ecca2659343a6c79174c582fe292829f481899f05e7a7301988/srsly-2.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:794d39fccd2b333d24f1b445acc78daf90f3f37d3c0f6f0167f25c56961804e7", size = 1052673, upload-time = "2025-01-17T09:25:17.658Z" }, - { url = "https://files.pythonhosted.org/packages/a8/69/321a41fe4d549b96dd010b6a77657e84eb181034f9d125e2feebcd8f2e5c/srsly-2.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df7fd77457c4d6c630f700b1019a8ad173e411e7cf7cfdea70e5ed86b608083b", size = 1062650, upload-time = "2025-01-17T09:25:20.704Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b8/3dfed2db5c7ecf275aaddb775e2ae17c576b09c848873188fce91e410129/srsly-2.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:1a4dddb2edb8f7974c9aa5ec46dc687a75215b3bbdc815ce3fc9ea68fe1e94b5", size = 632267, upload-time = "2025-01-17T09:25:23.713Z" }, - { url = "https://files.pythonhosted.org/packages/df/9c/a248bb49de499fe0990e3cb0fb341c2373d8863ef9a8b5799353cade5731/srsly-2.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58f0736794ce00a71d62a39cbba1d62ea8d5be4751df956e802d147da20ecad7", size = 635917, upload-time = "2025-01-17T09:25:25.109Z" }, - { url = "https://files.pythonhosted.org/packages/41/47/1bdaad84502df973ecb8ca658117234cf7fb20e1dec60da71dce82de993f/srsly-2.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8269c40859806d71920396d185f4f38dc985cdb6a28d3a326a701e29a5f629", size = 634374, upload-time = "2025-01-17T09:25:26.609Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2a/d73c71989fcf2a6d1fa518d75322aff4db01a8763f167f8c5e00aac11097/srsly-2.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889905900401fefc1032e22b73aecbed8b4251aa363f632b2d1f86fc16f1ad8e", size = 1108390, upload-time = "2025-01-17T09:25:29.32Z" }, - { url = "https://files.pythonhosted.org/packages/35/a3/9eda9997a8bd011caed18fdaa5ce606714eb06d8dab587ed0522b3e92ab1/srsly-2.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf454755f22589df49c25dc799d8af7b47dce3d861dded35baf0f0b6ceab4422", size = 1110712, upload-time = "2025-01-17T09:25:31.051Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ef/4b50bc05d06349f905b27f824cc23b652098efd4be19aead3af4981df647/srsly-2.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc0607c8a59013a51dde5c1b4e465558728e9e0a35dcfa73c7cbefa91a0aad50", size = 1081244, upload-time = "2025-01-17T09:25:32.611Z" }, - { url = "https://files.pythonhosted.org/packages/90/af/d4a2512d9a5048d2b18efead39d4c4404bddd4972935bbc68211292a736c/srsly-2.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d5421ba3ab3c790e8b41939c51a1d0f44326bfc052d7a0508860fb79a47aee7f", size = 1091692, upload-time = "2025-01-17T09:25:34.15Z" }, - { url = "https://files.pythonhosted.org/packages/bb/da/657a685f63028dcb00ccdc4ac125ed347c8bff6fa0dab6a9eb3dc45f3223/srsly-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:b96ea5a9a0d0379a79c46d255464a372fb14c30f59a8bc113e4316d131a530ab", size = 632627, upload-time = "2025-01-17T09:25:37.36Z" }, + { url = "https://files.pythonhosted.org/packages/37/08/448bcc87bb93bc19fccf70c2f0f993ac42aa41d5f44a19c60d00186aea09/srsly-2.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0cda6f65cc0dd1daf47e856b0d6c5d51db8a9343c5007723ca06903dcfe367d", size = 636045 }, + { url = "https://files.pythonhosted.org/packages/03/8a/379dd9014e56460e71346cf512632fb8cbc89aa6dfebe31dff21c9eb37ba/srsly-2.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf643e6f45c266cfacea54997a1f9cfe0113fadac1ac21a1ec5b200cfe477ba0", size = 634425 }, + { url = "https://files.pythonhosted.org/packages/95/69/46e672941b5f4403b0e2b14918d8e1393ca48e3338e2c01e549113261cdf/srsly-2.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:467ed25ddab09ca9404fda92519a317c803b5ea0849f846e74ba8b7843557df5", size = 1085032 }, + { url = "https://files.pythonhosted.org/packages/ce/d8/1039e663b87a06d2450148ebadc07eaf6f8b7dd7f7d5e2f4221050ce6702/srsly-2.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8113d202664b7d31025bdbe40b9d3536e8d7154d09520b6a1955818fa6d622", size = 1089469 }, + { url = "https://files.pythonhosted.org/packages/e9/62/f819ac665ecca2659343a6c79174c582fe292829f481899f05e7a7301988/srsly-2.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:794d39fccd2b333d24f1b445acc78daf90f3f37d3c0f6f0167f25c56961804e7", size = 1052673 }, + { url = "https://files.pythonhosted.org/packages/a8/69/321a41fe4d549b96dd010b6a77657e84eb181034f9d125e2feebcd8f2e5c/srsly-2.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df7fd77457c4d6c630f700b1019a8ad173e411e7cf7cfdea70e5ed86b608083b", size = 1062650 }, + { url = "https://files.pythonhosted.org/packages/d5/b8/3dfed2db5c7ecf275aaddb775e2ae17c576b09c848873188fce91e410129/srsly-2.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:1a4dddb2edb8f7974c9aa5ec46dc687a75215b3bbdc815ce3fc9ea68fe1e94b5", size = 632267 }, + { url = "https://files.pythonhosted.org/packages/df/9c/a248bb49de499fe0990e3cb0fb341c2373d8863ef9a8b5799353cade5731/srsly-2.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58f0736794ce00a71d62a39cbba1d62ea8d5be4751df956e802d147da20ecad7", size = 635917 }, + { url = "https://files.pythonhosted.org/packages/41/47/1bdaad84502df973ecb8ca658117234cf7fb20e1dec60da71dce82de993f/srsly-2.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8269c40859806d71920396d185f4f38dc985cdb6a28d3a326a701e29a5f629", size = 634374 }, + { url = "https://files.pythonhosted.org/packages/e5/2a/d73c71989fcf2a6d1fa518d75322aff4db01a8763f167f8c5e00aac11097/srsly-2.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889905900401fefc1032e22b73aecbed8b4251aa363f632b2d1f86fc16f1ad8e", size = 1108390 }, + { url = "https://files.pythonhosted.org/packages/35/a3/9eda9997a8bd011caed18fdaa5ce606714eb06d8dab587ed0522b3e92ab1/srsly-2.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf454755f22589df49c25dc799d8af7b47dce3d861dded35baf0f0b6ceab4422", size = 1110712 }, + { url = "https://files.pythonhosted.org/packages/8a/ef/4b50bc05d06349f905b27f824cc23b652098efd4be19aead3af4981df647/srsly-2.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc0607c8a59013a51dde5c1b4e465558728e9e0a35dcfa73c7cbefa91a0aad50", size = 1081244 }, + { url = "https://files.pythonhosted.org/packages/90/af/d4a2512d9a5048d2b18efead39d4c4404bddd4972935bbc68211292a736c/srsly-2.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d5421ba3ab3c790e8b41939c51a1d0f44326bfc052d7a0508860fb79a47aee7f", size = 1091692 }, + { url = "https://files.pythonhosted.org/packages/bb/da/657a685f63028dcb00ccdc4ac125ed347c8bff6fa0dab6a9eb3dc45f3223/srsly-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:b96ea5a9a0d0379a79c46d255464a372fb14c30f59a8bc113e4316d131a530ab", size = 632627 }, ] [[package]] @@ -2540,9 +2536,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "streamlit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/b9/f2d0593b80991eddb5f73c113c1d9d090b4564dba984e65b8823b9d119cc/st_tabs-0.1.1.tar.gz", hash = "sha256:6bb090d9c09ffa7a621b284ecc9ba84c56f366f656c65501f36e9cf591e4fbb6", size = 621255, upload-time = "2023-09-07T13:39:54.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/b9/f2d0593b80991eddb5f73c113c1d9d090b4564dba984e65b8823b9d119cc/st_tabs-0.1.1.tar.gz", hash = "sha256:6bb090d9c09ffa7a621b284ecc9ba84c56f366f656c65501f36e9cf591e4fbb6", size = 621255 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/e1/53591389caea01689dc923bbb69426d0377e28dd3cb85bb1ab0ed365bd9a/st_tabs-0.1.1-py3-none-any.whl", hash = "sha256:a12c3cc6aeaf7a37f86239447349729a187d7e19047ddc20b69dfa154c57120a", size = 627441, upload-time = "2023-09-07T13:39:51.39Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e1/53591389caea01689dc923bbb69426d0377e28dd3cb85bb1ab0ed365bd9a/st_tabs-0.1.1-py3-none-any.whl", hash = "sha256:a12c3cc6aeaf7a37f86239447349729a187d7e19047ddc20b69dfa154c57120a", size = 627441 }, ] [[package]] @@ -2554,9 +2550,9 @@ dependencies = [ { name = "executing" }, { name = "pure-eval" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, ] [[package]] @@ -2570,20 +2566,20 @@ dependencies = [ { name = "patsy" }, { name = "scipy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/cc/8c1bf59bf8203dea1bf2ea811cfe667d7bcc6909c83d8afb02b08e30f50b/statsmodels-0.14.5.tar.gz", hash = "sha256:de260e58cccfd2ceddf835b55a357233d6ca853a1aa4f90f7553a52cc71c6ddf", size = 20525016, upload-time = "2025-07-07T12:14:23.195Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/cc/8c1bf59bf8203dea1bf2ea811cfe667d7bcc6909c83d8afb02b08e30f50b/statsmodels-0.14.5.tar.gz", hash = "sha256:de260e58cccfd2ceddf835b55a357233d6ca853a1aa4f90f7553a52cc71c6ddf", size = 20525016 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2c/55b2a5d10c1a211ecab3f792021d2581bbe1c5ca0a1059f6715dddc6899d/statsmodels-0.14.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9fc2b5cdc0c95cba894849651fec1fa1511d365e3eb72b0cc75caac44077cd48", size = 10058241, upload-time = "2025-07-07T12:13:16.286Z" }, - { url = "https://files.pythonhosted.org/packages/66/d9/6967475805de06691e951072d05e40e3f1c71b6221bb92401193ee19bd2a/statsmodels-0.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b8d96b0bbaeabd3a557c35cc7249baa9cfbc6dd305c32a9f2cbdd7f46c037e7f", size = 9734017, upload-time = "2025-07-07T12:05:08.498Z" }, - { url = "https://files.pythonhosted.org/packages/df/a8/803c280419a7312e2472969fe72cf461c1210a27770a662cbe3b5cd7c6fe/statsmodels-0.14.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:145bc39b2cb201efb6c83cc3f2163c269e63b0d4809801853dec6f440bd3bc37", size = 10459677, upload-time = "2025-07-07T14:21:51.809Z" }, - { url = "https://files.pythonhosted.org/packages/a1/25/edf20acbd670934b02cd9344e29c9a03ce040122324b3491bb075ae76b2d/statsmodels-0.14.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7c14fb2617bb819fb2532e1424e1da2b98a3419a80e95f33365a72d437d474e", size = 10678631, upload-time = "2025-07-07T14:22:05.496Z" }, - { url = "https://files.pythonhosted.org/packages/64/22/8b1e38310272e766abd6093607000a81827420a3348f09eff08a9e54cbaf/statsmodels-0.14.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1e9742d8a5ac38a3bfc4b7f4b0681903920f20cbbf466d72b1fd642033846108", size = 10699273, upload-time = "2025-07-07T14:22:19.487Z" }, - { url = "https://files.pythonhosted.org/packages/d1/6f/6de51f1077b7cef34611f1d6721392ea170153251b4d977efcf6d100f779/statsmodels-0.14.5-cp310-cp310-win_amd64.whl", hash = "sha256:1cab9e6fce97caf4239cdb2df375806937da5d0b7ba2699b13af33a07f438464", size = 9644785, upload-time = "2025-07-07T12:05:20.927Z" }, - { url = "https://files.pythonhosted.org/packages/14/30/fd49902b30416b828de763e161c0d6e2cc04d119ae4fbdd3f3b43dc8f1be/statsmodels-0.14.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b7091a8442076c708c926de3603653a160955e80a2b6d931475b7bb8ddc02e5", size = 10053330, upload-time = "2025-07-07T12:07:39.689Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c1/2654541ff6f5790d01d1e5ba36405fde873f4a854f473e90b4fe56b37333/statsmodels-0.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:128872be8f3208f4446d91ea9e4261823902fc7997fee7e1a983eb62fd3b7c6e", size = 9735555, upload-time = "2025-07-07T12:13:28.935Z" }, - { url = "https://files.pythonhosted.org/packages/ce/da/6ebb64d0db4e86c0d2d9cde89e03247702da0ab191789f7813d4f9a348da/statsmodels-0.14.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ad5aee04ae7196c429df2174df232c057e478c5fa63193d01c8ec9aae04d31", size = 10307522, upload-time = "2025-07-07T14:22:32.853Z" }, - { url = "https://files.pythonhosted.org/packages/67/49/ac803ca093ec3845184a752a91cd84511245e1f97103b15cfe32794a3bb0/statsmodels-0.14.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f402fc793458dd6d96e099acb44cd1de1428565bf7ef3030878a8daff091f08a", size = 10474665, upload-time = "2025-07-07T14:22:46.011Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c8/ae82feb00582f4814fac5d2cb3ec32f93866b413cf5878b2fe93688ec63c/statsmodels-0.14.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:26c028832730aebfbfd4e7501694e1f9ad31ec8536e776716673f4e7afd4059a", size = 10713120, upload-time = "2025-07-07T14:23:00.067Z" }, - { url = "https://files.pythonhosted.org/packages/05/ac/4276459ea71aa46e2967ea283fc88ee5631c11f29a06787e16cf4aece1b8/statsmodels-0.14.5-cp311-cp311-win_amd64.whl", hash = "sha256:ec56f771d9529cdc17ed2fb2a950d100b6e83a7c5372aae8ac5bb065c474b856", size = 9640980, upload-time = "2025-07-07T12:05:33.085Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2c/55b2a5d10c1a211ecab3f792021d2581bbe1c5ca0a1059f6715dddc6899d/statsmodels-0.14.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9fc2b5cdc0c95cba894849651fec1fa1511d365e3eb72b0cc75caac44077cd48", size = 10058241 }, + { url = "https://files.pythonhosted.org/packages/66/d9/6967475805de06691e951072d05e40e3f1c71b6221bb92401193ee19bd2a/statsmodels-0.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b8d96b0bbaeabd3a557c35cc7249baa9cfbc6dd305c32a9f2cbdd7f46c037e7f", size = 9734017 }, + { url = "https://files.pythonhosted.org/packages/df/a8/803c280419a7312e2472969fe72cf461c1210a27770a662cbe3b5cd7c6fe/statsmodels-0.14.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:145bc39b2cb201efb6c83cc3f2163c269e63b0d4809801853dec6f440bd3bc37", size = 10459677 }, + { url = "https://files.pythonhosted.org/packages/a1/25/edf20acbd670934b02cd9344e29c9a03ce040122324b3491bb075ae76b2d/statsmodels-0.14.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7c14fb2617bb819fb2532e1424e1da2b98a3419a80e95f33365a72d437d474e", size = 10678631 }, + { url = "https://files.pythonhosted.org/packages/64/22/8b1e38310272e766abd6093607000a81827420a3348f09eff08a9e54cbaf/statsmodels-0.14.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1e9742d8a5ac38a3bfc4b7f4b0681903920f20cbbf466d72b1fd642033846108", size = 10699273 }, + { url = "https://files.pythonhosted.org/packages/d1/6f/6de51f1077b7cef34611f1d6721392ea170153251b4d977efcf6d100f779/statsmodels-0.14.5-cp310-cp310-win_amd64.whl", hash = "sha256:1cab9e6fce97caf4239cdb2df375806937da5d0b7ba2699b13af33a07f438464", size = 9644785 }, + { url = "https://files.pythonhosted.org/packages/14/30/fd49902b30416b828de763e161c0d6e2cc04d119ae4fbdd3f3b43dc8f1be/statsmodels-0.14.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b7091a8442076c708c926de3603653a160955e80a2b6d931475b7bb8ddc02e5", size = 10053330 }, + { url = "https://files.pythonhosted.org/packages/ca/c1/2654541ff6f5790d01d1e5ba36405fde873f4a854f473e90b4fe56b37333/statsmodels-0.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:128872be8f3208f4446d91ea9e4261823902fc7997fee7e1a983eb62fd3b7c6e", size = 9735555 }, + { url = "https://files.pythonhosted.org/packages/ce/da/6ebb64d0db4e86c0d2d9cde89e03247702da0ab191789f7813d4f9a348da/statsmodels-0.14.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ad5aee04ae7196c429df2174df232c057e478c5fa63193d01c8ec9aae04d31", size = 10307522 }, + { url = "https://files.pythonhosted.org/packages/67/49/ac803ca093ec3845184a752a91cd84511245e1f97103b15cfe32794a3bb0/statsmodels-0.14.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f402fc793458dd6d96e099acb44cd1de1428565bf7ef3030878a8daff091f08a", size = 10474665 }, + { url = "https://files.pythonhosted.org/packages/f0/c8/ae82feb00582f4814fac5d2cb3ec32f93866b413cf5878b2fe93688ec63c/statsmodels-0.14.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:26c028832730aebfbfd4e7501694e1f9ad31ec8536e776716673f4e7afd4059a", size = 10713120 }, + { url = "https://files.pythonhosted.org/packages/05/ac/4276459ea71aa46e2967ea283fc88ee5631c11f29a06787e16cf4aece1b8/statsmodels-0.14.5-cp311-cp311-win_amd64.whl", hash = "sha256:ec56f771d9529cdc17ed2fb2a950d100b6e83a7c5372aae8ac5bb065c474b856", size = 9640980 }, ] [[package]] @@ -2610,9 +2606,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "watchdog", marker = "sys_platform != 'darwin'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/05/7b0a78beb4589fa056435fdf2243bc28ba3b320f4bc54b2cbf7680d7848b/streamlit-1.43.0.tar.gz", hash = "sha256:c10c09f9d1251fa7f975dd360572f03cabc82b174f080e323bf7e556103c22e0", size = 9344982, upload-time = "2025-03-04T20:28:23.127Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/05/7b0a78beb4589fa056435fdf2243bc28ba3b320f4bc54b2cbf7680d7848b/streamlit-1.43.0.tar.gz", hash = "sha256:c10c09f9d1251fa7f975dd360572f03cabc82b174f080e323bf7e556103c22e0", size = 9344982 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/37/6ae099cf3029990fb2b35420296e3066c649a9dc5c286fa74643b1d7074f/streamlit-1.43.0-py2.py3-none-any.whl", hash = "sha256:cf94b1e9f1de75e4e383df53745230feaac4ac7a7e1f14a3ea362df134db8510", size = 9734392, upload-time = "2025-03-04T20:28:19.189Z" }, + { url = "https://files.pythonhosted.org/packages/e9/37/6ae099cf3029990fb2b35420296e3066c649a9dc5c286fa74643b1d7074f/streamlit-1.43.0-py2.py3-none-any.whl", hash = "sha256:cf94b1e9f1de75e4e383df53745230feaac4ac7a7e1f14a3ea362df134db8510", size = 9734392 }, ] [[package]] @@ -2625,18 +2621,18 @@ dependencies = [ { name = "rdflib" }, { name = "streamlit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/63/00a16b1500cde32d43177bba1c16d12369ed7d1c8b45ab8162f6552d8519/streamlit-agraph-0.0.45.tar.gz", hash = "sha256:b2b7cf1ad0a40dc906de50792b27f2878b0e186603cb3bc958ed78ca7e469cdd", size = 1299483, upload-time = "2023-01-28T10:26:00.46Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/63/00a16b1500cde32d43177bba1c16d12369ed7d1c8b45ab8162f6552d8519/streamlit-agraph-0.0.45.tar.gz", hash = "sha256:b2b7cf1ad0a40dc906de50792b27f2878b0e186603cb3bc958ed78ca7e469cdd", size = 1299483 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/80/8a666e700332a9fe19e458678c95fab4d78340251d2f12da7d2ad915458a/streamlit_agraph-0.0.45-py3-none-any.whl", hash = "sha256:38e7271ffd76a6769968c2e9dfc16cbac7621d62be15af98b62598e1446bee2f", size = 1312064, upload-time = "2023-01-28T10:25:58.043Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/8a666e700332a9fe19e458678c95fab4d78340251d2f12da7d2ad915458a/streamlit_agraph-0.0.45-py3-none-any.whl", hash = "sha256:38e7271ffd76a6769968c2e9dfc16cbac7621d62be15af98b62598e1446bee2f", size = 1312064 }, ] [[package]] name = "tenacity" version = "9.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, ] [[package]] @@ -2646,9 +2642,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nltk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/9b/8648f7ab89afb38de30aef9739a7f31491631635bd364042869162132bc4/textblob-0.18.0.post0.tar.gz", hash = "sha256:8131c52c630bcdf61d04c359f939c98d5b836a01fba224d9e7ae22fc274e0ccb", size = 639600, upload-time = "2024-02-15T20:39:50.452Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/9b/8648f7ab89afb38de30aef9739a7f31491631635bd364042869162132bc4/textblob-0.18.0.post0.tar.gz", hash = "sha256:8131c52c630bcdf61d04c359f939c98d5b836a01fba224d9e7ae22fc274e0ccb", size = 639600 } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/07/5fd2945356dd839974d3a25de8a142dc37293c21315729a41e775b5f3569/textblob-0.18.0.post0-py3-none-any.whl", hash = "sha256:dd0c7ec4eb7b9346ec0a3f136a63eba13e0f59890d2a693d3d6aeb8371949dca", size = 626330, upload-time = "2024-02-15T20:39:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/02/07/5fd2945356dd839974d3a25de8a142dc37293c21315729a41e775b5f3569/textblob-0.18.0.post0-py3-none-any.whl", hash = "sha256:dd0c7ec4eb7b9346ec0a3f136a63eba13e0f59890d2a693d3d6aeb8371949dca", size = 626330 }, ] [[package]] @@ -2669,98 +2665,98 @@ dependencies = [ { name = "srsly" }, { name = "wasabi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/ff/60c9bcfe28e56c905aac8e61a838c7afe5dc3073c9beed0b63a26ace0bb7/thinc-8.3.4.tar.gz", hash = "sha256:b5925482498bbb6dca0771e375b35c915818f735891e93d93a662dab15f6ffd8", size = 193903, upload-time = "2025-01-13T12:47:51.698Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/ff/60c9bcfe28e56c905aac8e61a838c7afe5dc3073c9beed0b63a26ace0bb7/thinc-8.3.4.tar.gz", hash = "sha256:b5925482498bbb6dca0771e375b35c915818f735891e93d93a662dab15f6ffd8", size = 193903 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/c8/13db2e346d2e199f679fc3f620da53af561ea74b43b38e5b4a0a79a12860/thinc-8.3.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:916ea79a7c7462664be9435679b7769b4fc1ecea3886db6da6118e4eb5cc8c8b", size = 843884, upload-time = "2025-01-13T12:46:58.876Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/c25d68b5030f91c8506dfbba706f24b1cd1d0d4950cb0e3de17d176a5411/thinc-8.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c985ce9cf82a611f4f348c721372d073537ca0e8b7bbb8bd865c1598ddd79d1", size = 779384, upload-time = "2025-01-13T12:47:02.35Z" }, - { url = "https://files.pythonhosted.org/packages/5d/5f/8a88959191f8c9f7eed61a7efec45f0222720c6318c09f9a058609810128/thinc-8.3.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fff4b30f8513832d13a31486e9074a7020de3d48f8a3d1527e369c242d6ebe9", size = 3673814, upload-time = "2025-01-13T12:47:04.317Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4f/ea998b85cece6c2441a2416c795476776a5c11f7f2c7fb478a00d407d7f6/thinc-8.3.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a9ee46d19b9f4cac13a5539f97978c857338a31e4bf8d9b3a7741dcbc792220f", size = 4685083, upload-time = "2025-01-13T12:47:07.706Z" }, - { url = "https://files.pythonhosted.org/packages/0b/d0/295add6fcac8b633877a3a8d4b323e8cac4f4078f4f48910deb8c29666cb/thinc-8.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:d08529d53f8652e15e4f3c0f6953e73f85cc71d3b6e4750d2d9ace23616dbe8f", size = 1492082, upload-time = "2025-01-13T12:47:09.452Z" }, - { url = "https://files.pythonhosted.org/packages/85/47/68187c78a04cdc31cbd3ae393068f994b60476b5ecac6dfe7d04b124aacf/thinc-8.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8bb4b47358a1855803b375f4432cefdf373f46ef249b554418d2e77c7323040", size = 839320, upload-time = "2025-01-13T12:47:12.317Z" }, - { url = "https://files.pythonhosted.org/packages/49/ea/066dd415e61fcef20083bbca41c2c02e640fea71326531f2619708efee1e/thinc-8.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:00ed92f9a34b9794f51fcd48467c863f4eb7c5b41559aef6ef3c980c21378fec", size = 774196, upload-time = "2025-01-13T12:47:15.315Z" }, - { url = "https://files.pythonhosted.org/packages/8c/68/36c1a92a374891e0d496677c59f5f9fdc1e57bbb214c487bb8bb3e9290c2/thinc-8.3.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85691fca84a6a1506f7ddbd2c1706a5524d56f65582e76b2e260a06d9e83e86d", size = 3922504, upload-time = "2025-01-13T12:47:22.07Z" }, - { url = "https://files.pythonhosted.org/packages/ec/8a/48e463240a586e91f83c87660986e520aa91fbd839f6631ee9bc0fbb3cbd/thinc-8.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eae1573fc19e514defc1bfd4f93f0b4bfc1dcefdb6d70bad1863825747f24800", size = 4932946, upload-time = "2025-01-13T12:47:24.177Z" }, - { url = "https://files.pythonhosted.org/packages/d9/98/f910b8d8113ab9b955a68e9bbf0d5bd0e828f22dd6d3c226af6ec3970817/thinc-8.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:81e8638f9bdc38e366674acc4b63cf7c6267266a15477963a5db21b3d9f1aa36", size = 1490133, upload-time = "2025-01-13T12:47:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/13db2e346d2e199f679fc3f620da53af561ea74b43b38e5b4a0a79a12860/thinc-8.3.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:916ea79a7c7462664be9435679b7769b4fc1ecea3886db6da6118e4eb5cc8c8b", size = 843884 }, + { url = "https://files.pythonhosted.org/packages/ff/32/c25d68b5030f91c8506dfbba706f24b1cd1d0d4950cb0e3de17d176a5411/thinc-8.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c985ce9cf82a611f4f348c721372d073537ca0e8b7bbb8bd865c1598ddd79d1", size = 779384 }, + { url = "https://files.pythonhosted.org/packages/5d/5f/8a88959191f8c9f7eed61a7efec45f0222720c6318c09f9a058609810128/thinc-8.3.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fff4b30f8513832d13a31486e9074a7020de3d48f8a3d1527e369c242d6ebe9", size = 3673814 }, + { url = "https://files.pythonhosted.org/packages/6f/4f/ea998b85cece6c2441a2416c795476776a5c11f7f2c7fb478a00d407d7f6/thinc-8.3.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a9ee46d19b9f4cac13a5539f97978c857338a31e4bf8d9b3a7741dcbc792220f", size = 4685083 }, + { url = "https://files.pythonhosted.org/packages/0b/d0/295add6fcac8b633877a3a8d4b323e8cac4f4078f4f48910deb8c29666cb/thinc-8.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:d08529d53f8652e15e4f3c0f6953e73f85cc71d3b6e4750d2d9ace23616dbe8f", size = 1492082 }, + { url = "https://files.pythonhosted.org/packages/85/47/68187c78a04cdc31cbd3ae393068f994b60476b5ecac6dfe7d04b124aacf/thinc-8.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8bb4b47358a1855803b375f4432cefdf373f46ef249b554418d2e77c7323040", size = 839320 }, + { url = "https://files.pythonhosted.org/packages/49/ea/066dd415e61fcef20083bbca41c2c02e640fea71326531f2619708efee1e/thinc-8.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:00ed92f9a34b9794f51fcd48467c863f4eb7c5b41559aef6ef3c980c21378fec", size = 774196 }, + { url = "https://files.pythonhosted.org/packages/8c/68/36c1a92a374891e0d496677c59f5f9fdc1e57bbb214c487bb8bb3e9290c2/thinc-8.3.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85691fca84a6a1506f7ddbd2c1706a5524d56f65582e76b2e260a06d9e83e86d", size = 3922504 }, + { url = "https://files.pythonhosted.org/packages/ec/8a/48e463240a586e91f83c87660986e520aa91fbd839f6631ee9bc0fbb3cbd/thinc-8.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eae1573fc19e514defc1bfd4f93f0b4bfc1dcefdb6d70bad1863825747f24800", size = 4932946 }, + { url = "https://files.pythonhosted.org/packages/d9/98/f910b8d8113ab9b955a68e9bbf0d5bd0e828f22dd6d3c226af6ec3970817/thinc-8.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:81e8638f9bdc38e366674acc4b63cf7c6267266a15477963a5db21b3d9f1aa36", size = 1490133 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } 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" }, + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, ] [[package]] name = "tiktoken" -version = "0.8.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/02/576ff3a6639e755c4f70997b2d315f56d6d71e0d046f4fb64cb81a3fb099/tiktoken-0.8.0.tar.gz", hash = "sha256:9ccbb2740f24542534369c5635cfd9b2b3c2490754a78ac8831d99f89f94eeb2", size = 35107, upload-time = "2024-10-03T22:44:04.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/86/ad0155a37c4f310935d5ac0b1ccf9bdb635dcb906e0a9a26b616dd55825a/tiktoken-0.11.0.tar.gz", hash = "sha256:3c518641aee1c52247c2b97e74d8d07d780092af79d5911a6ab5e79359d9b06a", size = 37648 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/ba/a35fad753bbca8ba0cc1b0f3402a70256a110ced7ac332cf84ba89fc87ab/tiktoken-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b07e33283463089c81ef1467180e3e00ab00d46c2c4bbcef0acab5f771d6695e", size = 1039905, upload-time = "2024-10-03T22:43:17.292Z" }, - { url = "https://files.pythonhosted.org/packages/91/05/13dab8fd7460391c387b3e69e14bf1e51ff71fe0a202cd2933cc3ea93fb6/tiktoken-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9269348cb650726f44dd3bbb3f9110ac19a8dcc8f54949ad3ef652ca22a38e21", size = 982417, upload-time = "2024-10-03T22:43:19.437Z" }, - { url = "https://files.pythonhosted.org/packages/e9/98/18ec4a8351a6cf4537e40cd6e19a422c10cce1ef00a2fcb716e0a96af58b/tiktoken-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e13f37bc4ef2d012731e93e0fef21dc3b7aea5bb9009618de9a4026844e560", size = 1144915, upload-time = "2024-10-03T22:43:21.385Z" }, - { url = "https://files.pythonhosted.org/packages/2e/28/cf3633018cbcc6deb7805b700ccd6085c9a5a7f72b38974ee0bffd56d311/tiktoken-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13d13c981511331eac0d01a59b5df7c0d4060a8be1e378672822213da51e0a2", size = 1177221, upload-time = "2024-10-03T22:43:23.325Z" }, - { url = "https://files.pythonhosted.org/packages/57/81/8a5be305cbd39d4e83a794f9e80c7f2c84b524587b7feb27c797b2046d51/tiktoken-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6b2ddbc79a22621ce8b1166afa9f9a888a664a579350dc7c09346a3b5de837d9", size = 1237398, upload-time = "2024-10-03T22:43:24.71Z" }, - { url = "https://files.pythonhosted.org/packages/dc/da/8d1cc3089a83f5cf11c2e489332752981435280285231924557350523a59/tiktoken-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d8c2d0e5ba6453a290b86cd65fc51fedf247e1ba170191715b049dac1f628005", size = 884215, upload-time = "2024-10-03T22:43:26.793Z" }, - { url = "https://files.pythonhosted.org/packages/f6/1e/ca48e7bfeeccaf76f3a501bd84db1fa28b3c22c9d1a1f41af9fb7579c5f6/tiktoken-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d622d8011e6d6f239297efa42a2657043aaed06c4f68833550cac9e9bc723ef1", size = 1039700, upload-time = "2024-10-03T22:43:28.315Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f8/f0101d98d661b34534769c3818f5af631e59c36ac6d07268fbfc89e539ce/tiktoken-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2efaf6199717b4485031b4d6edb94075e4d79177a172f38dd934d911b588d54a", size = 982413, upload-time = "2024-10-03T22:43:29.807Z" }, - { url = "https://files.pythonhosted.org/packages/ac/3c/2b95391d9bd520a73830469f80a96e3790e6c0a5ac2444f80f20b4b31051/tiktoken-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5637e425ce1fc49cf716d88df3092048359a4b3bbb7da762840426e937ada06d", size = 1144242, upload-time = "2024-10-04T04:42:53.66Z" }, - { url = "https://files.pythonhosted.org/packages/01/c4/c4a4360de845217b6aa9709c15773484b50479f36bb50419c443204e5de9/tiktoken-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb0e352d1dbe15aba082883058b3cce9e48d33101bdaac1eccf66424feb5b47", size = 1176588, upload-time = "2024-10-03T22:43:31.136Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a3/ef984e976822cd6c2227c854f74d2e60cf4cd6fbfca46251199914746f78/tiktoken-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56edfefe896c8f10aba372ab5706b9e3558e78db39dd497c940b47bf228bc419", size = 1237261, upload-time = "2024-10-03T22:43:32.75Z" }, - { url = "https://files.pythonhosted.org/packages/1e/86/eea2309dc258fb86c7d9b10db536434fc16420feaa3b6113df18b23db7c2/tiktoken-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:326624128590def898775b722ccc327e90b073714227175ea8febbc920ac0a99", size = 884537, upload-time = "2024-10-03T22:43:34.592Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4d/c6a2e7dca2b4f2e9e0bfd62b3fe4f114322e2c028cfba905a72bc76ce479/tiktoken-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8a9b517d6331d7103f8bef29ef93b3cca95fa766e293147fe7bacddf310d5917", size = 1059937 }, + { url = "https://files.pythonhosted.org/packages/41/54/3739d35b9f94cb8dc7b0db2edca7192d5571606aa2369a664fa27e811804/tiktoken-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b4ddb1849e6bf0afa6cc1c5d809fb980ca240a5fffe585a04e119519758788c0", size = 999230 }, + { url = "https://files.pythonhosted.org/packages/dd/f4/ec8d43338d28d53513004ebf4cd83732a135d11011433c58bf045890cc10/tiktoken-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10331d08b5ecf7a780b4fe4d0281328b23ab22cdb4ff65e68d56caeda9940ecc", size = 1130076 }, + { url = "https://files.pythonhosted.org/packages/94/80/fb0ada0a882cb453caf519a4bf0d117c2a3ee2e852c88775abff5413c176/tiktoken-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b062c82300341dc87e0258c69f79bed725f87e753c21887aea90d272816be882", size = 1183942 }, + { url = "https://files.pythonhosted.org/packages/2f/e9/6c104355b463601719582823f3ea658bc3aa7c73d1b3b7553ebdc48468ce/tiktoken-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:195d84bec46169af3b1349a1495c151d37a0ff4cba73fd08282736be7f92cc6c", size = 1244705 }, + { url = "https://files.pythonhosted.org/packages/94/75/eaa6068f47e8b3f0aab9e05177cce2cf5aa2cc0ca93981792e620d4d4117/tiktoken-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe91581b0ecdd8783ce8cb6e3178f2260a3912e8724d2f2d49552b98714641a1", size = 884152 }, + { url = "https://files.pythonhosted.org/packages/8a/91/912b459799a025d2842566fe1e902f7f50d54a1ce8a0f236ab36b5bd5846/tiktoken-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ae374c46afadad0f501046db3da1b36cd4dfbfa52af23c998773682446097cf", size = 1059743 }, + { url = "https://files.pythonhosted.org/packages/8c/e9/6faa6870489ce64f5f75dcf91512bf35af5864583aee8fcb0dcb593121f5/tiktoken-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25a512ff25dc6c85b58f5dd4f3d8c674dc05f96b02d66cdacf628d26a4e4866b", size = 999334 }, + { url = "https://files.pythonhosted.org/packages/a1/3e/a05d1547cf7db9dc75d1461cfa7b556a3b48e0516ec29dfc81d984a145f6/tiktoken-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2130127471e293d385179c1f3f9cd445070c0772be73cdafb7cec9a3684c0458", size = 1129402 }, + { url = "https://files.pythonhosted.org/packages/34/9a/db7a86b829e05a01fd4daa492086f708e0a8b53952e1dbc9d380d2b03677/tiktoken-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e43022bf2c33f733ea9b54f6a3f6b4354b909f5a73388fb1b9347ca54a069c", size = 1184046 }, + { url = "https://files.pythonhosted.org/packages/9d/bb/52edc8e078cf062ed749248f1454e9e5cfd09979baadb830b3940e522015/tiktoken-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:adb4e308eb64380dc70fa30493e21c93475eaa11669dea313b6bbf8210bfd013", size = 1244691 }, + { url = "https://files.pythonhosted.org/packages/60/d9/884b6cd7ae2570ecdcaffa02b528522b18fef1cbbfdbcaa73799807d0d3b/tiktoken-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:ece6b76bfeeb61a125c44bbefdfccc279b5288e6007fbedc0d32bfec602df2f2", size = 884392 }, ] [[package]] name = "toml" version = "0.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] [[package]] name = "tornado" version = "6.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" }, - { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" }, - { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" }, - { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" }, - { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" }, - { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" }, - { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" }, - { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" }, - { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" }, - { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948 }, + { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112 }, + { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672 }, + { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019 }, + { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252 }, + { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930 }, + { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351 }, + { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328 }, + { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396 }, + { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840 }, + { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596 }, ] [[package]] @@ -2770,23 +2766,23 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, ] [[package]] name = "traitlets" version = "5.14.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, ] [[package]] name = "typer" -version = "0.15.4" +version = "0.17.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -2794,18 +2790,18 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/89/c527e6c848739be8ceb5c44eb8208c52ea3515c6cf6406aa61932887bf58/typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3", size = 101559, upload-time = "2025-05-14T16:34:57.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/e8/2a73ccf9874ec4c7638f172efc8972ceab13a0e3480b389d6ed822f7a822/typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580", size = 103734 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/62/d4ba7afe2096d5659ec3db8b15d8665bdcb92a3c6ff0b95e99895b335a9c/typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173", size = 45258, upload-time = "2025-05-14T16:34:55.583Z" }, + { url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643 }, ] [[package]] name = "typing-extensions" version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, ] [[package]] @@ -2815,18 +2811,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, ] [[package]] name = "tzdata" version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, ] [[package]] @@ -2841,9 +2837,9 @@ dependencies = [ { name = "scipy" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/ee/6bc65bd375c812026a7af63fe9d09d409382120aff25f2152f1ba12af5ec/umap_learn-0.5.9.post2.tar.gz", hash = "sha256:bdf60462d779bd074ce177a0714ced17e6d161285590fa487f3f9548dd3c31c9", size = 95441, upload-time = "2025-07-03T00:18:02.479Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/ee/6bc65bd375c812026a7af63fe9d09d409382120aff25f2152f1ba12af5ec/umap_learn-0.5.9.post2.tar.gz", hash = "sha256:bdf60462d779bd074ce177a0714ced17e6d161285590fa487f3f9548dd3c31c9", size = 95441 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/b1/c24deeda9baf1fd491aaad941ed89e0fed6c583a117fd7b79e0a33a1e6c0/umap_learn-0.5.9.post2-py3-none-any.whl", hash = "sha256:fbe51166561e0e7fab00ef3d516ac2621243b8d15cf4bef9f656d701736b16a0", size = 90146, upload-time = "2025-07-03T00:18:01.042Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b1/c24deeda9baf1fd491aaad941ed89e0fed6c583a117fd7b79e0a33a1e6c0/umap_learn-0.5.9.post2-py3-none-any.whl", hash = "sha256:fbe51166561e0e7fab00ef3d516ac2621243b8d15cf4bef9f656d701736b16a0", size = 90146 }, ] [[package]] @@ -2876,7 +2872,7 @@ requires-dist = [ { name = "azure-identity", specifier = ">=1.16.0" }, { name = "azure-search-documents", specifier = ">=11.4.0" }, { name = "azure-storage-blob", specifier = ">=12.20.0" }, - { name = "graphrag", specifier = "==2.0.0" }, + { name = "graphrag", specifier = "==2.5.0" }, { name = "ipykernel", marker = "extra == 'dev'", specifier = ">=6.29.4" }, { name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.26.1" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.349" }, @@ -2892,9 +2888,9 @@ provides-extras = ["dev"] name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, ] [[package]] @@ -2904,36 +2900,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/f9/054e6e2f1071e963b5e746b48d1e3727470b2a490834d18ad92364929db3/wasabi-1.1.3.tar.gz", hash = "sha256:4bb3008f003809db0c3e28b4daf20906ea871a2bb43f9914197d540f4f2e0878", size = 30391, upload-time = "2024-05-31T16:56:18.99Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/f9/054e6e2f1071e963b5e746b48d1e3727470b2a490834d18ad92364929db3/wasabi-1.1.3.tar.gz", hash = "sha256:4bb3008f003809db0c3e28b4daf20906ea871a2bb43f9914197d540f4f2e0878", size = 30391 } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c", size = 27880, upload-time = "2024-05-31T16:56:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c", size = 27880 }, ] [[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" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } wheels = [ - { 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" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, ] [[package]] name = "wcwidth" version = "0.2.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, ] [[package]] @@ -2951,38 +2947,38 @@ dependencies = [ { name = "typer" }, { name = "wasabi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/1a/9c522dd61b52939c217925d3e55c95f9348b73a66a956f52608e1e59a2c0/weasel-0.4.1.tar.gz", hash = "sha256:aabc210f072e13f6744e5c3a28037f93702433405cd35673f7c6279147085aa9", size = 38417, upload-time = "2024-05-15T08:52:54.765Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/1a/9c522dd61b52939c217925d3e55c95f9348b73a66a956f52608e1e59a2c0/weasel-0.4.1.tar.gz", hash = "sha256:aabc210f072e13f6744e5c3a28037f93702433405cd35673f7c6279147085aa9", size = 38417 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/87/abd57374044e1f627f0a905ac33c1a7daab35a3a815abfea4e1bafd3fdb1/weasel-0.4.1-py3-none-any.whl", hash = "sha256:24140a090ea1ac512a2b2f479cc64192fd1d527a7f3627671268d08ed5ac418c", size = 50270, upload-time = "2024-05-15T08:52:52.977Z" }, + { url = "https://files.pythonhosted.org/packages/2a/87/abd57374044e1f627f0a905ac33c1a7daab35a3a815abfea4e1bafd3fdb1/weasel-0.4.1-py3-none-any.whl", hash = "sha256:24140a090ea1ac512a2b2f479cc64192fd1d527a7f3627671268d08ed5ac418c", size = 50270 }, ] [[package]] name = "wrapt" version = "1.17.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload-time = "2025-01-14T10:33:13.616Z" }, - { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload-time = "2025-01-14T10:33:15.947Z" }, - { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload-time = "2025-01-14T10:33:17.462Z" }, - { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload-time = "2025-01-14T10:33:21.282Z" }, - { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload-time = "2025-01-14T10:33:24.414Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload-time = "2025-01-14T10:33:26.152Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload-time = "2025-01-14T10:33:27.372Z" }, - { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload-time = "2025-01-14T10:33:28.52Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload-time = "2025-01-14T10:33:29.643Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload-time = "2025-01-14T10:33:30.832Z" }, - { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload-time = "2025-01-14T10:33:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" }, - { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" }, - { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" }, - { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" }, - { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" }, - { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" }, - { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" }, - { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" }, - { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307 }, + { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486 }, + { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777 }, + { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314 }, + { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947 }, + { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778 }, + { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716 }, + { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548 }, + { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334 }, + { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427 }, + { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774 }, + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, ] diff --git a/uv.lock b/uv.lock index 50e44c560c..b6e1a82c33 100644 --- a/uv.lock +++ b/uv.lock @@ -1,19 +1,35 @@ version = 1 revision = 3 -requires-python = ">=3.10, <3.13" +requires-python = ">=3.11, <3.13" resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", + "python_full_version >= '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.12' and sys_platform == 'emscripten'", + "python_full_version < '3.12' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[manifest] +members = [ + "graphrag", + "graphrag-cache", + "graphrag-chunking", + "graphrag-common", + "graphrag-input", + "graphrag-llm", + "graphrag-monorepo", + "graphrag-storage", + "graphrag-vectors", ] [[package]] name = "aiofiles" -version = "25.1.0" +version = "24.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, ] [[package]] @@ -32,7 +48,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11'" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, @@ -41,23 +56,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, - { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, - { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, - { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, - { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, - { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, - { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, - { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, - { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, - { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, @@ -94,15 +92,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, ] -[[package]] -name = "aiolimiter" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/23/b52debf471f7a1e42e362d959a3982bdcb4fe13a5d46e63d28868807a79c/aiolimiter-1.2.1.tar.gz", hash = "sha256:e02a37ea1a855d9e832252a105420ad4d15011505512a1a1d814647451b5cca9", size = 7185, upload-time = "2024-12-08T15:31:51.496Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/ba/df6e8e1045aebc4778d19b8a3a9bc1808adb1619ba94ca354d9ba17d86c3/aiolimiter-1.2.1-py3-none-any.whl", hash = "sha256:d3f249e9059a20badcb56b61601a83556133655c11d1eb3dd3e04ff069e5f3c7", size = 6711, upload-time = "2024-12-08T15:31:49.874Z" }, -] - [[package]] name = "aiosignal" version = "1.4.0" @@ -130,7 +119,6 @@ name = "anyio" version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions" }, ] @@ -139,15 +127,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] -[[package]] -name = "anytree" -version = "2.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/a8/eb55fab589c56f9b6be2b3fd6997aa04bb6f3da93b01154ce6fc8e799db2/anytree-2.13.0.tar.gz", hash = "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714", size = 48389, upload-time = "2025-04-08T21:06:30.662Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/98/f6aa7fe0783e42be3093d8ef1b0ecdc22c34c0d69640dfb37f56925cb141/anytree-2.13.0-py3-none-any.whl", hash = "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff", size = 45077, upload-time = "2025-04-08T21:06:29.494Z" }, -] - [[package]] name = "appnope" version = "0.1.4" @@ -188,11 +167,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, - { url = "https://files.pythonhosted.org/packages/11/2d/ba4e4ca8d149f8dcc0d952ac0967089e1d759c7e5fcf0865a317eb680fbb/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e", size = 24549, upload-time = "2025-07-30T10:02:00.101Z" }, - { url = "https://files.pythonhosted.org/packages/5c/82/9b2386cc75ac0bd3210e12a44bfc7fd1632065ed8b80d573036eecb10442/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d", size = 25539, upload-time = "2025-07-30T10:02:00.929Z" }, - { url = "https://files.pythonhosted.org/packages/31/db/740de99a37aa727623730c90d92c22c9e12585b3c98c54b7960f7810289f/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584", size = 28467, upload-time = "2025-07-30T10:02:02.08Z" }, - { url = "https://files.pythonhosted.org/packages/71/7a/47c4509ea18d755f44e2b92b7178914f0c113946d11e16e626df8eaa2b0b/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690", size = 27355, upload-time = "2025-07-30T10:02:02.867Z" }, - { url = "https://files.pythonhosted.org/packages/ee/82/82745642d3c46e7cea25e1885b014b033f4693346ce46b7f47483cf5d448/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520", size = 29187, upload-time = "2025-07-30T10:02:03.674Z" }, ] [[package]] @@ -224,23 +198,11 @@ wheels = [ name = "async-lru" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/ef/c3/bbf34f15ea88dfb649ab2c40f9d75081784a50573a9ea431563cab64adb8/async_lru-2.1.0.tar.gz", hash = "sha256:9eeb2fecd3fe42cc8a787fc32ead53a3a7158cc43d039c3c55ab3e4e5b2a80ed", size = 12041, upload-time = "2026-01-17T22:52:18.931Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2e/e9/eb6a5db5ac505d5d45715388e92bced7a5bb556facc4d0865d192823f2d2/async_lru-2.1.0-py3-none-any.whl", hash = "sha256:fa12dcf99a42ac1280bc16c634bbaf06883809790f6304d85cdab3f666f33a7e", size = 6933, upload-time = "2026-01-17T22:52:17.389Z" }, ] -[[package]] -name = "async-timeout" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, -] - [[package]] name = "attrs" version = "25.4.0" @@ -250,18 +212,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] -[[package]] -name = "autograd" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/1c/3c24ec03c8ba4decc742b1df5a10c52f98c84ca8797757f313e7bdcdf276/autograd-1.8.0.tar.gz", hash = "sha256:107374ded5b09fc8643ac925348c0369e7b0e73bbed9565ffd61b8fd04425683", size = 2562146, upload-time = "2025-05-05T12:49:02.502Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ea/e16f0c423f7d83cf8b79cae9452040fb7b2e020c7439a167ee7c317de448/autograd-1.8.0-py3-none-any.whl", hash = "sha256:4ab9084294f814cf56c280adbe19612546a35574d67c574b04933c7d2ecb7d78", size = 51478, upload-time = "2025-05-05T12:49:00.585Z" }, -] - [[package]] name = "azure-common" version = "1.1.28" @@ -299,7 +249,7 @@ wheels = [ [[package]] name = "azure-identity" -version = "1.25.1" +version = "1.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -308,9 +258,9 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/8d/1a6c41c28a37eab26dc85ab6c86992c700cd3f4a597d9ed174b0e9c69489/azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456", size = 279826, upload-time = "2025-10-06T20:30:02.194Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/91/cbaeff9eb0b838f0d35b4607ac1c6195c735c8eb17db235f8f60e622934c/azure_identity-1.19.0.tar.gz", hash = "sha256:500144dc18197d7019b81501165d4fa92225f03778f17d7ca8a2a180129a9c83", size = 263058, upload-time = "2024-10-08T15:41:33.554Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/7b/5652771e24fff12da9dde4c20ecf4682e606b104f26419d139758cc935a6/azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651", size = 191317, upload-time = "2025-10-06T20:30:04.251Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d5/3995ed12f941f4a41a273d9b1709282e825ef87ed8eab3833038fee54d59/azure_identity-1.19.0-py3-none-any.whl", hash = "sha256:e3f6558c181692d7509f09de10cca527c7dce426776454fb97df512a46527e81", size = 187587, upload-time = "2024-10-08T15:41:36.423Z" }, ] [[package]] @@ -352,49 +302,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - -[[package]] -name = "backports-datetime-fromisoformat" -version = "2.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/81/eff3184acb1d9dc3ce95a98b6f3c81a49b4be296e664db8e1c2eeabef3d9/backports_datetime_fromisoformat-2.0.3.tar.gz", hash = "sha256:b58edc8f517b66b397abc250ecc737969486703a66eb97e01e6d51291b1a139d", size = 23588, upload-time = "2024-12-28T20:18:15.017Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/4b/d6b051ca4b3d76f23c2c436a9669f3be616b8cf6461a7e8061c7c4269642/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f681f638f10588fa3c101ee9ae2b63d3734713202ddfcfb6ec6cea0778a29d4", size = 27561, upload-time = "2024-12-28T20:16:47.974Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/e39b0d471e55eb1b5c7c81edab605c02f71c786d59fb875f0a6f23318747/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cd681460e9142f1249408e5aee6d178c6d89b49e06d44913c8fdfb6defda8d1c", size = 34448, upload-time = "2024-12-28T20:16:50.712Z" }, - { url = "https://files.pythonhosted.org/packages/f2/28/7a5c87c5561d14f1c9af979231fdf85d8f9fad7a95ff94e56d2205e2520a/backports_datetime_fromisoformat-2.0.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:ee68bc8735ae5058695b76d3bb2aee1d137c052a11c8303f1e966aa23b72b65b", size = 27093, upload-time = "2024-12-28T20:16:52.994Z" }, - { url = "https://files.pythonhosted.org/packages/80/ba/f00296c5c4536967c7d1136107fdb91c48404fe769a4a6fd5ab045629af8/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8273fe7932db65d952a43e238318966eab9e49e8dd546550a41df12175cc2be4", size = 52836, upload-time = "2024-12-28T20:16:55.283Z" }, - { url = "https://files.pythonhosted.org/packages/e3/92/bb1da57a069ddd601aee352a87262c7ae93467e66721d5762f59df5021a6/backports_datetime_fromisoformat-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39d57ea50aa5a524bb239688adc1d1d824c31b6094ebd39aa164d6cadb85de22", size = 52798, upload-time = "2024-12-28T20:16:56.64Z" }, - { url = "https://files.pythonhosted.org/packages/df/ef/b6cfd355982e817ccdb8d8d109f720cab6e06f900784b034b30efa8fa832/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac6272f87693e78209dc72e84cf9ab58052027733cd0721c55356d3c881791cf", size = 52891, upload-time = "2024-12-28T20:16:58.887Z" }, - { url = "https://files.pythonhosted.org/packages/37/39/b13e3ae8a7c5d88b68a6e9248ffe7066534b0cfe504bf521963e61b6282d/backports_datetime_fromisoformat-2.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44c497a71f80cd2bcfc26faae8857cf8e79388e3d5fbf79d2354b8c360547d58", size = 52955, upload-time = "2024-12-28T20:17:00.028Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e4/70cffa3ce1eb4f2ff0c0d6f5d56285aacead6bd3879b27a2ba57ab261172/backports_datetime_fromisoformat-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:6335a4c9e8af329cb1ded5ab41a666e1448116161905a94e054f205aa6d263bc", size = 29323, upload-time = "2024-12-28T20:17:01.125Z" }, - { url = "https://files.pythonhosted.org/packages/62/f5/5bc92030deadf34c365d908d4533709341fb05d0082db318774fdf1b2bcb/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2e4b66e017253cdbe5a1de49e0eecff3f66cd72bcb1229d7db6e6b1832c0443", size = 27626, upload-time = "2024-12-28T20:17:03.448Z" }, - { url = "https://files.pythonhosted.org/packages/28/45/5885737d51f81dfcd0911dd5c16b510b249d4c4cf6f4a991176e0358a42a/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:43e2d648e150777e13bbc2549cc960373e37bf65bd8a5d2e0cef40e16e5d8dd0", size = 34588, upload-time = "2024-12-28T20:17:04.459Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6d/bd74de70953f5dd3e768c8fc774af942af0ce9f211e7c38dd478fa7ea910/backports_datetime_fromisoformat-2.0.3-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:4ce6326fd86d5bae37813c7bf1543bae9e4c215ec6f5afe4c518be2635e2e005", size = 27162, upload-time = "2024-12-28T20:17:06.752Z" }, - { url = "https://files.pythonhosted.org/packages/47/ba/1d14b097f13cce45b2b35db9898957578b7fcc984e79af3b35189e0d332f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7c8fac333bf860208fd522a5394369ee3c790d0aa4311f515fcc4b6c5ef8d75", size = 54482, upload-time = "2024-12-28T20:17:08.15Z" }, - { url = "https://files.pythonhosted.org/packages/25/e9/a2a7927d053b6fa148b64b5e13ca741ca254c13edca99d8251e9a8a09cfe/backports_datetime_fromisoformat-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4da5ab3aa0cc293dc0662a0c6d1da1a011dc1edcbc3122a288cfed13a0b45", size = 54362, upload-time = "2024-12-28T20:17:10.605Z" }, - { url = "https://files.pythonhosted.org/packages/c1/99/394fb5e80131a7d58c49b89e78a61733a9994885804a0bb582416dd10c6f/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58ea11e3bf912bd0a36b0519eae2c5b560b3cb972ea756e66b73fb9be460af01", size = 54162, upload-time = "2024-12-28T20:17:12.301Z" }, - { url = "https://files.pythonhosted.org/packages/88/25/1940369de573c752889646d70b3fe8645e77b9e17984e72a554b9b51ffc4/backports_datetime_fromisoformat-2.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a375c7dbee4734318714a799b6c697223e4bbb57232af37fbfff88fb48a14c6", size = 54118, upload-time = "2024-12-28T20:17:13.609Z" }, - { url = "https://files.pythonhosted.org/packages/b7/46/f275bf6c61683414acaf42b2df7286d68cfef03e98b45c168323d7707778/backports_datetime_fromisoformat-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:ac677b1664c4585c2e014739f6678137c8336815406052349c85898206ec7061", size = 29329, upload-time = "2024-12-28T20:17:16.124Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/69bbdde2e1e57c09b5f01788804c50e68b29890aada999f2b1a40519def9/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66ce47ee1ba91e146149cf40565c3d750ea1be94faf660ca733d8601e0848147", size = 27630, upload-time = "2024-12-28T20:17:19.442Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1d/1c84a50c673c87518b1adfeafcfd149991ed1f7aedc45d6e5eac2f7d19d7/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8b7e069910a66b3bba61df35b5f879e5253ff0821a70375b9daf06444d046fa4", size = 34707, upload-time = "2024-12-28T20:17:21.79Z" }, - { url = "https://files.pythonhosted.org/packages/71/44/27eae384e7e045cda83f70b551d04b4a0b294f9822d32dea1cbf1592de59/backports_datetime_fromisoformat-2.0.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a3b5d1d04a9e0f7b15aa1e647c750631a873b298cdd1255687bb68779fe8eb35", size = 27280, upload-time = "2024-12-28T20:17:24.503Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7a/a4075187eb6bbb1ff6beb7229db5f66d1070e6968abeb61e056fa51afa5e/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1b95986430e789c076610aea704db20874f0781b8624f648ca9fb6ef67c6e1", size = 55094, upload-time = "2024-12-28T20:17:25.546Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/3fced4230c10af14aacadc195fe58e2ced91d011217b450c2e16a09a98c8/backports_datetime_fromisoformat-2.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffe5f793db59e2f1d45ec35a1cf51404fdd69df9f6952a0c87c3060af4c00e32", size = 55605, upload-time = "2024-12-28T20:17:29.208Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0a/4b34a838c57bd16d3e5861ab963845e73a1041034651f7459e9935289cfd/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:620e8e73bd2595dfff1b4d256a12b67fce90ece3de87b38e1dde46b910f46f4d", size = 55353, upload-time = "2024-12-28T20:17:32.433Z" }, - { url = "https://files.pythonhosted.org/packages/d9/68/07d13c6e98e1cad85606a876367ede2de46af859833a1da12c413c201d78/backports_datetime_fromisoformat-2.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4cf9c0a985d68476c1cabd6385c691201dda2337d7453fb4da9679ce9f23f4e7", size = 55298, upload-time = "2024-12-28T20:17:34.919Z" }, - { url = "https://files.pythonhosted.org/packages/60/33/45b4d5311f42360f9b900dea53ab2bb20a3d61d7f9b7c37ddfcb3962f86f/backports_datetime_fromisoformat-2.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:d144868a73002e6e2e6fef72333e7b0129cecdd121aa8f1edba7107fd067255d", size = 29375, upload-time = "2024-12-28T20:17:36.018Z" }, - { url = "https://files.pythonhosted.org/packages/be/03/7eaa9f9bf290395d57fd30d7f1f2f9dff60c06a31c237dc2beb477e8f899/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90e202e72a3d5aae673fcc8c9a4267d56b2f532beeb9173361293625fe4d2039", size = 28980, upload-time = "2024-12-28T20:18:06.554Z" }, - { url = "https://files.pythonhosted.org/packages/47/80/a0ecf33446c7349e79f54cc532933780341d20cff0ee12b5bfdcaa47067e/backports_datetime_fromisoformat-2.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2df98ef1b76f5a58bb493dda552259ba60c3a37557d848e039524203951c9f06", size = 28449, upload-time = "2024-12-28T20:18:07.77Z" }, -] - [[package]] name = "backrefs" version = "6.1" @@ -407,15 +314,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, ] -[[package]] -name = "beartype" -version = "0.18.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/15/4e623478a9628ad4cee2391f19aba0b16c1dd6fedcb2a399f0928097b597/beartype-0.18.5.tar.gz", hash = "sha256:264ddc2f1da9ec94ff639141fbe33d22e12a9f75aa863b83b7046ffff1381927", size = 1193506, upload-time = "2024-04-21T07:25:58.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/43/7a1259741bd989723272ac7d381a43be932422abcff09a1d9f7ba212cb74/beartype-0.18.5-py3-none-any.whl", hash = "sha256:5301a14f2a9a5540fe47ec6d34d758e9cd8331d36c4760fc7a5499ab86310089", size = 917762, upload-time = "2024-04-21T07:25:55.758Z" }, -] - [[package]] name = "beautifulsoup4" version = "4.14.3" @@ -455,13 +353,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/d0/d0/d8cc8c9a4488a787e7fa430f6055e5bd1ddb22c340a751d9e901b82e2efe/blis-1.3.3.tar.gz", hash = "sha256:034d4560ff3cc43e8aa37e188451b0440e3261d989bb8a42ceee865607715ecd", size = 2644873, upload-time = "2025-11-17T12:28:30.511Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/db/d80daf6c060618c72acecf026410b806f620cdea62b2e72f3235d7389d05/blis-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:650f1d2b28e3c875927c63deebda463a6f9d237dff30e445bfe2127718c1a344", size = 6925724, upload-time = "2025-11-17T12:27:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/06/cd/7ac854c92e33cfccc0eded48e979a9fc26a447952d07a9c7c7da7c1d6eec/blis-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b0d42420ddd543eec51ccb99d38364a0c0833b6895eced37127822de6ecacff", size = 1233606, upload-time = "2025-11-17T12:27:16.107Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ae/ad3165fdbc4ef6afef585686a778c72cd67fb5aa16ab2fd2f4494186705e/blis-1.3.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0628a030d44aa71cac5973e40c9e95ec767abaaf2fd366a094b9398885f82f2", size = 2769094, upload-time = "2025-11-17T12:27:17.883Z" }, - { url = "https://files.pythonhosted.org/packages/25/d4/7b0820f139b4ea67606d01b59ba6afbee4552ce7b2fd179f2fb7908e294f/blis-1.3.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0114cf2d8f19e0ed210f9ae92594cd0a12efa1bbbce444028b0fc365bbbb8af", size = 11300520, upload-time = "2025-11-17T12:27:20.058Z" }, - { url = "https://files.pythonhosted.org/packages/85/f3/865a4322bdbeb944744c1908e67fdabecd476613a17204956cff12d568c9/blis-1.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7e88181e9dd8430029ebaf22d41bf79e756e8c95363e9471717102c66beb4a6d", size = 2962083, upload-time = "2025-11-17T12:27:22.098Z" }, - { url = "https://files.pythonhosted.org/packages/65/a2/c2842fa1e2e6bd56eb93e41b34859a9af8b5b63669ee0442bea585d8f607/blis-1.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62fb8c731347b0f98f5f81d19d339049e61489798738467d156c66cc329b0754", size = 14177001, upload-time = "2025-11-17T12:27:24.345Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9b/3b1532f23db8bdddf3a976e9acf51e8debd94c63be5dafb8ccbab3e62935/blis-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:631836d4f335e62c30aa50a1aa0170773265c73654d296361f95180006e88c04", size = 6184429, upload-time = "2025-11-17T12:27:27.054Z" }, { url = "https://files.pythonhosted.org/packages/a1/0a/a4c8736bc497d386b0ffc76d321f478c03f1a4725e52092f93b38beb3786/blis-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e10c8d3e892b1dbdff365b9d00e08291876fc336915bf1a5e9f188ed087e1a91", size = 6925522, upload-time = "2025-11-17T12:27:29.199Z" }, { url = "https://files.pythonhosted.org/packages/83/5a/3437009282f23684ecd3963a8b034f9307cdd2bf4484972e5a6b096bf9ac/blis-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66e6249564f1db22e8af1e0513ff64134041fa7e03c8dd73df74db3f4d8415a7", size = 1232787, upload-time = "2025-11-17T12:27:30.996Z" }, { url = "https://files.pythonhosted.org/packages/d1/0e/82221910d16259ce3017c1442c468a3f206a4143a96fbba9f5b5b81d62e8/blis-1.3.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7260da065958b4e5475f62f44895ef9d673b0f47dcf61b672b22b7dae1a18505", size = 2844596, upload-time = "2025-11-17T12:27:32.601Z" }, @@ -505,18 +396,6 @@ dependencies = [ ] 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/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, { 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" }, @@ -550,22 +429,6 @@ version = "3.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, @@ -617,9 +480,6 @@ wheels = [ name = "cloudpathlib" version = "0.23.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/f4/18/2ac35d6b3015a0c74e923d94fc69baf8307f7c3233de015d69f99e17afa8/cloudpathlib-0.23.0.tar.gz", hash = "sha256:eb38a34c6b8a048ecfd2b2f60917f7cbad4a105b7c979196450c2f541f4d6b4b", size = 53126, upload-time = "2025-10-07T22:47:56.278Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ae/8a/c4bb04426d608be4a3171efa2e233d2c59a5c8937850c10d098e126df18e/cloudpathlib-0.23.0-py3-none-any.whl", hash = "sha256:8520b3b01468fee77de37ab5d50b1b524ea6b4a8731c35d1b7407ac0cd716002", size = 62755, upload-time = "2025-10-07T22:47:54.905Z" }, @@ -634,6 +494,18 @@ 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 = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, +] + [[package]] name = "comm" version = "0.2.3" @@ -656,143 +528,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/00/3106b1854b45bd0474ced037dfe6b73b90fe68a68968cef47c23de3d43d2/confection-0.1.5-py3-none-any.whl", hash = "sha256:e29d3c3f8eac06b3f77eb9dfb4bf2fc6bcc9622a98ca00a698e3d019c6430b14", size = 35451, upload-time = "2024-05-31T16:16:59.075Z" }, ] -[[package]] -name = "contourpy" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, - { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, - { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, - { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, - { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, - { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, - { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, - { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, - { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, -] - -[[package]] -name = "contourpy" -version = "1.3.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, - { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, - { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, - { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, - { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, - { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, - { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, - { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, - { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, -] - [[package]] name = "coverage" -version = "7.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, - { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, - { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, - { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, - { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, - { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, - { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, - { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, - { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, - { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, - { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, - { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [[package]] @@ -801,7 +569,6 @@ version = "46.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } wheels = [ @@ -835,8 +602,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, @@ -845,29 +610,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, ] -[[package]] -name = "cycler" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, -] - [[package]] name = "cymem" version = "2.0.13" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/2f0fbb32535c3731b7c2974c569fb9325e0a38ed5565a08e1139a3b71e82/cymem-2.0.13.tar.gz", hash = "sha256:1c91a92ae8c7104275ac26bd4d29b08ccd3e7faff5893d3858cb6fadf1bc1588", size = 12320, upload-time = "2025-11-14T14:58:36.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/14/462018dd384ee1848ac9c1951534a813a325abbfc161a74e2cbcb38d2469/cymem-2.0.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8efc4f308169237aade0e82877a65a563833dec32eb7ab2326120253e0e9e918", size = 43747, upload-time = "2025-11-14T14:57:11.287Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9b/c123ba65dddcd8a2bc0b3c9046766c15abe0e257c315b3040eed22cce1e2/cymem-2.0.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e03bb575a96c59bc210d7d59862747f0012696b0dac3427ce8af33c7afb3d4a2", size = 43328, upload-time = "2025-11-14T14:57:12.578Z" }, - { url = "https://files.pythonhosted.org/packages/bd/be/7b7a4cf9cd2d37e674612a86fc90b3d59bff12177f83430e62b25afaf7fc/cymem-2.0.13-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1775d3fd34cf099929b79c3e48469283642463f977af6801231f3c0e5d9c9369", size = 231539, upload-time = "2025-11-14T14:57:14.441Z" }, - { url = "https://files.pythonhosted.org/packages/79/6d/d165c38cd4caaaf60942e2cec9998b667008f2384047ccfe0b4b5f7a1ffe/cymem-2.0.13-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84e2976e38cd663f758e40b5497fa5cd183d7c5fb0d04ce81a4b42a1ba124ff0", size = 229674, upload-time = "2025-11-14T14:57:15.685Z" }, - { url = "https://files.pythonhosted.org/packages/95/c1/af83c03a93f890ca81149561b18a4a67a9aa36a1109f15e291dd2703ab12/cymem-2.0.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed9de1b9b042f76fe5c312e4359eab58bf52ac7dfdf6887368a760410d809440", size = 229805, upload-time = "2025-11-14T14:57:17.289Z" }, - { url = "https://files.pythonhosted.org/packages/03/2d/12900758b80345d9aed5892a9d61e8a5f6abbbe5837e4def373a53cd0da2/cymem-2.0.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1366c7437a209230f4b797fae10227a8206d4021d37c9f9c0d31fd97ea4feb35", size = 234018, upload-time = "2025-11-14T14:57:18.512Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8b/5fcf5430fc81098aef58cc20340e51f37b49b9d8c15766e0d5d63e7288a3/cymem-2.0.13-cp310-cp310-win_amd64.whl", hash = "sha256:7700b116524b087e0169f10f267539223b48240ef2734c3a727a9e6b4db9a671", size = 40102, upload-time = "2025-11-14T14:57:19.972Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d3/cb6c83758fe399443b858faafb7096b72535621a7af7dd9a54ff0989fa14/cymem-2.0.13-cp310-cp310-win_arm64.whl", hash = "sha256:c8dbfddfe5c604974e17c6f373cedd4d25cd67f84812ede7dea12128fa0c2015", size = 36282, upload-time = "2025-11-14T14:57:21.398Z" }, { url = "https://files.pythonhosted.org/packages/10/64/1db41f7576a6b69f70367e3c15e968fd775ba7419e12059c9966ceb826f8/cymem-2.0.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:673183466b0ff2e060d97ec5116711d44200b8f7be524323e080d215ee2d44a5", size = 43587, upload-time = "2025-11-14T14:57:22.39Z" }, { url = "https://files.pythonhosted.org/packages/81/13/57f936fc08551323aab3f92ff6b7f4d4b89d5b4e495c870a67cb8d279757/cymem-2.0.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bee2791b3f6fc034ce41268851462bf662ff87e8947e35fb6dd0115b4644a61f", size = 43139, upload-time = "2025-11-14T14:57:23.363Z" }, { url = "https://files.pythonhosted.org/packages/32/a6/9345754be51e0479aa387b7b6cffc289d0fd3201aaeb8dade4623abd1e02/cymem-2.0.13-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f3aee3adf16272bca81c5826eed55ba3c938add6d8c9e273f01c6b829ecfde22", size = 245063, upload-time = "2025-11-14T14:57:24.839Z" }, @@ -892,10 +640,6 @@ version = "1.8.19" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/73/75/9e12d4d42349b817cd545b89247696c67917aab907012ae5b64bbfea3199/debugpy-1.8.19.tar.gz", hash = "sha256:eea7e5987445ab0b5ed258093722d5ecb8bb72217c5c9b1e21f64efe23ddebdb", size = 1644590, upload-time = "2025-12-15T21:53:28.044Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/98/d57054371887f37d3c959a7a8dc3c76b763acb65f5e78d849d7db7cadc5b/debugpy-1.8.19-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:fce6da15d73be5935b4438435c53adb512326a3e11e4f90793ea87cd9f018254", size = 2098493, upload-time = "2025-12-15T21:53:30.149Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dd/c517b9aa3500157a30e4f4c4f5149f880026bd039d2b940acd2383a85d8e/debugpy-1.8.19-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:e24b1652a1df1ab04d81e7ead446a91c226de704ff5dde6bd0a0dbaab07aa3f2", size = 3087875, upload-time = "2025-12-15T21:53:31.511Z" }, - { url = "https://files.pythonhosted.org/packages/d8/57/3d5a5b0da9b63445253107ead151eff29190c6ad7440c68d1a59d56613aa/debugpy-1.8.19-cp310-cp310-win32.whl", hash = "sha256:327cb28c3ad9e17bc925efc7f7018195fd4787c2fe4b7af1eec11f1d19bdec62", size = 5239378, upload-time = "2025-12-15T21:53:32.979Z" }, - { url = "https://files.pythonhosted.org/packages/a6/36/7f9053c4c549160c87ae7e43800138f2695578c8b65947114c97250983b6/debugpy-1.8.19-cp310-cp310-win_amd64.whl", hash = "sha256:b7dd275cf2c99e53adb9654f5ae015f70415bbe2bacbe24cfee30d54b6aa03c5", size = 5271129, upload-time = "2025-12-15T21:53:35.085Z" }, { url = "https://files.pythonhosted.org/packages/80/e2/48531a609b5a2aa94c6b6853afdfec8da05630ab9aaa96f1349e772119e9/debugpy-1.8.19-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:c5dcfa21de1f735a4f7ced4556339a109aa0f618d366ede9da0a3600f2516d8b", size = 2207620, upload-time = "2025-12-15T21:53:37.1Z" }, { url = "https://files.pythonhosted.org/packages/1b/d4/97775c01d56071969f57d93928899e5616a4cfbbf4c8cc75390d3a51c4a4/debugpy-1.8.19-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:806d6800246244004625d5222d7765874ab2d22f3ba5f615416cf1342d61c488", size = 3170796, upload-time = "2025-12-15T21:53:38.513Z" }, { url = "https://files.pythonhosted.org/packages/8d/7e/8c7681bdb05be9ec972bbb1245eb7c4c7b0679bb6a9e6408d808bc876d3d/debugpy-1.8.19-cp311-cp311-win32.whl", hash = "sha256:783a519e6dfb1f3cd773a9bda592f4887a65040cb0c7bd38dde410f4e53c40d4", size = 5164287, upload-time = "2025-12-15T21:53:40.857Z" }, @@ -946,7 +690,6 @@ dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "packaging" }, { name = "requirements-parser" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/aa/5cae0f25a2ac5334d5bd2782a6bcd80eecf184f433ff74b2fb0387cfbbb6/deptry-0.24.0.tar.gz", hash = "sha256:852e88af2087e03cdf9ece6916f3f58b74191ab51cc8074897951bd496ee7dbb", size = 440158, upload-time = "2025-11-09T00:31:44.637Z" } wheels = [ @@ -991,29 +734,12 @@ wheels = [ ] [[package]] -name = "environs" -version = "14.5.0" +name = "execnet" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "marshmallow" }, - { name = "python-dotenv" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/75/06801d5beeb398ed3903167af9376bb81c4ac41c44a53d45193065ebb1a8/environs-14.5.0.tar.gz", hash = "sha256:f7b8f6fcf3301bc674bc9c03e39b5986d116126ffb96764efd34c339ed9464ee", size = 35426, upload-time = "2025-11-02T21:30:36.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/f3/6961beb9a1e77d01dee1dd48f00fb3064429c8abcfa26aa863eb7cb2b6dd/environs-14.5.0-py3-none-any.whl", hash = "sha256:1abd3e3a5721fb09797438d6c902bc2f35d4580dfaffe68b8ee588b67b504e13", size = 17202, upload-time = "2025-11-02T21:30:35.186Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] @@ -1040,17 +766,6 @@ 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/ad/b2/731a6696e37cd20eed353f69a09f37a984a43c9713764ee3f7ad5f57f7f9/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6e6243d40f6c793c3e2ee14c13769e341b90be5ef0c23c82fa6515a96145181a", size = 516760, upload-time = "2025-10-19T22:25:21.509Z" }, - { url = "https://files.pythonhosted.org/packages/c5/79/c73c47be2a3b8734d16e628982653517f80bbe0570e27185d91af6096507/fastuuid-0.14.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:13ec4f2c3b04271f62be2e1ce7e95ad2dd1cf97e94503a3760db739afbd48f00", size = 264748, upload-time = "2025-10-19T22:41:52.873Z" }, - { url = "https://files.pythonhosted.org/packages/24/c5/84c1eea05977c8ba5173555b0133e3558dc628bcf868d6bf1689ff14aedc/fastuuid-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b2fdd48b5e4236df145a149d7125badb28e0a383372add3fbaac9a6b7a394470", size = 254537, upload-time = "2025-10-19T22:33:55.603Z" }, - { url = "https://files.pythonhosted.org/packages/0e/23/4e362367b7fa17dbed646922f216b9921efb486e7abe02147e4b917359f8/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f74631b8322d2780ebcf2d2d75d58045c3e9378625ec51865fe0b5620800c39d", size = 278994, upload-time = "2025-10-19T22:26:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/b2/72/3985be633b5a428e9eaec4287ed4b873b7c4c53a9639a8b416637223c4cd/fastuuid-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cffc144dc93eb604b87b179837f2ce2af44871a7b323f2bfed40e8acb40ba8", size = 280003, upload-time = "2025-10-19T22:23:45.415Z" }, - { url = "https://files.pythonhosted.org/packages/b3/6d/6ef192a6df34e2266d5c9deb39cd3eea986df650cbcfeaf171aa52a059c3/fastuuid-0.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a771f135ab4523eb786e95493803942a5d1fc1610915f131b363f55af53b219", size = 303583, upload-time = "2025-10-19T22:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/9d/11/8a2ea753c68d4fece29d5d7c6f3f903948cc6e82d1823bc9f7f7c0355db3/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4edc56b877d960b4eda2c4232f953a61490c3134da94f3c28af129fb9c62a4f6", size = 460955, upload-time = "2025-10-19T22:36:25.196Z" }, - { url = "https://files.pythonhosted.org/packages/23/42/7a32c93b6ce12642d9a152ee4753a078f372c9ebb893bc489d838dd4afd5/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bcc96ee819c282e7c09b2eed2b9bd13084e3b749fdb2faf58c318d498df2efbe", size = 480763, upload-time = "2025-10-19T22:24:28.451Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e9/a5f6f686b46e3ed4ed3b93770111c233baac87dd6586a411b4988018ef1d/fastuuid-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7a3c0bca61eacc1843ea97b288d6789fbad7400d16db24e36a66c28c268cfe3d", size = 452613, upload-time = "2025-10-19T22:25:06.827Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c9/18abc73c9c5b7fc0e476c1733b678783b2e8a35b0be9babd423571d44e98/fastuuid-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7f2f3efade4937fae4e77efae1af571902263de7b78a0aee1a1653795a093b2a", size = 155045, upload-time = "2025-10-19T22:28:32.732Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8a/d9e33f4eb4d4f6d9f2c5c7d7e96b5cdbb535c93f3b1ad6acce97ee9d4bf8/fastuuid-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae64ba730d179f439b0736208b4c279b8bc9c089b102aec23f86512ea458c8a4", size = 156122, upload-time = "2025-10-19T22:23:15.59Z" }, { 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" }, @@ -1085,62 +800,11 @@ wheels = [ ] [[package]] -name = "fnllm" -version = "0.4.1" +name = "flatbuffers" +version = "25.12.19" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiolimiter" }, - { name = "httpx" }, - { name = "json-repair" }, - { name = "pydantic" }, - { name = "tenacity" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/84/bc3d02134a46dd267afbed66a47dc281b252bd8171c94ad22bcc8f924f8b/fnllm-0.4.1.tar.gz", hash = "sha256:80a7450693691bf0832e12a2d70420647bfea35a43cb91c4a9cb5e2f39172b50", size = 93566, upload-time = "2025-08-21T15:24:44.101Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/6a/04db92a7e8d9cf9b73d3c29c38e16d5728069ec1be06a4723f74579499fa/fnllm-0.4.1-py3-none-any.whl", hash = "sha256:22f1b3316a90f29fde94bfe651e0e4963ff68cddb438035ef7c2161e39789ccf", size = 79273, upload-time = "2025-08-21T15:24:42.796Z" }, -] - -[package.optional-dependencies] -azure = [ - { name = "azure-identity" }, - { name = "azure-storage-blob" }, -] -openai = [ - { name = "openai" }, - { name = "tiktoken" }, -] - -[[package]] -name = "fonttools" -version = "4.61.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, - { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, - { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, - { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, - { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, - { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, - { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, - { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, - { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, - { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, - { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, - { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, - { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, - { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, - { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, - { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, ] [[package]] @@ -1158,22 +822,6 @@ 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/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, - { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, - { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, - { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, - { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, - { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, - { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, - { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, - { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, - { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, - { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, { 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" }, @@ -1218,44 +866,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, ] -[[package]] -name = "future" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, -] - -[[package]] -name = "gensim" -version = "4.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "smart-open" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1a/80/fe9d2e1ace968041814dbcfce4e8499a643a36c41267fa4b6c4f54cce420/gensim-4.4.0.tar.gz", hash = "sha256:a3f5b626da5518e79a479140361c663089fe7998df8ba52d56e1ded71ac5bdf5", size = 23260095, upload-time = "2025-10-18T02:06:45.962Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/88/1e7c7abf79cf88faca3d713fbb7068f58c9f44c77a3e72031cb3e40e43c3/gensim-4.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e29a2109819fdf5ff59bef670c8c22c1690d52239fe172b43e408908871de5f6", size = 24455330, upload-time = "2025-10-18T01:47:12.563Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2f/46a661db005730de7455090cb980b70147f04a3d162b49171582987d634e/gensim-4.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c4d8f2a5e69bc246931dfd8e03d0ce3f3bcf82adbbdbcf20dfc35c43b8e1035", size = 24444343, upload-time = "2025-10-18T01:47:57.596Z" }, - { url = "https://files.pythonhosted.org/packages/a3/d8/ea8f98e198d8682c0d82cba04303d26f646ef2592a558739d812bfe02a3f/gensim-4.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0977e5e5df03f829f322662e37ac973b93272c526f1432f865d214c0b573f98", size = 27591522, upload-time = "2025-10-18T01:48:48.543Z" }, - { url = "https://files.pythonhosted.org/packages/7a/6e/9b835483f776ad0ab6fd1197441000c4005b0a3219d456b25296966f0107/gensim-4.4.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d56613fcb77d4068c1be845843508dcd9d384ede34700a61bbeac32b947d1fc3", size = 27631604, upload-time = "2025-10-18T01:49:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/53/fe/e483909cfbfa8cc4bfd30aa9fb5170c04316cc22f23c9906529f08fb9095/gensim-4.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:724b93c9b6e92cd15837048c71b7fdd38059276c85dd1f9c0375576f0aea153f", size = 24395966, upload-time = "2025-10-18T01:50:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/52/7b/81b6c74b32700ee63f6720a60ca0c89ab59b12933257b47572c8af017658/gensim-4.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7590e7313848ca8f3ff064898bcd6ecf6ec71c752cf4d3ec83f7ac992bc7c088", size = 24463159, upload-time = "2025-10-18T01:51:09.7Z" }, - { url = "https://files.pythonhosted.org/packages/38/7c/18d40f341276a7461962512ca1fb716d5982db57615dfa272f651ecb96d7/gensim-4.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a027238b5eb544a17afe73ec227d6a7e0c6b4e2108b1131c0b8f291a0e0e2e", size = 24453170, upload-time = "2025-10-18T01:51:58.42Z" }, - { url = "https://files.pythonhosted.org/packages/68/88/6bd6919d31bdd473472ce1c18c24fcab5869b8b15166a424d11ce33a5eab/gensim-4.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e110e2d3533f5b35239850a96cb2016a586ecd85671d655079b3048332b7169", size = 27760793, upload-time = "2025-10-18T01:52:47.866Z" }, - { url = "https://files.pythonhosted.org/packages/d9/fa/85531b39c1beb5a4203929ba83d94d886cec40d0fb0bef8ca05fd1cc7a38/gensim-4.4.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91a7fa5e814e7b1bad4b2dffa8d62c1e55410d5cbdf930714c1997ffb4404db8", size = 27809988, upload-time = "2025-10-18T01:53:36.978Z" }, - { url = "https://files.pythonhosted.org/packages/10/c3/7e22d6f7d88c4ea6a3a84481f00538252659d285713c3b7e2e1537b0e7e1/gensim-4.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:5e2c1d584d1c7d16b2a0fe7d2f6f59a451422df7b5edb7e3ca46c8e462782127", size = 24396172, upload-time = "2025-10-18T01:54:25.711Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/d5285865ca54b93d41ccd8683c2d79952434957c76b411283c7a6c66ca69/gensim-4.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0845b2fa039dbea5667fb278b5414e70f6d48fd208ef51f33e84a78444288d8d", size = 24467245, upload-time = "2025-10-18T01:55:09.924Z" }, - { url = "https://files.pythonhosted.org/packages/32/59/f0ea443cbfb3b06e1d2e060217bb91f954845f6df38cbc9c5468b6c9c638/gensim-4.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1853fc5be730f692c444a826041fef9a2fc8d74c73bb59748904b2e3221daa86", size = 24455775, upload-time = "2025-10-18T01:55:52.866Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b8/9b0ba15756e41ccfdd852f9c65cd2b552f240c201dc3237ad8c178642e80/gensim-4.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23a2a4260f01c8f71bae5dd0e8a01bb247a2c789480c033e0eaba100b0ad4239", size = 27771345, upload-time = "2025-10-18T01:56:41.448Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/c29701826c963b04a43d5d7b87573a74040387ab9219e65b10f377d22b5b/gensim-4.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b73ff30af6ddd0d2ddf9473b1eb44603cd79ec14c87d93b75291802b991916c", size = 27864118, upload-time = "2025-10-18T01:57:32.428Z" }, - { url = "https://files.pythonhosted.org/packages/fd/f2/9ec6863143888bf390cdc5261f6d9e71d79bc95d98fb815679dba478d5f6/gensim-4.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3a3f9bc8d4178b01d114e1c58c5ab2333f131c7415fb3d8ec8f1ecfe4c5b544", size = 24400277, upload-time = "2025-10-18T01:58:17.629Z" }, -] - [[package]] name = "ghp-import" version = "2.1.0" @@ -1271,40 +881,158 @@ wheels = [ [[package]] name = "graphrag" version = "2.7.1" -source = { editable = "." } +source = { editable = "packages/graphrag" } dependencies = [ - { name = "aiofiles" }, - { name = "azure-cosmos" }, { name = "azure-identity" }, { name = "azure-search-documents" }, { name = "azure-storage-blob" }, + { name = "blis" }, { name = "devtools" }, - { name = "environs" }, - { name = "fnllm", extra = ["azure", "openai"] }, - { name = "future" }, - { name = "graspologic" }, + { name = "graphrag-cache" }, + { name = "graphrag-common" }, + { name = "graphrag-input" }, + { name = "graphrag-llm" }, + { name = "graphrag-storage" }, + { name = "graphrag-vectors" }, + { name = "graspologic-native" }, { name = "json-repair" }, - { name = "lancedb" }, - { name = "litellm" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx" }, { name = "nltk" }, { name = "numpy" }, - { name = "openai" }, { name = "pandas" }, { name = "pyarrow" }, { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, { name = "spacy" }, { name = "textblob" }, - { name = "tiktoken" }, { name = "tqdm" }, { name = "typer" }, { name = "typing-extensions" }, - { name = "umap-learn" }, ] +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = "~=1.19" }, + { name = "azure-search-documents", specifier = "~=11.5" }, + { name = "azure-storage-blob", specifier = "~=12.24" }, + { name = "blis", specifier = "~=1.0" }, + { name = "devtools", specifier = "~=0.12" }, + { name = "graphrag-cache", editable = "packages/graphrag-cache" }, + { name = "graphrag-common", editable = "packages/graphrag-common" }, + { name = "graphrag-input", editable = "packages/graphrag-input" }, + { name = "graphrag-llm", editable = "packages/graphrag-llm" }, + { name = "graphrag-storage", editable = "packages/graphrag-storage" }, + { name = "graphrag-vectors", editable = "packages/graphrag-vectors" }, + { name = "graspologic-native", specifier = "~=1.2" }, + { name = "json-repair", specifier = "~=0.30" }, + { name = "networkx", specifier = "~=3.4" }, + { name = "nltk", specifier = "==3.9.1" }, + { name = "numpy", specifier = "~=2.1" }, + { name = "pandas", specifier = "~=2.3" }, + { name = "pyarrow", specifier = "~=22.0" }, + { name = "pydantic", specifier = "~=2.10" }, + { name = "spacy", specifier = "~=3.8" }, + { name = "textblob", specifier = "~=0.18" }, + { name = "tqdm", specifier = "~=4.67" }, + { name = "typer", specifier = "~=0.16" }, + { name = "typing-extensions", specifier = "~=4.12" }, +] + +[[package]] +name = "graphrag-cache" +version = "2.7.1" +source = { editable = "packages/graphrag-cache" } +dependencies = [ + { name = "graphrag-common" }, + { name = "graphrag-storage" }, +] + +[package.metadata] +requires-dist = [ + { name = "graphrag-common", editable = "packages/graphrag-common" }, + { name = "graphrag-storage", editable = "packages/graphrag-storage" }, +] + +[[package]] +name = "graphrag-chunking" +version = "2.7.1" +source = { editable = "packages/graphrag-chunking" } +dependencies = [ + { name = "graphrag-common" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "graphrag-common", editable = "packages/graphrag-common" }, + { name = "pydantic", specifier = "~=2.10" }, +] + +[[package]] +name = "graphrag-common" +version = "2.7.1" +source = { editable = "packages/graphrag-common" } +dependencies = [ + { name = "python-dotenv" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "python-dotenv", specifier = "~=1.0" }, + { name = "pyyaml", specifier = "~=6.0" }, +] + +[[package]] +name = "graphrag-input" +version = "2.7.1" +source = { editable = "packages/graphrag-input" } +dependencies = [ + { name = "graphrag-common" }, + { name = "graphrag-storage" }, + { name = "markitdown" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "graphrag-common", editable = "packages/graphrag-common" }, + { name = "graphrag-storage", editable = "packages/graphrag-storage" }, + { name = "markitdown", specifier = "~=0.1.0" }, + { name = "pydantic", specifier = "~=2.10" }, +] + +[[package]] +name = "graphrag-llm" +version = "2.7.1" +source = { editable = "packages/graphrag-llm" } +dependencies = [ + { name = "azure-identity" }, + { name = "graphrag-cache" }, + { name = "graphrag-common" }, + { name = "jinja2" }, + { name = "litellm" }, + { name = "nest-asyncio2" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "azure-identity", specifier = "~=1.19.0" }, + { name = "graphrag-cache", editable = "packages/graphrag-cache" }, + { name = "graphrag-common", editable = "packages/graphrag-common" }, + { name = "jinja2", specifier = "~=3.1" }, + { name = "litellm", specifier = "~=1.80" }, + { name = "nest-asyncio2", specifier = "~=1.7" }, + { name = "pydantic", specifier = "~=2.10" }, + { name = "typing-extensions", specifier = "~=4.12" }, +] + +[[package]] +name = "graphrag-monorepo" +version = "0.0.0" +source = { virtual = "." } + [package.dev-dependencies] dev = [ { name = "coverage" }, @@ -1323,96 +1051,90 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-dotenv" }, { name = "pytest-timeout" }, + { name = "pytest-xdist", extra = ["psutil"] }, { name = "ruff" }, { name = "semversioner" }, { name = "update-toml" }, ] [package.metadata] -requires-dist = [ - { name = "aiofiles", specifier = ">=24.1.0" }, - { name = "azure-cosmos", specifier = ">=4.9.0" }, - { name = "azure-identity", specifier = ">=1.19.0" }, - { name = "azure-search-documents", specifier = ">=11.5.2" }, - { name = "azure-storage-blob", specifier = ">=12.24.0" }, - { name = "devtools", specifier = ">=0.12.2" }, - { name = "environs", specifier = ">=11.0.0" }, - { name = "fnllm", extras = ["azure", "openai"], specifier = ">=0.4.1" }, - { name = "future", specifier = ">=1.0.0" }, - { name = "graspologic", specifier = ">=3.4.1" }, - { name = "json-repair", specifier = ">=0.30.3" }, - { name = "lancedb", specifier = ">=0.17.0" }, - { name = "litellm", specifier = ">=1.77.1" }, - { name = "networkx", specifier = ">=3.4.2" }, - { name = "nltk", specifier = "==3.9.1" }, - { name = "numpy", specifier = ">=1.25.2" }, - { name = "openai", specifier = ">=1.68.0" }, - { name = "pandas", specifier = ">=2.2.3,<3" }, - { name = "pyarrow", specifier = ">=17.0.0" }, - { name = "pydantic", specifier = ">=2.10.3" }, - { name = "python-dotenv", specifier = ">=1.0.1" }, - { name = "pyyaml", specifier = ">=6.0.2" }, - { name = "spacy", specifier = ">=3.8.4" }, - { name = "textblob", specifier = ">=0.18.0.post0" }, - { name = "tiktoken", specifier = ">=0.11.0" }, - { name = "tqdm", specifier = ">=4.67.1" }, - { name = "typer", specifier = ">=0.16.0" }, - { name = "typing-extensions", specifier = ">=4.12.2" }, - { name = "umap-learn", specifier = ">=0.5.6" }, -] [package.metadata.requires-dev] dev = [ - { name = "coverage", specifier = ">=7.6.9" }, - { name = "deptry", specifier = ">=0.21.1" }, - { name = "ipykernel", specifier = ">=6.29.5" }, - { name = "jupyter", specifier = ">=1.1.1" }, - { name = "mkdocs-exclude-search", specifier = ">=0.6.6" }, - { name = "mkdocs-jupyter", specifier = ">=0.25.1" }, - { name = "mkdocs-material", specifier = ">=9.5.48" }, - { name = "mkdocs-typer", specifier = ">=0.0.3" }, - { name = "nbconvert", specifier = ">=7.16.4" }, - { name = "pandas-stubs", specifier = ">=2.3.0.250703" }, - { name = "poethepoet", specifier = ">=0.31.1" }, - { name = "pyright", specifier = ">=1.1.390" }, - { name = "pytest", specifier = ">=8.3.4" }, - { name = "pytest-asyncio", specifier = ">=0.24.0" }, - { name = "pytest-dotenv", specifier = ">=0.5.2" }, - { name = "pytest-timeout", specifier = ">=2.3.1" }, - { name = "ruff", specifier = ">=0.8.2" }, - { name = "semversioner", specifier = ">=2.0.5" }, - { name = "update-toml", specifier = ">=0.2.1" }, -] - -[[package]] -name = "graspologic" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } + { name = "coverage", specifier = "~=7.6" }, + { name = "deptry", specifier = "~=0.21" }, + { name = "ipykernel", specifier = "~=6.29" }, + { name = "jupyter", specifier = "~=1.1" }, + { name = "mkdocs-exclude-search", specifier = "~=0.6" }, + { name = "mkdocs-jupyter", specifier = "~=0.25" }, + { name = "mkdocs-material", specifier = "~=9.5" }, + { name = "mkdocs-typer", specifier = "~=0.0.3" }, + { name = "nbconvert", specifier = "~=7.16" }, + { name = "pandas-stubs", specifier = "~=2.3" }, + { name = "poethepoet", specifier = "~=0.31" }, + { name = "pyright", specifier = "~=1.1" }, + { name = "pytest", specifier = "~=8.3" }, + { name = "pytest-asyncio", specifier = "~=0.24" }, + { name = "pytest-dotenv", specifier = "~=0.5" }, + { name = "pytest-timeout", specifier = "~=2.3" }, + { name = "pytest-xdist", extras = ["psutil"], specifier = "~=3.8.0" }, + { name = "ruff", specifier = "~=0.8" }, + { name = "semversioner", specifier = "~=2.0" }, + { name = "update-toml", specifier = "~=0.2" }, +] + +[[package]] +name = "graphrag-storage" +version = "2.7.1" +source = { editable = "packages/graphrag-storage" } dependencies = [ - { name = "anytree" }, - { name = "beartype" }, - { name = "future" }, - { name = "gensim" }, - { name = "graspologic-native" }, - { name = "hyppo" }, - { name = "joblib" }, - { name = "matplotlib" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "aiofiles" }, + { name = "azure-cosmos" }, + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "graphrag-common" }, + { name = "pandas" }, + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiofiles", specifier = "~=24.1" }, + { name = "azure-cosmos", specifier = "~=4.9" }, + { name = "azure-identity", specifier = "~=1.19" }, + { name = "azure-storage-blob", specifier = "~=12.24" }, + { name = "graphrag-common", editable = "packages/graphrag-common" }, + { name = "pandas", specifier = "~=2.3" }, + { name = "pydantic", specifier = "~=2.10" }, +] + +[[package]] +name = "graphrag-vectors" +version = "2.7.1" +source = { editable = "packages/graphrag-vectors" } +dependencies = [ + { name = "azure-core" }, + { name = "azure-cosmos" }, + { name = "azure-identity" }, + { name = "azure-search-documents" }, + { name = "graphrag-common" }, + { name = "lancedb" }, { name = "numpy" }, - { name = "pot" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "seaborn" }, - { name = "statsmodels" }, - { name = "typing-extensions" }, - { name = "umap-learn" }, + { name = "pyarrow" }, + { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/bb/0fe2ef85ea775e7b8416b2cf90097aa4b5e0c9c2271d7fe6789bab27d0ca/graspologic-3.4.4.tar.gz", hash = "sha256:79878caf367da3e89046a4ec94291c5b1a5da569f19fdd879d8b45c3563d7110", size = 5134258, upload-time = "2025-09-08T21:44:01.969Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/b0/e26eb8fc25f3093ad168fba4101bdcf43258b91672546d20a2b64283845c/graspologic-3.4.4-py3-none-any.whl", hash = "sha256:4ea5cd50f10eaff3fa90f18a8f66b1f5f42c724ac6aeb95e9f081632fc8d2d00", size = 5200993, upload-time = "2025-09-08T21:43:59.843Z" }, + +[package.metadata] +requires-dist = [ + { name = "azure-core", specifier = "~=1.32" }, + { name = "azure-cosmos", specifier = "~=4.9" }, + { name = "azure-identity", specifier = "~=1.19" }, + { name = "azure-search-documents", specifier = "~=11.6" }, + { name = "graphrag-common", editable = "packages/graphrag-common" }, + { name = "lancedb", specifier = "~=0.24.1" }, + { name = "numpy", specifier = "~=2.1" }, + { name = "pyarrow", specifier = "~=22.0" }, + { name = "pydantic", specifier = "~=2.10" }, ] [[package]] @@ -1427,47 +1149,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/51/21097af79f3d68626539ab829bdbf6cc42933f020e161972927d916e394c/graspologic_native-1.2.5-cp38-abi3-win_amd64.whl", hash = "sha256:c3ef2172d774083d7e2c8e77daccd218571ddeebeb2c1703cebb1a2cc4c56e07", size = 210438, upload-time = "2025-04-02T19:34:21.139Z" }, ] -[[package]] -name = "grpcio" -version = "1.76.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, - { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, - { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, - { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, - { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, - { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, - { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" }, - { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" }, - { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, - { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, - { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, - { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, - { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, - { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, - { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, - { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, - { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, - { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, - { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, - { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, - { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, - { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, - { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, - { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -1522,7 +1203,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.3.3" +version = "1.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -1536,31 +1217,21 @@ dependencies = [ { name = "typer-slim" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/c3/544cd4cdd4b3c6de8591b56bb69efc3682e9ac81e36135c02e909dd98c5b/huggingface_hub-1.3.3.tar.gz", hash = "sha256:f8be6f468da4470db48351e8c77d6d8115dff9b3daeb30276e568767b1ff7574", size = 627649, upload-time = "2026-01-22T13:59:46.931Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/25/74af9d16cd59ae15b12467a79a84aa0fe24be4aba68fc4da0c1864d49c17/huggingface_hub-1.3.4.tar.gz", hash = "sha256:c20d5484a611b7b7891d272e8fc9f77d5de025b0480bdacfa858efb3780b455f", size = 627683, upload-time = "2026-01-26T14:05:10.656Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/e8/0d032698916b9773b710c46e3b8e0154fc34cd017b151cc316c84c6c34fe/huggingface_hub-1.3.3-py3-none-any.whl", hash = "sha256:44af7b62380efc87c1c3bde7e1bf0661899b5bdfca1fc60975c61ee68410e10e", size = 536604, upload-time = "2026-01-22T13:59:45.391Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/3d0c34c345043c6a398a5882e196b2220dc5861adfa18322448b90908f26/huggingface_hub-1.3.4-py3-none-any.whl", hash = "sha256:a0c526e76eb316e96a91e8a1a7a93cf66b0dd210be1a17bd5fc5ae53cba76bfd", size = 536611, upload-time = "2026-01-26T14:05:08.549Z" }, ] [[package]] -name = "hyppo" -version = "0.5.2" +name = "humanfriendly" +version = "10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "autograd" }, - { name = "future" }, - { name = "numba" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "patsy" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "statsmodels" }, + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/a6/0d84fe8486a1447da8bdb8ebb249d525fd8c1d0fe038bceb003c6e0513f9/hyppo-0.5.2.tar.gz", hash = "sha256:4634d15516248a43d25c241ed18beeb79bb3210360f7253693b3f154fe8c9879", size = 125115, upload-time = "2025-05-24T18:33:27.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/c4/d46858cfac3c0aad314a1fc378beae5c8cac499b677650a34b5a6a3d4328/hyppo-0.5.2-py3-none-any.whl", hash = "sha256:5cc18f9e158fe2cf1804c9a1e979e807118ee89a303f29dc5cb8891d92d44ef3", size = 192272, upload-time = "2025-05-24T18:33:25.904Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, ] [[package]] @@ -1601,8 +1272,7 @@ dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, @@ -1618,51 +1288,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl", hash = "sha256:abe5386f6ced727a70e0eb0cf1da801fa7c5fa6ff82147747d5a0406cd8c94af", size = 117003, upload-time = "2025-10-20T11:42:37.502Z" }, ] -[[package]] -name = "ipython" -version = "8.38.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.11'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "jedi", marker = "python_full_version < '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, - { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "stack-data", marker = "python_full_version < '3.11'" }, - { name = "traitlets", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, -] - [[package]] name = "ipython" version = "9.9.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.11'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, - { name = "jedi", marker = "python_full_version >= '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, - { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "stack-data", marker = "python_full_version >= '3.11'" }, - { name = "traitlets", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/dd/fb08d22ec0c27e73c8bc8f71810709870d51cadaf27b7ddd3f011236c100/ipython-9.9.0.tar.gz", hash = "sha256:48fbed1b2de5e2c7177eefa144aba7fcb82dac514f09b57e2ac9da34ddb54220", size = 4425043, upload-time = "2026-01-05T12:36:46.233Z" } wheels = [ @@ -1674,7 +1315,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -1687,8 +1328,7 @@ version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "comm" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython" }, { name = "jupyterlab-widgets" }, { name = "traitlets" }, { name = "widgetsnbextension" }, @@ -1749,18 +1389,6 @@ version = "0.12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652, upload-time = "2025-11-09T20:46:41.021Z" }, - { url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829, upload-time = "2025-11-09T20:46:43.281Z" }, - { url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568, upload-time = "2025-11-09T20:46:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052, upload-time = "2025-11-09T20:46:46.818Z" }, - { url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585, upload-time = "2025-11-09T20:46:48.319Z" }, - { url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541, upload-time = "2025-11-09T20:46:49.643Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423, upload-time = "2025-11-09T20:46:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958, upload-time = "2025-11-09T20:46:53.432Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084, upload-time = "2025-11-09T20:46:54.848Z" }, - { url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054, upload-time = "2025-11-09T20:46:56.487Z" }, - { url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368, upload-time = "2025-11-09T20:46:58.638Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847, upload-time = "2025-11-09T20:47:00.295Z" }, { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, @@ -1808,11 +1436,11 @@ wheels = [ [[package]] name = "json-repair" -version = "0.55.0" +version = "0.55.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/74/0f39677fa7c0127129c3f1a37c94d05c30a968ba3047200e54dea375b09a/json_repair-0.55.0.tar.gz", hash = "sha256:9fafb47d92582ef4bdd3520656bdb0fcb37b46cf6aa99c1926b7895abc0a3a4b", size = 38828, upload-time = "2026-01-01T20:29:02.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/71d6bb078d167c0d0959776cee6b6bb8d2ad843f512a5222d7151dde4955/json_repair-0.55.1.tar.gz", hash = "sha256:b27aa0f6bf2e5bf58554037468690446ef26f32ca79c8753282adb3df25fb888", size = 39231, upload-time = "2026-01-23T09:37:20.93Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/00/d7d6b6b3257f9b1f997a558b6f7087b8af7c0a2f525f4fbd864c267a88ab/json_repair-0.55.0-py3-none-any.whl", hash = "sha256:bcf4880f5e6ad21a0f70ab034e3d1d398c2ae9698dc5717d7015afbac77b8ed7", size = 29570, upload-time = "2026-01-01T20:29:01.042Z" }, + { url = "https://files.pythonhosted.org/packages/56/da/289ba9eb550ae420cfc457926f6c49b87cacf8083ee9927e96921888a665/json_repair-0.55.1-py3-none-any.whl", hash = "sha256:a1bcc151982a12bc3ef9e9528198229587b1074999cfe08921ab6333b0c8e206", size = 29743, upload-time = "2026-01-23T09:37:19.404Z" }, ] [[package]] @@ -1912,8 +1540,7 @@ version = "6.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ipykernel" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "prompt-toolkit" }, @@ -2015,7 +1642,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.5.2" +version = "4.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -2029,13 +1656,12 @@ dependencies = [ { name = "notebook-shim" }, { name = "packaging" }, { name = "setuptools" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/dc/2c8c4ff1aee27ac999ba04c373c5d0d7c6c181b391640d7b916b884d5985/jupyterlab-4.5.2.tar.gz", hash = "sha256:c80a6b9f6dace96a566d590c65ee2785f61e7cd4aac5b4d453dcc7d0d5e069b7", size = 23990371, upload-time = "2026-01-12T12:27:08.493Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/76/393eae3349f9a39bf21f8f5406e5244d36e2bfc932049b6070c271f92764/jupyterlab-4.5.3.tar.gz", hash = "sha256:4a159f71067cb38e4a82e86a42de8e7e926f384d7f2291964f282282096d27e8", size = 23939231, upload-time = "2026-01-23T15:04:25.768Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/78/7e455920f104ef2aa94a4c0d2b40e5b44334ee7057eae1aa1fb97b9631ad/jupyterlab-4.5.2-py3-none-any.whl", hash = "sha256:76466ebcfdb7a9bb7e2fbd6459c0e2c032ccf75be673634a84bee4b3e6b13ab6", size = 12385807, upload-time = "2026-01-12T12:27:03.923Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9a/0bf9a7a45f0006d7ff4fdc4fc313de4255acab02bf4db1887c65f0472c01/jupyterlab-4.5.3-py3-none-any.whl", hash = "sha256:63c9f3a48de72ba00df766ad6eed416394f5bb883829f11eeff0872302520ba7", size = 12391761, upload-time = "2026-01-23T15:04:21.214Z" }, ] [[package]] @@ -2076,7 +1702,7 @@ wheels = [ [[package]] name = "jupytext" -version = "1.19.0" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, @@ -2084,118 +1710,33 @@ dependencies = [ { name = "nbformat" }, { name = "packaging" }, { name = "pyyaml" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/84/79a28abd8e6a9376fa623670ab8ac7ebcf45b10f2974e0121bb5e8e086a2/jupytext-1.19.0.tar.gz", hash = "sha256:724c1f75c850a12892ccbcdff33004ede33965d0da8520ab9ea74b39ff51283a", size = 4306554, upload-time = "2026-01-18T17:41:58.959Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/8c/e27aaea0a3fbea002f0e138902432e64f35b39d942cfa13bdc5dd63ce310/jupytext-1.19.0-py3-none-any.whl", hash = "sha256:6e82527920600883088c5825f5d4a5bd06a2676d4958d4f3bc622bad2439c0ac", size = 169904, upload-time = "2026-01-18T17:41:57.467Z" }, -] - -[[package]] -name = "kiwisolver" -version = "1.4.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, - { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, - { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, - { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, - { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, - { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, - { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, - { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, - { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, - { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, - { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, - { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, - { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, - { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, - { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, - { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, - { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, - { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, - { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, - { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, -] - -[[package]] -name = "lance-namespace" -version = "0.4.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "lance-namespace-urllib3-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/b5/0c3c55cf336b1e90392c2e24ac833551659e8bb3c61644b2d94825eb31bd/lance_namespace-0.4.5.tar.gz", hash = "sha256:0aee0abed3a1fa762c2955c7d12bb3004cea5c82ba28f6fcb9fe79d0cc19e317", size = 9827, upload-time = "2026-01-07T19:20:23.005Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/a5/80c02f307c8ce863cb33e27daf049315e9d96979e14eead700923b5ec9cc/jupytext-1.19.1.tar.gz", hash = "sha256:82587c07e299173c70ed5e8ec7e75183edf1be289ed518bab49ad0d4e3d5f433", size = 4307829, upload-time = "2026-01-25T21:35:13.276Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/88/173687dad72baf819223e3b506898e386bc88c26ff8da5e8013291e02daf/lance_namespace-0.4.5-py3-none-any.whl", hash = "sha256:cd1a4f789de03ba23a0c16f100b1464cca572a5d04e428917a54d09db912d548", size = 11703, upload-time = "2026-01-07T19:20:25.394Z" }, -] - -[[package]] -name = "lance-namespace-urllib3-client" -version = "0.4.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dateutil" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/97/a9/4e527c2f05704565618b239b0965f829d1a194837f01234af3f8e2f33d92/lance_namespace_urllib3_client-0.4.5.tar.gz", hash = "sha256:184deda8cf8700926d994618187053c644eb1f2866a4479e7b80843cacc92b1c", size = 159726, upload-time = "2026-01-07T19:20:24.025Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/86/0adee7190408a28dcc5a0562c674537457e3de59ee51d1c724ecdc4a9930/lance_namespace_urllib3_client-0.4.5-py3-none-any.whl", hash = "sha256:2ee154d616ba4721f0bfdf043d33c4fef2e79d380653e2f263058ab00fb4adf4", size = 277969, upload-time = "2026-01-07T19:20:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl", hash = "sha256:d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9", size = 170478, upload-time = "2026-01-25T21:35:11.17Z" }, ] [[package]] name = "lancedb" -version = "0.27.0" +version = "0.24.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecation" }, - { name = "lance-namespace" }, { name = "numpy" }, - { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "overrides" }, { name = "packaging" }, { name = "pyarrow" }, { name = "pydantic" }, { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/57/c6f557499f94c8ab4b3afe9af26510dfa4aeca9b5d30806e3dd4d25d14e3/lancedb-0.27.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:b0f2ad52fef7248056f8db1e6ca2b2c052807f1c8074e3e3e33a44f5082465b7", size = 43534233, upload-time = "2026-01-22T01:28:00.689Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1b/dbb097f589fba64acf981161e0e42978b3a39ffe3c1680ac7bbf00edffa8/lancedb-0.27.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d643ee9d86e1250f0e63a68841c8f4da73a2c35fdc5e0d741e731ce30f4f0e41", size = 45411790, upload-time = "2026-01-22T01:36:03.829Z" }, - { url = "https://files.pythonhosted.org/packages/38/36/1e643e3b409416182cdb4c8df9fccf5101ef6b19580aaadc917739a7c8ec/lancedb-0.27.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b90c12d36be1362f7f9b9b897c80400389d9c81ba4f6c2297f374d0ec5facb3a", size = 48497419, upload-time = "2026-01-22T01:39:22.941Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f8/52fc37bb20d7cf43771d5251acc351223d8833afd4e330ee6ed2cc4bf312/lancedb-0.27.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:79422a916843c08435ff2f7f2e9778a4efda8a73c847ce21df80e548bae0f752", size = 45426994, upload-time = "2026-01-22T01:34:53.128Z" }, - { url = "https://files.pythonhosted.org/packages/ce/12/cbd8b634ad0d6e821fadd4fa63332c277b382d89860389341419b995222f/lancedb-0.27.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2d70eb1ec33a18d5afc1210e30870c3ca29ef562271253b6953f6c32208caa8c", size = 48542425, upload-time = "2026-01-22T01:40:15.522Z" }, - { url = "https://files.pythonhosted.org/packages/c9/3d/dc276a98e53a17a4c8d31c93852f2b69e23a9145bf9b616a0d813cb56aa8/lancedb-0.27.0-cp39-abi3-win_amd64.whl", hash = "sha256:3c36ba6941a69405e45750d3bbc94899d141fe68c7e24c3b92ecb7c4e4d84bc7", size = 53378515, upload-time = "2026-01-22T02:10:10.048Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/6e51c15c0ea9dc19c6eb037a0c284ccae49fc2e8425b934a6a1ccdc0e6d6/lancedb-0.24.3-cp39-abi3-macosx_10_15_x86_64.whl", hash = "sha256:65b5aa6201d6f1d921694dce614a2686c1272e25926cc7809ad8eb89a8eb85fa", size = 33401550, upload-time = "2025-08-15T19:02:35.274Z" }, + { url = "https://files.pythonhosted.org/packages/1b/78/d020464db8f189923caba69eaa9281c8b0a6c5c6cef3841cca3a17feb00e/lancedb-0.24.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:f2b86928b3175afde9507ca2e39f545df380ca52c415511ffd52fab8b09937ec", size = 30810069, upload-time = "2025-08-15T19:15:55.771Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/3317ebf9c6397591b9beccd091a87e23f7d8186ddaa7cd0c1aa318e09323/lancedb-0.24.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7756d020c2e6d22130d8f59f32b816fbfda5f65ecc80c5b1bbeb01bf55dc834", size = 31708034, upload-time = "2025-08-15T18:23:36.783Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/b91a7bfe0138cec86f2e9930160112f38da923333da16f2bfaac85649d45/lancedb-0.24.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c933d2680e7bb28d99cb32b2395e808593a845a2b5ab59030e7d840d97e91f2", size = 34916331, upload-time = "2025-08-15T18:26:11.104Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c6/b475d3addf841f803f7c54a64427ef0e973ce340295f59adbd0358097a69/lancedb-0.24.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:60ecfaabd1d33f435b498624f988e610cafacd5dfea26368e5582647730495e0", size = 31718200, upload-time = "2025-08-15T18:22:17.472Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/3e0e25b7c6dcd4f6b0e977cb886965070ca05d7994819834484cbe8c8d00/lancedb-0.24.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:78632bd98e6c317609ce844eb737d1dd306786291ed2d98009e37377be13e1b4", size = 34963956, upload-time = "2025-08-15T18:25:47.58Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/063e78eaf61ee8c1428e1063d9c29c5fab8b2d7f5d324a13716419a29a57/lancedb-0.24.3-cp39-abi3-win_amd64.whl", hash = "sha256:d5eb70b8a3b66d728c183f9b77ddfed13894c12880f647bf5cd85e477ad5368b", size = 36945181, upload-time = "2025-08-15T18:44:06.182Z" }, ] [[package]] @@ -2209,13 +1750,12 @@ wheels = [ [[package]] name = "litellm" -version = "1.81.1" +version = "1.81.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "click" }, { name = "fastuuid" }, - { name = "grpcio" }, { name = "httpx" }, { name = "importlib-metadata" }, { name = "jinja2" }, @@ -2226,29 +1766,27 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/2b/299d54f95e02e9ed551c186e881a4ac0eaa5a948a6be93ecaa26a748f1be/litellm-1.81.1.tar.gz", hash = "sha256:9c758db8abff04a2f1f43582d042080e36f245fe34cfbafe2f8b7ca8f1de29b6", size = 13487469, upload-time = "2026-01-21T12:55:58.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/dd/d70835d5b231617761717cd5ba60342b677693093a71d5ce13ae9d254aee/litellm-1.81.3.tar.gz", hash = "sha256:a7688b429a88abfdd02f2a8c3158ebb5385689cfb7f9d4ac1473d018b2047e1b", size = 13612652, upload-time = "2026-01-25T02:45:58.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/73/8d100c4e48935f6a381df60f894ca9c063ea412ce354fbe7a17770ad4092/litellm-1.81.1-py3-none-any.whl", hash = "sha256:503512a8a7f3cddf9d8fed6182c14f1e77c5655635fe67b09efb09c75234bb87", size = 11795146, upload-time = "2026-01-21T12:55:55.613Z" }, + { url = "https://files.pythonhosted.org/packages/83/62/d3f53c665261fdd5bb2401246e005a4ea8194ad1c4d8c663318ae3d638bf/litellm-1.81.3-py3-none-any.whl", hash = "sha256:3f60fd8b727587952ad3dd18b68f5fed538d6f43d15bb0356f4c3a11bccb2b92", size = 11946995, upload-time = "2026-01-25T02:45:55.887Z" }, ] [[package]] -name = "llvmlite" -version = "0.46.0" +name = "magika" +version = "0.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } +dependencies = [ + { name = "click" }, + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/f3/3d1dcdd7b9c41d589f5cff252d32ed91cdf86ba84391cfc81d9d8773571d/magika-0.6.3.tar.gz", hash = "sha256:7cc52aa7359af861957043e2bf7265ed4741067251c104532765cd668c0c0cb1", size = 3042784, upload-time = "2025-10-30T15:22:34.499Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a5/a4d916f1015106e1da876028606a8e87fd5d5c840f98c87bc2d5153b6a2f/llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e", size = 56275176, upload-time = "2025-12-08T18:14:37.944Z" }, - { url = "https://files.pythonhosted.org/packages/79/7f/a7f2028805dac8c1a6fae7bda4e739b7ebbcd45b29e15bf6d21556fcd3d5/llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910", size = 55128629, upload-time = "2025-12-08T18:14:41.674Z" }, - { url = "https://files.pythonhosted.org/packages/b2/bc/4689e1ba0c073c196b594471eb21be0aa51d9e64b911728aa13cd85ef0ae/llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559", size = 38138651, upload-time = "2025-12-08T18:14:45.845Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766, upload-time = "2025-12-08T18:14:48.836Z" }, - { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175, upload-time = "2025-12-08T18:14:51.604Z" }, - { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630, upload-time = "2025-12-08T18:14:55.107Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/8f5a37a65fc9b7b17408508145edd5f86263ad69c19d3574e818f533a0eb/llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93", size = 38138652, upload-time = "2025-12-08T18:14:58.171Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, - { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, - { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, - { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e4/35c323beb3280482c94299d61626116856ac2d4ec16ecef50afc4fdd4291/magika-0.6.3-py3-none-any.whl", hash = "sha256:eda443d08006ee495e02083b32e51b98cb3696ab595a7d13900d8e2ef506ec9d", size = 2969474, upload-time = "2025-10-30T15:22:25.298Z" }, + { url = "https://files.pythonhosted.org/packages/25/8f/132b0d7cd51c02c39fd52658a5896276c30c8cc2fd453270b19db8c40f7e/magika-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:86901e64b05dde5faff408c9b8245495b2e1fd4c226e3393d3d2a3fee65c504b", size = 13358841, upload-time = "2025-10-30T15:22:27.413Z" }, + { url = "https://files.pythonhosted.org/packages/c4/03/5ed859be502903a68b7b393b17ae0283bf34195cfcca79ce2dc25b9290e7/magika-0.6.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3d9661eedbdf445ac9567e97e7ceefb93545d77a6a32858139ea966b5806fb64", size = 15367335, upload-time = "2025-10-30T15:22:29.907Z" }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f8ee7d644affa3b80efdd623a3d75865c8f058f3950cb87fb0c48e3559bc/magika-0.6.3-py3-none-win_amd64.whl", hash = "sha256:e57f75674447b20cab4db928ae58ab264d7d8582b55183a0b876711c2b2787f3", size = 12692831, upload-time = "2025-10-30T15:22:32.063Z" }, ] [[package]] @@ -2272,23 +1810,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[[package]] +name = "markdownify" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09", size = 18816, upload-time = "2025-11-16T19:21:18.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ce/f1e3e9d959db134cedf06825fae8d5b294bd368aacdd0831a3975b7c4d55/markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a", size = 15724, upload-time = "2025-11-16T19:21:17.622Z" }, +] + +[[package]] +name = "markitdown" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "charset-normalizer" }, + { name = "defusedxml" }, + { name = "magika" }, + { name = "markdownify" }, + { name = "onnxruntime", marker = "sys_platform == 'win32'" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/4d/06567465c1886c2ea47bac24eab0c96bb6b4ecea47224323409dc9cbb614/markitdown-0.1.4.tar.gz", hash = "sha256:e72a481d1a50c82ff744e85e3289f79a940c5d0ad5ffa2b37c33de814c195bb1", size = 39951, upload-time = "2025-12-01T18:20:30.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/b3/6138d2b23d5b534b0fa3736987a2e11bcef5419cbc9286c8afd229d21558/markitdown-0.1.4-py3-none-any.whl", hash = "sha256:d7f3805716b22545f693d355e28e89584226c0614b3b80b7c4a3f825f068492d", size = 58314, upload-time = "2025-12-01T18:20:32.345Z" }, +] + [[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/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { 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" }, @@ -2313,65 +1871,6 @@ wheels = [ { 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" }, ] -[[package]] -name = "marshmallow" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backports-datetime-fromisoformat", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/34/55d47aab1ef03fb5aab96257a31acfc58791d274cf86c044e6e75e6d3bfe/marshmallow-4.2.0.tar.gz", hash = "sha256:908acabd5aa14741419d3678d3296bda6abe28a167b7dcd05969ceb8256943ac", size = 221225, upload-time = "2026-01-04T16:07:36.921Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/b6/0a907f92c2158c9841da0227c7074ce1490f578f34d67cbba82ba8f9146e/marshmallow-4.2.0-py3-none-any.whl", hash = "sha256:1dc369bd13a8708a9566d6f73d1db07d50142a7580f04fd81e1c29a4d2e10af4", size = 48448, upload-time = "2026-01-04T16:07:34.269Z" }, -] - -[[package]] -name = "matplotlib" -version = "3.10.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, - { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, - { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, - { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, - { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, - { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, - { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, - { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, - { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, - { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, - { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, - { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, -] - [[package]] name = "matplotlib-inline" version = "0.2.1" @@ -2418,9 +1917,6 @@ wheels = [ name = "mistune" version = "3.2.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, @@ -2537,6 +2033,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/34/4d6722b7cdb5e37474272205df6f2080ad01aff74570820a83dedb314f1b/mkdocs_typer-0.0.3-py3-none-any.whl", hash = "sha256:b2a9a44da590a7100114fde4de9123fedfea692d229379984db20ee3b3f12d7c", size = 11564, upload-time = "2023-06-21T16:33:38.597Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + [[package]] name = "msal" version = "1.34.0" @@ -2565,68 +2070,47 @@ wheels = [ [[package]] name = "multidict" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" }, - { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993, upload-time = "2025-10-06T14:48:28.4Z" }, - { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607, upload-time = "2025-10-06T14:48:29.581Z" }, - { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847, upload-time = "2025-10-06T14:48:32.107Z" }, - { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616, upload-time = "2025-10-06T14:48:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333, upload-time = "2025-10-06T14:48:35.9Z" }, - { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239, upload-time = "2025-10-06T14:48:37.302Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618, upload-time = "2025-10-06T14:48:38.963Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655, upload-time = "2025-10-06T14:48:40.312Z" }, - { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245, upload-time = "2025-10-06T14:48:41.848Z" }, - { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523, upload-time = "2025-10-06T14:48:43.749Z" }, - { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129, upload-time = "2025-10-06T14:48:45.225Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" }, - { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" }, - { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" }, - { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" }, - { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, - { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, - { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, - { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, - { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, - { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +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/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]] @@ -2635,14 +2119,6 @@ version = "1.0.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/23/2e/88c147931ea9725d634840d538622e94122bceaf346233349b7b5c62964b/murmurhash-1.0.15.tar.gz", hash = "sha256:58e2b27b7847f9e2a6edf10b47a8c8dd70a4705f45dccb7bf76aeadacf56ba01", size = 13291, upload-time = "2025-11-14T09:51:15.272Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/3c/5e59e29fe971365d27f191a5cbf8a5fb492746e458604fe5d39810da4668/murmurhash-1.0.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4989c16053a9a83b02c520dd00a31f0877d5fd2ab8a9b6b75ed9eba0e25c489", size = 27463, upload-time = "2025-11-14T09:49:53.158Z" }, - { url = "https://files.pythonhosted.org/packages/38/3d/ace00a9b82beaa99a8a7a52e98171cfbf13c0066d2f820e84a5d572e3bd0/murmurhash-1.0.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:899068ba3d7c371e7edd093852c634cce802fefd9aaddfcc0d2fda1d7433c7f9", size = 27714, upload-time = "2025-11-14T09:49:54.855Z" }, - { url = "https://files.pythonhosted.org/packages/10/0f/34f1c4f97424ea1bc72b1e3bdf61ac34f4c5555ec9163721f1e4cafe5b1d/murmurhash-1.0.15-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe883982114de576c793fd1cf55945c8ee6453ad4c4785ac1a48f84e74fdc650", size = 122570, upload-time = "2025-11-14T09:49:55.977Z" }, - { url = "https://files.pythonhosted.org/packages/b9/75/0019717a16ce5a7b088fc50a3ecb513035e4196c5e569bf4a2e16bcc0414/murmurhash-1.0.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:342277d8d7f712d136507fb3ccdba26c076a34ca0f8d1b96f65f0daa556da2e9", size = 123194, upload-time = "2025-11-14T09:49:57.462Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a4/c1c95ce60b816c2255098164e424752779269c93f5d6dceaa213346789a2/murmurhash-1.0.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc54facccb32fe1e97d6231edd4f3e2937467c35658b26aa35bbd6a87ebb7cb0", size = 122461, upload-time = "2025-11-14T09:49:58.686Z" }, - { url = "https://files.pythonhosted.org/packages/63/28/e1f79369a6e8d1a5901346ed2fd3a5c56e647d0b849044870c071cb64e1c/murmurhash-1.0.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e525bbd8e26e6b9ab1b56758a59b16c2fffd73bad2f7b8bf361c16f70ff1d980", size = 121676, upload-time = "2025-11-14T09:49:59.888Z" }, - { url = "https://files.pythonhosted.org/packages/1d/7c/e2be1f5387e5898f6551cf81c4220975858b9dbda4d471b133750945599a/murmurhash-1.0.15-cp310-cp310-win_amd64.whl", hash = "sha256:2224f30f7729717644745a6f513ea7662517dfe7b1867cf1588177f64c61df3c", size = 25156, upload-time = "2025-11-14T09:50:01.016Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/0df6e1a753de68368662cbbb8f88558e2c877d3886ac12b30953fb8ed335/murmurhash-1.0.15-cp310-cp310-win_arm64.whl", hash = "sha256:8a181494b5f03ba831f9a13f2de3aab9ef591e508e57239043d65c5c592f5837", size = 23270, upload-time = "2025-11-14T09:50:01.99Z" }, { url = "https://files.pythonhosted.org/packages/6b/ca/77d3e69924a8eb4508bb4f0ad34e46adbeedeb93616a71080e61e53dad71/murmurhash-1.0.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f32307fb9347680bb4fe1cbef6362fb39bd994f1b59abd8c09ca174e44199081", size = 27397, upload-time = "2025-11-14T09:50:03.077Z" }, { url = "https://files.pythonhosted.org/packages/e6/53/a936f577d35b245d47b310f29e5e9f09fcac776c8c992f1ab51a9fb0cee2/murmurhash-1.0.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:539d8405885d1d19c005f3a2313b47e8e54b0ee89915eb8dfbb430b194328e6c", size = 27692, upload-time = "2025-11-14T09:50:04.144Z" }, { url = "https://files.pythonhosted.org/packages/4d/64/5f8cfd1fd9cbeb43fcff96672f5bd9e7e1598d1c970f808ecd915490dc20/murmurhash-1.0.15-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4cd739a00f5a4602201b74568ddabae46ec304719d9be752fd8f534a9464b5e", size = 128396, upload-time = "2025-11-14T09:50:05.268Z" }, @@ -2726,25 +2202,18 @@ wheels = [ ] [[package]] -name = "networkx" -version = "3.4.2" +name = "nest-asyncio2" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/eb/ecf8bbf9d22a4e8f7be1628336fe0202da7660790053aa28abeb6c15eb14/nest_asyncio2-1.7.1.tar.gz", hash = "sha256:a1fe5bbbd20894dcceb1842322d74992c5834d5ab692af2c4f59a9a4fcf75fe8", size = 13797, upload-time = "2025-11-20T20:46:07.085Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, + { url = "https://files.pythonhosted.org/packages/8c/48/c1f1ddcfd04bba60470235c2f83733ecff43ebe068dc7715aab60bc92ad8/nest_asyncio2-1.7.1-py3-none-any.whl", hash = "sha256:f83bc1744c3cfa7d47fd29431e5e168db6cb76eda1bb20108955c32f60d7eddf", size = 7504, upload-time = "2025-11-20T20:46:05.704Z" }, ] [[package]] name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, @@ -2776,7 +2245,7 @@ wheels = [ [[package]] name = "notebook" -version = "7.5.2" +version = "7.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, @@ -2785,9 +2254,9 @@ dependencies = [ { name = "notebook-shim" }, { name = "tornado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/b6/6b2c653570b02e4ec2a94c0646a4a25132be0749617776d0b72a2bcedb9b/notebook-7.5.2.tar.gz", hash = "sha256:83e82f93c199ca730313bea1bb24bc279ea96f74816d038a92d26b6b9d5f3e4a", size = 14059605, upload-time = "2026-01-12T14:56:53.483Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/cb/cc7f4df5cee315dd126a47eb60890690a0438d5e0dd40c32d60ce16de377/notebook-7.5.3.tar.gz", hash = "sha256:393ceb269cf9fdb02a3be607a57d7bd5c2c14604f1818a17dbeb38e04f98cbfa", size = 14073140, upload-time = "2026-01-26T07:28:36.605Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/55/b754cd51c6011d90ef03e3f06136f1ebd44658b9529dbcf0c15fc0d6a0b7/notebook-7.5.2-py3-none-any.whl", hash = "sha256:17d078a98603d70d62b6b4b3fcb67e87d7a68c398a7ae9b447eb2d7d9aec9979", size = 14468915, upload-time = "2026-01-12T14:56:47.87Z" }, + { url = "https://files.pythonhosted.org/packages/96/98/9286e7f35e5584ebb79f997f2fb0cb66745c86f6c5fccf15ba32aac5e908/notebook-7.5.3-py3-none-any.whl", hash = "sha256:c997bfa1a2a9eb58c9bbb7e77d50428befb1033dd6f02c482922e96851d67354", size = 14481744, upload-time = "2026-01-26T07:28:31.867Z" }, ] [[package]] @@ -2803,59 +2272,65 @@ wheels = [ ] [[package]] -name = "numba" -version = "0.63.1" +name = "numpy" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "llvmlite" }, +sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, + { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" }, + { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" }, + { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, + { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, + { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, + { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, + { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, + { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, + { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/ce/5283d4ffa568f795bb0fd61ee1f0efc0c6094b94209259167fc8d4276bde/numba-0.63.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6d6bf5bf00f7db629305caaec82a2ffb8abe2bf45eaad0d0738dc7de4113779", size = 2680810, upload-time = "2025-12-10T02:56:55.269Z" }, - { url = "https://files.pythonhosted.org/packages/0f/72/a8bda517e26d912633b32626333339b7c769ea73a5c688365ea5f88fd07e/numba-0.63.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08653d0dfc9cc9c4c9a8fba29ceb1f2d5340c3b86c4a7e5e07e42b643bc6a2f4", size = 3739735, upload-time = "2025-12-10T02:56:57.922Z" }, - { url = "https://files.pythonhosted.org/packages/ca/17/1913b7c1173b2db30fb7a9696892a7c4c59aeee777a9af6859e9e01bac51/numba-0.63.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09eebf5650246ce2a4e9a8d38270e2d4b0b0ae978103bafb38ed7adc5ea906e", size = 3446707, upload-time = "2025-12-10T02:56:59.837Z" }, - { url = "https://files.pythonhosted.org/packages/b4/77/703db56c3061e9fdad5e79c91452947fdeb2ec0bdfe4affe9b144e7025e0/numba-0.63.1-cp310-cp310-win_amd64.whl", hash = "sha256:f8bba17421d865d8c0f7be2142754ebce53e009daba41c44cf6909207d1a8d7d", size = 2747374, upload-time = "2025-12-10T02:57:07.908Z" }, - { url = "https://files.pythonhosted.org/packages/70/90/5f8614c165d2e256fbc6c57028519db6f32e4982475a372bbe550ea0454c/numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3", size = 2680501, upload-time = "2025-12-10T02:57:09.797Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9d/d0afc4cf915edd8eadd9b2ab5b696242886ee4f97720d9322650d66a88c6/numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25", size = 3744945, upload-time = "2025-12-10T02:57:11.697Z" }, - { url = "https://files.pythonhosted.org/packages/05/a9/d82f38f2ab73f3be6f838a826b545b80339762ee8969c16a8bf1d39395a8/numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c", size = 3450827, upload-time = "2025-12-10T02:57:13.709Z" }, - { url = "https://files.pythonhosted.org/packages/18/3f/a9b106e93c5bd7434e65f044bae0d204e20aa7f7f85d72ceb872c7c04216/numba-0.63.1-cp311-cp311-win_amd64.whl", hash = "sha256:1ecbff7688f044b1601be70113e2fb1835367ee0b28ffa8f3adf3a05418c5c87", size = 2747262, upload-time = "2025-12-10T02:57:15.664Z" }, - { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" }, - { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" }, - { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" }, -] - -[[package]] -name = "numpy" -version = "1.26.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, - { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, - { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, - { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/95/8d/2634e2959b34aa8a0037989f4229e9abcfa484e9c228f99633b3241768a6/onnxruntime-1.20.1-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:06bfbf02ca9ab5f28946e0f912a562a5f005301d0c419283dc57b3ed7969bb7b", size = 30998725, upload-time = "2024-11-21T00:48:51.013Z" }, + { url = "https://files.pythonhosted.org/packages/a5/da/c44bf9bd66cd6d9018a921f053f28d819445c4d84b4dd4777271b0fe52a2/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6243e34d74423bdd1edf0ae9596dd61023b260f546ee17d701723915f06a9f7", size = 11955227, upload-time = "2024-11-21T00:48:54.556Z" }, + { url = "https://files.pythonhosted.org/packages/11/ac/4120dfb74c8e45cce1c664fc7f7ce010edd587ba67ac41489f7432eb9381/onnxruntime-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eec64c0269dcdb8d9a9a53dc4d64f87b9e0c19801d9321246a53b7eb5a7d1bc", size = 13331703, upload-time = "2024-11-21T00:48:57.97Z" }, + { url = "https://files.pythonhosted.org/packages/12/f1/cefacac137f7bb7bfba57c50c478150fcd3c54aca72762ac2c05ce0532c1/onnxruntime-1.20.1-cp311-cp311-win32.whl", hash = "sha256:a19bc6e8c70e2485a1725b3d517a2319603acc14c1f1a017dda0afe6d4665b41", size = 9813977, upload-time = "2024-11-21T00:49:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2d/2d4d202c0bcfb3a4cc2b171abb9328672d7f91d7af9ea52572722c6d8d96/onnxruntime-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:8508887eb1c5f9537a4071768723ec7c30c28eb2518a00d0adcd32c89dea3221", size = 11329895, upload-time = "2024-11-21T00:49:03.845Z" }, + { url = "https://files.pythonhosted.org/packages/e5/39/9335e0874f68f7d27103cbffc0e235e32e26759202df6085716375c078bb/onnxruntime-1.20.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:22b0655e2bf4f2161d52706e31f517a0e54939dc393e92577df51808a7edc8c9", size = 31007580, upload-time = "2024-11-21T00:49:07.029Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9d/a42a84e10f1744dd27c6f2f9280cc3fb98f869dd19b7cd042e391ee2ab61/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f56e898815963d6dc4ee1c35fc6c36506466eff6d16f3cb9848cea4e8c8172", size = 11952833, upload-time = "2024-11-21T00:49:10.563Z" }, + { url = "https://files.pythonhosted.org/packages/47/42/2f71f5680834688a9c81becbe5c5bb996fd33eaed5c66ae0606c3b1d6a02/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e", size = 13333903, upload-time = "2024-11-21T00:49:12.984Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/aabfdf91d013320aa2fc46cf43c88ca0182860ff15df872b4552254a9680/onnxruntime-1.20.1-cp312-cp312-win32.whl", hash = "sha256:bd386cc9ee5f686ee8a75ba74037750aca55183085bf1941da8efcfe12d5b120", size = 9814562, upload-time = "2024-11-21T00:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/76979e0b744307d488c79e41051117634b956612cc731f1028eb17ee7294/onnxruntime-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:19c2d843eb074f385e8bbb753a40df780511061a63f9def1b216bf53860223fb", size = 11331482, upload-time = "2024-11-21T00:49:19.412Z" }, ] [[package]] @@ -2906,7 +2381,7 @@ wheels = [ [[package]] name = "pandas" -version = "2.2.3" +version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -2914,29 +2389,22 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827, upload-time = "2024-09-20T13:08:42.347Z" }, - { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897, upload-time = "2024-09-20T13:08:45.807Z" }, - { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908, upload-time = "2024-09-20T18:37:13.513Z" }, - { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210, upload-time = "2024-09-20T13:08:48.325Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292, upload-time = "2024-09-20T19:01:54.443Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379, upload-time = "2024-09-20T13:08:50.882Z" }, - { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471, upload-time = "2024-09-20T13:08:53.332Z" }, - { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, - { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, - { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, - { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, - { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, - { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, - { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, - { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, - { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, - { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, ] [[package]] @@ -2981,23 +2449,11 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, -] - -[[package]] -name = "patsy" -version = "1.0.2" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/44/ed13eccdd0519eff265f44b670d46fbb0ec813e2274932dc1c0e48520f7d/patsy-1.0.2.tar.gz", hash = "sha256:cdc995455f6233e90e22de72c37fcadb344e7586fb83f06696f54d92f8ce74c0", size = 399942, upload-time = "2025-10-20T16:17:37.535Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/70/ba4b949bdc0490ab78d545459acd7702b211dfccf7eb89bbc1060f52818d/patsy-1.0.2-py2.py3-none-any.whl", hash = "sha256:37bfddbc58fcf0362febb5f54f10743f8b21dd2aa73dec7e7ef59d1b02ae668a", size = 233301, upload-time = "2025-10-20T16:17:36.563Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -3005,61 +2461,13 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] -[[package]] -name = "pillow" -version = "12.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/41/f73d92b6b883a579e79600d391f2e21cb0df767b2714ecbd2952315dfeef/pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd", size = 5304089, upload-time = "2026-01-02T09:10:24.953Z" }, - { url = "https://files.pythonhosted.org/packages/94/55/7aca2891560188656e4a91ed9adba305e914a4496800da6b5c0a15f09edf/pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0", size = 4657815, upload-time = "2026-01-02T09:10:27.063Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d2/b28221abaa7b4c40b7dba948f0f6a708bd7342c4d47ce342f0ea39643974/pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8", size = 6222593, upload-time = "2026-01-02T09:10:29.115Z" }, - { url = "https://files.pythonhosted.org/packages/71/b8/7a61fb234df6a9b0b479f69e66901209d89ff72a435b49933f9122f94cac/pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1", size = 8027579, upload-time = "2026-01-02T09:10:31.182Z" }, - { url = "https://files.pythonhosted.org/packages/ea/51/55c751a57cc524a15a0e3db20e5cde517582359508d62305a627e77fd295/pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda", size = 6335760, upload-time = "2026-01-02T09:10:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7c/60e3e6f5e5891a1a06b4c910f742ac862377a6fe842f7184df4a274ce7bf/pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7", size = 7027127, upload-time = "2026-01-02T09:10:35.009Z" }, - { url = "https://files.pythonhosted.org/packages/06/37/49d47266ba50b00c27ba63a7c898f1bb41a29627ced8c09e25f19ebec0ff/pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a", size = 6449896, upload-time = "2026-01-02T09:10:36.793Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/67fd87d2913902462cd9b79c6211c25bfe95fcf5783d06e1367d6d9a741f/pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef", size = 7151345, upload-time = "2026-01-02T09:10:39.064Z" }, - { url = "https://files.pythonhosted.org/packages/bd/15/f8c7abf82af68b29f50d77c227e7a1f87ce02fdc66ded9bf603bc3b41180/pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09", size = 6325568, upload-time = "2026-01-02T09:10:41.035Z" }, - { url = "https://files.pythonhosted.org/packages/d4/24/7d1c0e160b6b5ac2605ef7d8be537e28753c0db5363d035948073f5513d7/pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91", size = 7032367, upload-time = "2026-01-02T09:10:43.09Z" }, - { url = "https://files.pythonhosted.org/packages/f4/03/41c038f0d7a06099254c60f618d0ec7be11e79620fc23b8e85e5b31d9a44/pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea", size = 2452345, upload-time = "2026-01-02T09:10:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/43/c4/bf8328039de6cc22182c3ef007a2abfbbdab153661c0a9aa78af8d706391/pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3", size = 5304057, upload-time = "2026-01-02T09:10:46.627Z" }, - { url = "https://files.pythonhosted.org/packages/43/06/7264c0597e676104cc22ca73ee48f752767cd4b1fe084662620b17e10120/pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0", size = 4657811, upload-time = "2026-01-02T09:10:49.548Z" }, - { url = "https://files.pythonhosted.org/packages/72/64/f9189e44474610daf83da31145fa56710b627b5c4c0b9c235e34058f6b31/pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451", size = 6232243, upload-time = "2026-01-02T09:10:51.62Z" }, - { url = "https://files.pythonhosted.org/packages/ef/30/0df458009be6a4caca4ca2c52975e6275c387d4e5c95544e34138b41dc86/pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e", size = 8037872, upload-time = "2026-01-02T09:10:53.446Z" }, - { url = "https://files.pythonhosted.org/packages/e4/86/95845d4eda4f4f9557e25381d70876aa213560243ac1a6d619c46caaedd9/pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84", size = 6345398, upload-time = "2026-01-02T09:10:55.426Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1f/8e66ab9be3aaf1435bc03edd1ebdf58ffcd17f7349c1d970cafe87af27d9/pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0", size = 7034667, upload-time = "2026-01-02T09:10:57.11Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f6/683b83cb9b1db1fb52b87951b1c0b99bdcfceaa75febf11406c19f82cb5e/pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b", size = 6458743, upload-time = "2026-01-02T09:10:59.331Z" }, - { url = "https://files.pythonhosted.org/packages/9a/7d/de833d63622538c1d58ce5395e7c6cb7e7dce80decdd8bde4a484e095d9f/pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18", size = 7159342, upload-time = "2026-01-02T09:11:01.82Z" }, - { url = "https://files.pythonhosted.org/packages/8c/40/50d86571c9e5868c42b81fe7da0c76ca26373f3b95a8dd675425f4a92ec1/pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64", size = 6328655, upload-time = "2026-01-02T09:11:04.556Z" }, - { url = "https://files.pythonhosted.org/packages/6c/af/b1d7e301c4cd26cd45d4af884d9ee9b6fab893b0ad2450d4746d74a6968c/pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75", size = 7031469, upload-time = "2026-01-02T09:11:06.538Z" }, - { url = "https://files.pythonhosted.org/packages/48/36/d5716586d887fb2a810a4a61518a327a1e21c8b7134c89283af272efe84b/pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304", size = 2452515, upload-time = "2026-01-02T09:11:08.226Z" }, - { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, - { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, - { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, - { url = "https://files.pythonhosted.org/packages/8b/bc/224b1d98cffd7164b14707c91aac83c07b047fbd8f58eba4066a3e53746a/pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377", size = 5228605, upload-time = "2026-01-02T09:13:14.084Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ca/49ca7769c4550107de049ed85208240ba0f330b3f2e316f24534795702ce/pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72", size = 4622245, upload-time = "2026-01-02T09:13:15.964Z" }, - { url = "https://files.pythonhosted.org/packages/73/48/fac807ce82e5955bcc2718642b94b1bd22a82a6d452aea31cbb678cddf12/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c", size = 5247593, upload-time = "2026-01-02T09:13:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/d2/95/3e0742fe358c4664aed4fd05d5f5373dcdad0b27af52aa0972568541e3f4/pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd", size = 6989008, upload-time = "2026-01-02T09:13:20.083Z" }, - { url = "https://files.pythonhosted.org/packages/5a/74/fe2ac378e4e202e56d50540d92e1ef4ff34ed687f3c60f6a121bcf99437e/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc", size = 5313824, upload-time = "2026-01-02T09:13:22.405Z" }, - { url = "https://files.pythonhosted.org/packages/f3/77/2a60dee1adee4e2655ac328dd05c02a955c1cd683b9f1b82ec3feb44727c/pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a", size = 5963278, upload-time = "2026-01-02T09:13:24.706Z" }, - { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, -] - [[package]] name = "platformdirs" version = "4.5.1" @@ -3085,47 +2493,12 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/9d/054c8435b03324ed9abd5d5ab8c45065b1f42c23952cd23f13a5921d8465/poethepoet-0.40.0.tar.gz", hash = "sha256:91835f00d03d6c4f0e146f80fa510e298ad865e7edd27fe4cb9c94fdc090791b", size = 81114, upload-time = "2026-01-05T19:09:13.116Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/bc/73327d12b176abea7a3c6c7d760e1a953992f7b59d72c0354e39d7a353b5/poethepoet-0.40.0-py3-none-any.whl", hash = "sha256:afd276ae31d5c53573c0c14898118d4848ccee3709b6b0be6a1c6cbe522bbc8a", size = 106672, upload-time = "2026-01-05T19:09:11.536Z" }, ] -[[package]] -name = "pot" -version = "0.9.6.post1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/8b/5f939eaf1fbeb7ff914fe540d659486951a056e5537b8f454362045b6c72/pot-0.9.6.post1.tar.gz", hash = "sha256:9b6cc14a8daecfe1268268168cf46548f9130976b22b24a9e8ec62a734be6c43", size = 604243, upload-time = "2025-09-22T12:51:14.894Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/65/3ed0362444818585d62521f9bf5e6166b8626a714354bc2c8ea5fbdbcbe6/pot-0.9.6.post1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2127b310a13f03951be450812e7dfdf62c5484bc6219bd0e0639f0347b3b60dd", size = 595401, upload-time = "2025-09-22T12:50:23.421Z" }, - { url = "https://files.pythonhosted.org/packages/07/9b/5145c4264953f03f054d4dc4ce1d8f337eb5827896f9e6a51267432ab86d/pot-0.9.6.post1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef7d50dbc851d8b69a6c5305fcad197f149047093e5f4555aed1ea77d1d7823b", size = 464517, upload-time = "2025-09-22T12:50:25.003Z" }, - { url = "https://files.pythonhosted.org/packages/83/23/9724a5a1ebfd4769377d5293208465ef8e803fbcf85350d5d38af349cbea/pot-0.9.6.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1de9cf2af8920c5902f1ee779cf2bf388d5677618735ce91f65d7f8e0ead629e", size = 450810, upload-time = "2025-09-22T12:50:26.28Z" }, - { url = "https://files.pythonhosted.org/packages/df/e9/f8f343588d2a18cd0c77fcf6b6f275642dea3cdf4f0e28e16c6e78198aec/pot-0.9.6.post1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b17c1373366f8ebd745d159793f415660ec45e69048305bb8597267d900145ab", size = 1459588, upload-time = "2025-09-22T12:50:27.739Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7d/1529014aebb9d5fd54538115886d005d371a624b1ecaf5c2525b45ad0f77/pot-0.9.6.post1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48924f34d61b909e68651f3fe9fc1a892c69ae38d3c52bc832f95a28569c0e0e", size = 1478099, upload-time = "2025-09-22T12:50:29.201Z" }, - { url = "https://files.pythonhosted.org/packages/4e/87/84cfc49d4d0eb3e7b6cfc8352f0e73f62d456f6ce875da612b919a6bff6f/pot-0.9.6.post1-cp310-cp310-win32.whl", hash = "sha256:06e21b4dcebc2e8e318a96889243580ea64364830d05d53c4d038afedbe072cc", size = 443775, upload-time = "2025-09-22T12:50:30.84Z" }, - { url = "https://files.pythonhosted.org/packages/c4/21/9731ac0b125f755bb513a4ee081dca0ca5335e9059fb3332dd7c50d28415/pot-0.9.6.post1-cp310-cp310-win_amd64.whl", hash = "sha256:d35bb0169ef242fc2ce4f610572a5d11ac11d646698cbdf8cbb45d828f3c514b", size = 458481, upload-time = "2025-09-22T12:50:32.431Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fc/3f4014bd6713c5b4c8a329b12c52842443b2284f52213a80e697b76b9f20/pot-0.9.6.post1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7fd8482a0262e5c875c05cf52e9c087e7c8bc473ef05d175887ad16e3c0443b7", size = 599499, upload-time = "2025-09-22T12:50:33.796Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/b22b789ee3a81c11c6f39ff08ed6a2e797a2a75a831fae996f4057db4771/pot-0.9.6.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c0bfac9daec0095061279a709f52be740e09363a62fe4c7edc843a4a0f6144c6", size = 466484, upload-time = "2025-09-22T12:50:34.973Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ae/2b35b96562bd72baf6de9583458878738f4508eef70d6fa9dd5867760d6a/pot-0.9.6.post1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:703853f7ba0ae2afed8203ea3478e87ef5f39d55cd75b1a39bb622867d1d5628", size = 453014, upload-time = "2025-09-22T12:50:36.157Z" }, - { url = "https://files.pythonhosted.org/packages/44/7e/f49d0593338a3b7f2c88c4cd6f1285c084e8ce05d52d42ac6f89f4f7ec0c/pot-0.9.6.post1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68268b4dd926976cf0604d466a57dff2ca44372e8ae9c879ba1f3d2a51e3be3d", size = 1494875, upload-time = "2025-09-22T12:50:37.903Z" }, - { url = "https://files.pythonhosted.org/packages/15/91/844c8437caaca6d6a71b38623df75c43642a116d399316adb1d0a9280c85/pot-0.9.6.post1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7568ddc957d3a16739bd24f9e07ce655166d27ebbc8786aad692cc5ba5d4c59", size = 1514551, upload-time = "2025-09-22T12:50:39.616Z" }, - { url = "https://files.pythonhosted.org/packages/ac/de/34a50565c37c0b71725a8075ff1ad2de62213d2e119276b546ef20356ac2/pot-0.9.6.post1-cp311-cp311-win32.whl", hash = "sha256:9649b736ea5dddad3a89d55a4a3bb0078610307ba64cac2efebe6bfcf8cfe785", size = 443490, upload-time = "2025-09-22T12:50:41.162Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fa/453730c1b10094ab4d2ecd0b5fbfcdfe0305419cf01e32a2d31efd333559/pot-0.9.6.post1-cp311-cp311-win_amd64.whl", hash = "sha256:e161e49a22d5a925993baace4679f4e32fc2ade8f45ad73cf8417e13df5bd337", size = 458509, upload-time = "2025-09-22T12:50:43.597Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/13622807461f9f6082a8cd6768f9b4a810bc3a8fda474b81572da94b4d23/pot-0.9.6.post1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f7c542fc20662e35c24dd82eeff8a737220757434d7f0038664a7322221452f7", size = 599240, upload-time = "2025-09-22T12:50:44.848Z" }, - { url = "https://files.pythonhosted.org/packages/c6/5c/b4e017560531f53d06798c681b0d0a9488bb8116bc98da9d399a3d096391/pot-0.9.6.post1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c1755516a7354cbd6110ad2e5f341b98b9968240c2f0f67b0ff5e3ebcb3105bd", size = 464695, upload-time = "2025-09-22T12:50:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/07/9f/57e49b3f7173359741053c5e2766a45dcf649d767c2e967ef93526c9045f/pot-0.9.6.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3207362d3e3b5aaa783f452aa85f66e83edbefb5764f34662860af54ac72ee6", size = 454726, upload-time = "2025-09-22T12:50:47.953Z" }, - { url = "https://files.pythonhosted.org/packages/30/60/fa72dd6094f7dbe6b38e2c6907af8cd0f18c6bd107e0cf4874deddaba883/pot-0.9.6.post1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f6659c5657e6d7e9f98f4a82e0ed64f88e9fce69b2e557416d156343919ba3", size = 1503391, upload-time = "2025-09-22T12:50:49.336Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3f/cc519c1176116271b6282268a705162fa042c16cc922bc56039445c9d697/pot-0.9.6.post1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f1b0148ae17bec0ed12264c6da3a05e13913b716e2a8c9043242b5d8349d8df", size = 1528170, upload-time = "2025-09-22T12:50:50.625Z" }, - { url = "https://files.pythonhosted.org/packages/f5/01/0132c94404cd0b1b2f21c4a49698db9dcd6107c47c02b22df1ed38206b2a/pot-0.9.6.post1-cp312-cp312-win32.whl", hash = "sha256:571e543cc2b0a462365002203595baf2b89c3d064cce4fce70fd1231e832c21f", size = 440577, upload-time = "2025-09-22T12:50:51.716Z" }, - { url = "https://files.pythonhosted.org/packages/c1/6d/23229c0e198a4f7fb27750b3ef8497e6ebed23fe531ed64b5194da8b2b02/pot-0.9.6.post1-cp312-cp312-win_amd64.whl", hash = "sha256:b1d8bd9a334c72baa37f9a2b268de5366c23c0f9c9e3d6dc25d150137ec2823c", size = 455404, upload-time = "2025-09-22T12:50:52.956Z" }, -] - [[package]] name = "preshed" version = "3.0.12" @@ -3136,14 +2509,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/bf/34/eb4f5f0f678e152a96e826da867d2f41c4b18a2d589e40e1dd3347219e91/preshed-3.0.12.tar.gz", hash = "sha256:b73f9a8b54ee1d44529cc6018356896cff93d48f755f29c134734d9371c0d685", size = 15027, upload-time = "2025-11-17T13:00:33.621Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/d0/1245d6d89b051dd5356ffaaa43da05408f37d2da4cfadcf77356ba46da4f/preshed-3.0.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8f0bc207bb5bfe69e3a232367c264cac900dc14e9219cd061b98eaca9e7da61", size = 128866, upload-time = "2025-11-17T12:59:06.633Z" }, - { url = "https://files.pythonhosted.org/packages/24/24/f06650f22450888434a51b17971b650186d2e68f5eaf292e6e8e4be7974c/preshed-3.0.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8a8d571c044ddab5369d30d172c87545f44daa1510bde92b7e0144a8f4f92b", size = 124848, upload-time = "2025-11-17T12:59:08.641Z" }, - { url = "https://files.pythonhosted.org/packages/88/a1/78bdd4938c3286998c0609491c4a0a8aee2f4de4003364112c295a2f32b8/preshed-3.0.12-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6cca080ac9bbc978625c8f0c56ef17471162193c7c1a4622fbde7721da1bdd40", size = 780279, upload-time = "2025-11-17T12:59:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f8/6fbf083346a007927a9e4ce3686ae54ba74191e74fc3af34863ea7be9dea/preshed-3.0.12-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cfd3672007c7b7cac554a0e5f263d7bc94109dc508ee1ef43b2f6ec8c2e2e9e8", size = 781954, upload-time = "2025-11-17T12:59:11.574Z" }, - { url = "https://files.pythonhosted.org/packages/91/c3/f28c7a6cc03e85002780b75249c3557c0fe503792ac66a7b9c5379569999/preshed-3.0.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e01609074713aba93a8143480e67942fbe6898fe134b98d813819bec42a8cae7", size = 799772, upload-time = "2025-11-17T12:59:14.371Z" }, - { url = "https://files.pythonhosted.org/packages/46/25/ca22fa0db162e286db7a94a4f08c1ceb4872d3d64610b807148935ae084c/preshed-3.0.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:30d8a53015663b0d666012bc10d22e8bdd7359191d84a8980ae902e0b87caf24", size = 820532, upload-time = "2025-11-17T12:59:16.281Z" }, - { url = "https://files.pythonhosted.org/packages/0f/57/459a6eea7e15034756f4c2650a9aba6d023aa7976748b18476bd4c0b6fef/preshed-3.0.12-cp310-cp310-win_amd64.whl", hash = "sha256:bf2235bbe09b4862b914086f37a065cc84259e1b53c8ed996cbbd6519ea36b62", size = 117482, upload-time = "2025-11-17T12:59:18.36Z" }, - { url = "https://files.pythonhosted.org/packages/80/1f/a7b648a57d259891bd9b2c8ef1978622fa37b46a9368f054881488b9b4fe/preshed-3.0.12-cp310-cp310-win_arm64.whl", hash = "sha256:139d08b10693bfccb0ea000f47dcca5fc4a78fc1b96c1832c920be9b0a4c8f04", size = 105504, upload-time = "2025-11-17T12:59:19.562Z" }, { url = "https://files.pythonhosted.org/packages/1e/54/d1e02d0a0ea348fb6a769506166e366abfe87ee917c2f11f7139c7acbf10/preshed-3.0.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc45fda3fd4ae1ae15c37f18f0777cf389ce9184ef8884b39b18894416fd1341", size = 128439, upload-time = "2025-11-17T12:59:21.317Z" }, { url = "https://files.pythonhosted.org/packages/8c/cb/685ca57ca6e438345b3f6c20226705a0e056a3de399a5bf8a9ee89b3dd2b/preshed-3.0.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75d6e628bc78c022dbb9267242715718f862c3105927732d166076ff009d65de", size = 124544, upload-time = "2025-11-17T12:59:22.944Z" }, { url = "https://files.pythonhosted.org/packages/f8/07/018fcd3bf298304e1570065cf80601ac16acd29f799578fd47b715dd3ca2/preshed-3.0.12-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b901cff5c814facf7a864b0a4c14a16d45fa1379899a585b3fb48ee36a2dccdb", size = 824728, upload-time = "2025-11-17T12:59:24.614Z" }, @@ -3189,21 +2554,6 @@ version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, - { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, - { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, - { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, - { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, - { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, - { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, - { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, @@ -3237,6 +2587,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/b8/cda15d9d46d03d4aa3a67cb6bffe05173440ccf86a9541afaf7ac59a1b6b/protobuf-6.33.4.tar.gz", hash = "sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91", size = 444346, upload-time = "2026-01-12T18:33:40.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/be/24ef9f3095bacdf95b458543334d0c4908ccdaee5130420bf064492c325f/protobuf-6.33.4-cp310-abi3-win32.whl", hash = "sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d", size = 425612, upload-time = "2026-01-12T18:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/31/ad/e5693e1974a28869e7cd244302911955c1cebc0161eb32dfa2b25b6e96f0/protobuf-6.33.4-cp310-abi3-win_amd64.whl", hash = "sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc", size = 436962, upload-time = "2026-01-12T18:33:31.345Z" }, + { url = "https://files.pythonhosted.org/packages/66/15/6ee23553b6bfd82670207ead921f4d8ef14c107e5e11443b04caeb5ab5ec/protobuf-6.33.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0", size = 427612, upload-time = "2026-01-12T18:33:32.646Z" }, + { url = "https://files.pythonhosted.org/packages/2b/48/d301907ce6d0db75f959ca74f44b475a9caa8fcba102d098d3c3dd0f2d3f/protobuf-6.33.4-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e", size = 324484, upload-time = "2026-01-12T18:33:33.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/1c/e53078d3f7fe710572ab2dcffd993e1e3b438ae71cfc031b71bae44fcb2d/protobuf-6.33.4-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6", size = 339256, upload-time = "2026-01-12T18:33:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8e/971c0edd084914f7ee7c23aa70ba89e8903918adca179319ee94403701d5/protobuf-6.33.4-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9", size = 323311, upload-time = "2026-01-12T18:33:36.305Z" }, + { url = "https://files.pythonhosted.org/packages/75/b1/1dc83c2c661b4c62d56cc081706ee33a4fc2835bd90f965baa2663ef7676/protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc", size = 170532, upload-time = "2026-01-12T18:33:39.199Z" }, +] + [[package]] name = "psutil" version = "7.2.1" @@ -3273,31 +2638,24 @@ wheels = [ [[package]] name = "pyarrow" -version = "23.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/33/ffd9c3eb087fa41dd79c3cf20c4c0ae3cdb877c4f8e1107a446006344924/pyarrow-23.0.0.tar.gz", hash = "sha256:180e3150e7edfcd182d3d9afba72f7cf19839a497cc76555a8dce998a8f67615", size = 1167185, upload-time = "2026-01-18T16:19:42.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/2f/23e042a5aa99bcb15e794e14030e8d065e00827e846e53a66faec73c7cd6/pyarrow-23.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cbdc2bf5947aa4d462adcf8453cf04aee2f7932653cb67a27acd96e5e8528a67", size = 34281861, upload-time = "2026-01-18T16:13:34.332Z" }, - { url = "https://files.pythonhosted.org/packages/8b/65/1651933f504b335ec9cd8f99463718421eb08d883ed84f0abd2835a16cad/pyarrow-23.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:4d38c836930ce15cd31dce20114b21ba082da231c884bdc0a7b53e1477fe7f07", size = 35825067, upload-time = "2026-01-18T16:13:42.549Z" }, - { url = "https://files.pythonhosted.org/packages/84/ec/d6fceaec050c893f4e35c0556b77d4cc9973fcc24b0a358a5781b1234582/pyarrow-23.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4222ff8f76919ecf6c716175a0e5fddb5599faeed4c56d9ea41a2c42be4998b2", size = 44458539, upload-time = "2026-01-18T16:13:52.975Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d9/369f134d652b21db62fe3ec1c5c2357e695f79eb67394b8a93f3a2b2cffa/pyarrow-23.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:87f06159cbe38125852657716889296c83c37b4d09a5e58f3d10245fd1f69795", size = 47535889, upload-time = "2026-01-18T16:14:03.693Z" }, - { url = "https://files.pythonhosted.org/packages/a3/95/f37b6a252fdbf247a67a78fb3f61a529fe0600e304c4d07741763d3522b1/pyarrow-23.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1675c374570d8b91ea6d4edd4608fa55951acd44e0c31bd146e091b4005de24f", size = 48157777, upload-time = "2026-01-18T16:14:12.483Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ab/fb94923108c9c6415dab677cf1f066d3307798eafc03f9a65ab4abc61056/pyarrow-23.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:247374428fde4f668f138b04031a7e7077ba5fa0b5b1722fdf89a017bf0b7ee0", size = 50580441, upload-time = "2026-01-18T16:14:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/ae/78/897ba6337b517fc8e914891e1bd918da1c4eb8e936a553e95862e67b80f6/pyarrow-23.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:de53b1bd3b88a2ee93c9af412c903e57e738c083be4f6392288294513cd8b2c1", size = 27530028, upload-time = "2026-01-18T16:14:27.353Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c0/57fe251102ca834fee0ef69a84ad33cc0ff9d5dfc50f50b466846356ecd7/pyarrow-23.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5574d541923efcbfdf1294a2746ae3b8c2498a2dc6cd477882f6f4e7b1ac08d3", size = 34276762, upload-time = "2026-01-18T16:14:34.128Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4e/24130286548a5bc250cbed0b6bbf289a2775378a6e0e6f086ae8c68fc098/pyarrow-23.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:2ef0075c2488932e9d3c2eb3482f9459c4be629aa673b725d5e3cf18f777f8e4", size = 35821420, upload-time = "2026-01-18T16:14:40.699Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/a869e8529d487aa2e842d6c8865eb1e2c9ec33ce2786eb91104d2c3e3f10/pyarrow-23.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:65666fc269669af1ef1c14478c52222a2aa5c907f28b68fb50a203c777e4f60c", size = 44457412, upload-time = "2026-01-18T16:14:49.051Z" }, - { url = "https://files.pythonhosted.org/packages/36/81/1de4f0edfa9a483bbdf0082a05790bd6a20ed2169ea12a65039753be3a01/pyarrow-23.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4d85cb6177198f3812db4788e394b757223f60d9a9f5ad6634b3e32be1525803", size = 47534285, upload-time = "2026-01-18T16:14:56.748Z" }, - { url = "https://files.pythonhosted.org/packages/f2/04/464a052d673b5ece074518f27377861662449f3c1fdb39ce740d646fd098/pyarrow-23.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1a9ff6fa4141c24a03a1a434c63c8fa97ce70f8f36bccabc18ebba905ddf0f17", size = 48157913, upload-time = "2026-01-18T16:15:05.114Z" }, - { url = "https://files.pythonhosted.org/packages/f4/1b/32a4de9856ee6688c670ca2def588382e573cce45241a965af04c2f61687/pyarrow-23.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:84839d060a54ae734eb60a756aeacb62885244aaa282f3c968f5972ecc7b1ecc", size = 50582529, upload-time = "2026-01-18T16:15:12.846Z" }, - { url = "https://files.pythonhosted.org/packages/db/c7/d6581f03e9b9e44ea60b52d1750ee1a7678c484c06f939f45365a45f7eef/pyarrow-23.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a149a647dbfe928ce8830a713612aa0b16e22c64feac9d1761529778e4d4eaa5", size = 27542646, upload-time = "2026-01-18T16:15:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/3d/bd/c861d020831ee57609b73ea721a617985ece817684dc82415b0bc3e03ac3/pyarrow-23.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5961a9f646c232697c24f54d3419e69b4261ba8a8b66b0ac54a1851faffcbab8", size = 34189116, upload-time = "2026-01-18T16:15:28.054Z" }, - { url = "https://files.pythonhosted.org/packages/8c/23/7725ad6cdcbaf6346221391e7b3eecd113684c805b0a95f32014e6fa0736/pyarrow-23.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:632b3e7c3d232f41d64e1a4a043fb82d44f8a349f339a1188c6a0dd9d2d47d8a", size = 35803831, upload-time = "2026-01-18T16:15:33.798Z" }, - { url = "https://files.pythonhosted.org/packages/57/06/684a421543455cdc2944d6a0c2cc3425b028a4c6b90e34b35580c4899743/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:76242c846db1411f1d6c2cc3823be6b86b40567ee24493344f8226ba34a81333", size = 44436452, upload-time = "2026-01-18T16:15:41.598Z" }, - { url = "https://files.pythonhosted.org/packages/c6/6f/8f9eb40c2328d66e8b097777ddcf38494115ff9f1b5bc9754ba46991191e/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b73519f8b52ae28127000986bf228fda781e81d3095cd2d3ece76eb5cf760e1b", size = 47557396, upload-time = "2026-01-18T16:15:51.252Z" }, - { url = "https://files.pythonhosted.org/packages/10/6e/f08075f1472e5159553501fde2cc7bc6700944bdabe49a03f8a035ee6ccd/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:068701f6823449b1b6469120f399a1239766b117d211c5d2519d4ed5861f75de", size = 48147129, upload-time = "2026-01-18T16:16:00.299Z" }, - { url = "https://files.pythonhosted.org/packages/7d/82/d5a680cd507deed62d141cc7f07f7944a6766fc51019f7f118e4d8ad0fb8/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1801ba947015d10e23bca9dd6ef5d0e9064a81569a89b6e9a63b59224fd060df", size = 50596642, upload-time = "2026-01-18T16:16:08.502Z" }, - { url = "https://files.pythonhosted.org/packages/a9/26/4f29c61b3dce9fa7780303b86895ec6a0917c9af927101daaaf118fbe462/pyarrow-23.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:52265266201ec25b6839bf6bd4ea918ca6d50f31d13e1cf200b4261cd11dc25c", size = 27660628, upload-time = "2026-01-18T16:16:15.28Z" }, +version = "22.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, + { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, + { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, + { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, + { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" }, ] [[package]] @@ -3333,19 +2691,6 @@ dependencies = [ ] 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/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, { 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" }, @@ -3382,14 +2727,6 @@ wheels = [ { 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/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, { 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" }, @@ -3425,42 +2762,24 @@ crypto = [ [[package]] name = "pymdown-extensions" -version = "10.20" +version = "10.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/6c/9e370934bfa30e889d12e61d0dae009991294f40055c238980066a7fbd83/pymdown_extensions-10.20.1.tar.gz", hash = "sha256:e7e39c865727338d434b55f1dd8da51febcffcaebd6e1a0b9c836243f660740a", size = 852860, upload-time = "2026-01-24T05:56:56.758Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, + { url = "https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl", hash = "sha256:24af7feacbca56504b313b7b418c4f5e1317bb5fea60f03d57be7fcc40912aa0", size = 268768, upload-time = "2026-01-24T05:56:54.537Z" }, ] [[package]] -name = "pynndescent" -version = "0.6.0" +name = "pyreadline3" +version = "3.5.4" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "joblib" }, - { name = "llvmlite" }, - { name = "numba" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" }, -] - -[[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" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, ] [[package]] @@ -3478,34 +2797,30 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +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/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { 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 = "1.3.0" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +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/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, + { 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]] @@ -3533,6 +2848,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[package.optional-dependencies] +psutil = [ + { name = "psutil" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -3578,7 +2911,6 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/bb/a7cc2967c5c4eceb6cc49cfe39447d4bfc56e6c865e7c2249b6eb978935f/pywinpty-3.0.2.tar.gz", hash = "sha256:1505cc4cb248af42cb6285a65c9c2086ee9e7e574078ee60933d5d7fa86fb004", size = 30669, upload-time = "2025-10-03T21:16:29.205Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/f5/b17ae550841949c217ad557ee445b4a14e9c0b506ae51ee087eff53428a6/pywinpty-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:65db57fd3387d71e8372b6a54269cbcd0f6dfa6d4616a29e0af749ec19f5c558", size = 2050330, upload-time = "2025-10-03T21:20:15.656Z" }, { url = "https://files.pythonhosted.org/packages/a6/a1/409c1651c9f874d598c10f51ff586c416625601df4bca315d08baec4c3e3/pywinpty-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:327790d70e4c841ebd9d0f295a780177149aeb405bca44c7115a3de5c2054b23", size = 2050304, upload-time = "2025-10-03T21:19:29.466Z" }, { url = "https://files.pythonhosted.org/packages/02/4e/1098484e042c9485f56f16eb2b69b43b874bd526044ee401512234cf9e04/pywinpty-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:99fdd9b455f0ad6419aba6731a7a0d2f88ced83c3c94a80ff9533d95fa8d8a9e", size = 2050391, upload-time = "2025-10-03T21:19:01.642Z" }, ] @@ -3589,15 +2921,6 @@ 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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { 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" }, @@ -3640,16 +2963,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, - { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, - { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, - { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, - { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, - { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, - { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, @@ -3670,11 +2983,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, - { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, - { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, - { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, @@ -3702,23 +3010,6 @@ version = "2026.1.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, - { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, - { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, - { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, - { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, - { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, - { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, - { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, - { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, - { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, - { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, - { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, @@ -3815,15 +3106,15 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, ] [[package]] @@ -3832,20 +3123,6 @@ 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/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, { 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" }, @@ -3916,158 +3193,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] -[[package]] -name = "scikit-learn" -version = "1.7.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "joblib", marker = "python_full_version < '3.11'" }, - { name = "numpy", marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, - { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, - { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, - { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, - { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, - { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, - { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, - { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, - { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, - { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, - { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, - { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, - { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, - { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, -] - -[[package]] -name = "scikit-learn" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] -dependencies = [ - { name = "joblib", marker = "python_full_version >= '3.11'" }, - { name = "numpy", marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, -] -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" }, -] - -[[package]] -name = "scipy" -version = "1.15.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, - { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, - { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, - { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, - { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, - { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, - { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, - { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, - { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, - { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, - { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, -] - -[[package]] -name = "scipy" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, - { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, - { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, - { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, - { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, - { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, - { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, - { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, - { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, - { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, -] - -[[package]] -name = "seaborn" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, - { name = "numpy" }, - { name = "pandas" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, -] - [[package]] name = "semversioner" version = "2.0.8" @@ -4093,11 +3218,11 @@ wheels = [ [[package]] name = "setuptools" -version = "80.10.1" +version = "80.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/ff/f75651350db3cf2ef767371307eb163f3cc1ac03e16fdf3ac347607f7edb/setuptools-80.10.1.tar.gz", hash = "sha256:bf2e513eb8144c3298a3bd28ab1a5edb739131ec5c22e045ff93cd7f5319703a", size = 1229650, upload-time = "2026-01-21T09:42:03.061Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/76/f963c61683a39084aa575f98089253e1e852a4417cb8a3a8a422923a5246/setuptools-80.10.1-py3-none-any.whl", hash = "sha256:fc30c51cbcb8199a219c12cc9c281b5925a4978d212f84229c909636d9f6984e", size = 1099859, upload-time = "2026-01-21T09:42:00.688Z" }, + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, ] [[package]] @@ -4174,13 +3299,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/59/9f/424244b0e2656afc9ff82fb7a96931a47397bfce5ba382213827b198312a/spacy-3.8.11.tar.gz", hash = "sha256:54e1e87b74a2f9ea807ffd606166bf29ac45e2bd81ff7f608eadc7b05787d90d", size = 1326804, upload-time = "2025-11-17T20:40:03.079Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/63/f23db7119e0bb7740d74eff4583543824be84e7c0aad1c87683b8f40a17e/spacy-3.8.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9cc7f775cfc41ccb8be63bd6258a1ec4613d4ad3859f2ba2c079f34240b21f6", size = 6499016, upload-time = "2025-11-17T20:38:22.359Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e4/e8c0f0561e8b29b4f38ba3d491fca427faa750765df3e27850036af28762/spacy-3.8.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be9d665be8581926fba4303543ba189d34e8517803052551b000cf1a1af33b87", size = 6159121, upload-time = "2025-11-17T20:38:24.85Z" }, - { url = "https://files.pythonhosted.org/packages/15/7a/7ce7320f2a384023240fad0e6b7ffb2e3717ae4cc09ec0770706fd20c419/spacy-3.8.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:06e46ad776a1b20cc6296fe04890dea8a7b4e4653d7e8c143dd4a707f7ae2670", size = 30763429, upload-time = "2025-11-17T20:38:27.001Z" }, - { url = "https://files.pythonhosted.org/packages/db/36/b16df8f5ba8d5fc3d2b23f004eb55f3edf4f3345e743efdd560b6b20faf8/spacy-3.8.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e1b91199926eb9de507f7bfc63090b17ee9a12663bcfc76357560c2c7ef4750a", size = 31002535, upload-time = "2025-11-17T20:38:30.115Z" }, - { url = "https://files.pythonhosted.org/packages/6e/be/58183313f1401fff896d3dd8f8da977847fb1c205a2c2a8a7030e81da265/spacy-3.8.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1d4c506adcbefd19ead59daca2e0e61ce669ff35372cc9c23aae1b292c57f94", size = 31033341, upload-time = "2025-11-17T20:38:33.06Z" }, - { url = "https://files.pythonhosted.org/packages/94/08/d490ed3a4ea070734c58cf1f2e3e6081a20630067bca2c58d5dbcfb36558/spacy-3.8.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d885a2bf427c854c5a5f1dda7451924a1f2c036aefaa2946c741201ff05a915a", size = 31882346, upload-time = "2025-11-17T20:38:35.596Z" }, - { url = "https://files.pythonhosted.org/packages/79/38/e64856b3f768754def0f5dc4c5fb3f692d96a193eec7e2eee03d37c233b6/spacy-3.8.11-cp310-cp310-win_amd64.whl", hash = "sha256:909d12ff2365c2e7ebf0258ddc566d2b361ef1fd2e7684ce1af5f7022111e366", size = 15346864, upload-time = "2025-11-17T20:38:37.95Z" }, { url = "https://files.pythonhosted.org/packages/74/d3/0c795e6f31ee3535b6e70d08e89fc22247b95b61f94fc8334a01d39bf871/spacy-3.8.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a12d83e8bfba07563300ae5e0086548e41aa4bfe3734c97dda87e0eec813df0d", size = 6487958, upload-time = "2025-11-17T20:38:40.378Z" }, { url = "https://files.pythonhosted.org/packages/4e/2a/83ca9b4d0a2b31adcf0ced49fa667212d12958f75d4e238618a60eb50b10/spacy-3.8.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e07a50b69500ef376326545353a470f00d1ed7203c76341b97242af976e3681a", size = 6148078, upload-time = "2025-11-17T20:38:42.524Z" }, { url = "https://files.pythonhosted.org/packages/2c/f0/ff520df18a6152ba2dbf808c964014308e71a48feb4c7563f2a6cd6e668d/spacy-3.8.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:718b7bb5e83c76cb841ed6e407f7b40255d0b46af7101a426c20e04af3afd64e", size = 32056451, upload-time = "2025-11-17T20:38:44.92Z" }, @@ -4226,13 +3344,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/cf/77/5633c4ba65e3421b72b5b4bd93aa328360b351b3a1e5bf3c90eb224668e5/srsly-2.5.2.tar.gz", hash = "sha256:4092bc843c71b7595c6c90a0302a197858c5b9fe43067f62ae6a45bc3baa1c19", size = 492055, upload-time = "2025-11-17T14:11:02.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/58/ff9fd981b6e0fae261c48a3a941aeca5735eace4a137de883c8d69029bc7/srsly-2.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5491fe0683da900cd0c563538510c70a007380e1f6b29ebbb5225e7590981e2a", size = 655635, upload-time = "2025-11-17T14:09:41.167Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a6/5b03c2a3b407caec3e7a5df61523154de3c5d36dc2f9328be91d3df368d5/srsly-2.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7375c2955935b73a6cad3851fe819c2f4ec506504afe7ca92b917555e6850fae", size = 653395, upload-time = "2025-11-17T14:09:42.827Z" }, - { url = "https://files.pythonhosted.org/packages/62/5d/1829a208d6d291c1ab3b81acd6e7a9f11984afc674ba2778e57984eee1a7/srsly-2.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0709a97ca463c1e85b03432c7d8028c82439f0248816707bafc553ffe66ec6f9", size = 1121898, upload-time = "2025-11-17T14:09:44.461Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ce/71766be1488ce4058dc5eded6f5c0ce7cbb18ff7263f3cc718fe8b1033ad/srsly-2.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea2ee0122312802ed531fee6de679d74ce99ce8addce49aff8d52ee670d810f8", size = 1122831, upload-time = "2025-11-17T14:09:46.011Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5c/259e5b0e70c22c5bbd1327a79bb4b2d75efb38295475229e9310251c240e/srsly-2.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c2e9fc418585832c7ce01bfc7fe85b96afe11165eb9a31ff0ed52aa3e32ec08b", size = 1080719, upload-time = "2025-11-17T14:09:47.685Z" }, - { url = "https://files.pythonhosted.org/packages/32/c4/20face1113cfa436434c7c152b374edae1631177d0d44dd60103297ffe03/srsly-2.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3df0ef22d571e733b181ac488823b01f4dd13da23497f46956839c718e48f36b", size = 1092783, upload-time = "2025-11-17T14:09:49.295Z" }, - { url = "https://files.pythonhosted.org/packages/c1/aa/16c405cf830bf3d843a631d62681403eb44563e27a42648f417f40209045/srsly-2.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:a116b926dd24702f5474f6367d8083412f218ddf82d5c7b5831a7b2ba3d8bd55", size = 654041, upload-time = "2025-11-17T14:09:51.056Z" }, { url = "https://files.pythonhosted.org/packages/59/6e/2e3d07b38c1c2e98487f0af92f93b392c6741062d85c65cdc18c7b77448a/srsly-2.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e07babdcece2405b32c9eea25ef415749f214c889545e38965622bb66837ce", size = 655286, upload-time = "2025-11-17T14:09:52.468Z" }, { url = "https://files.pythonhosted.org/packages/a1/e7/587bcade6b72f919133e587edf60e06039d88049aef9015cd0bdea8df189/srsly-2.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1718fe40b73e5cc73b14625233f57e15fb23643d146f53193e8fe653a49e9a0f", size = 653094, upload-time = "2025-11-17T14:09:53.837Z" }, { url = "https://files.pythonhosted.org/packages/8d/24/5c3aabe292cb4eb906c828f2866624e3a65603ef0a73e964e486ff146b84/srsly-2.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7b07e6103db7dd3199c0321935b0c8b9297fd6e018a66de97dc836068440111", size = 1141286, upload-time = "2025-11-17T14:09:55.535Z" }, @@ -4264,46 +3375,15 @@ wheels = [ ] [[package]] -name = "statsmodels" -version = "0.14.6" +name = "sympy" +version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "packaging" }, - { name = "pandas" }, - { name = "patsy" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/81/e8d74b34f85285f7335d30c5e3c2d7c0346997af9f3debf9a0a9a63de184/statsmodels-0.14.6.tar.gz", hash = "sha256:4d17873d3e607d398b85126cd4ed7aad89e4e9d89fc744cdab1af3189a996c2a", size = 20689085, upload-time = "2025-12-05T23:08:39.522Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/6d/9ec309a175956f88eb8420ac564297f37cf9b1f73f89db74da861052dc29/statsmodels-0.14.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4ff0649a2df674c7ffb6fa1a06bffdb82a6adf09a48e90e000a15a6aaa734b0", size = 10142419, upload-time = "2025-12-05T19:27:35.625Z" }, - { url = "https://files.pythonhosted.org/packages/86/8f/338c5568315ec5bf3ac7cd4b71e34b98cb3b0f834919c0c04a0762f878a1/statsmodels-0.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:109012088b3e370080846ab053c76d125268631410142daad2f8c10770e8e8d9", size = 10022819, upload-time = "2025-12-05T19:27:49.385Z" }, - { url = "https://files.pythonhosted.org/packages/b0/77/5fc4cbc2d608f9b483b0675f82704a8bcd672962c379fe4d82100d388dbf/statsmodels-0.14.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93bd5d220f3cb6fc5fc1bffd5b094966cab8ee99f6c57c02e95710513d6ac3f", size = 10118927, upload-time = "2025-12-05T23:07:51.256Z" }, - { url = "https://files.pythonhosted.org/packages/94/55/b86c861c32186403fe121d9ab27bc16d05839b170d92a978beb33abb995e/statsmodels-0.14.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06eec42d682fdb09fe5d70a05930857efb141754ec5a5056a03304c1b5e32fd9", size = 10413015, upload-time = "2025-12-05T23:08:53.95Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/daf0dba729ccdc4176605f4a0fd5cfe71cdda671749dca10e74a732b8b1c/statsmodels-0.14.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0444e88557df735eda7db330806fe09d51c9f888bb1f5906cb3a61fb1a3ed4a8", size = 10441248, upload-time = "2025-12-05T23:09:09.353Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1c/2e10b7c7cc44fa418272996bf0427b8016718fd62f995d9c1f7ab37adf35/statsmodels-0.14.6-cp310-cp310-win_amd64.whl", hash = "sha256:e83a9abe653835da3b37fb6ae04b45480c1de11b3134bd40b09717192a1456ea", size = 9583410, upload-time = "2025-12-05T19:28:02.086Z" }, - { url = "https://files.pythonhosted.org/packages/a9/4d/df4dd089b406accfc3bb5ee53ba29bb3bdf5ae61643f86f8f604baa57656/statsmodels-0.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ad5c2810fc6c684254a7792bf1cbaf1606cdee2a253f8bd259c43135d87cfb4", size = 10121514, upload-time = "2025-12-05T19:28:16.521Z" }, - { url = "https://files.pythonhosted.org/packages/82/af/ec48daa7f861f993b91a0dcc791d66e1cf56510a235c5cbd2ab991a31d5c/statsmodels-0.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:341fa68a7403e10a95c7b6e41134b0da3a7b835ecff1eb266294408535a06eb6", size = 10003346, upload-time = "2025-12-05T19:28:29.568Z" }, - { url = "https://files.pythonhosted.org/packages/a9/2c/c8f7aa24cd729970728f3f98822fb45149adc216f445a9301e441f7ac760/statsmodels-0.14.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf1dfe2a3ca56f5529118baf33a13efed2783c528f4a36409b46bbd2d9d48eb", size = 10129872, upload-time = "2025-12-05T23:09:25.724Z" }, - { url = "https://files.pythonhosted.org/packages/40/c6/9ae8e9b0721e9b6eb5f340c3a0ce8cd7cce4f66e03dd81f80d60f111987f/statsmodels-0.14.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3764ba8195c9baf0925a96da0743ff218067a269f01d155ca3558deed2658ca", size = 10381964, upload-time = "2025-12-05T23:09:41.326Z" }, - { url = "https://files.pythonhosted.org/packages/28/8c/cf3d30c8c2da78e2ad1f50ade8b7fabec3ff4cdfc56fbc02e097c4577f90/statsmodels-0.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e8d2e519852adb1b420e018f5ac6e6684b2b877478adf7fda2cfdb58f5acb5d", size = 10409611, upload-time = "2025-12-05T23:09:57.131Z" }, - { url = "https://files.pythonhosted.org/packages/bf/cc/018f14ecb58c6cb89de9d52695740b7d1f5a982aa9ea312483ea3c3d5f77/statsmodels-0.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:2738a00fca51196f5a7d44b06970ace6b8b30289839e4808d656f8a98e35faa7", size = 9580385, upload-time = "2025-12-05T19:28:42.778Z" }, - { url = "https://files.pythonhosted.org/packages/25/ce/308e5e5da57515dd7cab3ec37ea2d5b8ff50bef1fcc8e6d31456f9fae08e/statsmodels-0.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe76140ae7adc5ff0e60a3f0d56f4fffef484efa803c3efebf2fcd734d72ecb5", size = 10091932, upload-time = "2025-12-05T19:28:55.446Z" }, - { url = "https://files.pythonhosted.org/packages/05/30/affbabf3c27fb501ec7b5808230c619d4d1a4525c07301074eb4bda92fa9/statsmodels-0.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26d4f0ed3b31f3c86f83a92f5c1f5cbe63fc992cd8915daf28ca49be14463a1c", size = 9997345, upload-time = "2025-12-05T19:29:10.278Z" }, - { url = "https://files.pythonhosted.org/packages/48/f5/3a73b51e6450c31652c53a8e12e24eac64e3824be816c0c2316e7dbdcb7d/statsmodels-0.14.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c00a42863e4f4733ac9d078bbfad816249c01451740e6f5053ecc7db6d6368", size = 10058649, upload-time = "2025-12-05T23:10:12.775Z" }, - { url = "https://files.pythonhosted.org/packages/81/68/dddd76117df2ef14c943c6bbb6618be5c9401280046f4ddfc9fb4596a1b8/statsmodels-0.14.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b58cf7474aa9e7e3b0771a66537148b2df9b5884fbf156096c0e6c1ff0469d", size = 10339446, upload-time = "2025-12-05T23:10:28.503Z" }, - { url = "https://files.pythonhosted.org/packages/56/4a/dce451c74c4050535fac1ec0c14b80706d8fc134c9da22db3c8a0ec62c33/statsmodels-0.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81e7dcc5e9587f2567e52deaff5220b175bf2f648951549eae5fc9383b62bc37", size = 10368705, upload-time = "2025-12-05T23:10:44.339Z" }, - { url = "https://files.pythonhosted.org/packages/60/15/3daba2df40be8b8a9a027d7f54c8dedf24f0d81b96e54b52293f5f7e3418/statsmodels-0.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:b5eb07acd115aa6208b4058211138393a7e6c2cf12b6f213ede10f658f6a714f", size = 9543991, upload-time = "2025-12-05T23:10:58.536Z" }, + { name = "mpmath" }, ] - -[[package]] -name = "tenacity" -version = "9.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] @@ -4352,13 +3432,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/2f/3a/2d0f0be132b9faaa6d56f04565ae122684273e4bf4eab8dee5f48dc00f68/thinc-8.3.10.tar.gz", hash = "sha256:5a75109f4ee1c968fc055ce651a17cb44b23b000d9e95f04a4d047ab3cb3e34e", size = 194196, upload-time = "2025-11-17T17:21:46.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/bc/d3c364c0278e420e0e3d328cbae7cd7aac8d2cfe4d9b8022a12e99f03755/thinc-8.3.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fbe0313cb3c898f4e6a3f13b704af51f4bf8f927078deb0fe2d6eaf3c6c5b31b", size = 821615, upload-time = "2025-11-17T17:20:31.257Z" }, - { url = "https://files.pythonhosted.org/packages/0e/97/70fe96d86fe5d024882fd96f054be94f87828da67862749aa439de33d452/thinc-8.3.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:892ac91cf7cc8d3ac9a4527c68ead37a96e87132c9f589de56b057b50358e895", size = 772280, upload-time = "2025-11-17T17:20:34.408Z" }, - { url = "https://files.pythonhosted.org/packages/08/a8/a6906490a756a4ad09781bcd02490e5427d942a918abed8424f639d317c3/thinc-8.3.10-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0fbf142050feb5490f6366e251d48e0429315abe487faa7d371fac4d043efd1e", size = 3881222, upload-time = "2025-11-17T17:20:36.525Z" }, - { url = "https://files.pythonhosted.org/packages/e6/bf/bebeddbab816c4d909455499f7e1b0a88cec9497fd737412e1189971d193/thinc-8.3.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:470b05fd1af4024cf183f387f71270943f652dd711304d1fa8b672d268052af8", size = 3905534, upload-time = "2025-11-17T17:20:38.901Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c4/c78f1e1091b73dbeee8623f856e2dd25888aab600ded5fa9944dfbe38efb/thinc-8.3.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06ebf4aa642991b8dc5c2a6db4c0aedf6d5589a361c93531ec3721d76eabe859", size = 4888188, upload-time = "2025-11-17T17:20:41.394Z" }, - { url = "https://files.pythonhosted.org/packages/ca/bc/36297efade38e0f3e56795f49094d19fbe560bda60a42ce134bbfc1796da/thinc-8.3.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:026999d749075c890fbb1df47d75389a81b712afccea519a5c7bb86783d0cd73", size = 5033361, upload-time = "2025-11-17T17:20:45.332Z" }, - { url = "https://files.pythonhosted.org/packages/a8/bf/70d97758b5b1c7ee06afca8240b6e02bdf5b18d18eb59b873e319b3e01b2/thinc-8.3.10-cp310-cp310-win_amd64.whl", hash = "sha256:8d5ae7d96ff3ea2e4f23bd4005c773f4765f41b11dfb79598a81e5feb1437b91", size = 1792397, upload-time = "2025-11-17T17:20:47.014Z" }, { url = "https://files.pythonhosted.org/packages/38/43/01b662540888140b5e9f76c957c7118c203cb91f17867ce78fc4f2d3800f/thinc-8.3.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72793e0bd3f0f391ca36ab0996b3c21db7045409bd3740840e7d6fcd9a044d81", size = 818632, upload-time = "2025-11-17T17:20:49.123Z" }, { url = "https://files.pythonhosted.org/packages/f0/ba/e0edcc84014bdde1bc9a082408279616a061566a82b5e3b90b9e64f33c1b/thinc-8.3.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b13311acb061e04e3a0c4bd677b85ec2971e3a3674558252443b5446e378256", size = 770622, upload-time = "2025-11-17T17:20:50.467Z" }, { url = "https://files.pythonhosted.org/packages/f3/51/0558f8cb69c13e1114428726a3fb36fe1adc5821a62ccd3fa7b7c1a5bd9a/thinc-8.3.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ffddcf311fb7c998eb8988d22c618dc0f33b26303853c0445edb8a69819ac60", size = 4094652, upload-time = "2025-11-17T17:20:52.104Z" }, @@ -4377,15 +3450,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/44/32e2a5018a1165a304d25eb9b1c74e5310da19a533a35331e8d824dc6a88/thinc-8.3.10-cp312-cp312-win_arm64.whl", hash = "sha256:234b7e57a6ef4e0260d99f4e8fdc328ed12d0ba9bbd98fdaa567294a17700d1c", size = 1642224, upload-time = "2025-11-17T17:21:14.371Z" }, ] -[[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" @@ -4396,13 +3460,6 @@ dependencies = [ ] 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/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, - { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, - { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, - { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, { 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" }, @@ -4455,37 +3512,6 @@ wheels = [ { 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" }, - { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, - { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, - { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] @@ -4604,25 +3630,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] -[[package]] -name = "umap-learn" -version = "0.5.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numba" }, - { name = "numpy" }, - { name = "pynndescent" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/9a/a1e4a257a9aa979dac4f6d5781dac929cbb0949959e2003ed82657d10b0f/umap_learn-0.5.11.tar.gz", hash = "sha256:31566ffd495fbf05d7ab3efcba703861c0f5e6fc6998a838d0e2becdd00e54f5", size = 96409, upload-time = "2026-01-12T20:44:47.553Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/d2/fcf7192dd1cd8c090b6cfd53fa223c4fb2887a17c47e06bc356d44f40dfb/umap_learn-0.5.11-py3-none-any.whl", hash = "sha256:cb17adbde9d544ba79481b3ab4d81ac222e940f3d9219307bea6044f869af3cc", size = 90890, upload-time = "2026-01-12T20:44:46.511Z" }, -] - [[package]] name = "update-toml" version = "0.2.1" @@ -4671,17 +3678,12 @@ 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/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, { 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/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, { 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" }, @@ -4696,11 +3698,11 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.3.1" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/6f/e1ea6dcb21da43d581284d8d5a715c2affb906aa3ed301f77f7f5ae0e7d5/wcwidth-0.3.1.tar.gz", hash = "sha256:5aedb626a9c0d941b990cfebda848d538d45c9493a3384d080aff809143bd3be", size = 233057, upload-time = "2026-01-22T22:08:25.231Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/6e/62daec357285b927e82263a81f3b4c1790215bc77c42530ce4a69d501a43/wcwidth-0.5.0.tar.gz", hash = "sha256:f89c103c949a693bf563377b2153082bf58e309919dfb7f27b04d862a0089333", size = 246585, upload-time = "2026-01-27T01:31:44.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/9c/9d951691bf1224772f6082d3b2e8c110edfd9622627908ad75bb0f691979/wcwidth-0.3.1-py3-none-any.whl", hash = "sha256:b2d355df3ec5d51bfc973a22fb4ea9a03b12fdcbf00d0abd22a2c78b12ccc177", size = 85746, upload-time = "2026-01-22T22:08:23.564Z" }, + { url = "https://files.pythonhosted.org/packages/f2/3e/45583b67c2ff08ad5a582d316fcb2f11d6cf0a50c7707ac09d212d25bc98/wcwidth-0.5.0-py3-none-any.whl", hash = "sha256:1efe1361b83b0ff7877b81ba57c8562c99cf812158b778988ce17ec061095695", size = 93772, upload-time = "2026-01-27T01:31:43.432Z" }, ] [[package]] @@ -4765,18 +3767,6 @@ version = "2.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/0d/12d8c803ed2ce4e5e7d5b9f5f602721f9dfef82c95959f3ce97fa584bb5c/wrapt-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64b103acdaa53b7caf409e8d45d39a8442fe6dcfec6ba3f3d141e0cc2b5b4dbd", size = 77481, upload-time = "2025-11-07T00:43:11.103Z" }, - { url = "https://files.pythonhosted.org/packages/05/3e/4364ebe221ebf2a44d9fc8695a19324692f7dd2795e64bd59090856ebf12/wrapt-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91bcc576260a274b169c3098e9a3519fb01f2989f6d3d386ef9cbf8653de1374", size = 60692, upload-time = "2025-11-07T00:43:13.697Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ff/ae2a210022b521f86a8ddcdd6058d137c051003812b0388a5e9a03d3fe10/wrapt-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab594f346517010050126fcd822697b25a7031d815bb4fbc238ccbe568216489", size = 61574, upload-time = "2025-11-07T00:43:14.967Z" }, - { url = "https://files.pythonhosted.org/packages/c6/93/5cf92edd99617095592af919cb81d4bff61c5dbbb70d3c92099425a8ec34/wrapt-2.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:36982b26f190f4d737f04a492a68accbfc6fa042c3f42326fdfbb6c5b7a20a31", size = 113688, upload-time = "2025-11-07T00:43:18.275Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0a/e38fc0cee1f146c9fb266d8ef96ca39fb14a9eef165383004019aa53f88a/wrapt-2.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23097ed8bc4c93b7bf36fa2113c6c733c976316ce0ee2c816f64ca06102034ef", size = 115698, upload-time = "2025-11-07T00:43:19.407Z" }, - { url = "https://files.pythonhosted.org/packages/b0/85/bef44ea018b3925fb0bcbe9112715f665e4d5309bd945191da814c314fd1/wrapt-2.0.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bacfe6e001749a3b64db47bcf0341da757c95959f592823a93931a422395013", size = 112096, upload-time = "2025-11-07T00:43:16.5Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0b/733a2376e413117e497aa1a5b1b78e8f3a28c0e9537d26569f67d724c7c5/wrapt-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8ec3303e8a81932171f455f792f8df500fc1a09f20069e5c16bd7049ab4e8e38", size = 114878, upload-time = "2025-11-07T00:43:20.81Z" }, - { url = "https://files.pythonhosted.org/packages/da/03/d81dcb21bbf678fcda656495792b059f9d56677d119ca022169a12542bd0/wrapt-2.0.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3f373a4ab5dbc528a94334f9fe444395b23c2f5332adab9ff4ea82f5a9e33bc1", size = 111298, upload-time = "2025-11-07T00:43:22.229Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d5/5e623040e8056e1108b787020d56b9be93dbbf083bf2324d42cde80f3a19/wrapt-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f49027b0b9503bf6c8cdc297ca55006b80c2f5dd36cecc72c6835ab6e10e8a25", size = 113361, upload-time = "2025-11-07T00:43:24.301Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f3/de535ccecede6960e28c7b722e5744846258111d6c9f071aa7578ea37ad3/wrapt-2.0.1-cp310-cp310-win32.whl", hash = "sha256:8330b42d769965e96e01fa14034b28a2a7600fbf7e8f0cc90ebb36d492c993e4", size = 58035, upload-time = "2025-11-07T00:43:28.96Z" }, - { url = "https://files.pythonhosted.org/packages/21/15/39d3ca5428a70032c2ec8b1f1c9d24c32e497e7ed81aed887a4998905fcc/wrapt-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1218573502a8235bb8a7ecaed12736213b22dcde9feab115fa2989d42b5ded45", size = 60383, upload-time = "2025-11-07T00:43:25.804Z" }, - { url = "https://files.pythonhosted.org/packages/43/c2/dfd23754b7f7a4dce07e08f4309c4e10a40046a83e9ae1800f2e6b18d7c1/wrapt-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:eda8e4ecd662d48c28bb86be9e837c13e45c58b8300e43ba3c9b4fa9900302f7", size = 58894, upload-time = "2025-11-07T00:43:27.074Z" }, { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" }, { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" }, { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" }, @@ -4815,22 +3805,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, - { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, - { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, - { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, - { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, - { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, - { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, - { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, - { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, - { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, - { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, - { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, - { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, - { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, - { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" },