From b5d20428f25a2e254322cdee92e3141cc17948c4 Mon Sep 17 00:00:00 2001 From: kl3inIT Date: Sat, 25 Jul 2026 05:25:24 +0700 Subject: [PATCH 1/3] Complete secure GraphRAG assistant experience --- .github/workflows/ci.yml | 42 + .gitignore | 3 + apps/api/build.gradle.kts | 2 + .../api/OrgMemoryApiApplication.java | 4 +- .../api/assistant/AssistantController.java | 15 +- .../api/assistant/AssistantStreamPart.java | 6 +- .../api/assistant/UiMessageStream.java | 6 +- .../knowledge/CitationContentController.java | 44 +- .../KnowledgeAssetLifecycleController.java | 12 + .../api/source/SourceController.java | 1 - apps/api/src/main/resources/application.yml | 14 + .../AssistantControllerStreamingTests.java | 4 +- .../api/assistant/UiMessageStreamTests.java | 7 +- .../CitationContentControllerTests.java | 52 +- .../source/SourceUploadIntegrationTests.java | 1 - apps/worker/build.gradle.kts | 2 + .../worker/OrgMemoryWorkerApplication.java | 2 + .../worker/graph/GraphIndexingProcessor.java | 198 +- .../worker/graph/GraphIndexingProperties.java | 53 +- .../graph/GraphPublicationCommitter.java | 27 +- .../graph/SpringAiGraphExtractorFactory.java | 3 +- .../worker/src/main/resources/application.yml | 22 +- .../graph/GraphIndexingProcessorTests.java | 86 +- .../graph/GraphPublicationCommitterTests.java | 24 +- ...urceIngestionPipelineIntegrationTests.java | 31 +- components/graph-rag-core/build.gradle.kts | 8 + .../extraction/LightRagExtractionPrompt.java | 12 + .../indexing/GraphContributionAssembler.java | 3 + .../indexing/LightRagEmbeddingPayloads.java | 52 + .../observability/GraphRagEventSink.java | 14 + .../port/GraphProjectionManifest.java | 20 +- .../port/GraphRevisionProjection.java | 18 +- .../processing/GraphProcessingProfile.java | 287 +++ .../LightRagGraphProcessingProfiles.java | 26 + .../LightRagEmbeddingPayloadsTests.java | 30 + .../observability/GraphRagEventSinkTests.java | 44 + .../port/GraphProjectionManifestTests.java | 25 +- .../GraphProcessingProfileTests.java | 115 + .../query/LightRagUpstreamOracleTests.java | 120 + contracts/openapi.json | 2 +- .../core/assistant/AssistantCitation.java | 21 + .../assistant/AssistantPromptFactory.java | 12 +- .../core/assistant/AssistantService.java | 5 +- .../core/assistant/AssistantTurn.java | 11 +- .../core/knowledge/ClaimedGraphIndex.java | 2 + .../knowledge/CreateUploadSourceCommand.java | 1 - .../core/knowledge/GraphIndexJob.java | 37 +- .../core/knowledge/GraphIndexJobQueue.java | 73 +- .../knowledge/GraphIndexJobRepository.java | 6 +- .../core/knowledge/GraphIndexJobView.java | 2 + .../knowledge/GraphIndexLifecycleService.java | 51 +- .../knowledge/GraphIndexingCoordinator.java | 12 +- .../knowledge/GraphProcessingProfileRef.java | 20 + .../GraphProcessingProfileRegistry.java | 65 + .../GraphProcessingProfileRepository.java | 11 + .../GraphProcessingProfileResolver.java | 40 + .../knowledge/GraphProcessingProperties.java | 79 + ...aphRagKnowledgeRetrievalConfiguration.java | 8 +- .../GraphRagKnowledgeRetrievalService.java | 92 +- .../PersistedGraphProcessingProfile.java | 35 + .../core/knowledge/SourceUploadService.java | 9 +- .../resources/db/migration/V1__baseline.sql | 44 +- .../core/assistant/AssistantServiceTests.java | 18 +- .../GraphIndexLifecycleServiceTests.java | 97 + .../GraphIndexingCoordinatorTests.java | 49 +- ...raphRagKnowledgeRetrievalServiceTests.java | 14 +- .../knowledge/SourceUploadServiceTests.java | 68 +- .../lightrag-v1.5.4-parity-manifest.md | 12 +- .../graph-rag-production-hardening.md | 123 + docs/specs/domains/assistant-and-mcp.md | 19 +- docs/specs/domains/knowledge-ingestion.md | 6 +- docs/specs/domains/secure-graph-rag.md | 13 +- docs/tests/domains/assistant-and-mcp.md | 6 + evaluation/README.md | 56 + .../baselines/lightrag-v1.5.4-oracle.json | 92 + evaluation/fixtures/sample-evaluation.json | 20 + evaluation/oracle/generate_lightrag_v1_5_4.py | 211 ++ evaluation/pyproject.toml | 41 + evaluation/src/orgmemory_eval/__init__.py | 1 + evaluation/src/orgmemory_eval/models.py | 37 + evaluation/src/orgmemory_eval/ragas_runner.py | 352 +++ evaluation/tests/test_models.py | 45 + evaluation/tests/test_ragas_runner.py | 136 ++ evaluation/tests/test_upstream_oracle.py | 33 + evaluation/uv.lock | 2023 +++++++++++++++++ .../OpenAiCompatibleChatModelProvider.java | 5 +- .../graph-rag-observability/build.gradle.kts | 14 + ...raphRagObservabilityAutoConfiguration.java | 21 + .../OpenTelemetryGraphRagEventSink.java | 95 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../OpenTelemetryGraphRagEventSinkTests.java | 85 + settings.gradle.kts | 1 + web/package.json | 2 + web/playwright.config.ts | 34 + web/pnpm-lock.yaml | 38 + .../ai-elements/message-response.tsx | 5 +- .../assistant/components/assistant-answer.tsx | 131 ++ .../assistant/components/assistant-page.tsx | 117 +- .../components/assistant-sources-panel.tsx | 94 +- web/test/e2e/assistant-pipeline.spec.ts | 341 +++ 100 files changed, 6194 insertions(+), 335 deletions(-) create mode 100644 components/graph-rag-core/src/main/java/com/orgmemory/graphrag/indexing/LightRagEmbeddingPayloads.java create mode 100644 components/graph-rag-core/src/main/java/com/orgmemory/graphrag/processing/GraphProcessingProfile.java create mode 100644 components/graph-rag-core/src/main/java/com/orgmemory/graphrag/processing/LightRagGraphProcessingProfiles.java create mode 100644 components/graph-rag-core/src/test/java/com/orgmemory/graphrag/indexing/LightRagEmbeddingPayloadsTests.java create mode 100644 components/graph-rag-core/src/test/java/com/orgmemory/graphrag/observability/GraphRagEventSinkTests.java create mode 100644 components/graph-rag-core/src/test/java/com/orgmemory/graphrag/processing/GraphProcessingProfileTests.java create mode 100644 components/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagUpstreamOracleTests.java create mode 100644 core/src/main/java/com/orgmemory/core/assistant/AssistantCitation.java create mode 100644 core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRef.java create mode 100644 core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRegistry.java create mode 100644 core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRepository.java create mode 100644 core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileResolver.java create mode 100644 core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProperties.java create mode 100644 core/src/main/java/com/orgmemory/core/knowledge/PersistedGraphProcessingProfile.java create mode 100644 core/src/test/java/com/orgmemory/core/knowledge/GraphIndexLifecycleServiceTests.java create mode 100644 docs/runbooks/graph-rag-production-hardening.md create mode 100644 evaluation/README.md create mode 100644 evaluation/baselines/lightrag-v1.5.4-oracle.json create mode 100644 evaluation/fixtures/sample-evaluation.json create mode 100644 evaluation/oracle/generate_lightrag_v1_5_4.py create mode 100644 evaluation/pyproject.toml create mode 100644 evaluation/src/orgmemory_eval/__init__.py create mode 100644 evaluation/src/orgmemory_eval/models.py create mode 100644 evaluation/src/orgmemory_eval/ragas_runner.py create mode 100644 evaluation/tests/test_models.py create mode 100644 evaluation/tests/test_ragas_runner.py create mode 100644 evaluation/tests/test_upstream_oracle.py create mode 100644 evaluation/uv.lock create mode 100644 integrations/graph-rag-observability/build.gradle.kts create mode 100644 integrations/graph-rag-observability/src/main/java/com/orgmemory/integrations/graphrag/observability/GraphRagObservabilityAutoConfiguration.java create mode 100644 integrations/graph-rag-observability/src/main/java/com/orgmemory/integrations/graphrag/observability/OpenTelemetryGraphRagEventSink.java create mode 100644 integrations/graph-rag-observability/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 integrations/graph-rag-observability/src/test/java/com/orgmemory/integrations/graphrag/observability/OpenTelemetryGraphRagEventSinkTests.java create mode 100644 web/playwright.config.ts create mode 100644 web/src/features/assistant/components/assistant-answer.tsx create mode 100644 web/test/e2e/assistant-pipeline.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8e18f8c..72bbf59a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,12 @@ jobs: - name: Build production bundle run: pnpm build + - name: Install Playwright browser + run: pnpm exec playwright install --with-deps chromium + + - name: Run browser tests + run: pnpm test:e2e + - name: Check generated route tree run: git diff --exit-code -- src/routeTree.gen.ts @@ -108,6 +114,39 @@ jobs: working-directory: integrations/authorization-openfga/src/test/openfga run: fga model test --tests store.fga.yaml + evaluation: + name: Evaluation · Python 3.13 + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: evaluation + steps: + - name: Check out repository + uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v6.2.0 + with: + python-version: "3.13" + + - name: Set up uv + uses: astral-sh/setup-uv@v8.1.0 + with: + enable-cache: true + cache-dependency-glob: evaluation/uv.lock + + - name: Install locked evaluation environment + run: uv sync --frozen --dev + + - name: Lint evaluation tooling + run: uv run ruff check . + + - name: Test deterministic evaluation contracts + run: uv run pytest + gate: name: CI Gate if: always() @@ -115,6 +154,7 @@ jobs: - backend - web - openfga + - evaluation runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -124,8 +164,10 @@ jobs: BACKEND_RESULT: ${{ needs.backend.result }} WEB_RESULT: ${{ needs.web.result }} OPENFGA_RESULT: ${{ needs.openfga.result }} + EVALUATION_RESULT: ${{ needs.evaluation.result }} run: | set -euo pipefail [[ "$BACKEND_RESULT" == "success" ]] [[ "$WEB_RESULT" == "success" ]] [[ "$OPENFGA_RESULT" == "success" ]] + [[ "$EVALUATION_RESULT" == "success" ]] diff --git a/.gitignore b/.gitignore index bb64b2ff..09fbd8df 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,9 @@ tmp/ output/ test-results/ .tools/ +.venv/ +__pycache__/ +*.py[cod] # --- OS --- .DS_Store diff --git a/apps/api/build.gradle.kts b/apps/api/build.gradle.kts index 941e653a..666ccbae 100644 --- a/apps/api/build.gradle.kts +++ b/apps/api/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation(project(":integrations:authorization-openfga")) implementation(project(":integrations:connectors")) implementation(project(":integrations:graph-rag-postgres")) + implementation(project(":integrations:graph-rag-observability")) implementation(project(":integrations:graph-rag-spring-ai")) implementation(project(":integrations:object-storage-minio")) @@ -18,6 +19,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-security-oauth2-resource-server") implementation("org.springframework.boot:spring-boot-starter-session-jdbc") implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-opentelemetry") implementation("org.springframework.boot:spring-boot-starter-flyway") implementation("org.flywaydb:flyway-database-postgresql") implementation(libs.springdoc.webmvc) diff --git a/apps/api/src/main/java/com/orgmemory/api/OrgMemoryApiApplication.java b/apps/api/src/main/java/com/orgmemory/api/OrgMemoryApiApplication.java index fb026dda..95afbcc9 100644 --- a/apps/api/src/main/java/com/orgmemory/api/OrgMemoryApiApplication.java +++ b/apps/api/src/main/java/com/orgmemory/api/OrgMemoryApiApplication.java @@ -1,6 +1,7 @@ package com.orgmemory.api; import com.orgmemory.core.knowledge.GraphExplorerProperties; +import com.orgmemory.core.knowledge.GraphProcessingProperties; import com.orgmemory.core.knowledge.KnowledgeEmbeddingProperties; import com.orgmemory.core.knowledge.KnowledgeRetrievalProperties; import com.orgmemory.core.knowledge.SourceIngestionProperties; @@ -24,7 +25,8 @@ SecretCipherProperties.class, KnowledgeRetrievalProperties.class, KnowledgeEmbeddingProperties.class, - GraphExplorerProperties.class + GraphExplorerProperties.class, + GraphProcessingProperties.class }) public class OrgMemoryApiApplication { diff --git a/apps/api/src/main/java/com/orgmemory/api/assistant/AssistantController.java b/apps/api/src/main/java/com/orgmemory/api/assistant/AssistantController.java index 47bb5949..cc2bd1af 100644 --- a/apps/api/src/main/java/com/orgmemory/api/assistant/AssistantController.java +++ b/apps/api/src/main/java/com/orgmemory/api/assistant/AssistantController.java @@ -1,9 +1,9 @@ package com.orgmemory.api.assistant; import com.orgmemory.api.security.CurrentActorProvider; +import com.orgmemory.core.assistant.AssistantCitation; import com.orgmemory.core.assistant.AssistantService; import com.orgmemory.core.assistant.AssistantTurn; -import com.orgmemory.core.knowledge.RetrievedKnowledgeEvidence; import com.orgmemory.core.organization.CurrentActor; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; @@ -83,20 +83,25 @@ Flux parts(AssistantTurn turn) { : streamedTokens); return Flux.concat( Flux.just(new AssistantStreamPart.StartStep()), - Flux.fromIterable(turn.evidence()) + Flux.fromIterable(turn.citations()) .map(AssistantController::sourcePart), text, Flux.just(new AssistantStreamPart.FinishStep())); } - private static AssistantStreamPart sourcePart(RetrievedKnowledgeEvidence evidence) { - String sourceId = "urn:orgmemory:citation:" + evidence.chunkId(); + private static AssistantStreamPart sourcePart(AssistantCitation citation) { + var evidence = citation.evidence(); + String sourceId = "urn:orgmemory:citation:" + + citation.number() + + ":" + + evidence.chunkId(); String title = evidence.startPage() == null ? evidence.title() : evidence.title() + " · page " + evidence.startPage(); return new AssistantStreamPart.SourceUrl( sourceId, "/api/citations/" + evidence.chunkId() + "/content", - title); + title, + citation.number()); } } diff --git a/apps/api/src/main/java/com/orgmemory/api/assistant/AssistantStreamPart.java b/apps/api/src/main/java/com/orgmemory/api/assistant/AssistantStreamPart.java index 6ad50210..0f02035b 100644 --- a/apps/api/src/main/java/com/orgmemory/api/assistant/AssistantStreamPart.java +++ b/apps/api/src/main/java/com/orgmemory/api/assistant/AssistantStreamPart.java @@ -17,7 +17,11 @@ record TextDelta(String id, String delta) implements AssistantStreamPart { record TextEnd(String id) implements AssistantStreamPart { } - record SourceUrl(String sourceId, String url, String title) implements AssistantStreamPart { + record SourceUrl( + String sourceId, + String url, + String title, + int citationNumber) implements AssistantStreamPart { } record SourceDocument(String sourceId, String mediaType, String title, String filename) diff --git a/apps/api/src/main/java/com/orgmemory/api/assistant/UiMessageStream.java b/apps/api/src/main/java/com/orgmemory/api/assistant/UiMessageStream.java index f3714a63..0d6f7b12 100644 --- a/apps/api/src/main/java/com/orgmemory/api/assistant/UiMessageStream.java +++ b/apps/api/src/main/java/com/orgmemory/api/assistant/UiMessageStream.java @@ -104,7 +104,11 @@ private static Map payload(AssistantStreamPart part) { "type", "source-url", "sourceId", source.sourceId(), "url", source.url(), - "title", source.title()); + "title", source.title(), + "providerMetadata", fields( + "orgmemory", fields( + "citationNumber", + source.citationNumber()))); case AssistantStreamPart.SourceDocument source -> fields( "type", "source-document", "sourceId", source.sourceId(), diff --git a/apps/api/src/main/java/com/orgmemory/api/knowledge/CitationContentController.java b/apps/api/src/main/java/com/orgmemory/api/knowledge/CitationContentController.java index d31514ae..af280a20 100644 --- a/apps/api/src/main/java/com/orgmemory/api/knowledge/CitationContentController.java +++ b/apps/api/src/main/java/com/orgmemory/api/knowledge/CitationContentController.java @@ -3,6 +3,8 @@ import com.orgmemory.api.security.CurrentActorProvider; import com.orgmemory.core.knowledge.CitationContentService; import io.swagger.v3.oas.annotations.Operation; +import java.nio.file.Path; +import java.util.Locale; import java.util.UUID; import org.springframework.http.CacheControl; import org.springframework.http.ContentDisposition; @@ -47,14 +49,16 @@ ResponseEntity content( citation.stream().transferTo(output); } }; + MediaType responseMediaType = safeMediaType( + citation.fileName()); return ResponseEntity.ok() - .contentType(safeMediaType(citation.mediaType())) + .contentType(responseMediaType) .contentLength(citation.contentLength()) .cacheControl(CacheControl.noStore()) .header("X-Request-ID", requestId) .header( HttpHeaders.CONTENT_DISPOSITION, - ContentDisposition.inline() + contentDisposition(responseMediaType) .filename( citation.fileName(), java.nio.charset.StandardCharsets.UTF_8) @@ -64,11 +68,37 @@ ResponseEntity content( .body(body); } - private static MediaType safeMediaType(String value) { - try { - return MediaType.parseMediaType(value); - } catch (IllegalArgumentException ignored) { - return MediaType.APPLICATION_OCTET_STREAM; + private static MediaType safeMediaType(String fileName) { + String normalized = Path.of(fileName) + .getFileName() + .toString(); + int separator = normalized.lastIndexOf('.'); + String extension = separator < 0 + ? "" + : normalized.substring(separator + 1) + .toLowerCase(Locale.ROOT); + return switch (extension) { + case "pdf" -> MediaType.APPLICATION_PDF; + case "txt", "md" -> MediaType.TEXT_PLAIN; + case "png" -> MediaType.IMAGE_PNG; + case "jpg", "jpeg" -> MediaType.IMAGE_JPEG; + case "gif" -> MediaType.IMAGE_GIF; + case "webp" -> MediaType.parseMediaType("image/webp"); + case "docx" -> MediaType.parseMediaType( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + case "pptx" -> MediaType.parseMediaType( + "application/vnd.openxmlformats-officedocument.presentationml.presentation"); + default -> MediaType.APPLICATION_OCTET_STREAM; + }; + } + + private static ContentDisposition.Builder contentDisposition( + MediaType mediaType) { + if (MediaType.APPLICATION_PDF.equals(mediaType) + || MediaType.TEXT_PLAIN.equals(mediaType) + || "image".equals(mediaType.getType())) { + return ContentDisposition.inline(); } + return ContentDisposition.attachment(); } } diff --git a/apps/api/src/main/java/com/orgmemory/api/knowledge/KnowledgeAssetLifecycleController.java b/apps/api/src/main/java/com/orgmemory/api/knowledge/KnowledgeAssetLifecycleController.java index 50d3681c..f2ca292b 100644 --- a/apps/api/src/main/java/com/orgmemory/api/knowledge/KnowledgeAssetLifecycleController.java +++ b/apps/api/src/main/java/com/orgmemory/api/knowledge/KnowledgeAssetLifecycleController.java @@ -72,4 +72,16 @@ GraphIndexJobView resumeGraphJob( @PathVariable UUID jobId, Authentication authentication) { return graphJobs.resume(actors.current(authentication), jobId); } + + @PostMapping("/{knowledgeAssetId}/graph-index") + @Operation( + operationId = "ensureKnowledgeAssetGraphIndex", + summary = "Ensure graph indexing uses the current processing profile") + @ResponseStatus(HttpStatus.ACCEPTED) + GraphIndexJobView ensureGraphIndex( + @PathVariable UUID knowledgeAssetId, + Authentication authentication) { + return graphJobs.ensureCurrentProfile( + actors.current(authentication), knowledgeAssetId); + } } diff --git a/apps/api/src/main/java/com/orgmemory/api/source/SourceController.java b/apps/api/src/main/java/com/orgmemory/api/source/SourceController.java index e88cbe03..d67fa98c 100644 --- a/apps/api/src/main/java/com/orgmemory/api/source/SourceController.java +++ b/apps/api/src/main/java/com/orgmemory/api/source/SourceController.java @@ -58,7 +58,6 @@ SourceResponse upload( new CreateUploadSourceCommand( actor, file.getOriginalFilename(), - file.getContentType(), file.getSize(), classification, knowledgeSpaceId), diff --git a/apps/api/src/main/resources/application.yml b/apps/api/src/main/resources/application.yml index 4bb7c63b..092a76ac 100644 --- a/apps/api/src/main/resources/application.yml +++ b/apps/api/src/main/resources/application.yml @@ -2,6 +2,10 @@ spring: application: name: orgmemory-api ai: + chat: + observations: + log-prompt: false + log-completion: false model: chat: ${ORGMEMORY_AI_MODEL_CHAT:none} embedding: ${ORGMEMORY_AI_MODEL_EMBEDDING:none} @@ -90,6 +94,13 @@ orgmemory: model: ${ORGMEMORY_OPENAI_EMBEDDING_MODEL:text-embedding-3-large} dimensions: ${ORGMEMORY_OPENAI_EMBEDDING_DIMENSIONS:1536} graph-rag: + processing: + maximum-entities-per-chunk: ${ORGMEMORY_GRAPH_MAX_ENTITIES_PER_CHUNK:40} + maximum-relations-per-chunk: ${ORGMEMORY_GRAPH_MAX_RELATIONS_PER_CHUNK:60} + entity-type-guidance: ${ORGMEMORY_GRAPH_ENTITY_TYPES:PERSON,ORGANIZATION,TEAM,ROLE,POLICY,PROCESS,SYSTEM,PRODUCT,DOCUMENT,LOCATION,EVENT,CONCEPT,OTHER} + maximum-gleaning-rounds: ${ORGMEMORY_GRAPH_MAX_GLEANING_ROUNDS:1} + maximum-gleaning-input-tokens: ${ORGMEMORY_GRAPH_MAX_GLEANING_INPUT_TOKENS:24000} + maximum-section-context-tokens: ${ORGMEMORY_GRAPH_MAX_SECTION_CONTEXT_TOKENS:256} explorer: default-entity-limit: ${ORGMEMORY_GRAPH_EXPLORER_DEFAULT_ENTITY_LIMIT:200} maximum-entity-limit: ${ORGMEMORY_GRAPH_EXPLORER_MAXIMUM_ENTITY_LIMIT:1000} @@ -136,6 +147,9 @@ server: same-site: lax management: + tracing: + sampling: + probability: ${ORGMEMORY_TRACING_SAMPLING_PROBABILITY:0.1} endpoints: web: exposure: diff --git a/apps/api/src/test/java/com/orgmemory/api/assistant/AssistantControllerStreamingTests.java b/apps/api/src/test/java/com/orgmemory/api/assistant/AssistantControllerStreamingTests.java index e39ab1ec..779e6e93 100644 --- a/apps/api/src/test/java/com/orgmemory/api/assistant/AssistantControllerStreamingTests.java +++ b/apps/api/src/test/java/com/orgmemory/api/assistant/AssistantControllerStreamingTests.java @@ -5,6 +5,7 @@ import static org.mockito.Mockito.mock; import com.orgmemory.api.security.CurrentActorProvider; +import com.orgmemory.core.assistant.AssistantCitation; import com.orgmemory.core.assistant.AssistantService; import com.orgmemory.core.assistant.AssistantTurn; import com.orgmemory.core.knowledge.RetrievedKnowledgeEvidence; @@ -24,7 +25,7 @@ void streamsTheVerifiedRequestSnapshotWithoutWaitingForModelCompletion() { Sinks.many().unicast().onBackpressureBuffer(); AssistantTurn turn = new AssistantTurn( "request-1", - List.of(evidence), + List.of(new AssistantCitation(1, evidence)), model.asFlux()); StepVerifier.create(controller().parts(turn)) @@ -40,6 +41,7 @@ void streamsTheVerifiedRequestSnapshotWithoutWaitingForModelCompletion() { + evidence.chunkId() + "/content", source.url()); + assertEquals(1, source.citationNumber()); }) .then(() -> model.tryEmitNext("answer")) .assertNext(part -> assertInstanceOf( diff --git a/apps/api/src/test/java/com/orgmemory/api/assistant/UiMessageStreamTests.java b/apps/api/src/test/java/com/orgmemory/api/assistant/UiMessageStreamTests.java index 5b5f728b..10b46075 100644 --- a/apps/api/src/test/java/com/orgmemory/api/assistant/UiMessageStreamTests.java +++ b/apps/api/src/test/java/com/orgmemory/api/assistant/UiMessageStreamTests.java @@ -20,7 +20,10 @@ void emitsAiSdkUiMessageFramesInOrder() { Flux.just( new AssistantStreamPart.StartStep(), new AssistantStreamPart.SourceUrl( - "citation-1", "https://example.test/handbook", "Employee Handbook"), + "citation-1", + "https://example.test/handbook", + "Employee Handbook", + 1), new AssistantStreamPart.TextStart("answer"), new AssistantStreamPart.TextDelta("answer", "Sixty days. [1]"), new AssistantStreamPart.TextEnd("answer"), @@ -36,7 +39,7 @@ void emitsAiSdkUiMessageFramesInOrder() { assertThat(data.getFirst()).contains("\"type\":\"start\"").contains("\"messageId\":"); assertThat(data.subList(1, data.size())).containsExactly( "{\"type\":\"start-step\"}", - "{\"type\":\"source-url\",\"sourceId\":\"citation-1\",\"url\":\"https://example.test/handbook\",\"title\":\"Employee Handbook\"}", + "{\"type\":\"source-url\",\"sourceId\":\"citation-1\",\"url\":\"https://example.test/handbook\",\"title\":\"Employee Handbook\",\"providerMetadata\":{\"orgmemory\":{\"citationNumber\":1}}}", "{\"type\":\"text-start\",\"id\":\"answer\"}", "{\"type\":\"text-delta\",\"id\":\"answer\",\"delta\":\"Sixty days. [1]\"}", "{\"type\":\"text-end\",\"id\":\"answer\"}", diff --git a/apps/api/src/test/java/com/orgmemory/api/knowledge/CitationContentControllerTests.java b/apps/api/src/test/java/com/orgmemory/api/knowledge/CitationContentControllerTests.java index 6654c841..12ef683c 100644 --- a/apps/api/src/test/java/com/orgmemory/api/knowledge/CitationContentControllerTests.java +++ b/apps/api/src/test/java/com/orgmemory/api/knowledge/CitationContentControllerTests.java @@ -87,11 +87,42 @@ void streamsVerifiedEvidenceThroughTheBackendWithoutExposingStorageKeys() } @Test - void fallsBackToBinaryForAnUntrustedStoredMediaType() { + void ignoresAnUntrustedStoredMediaTypeAndUsesTheSafeFileType() { CitationContent citation = citation( new CloseTrackingInputStream(new byte[0]), 0, - "not a media type"); + "text/html", + "leave-policy.txt"); + CitationContentService citations = + mock(CitationContentService.class); + CurrentActorProvider actors = mock(CurrentActorProvider.class); + Authentication authentication = mock(Authentication.class); + when(actors.current(authentication)).thenReturn(ACTOR); + when(citations.open( + org.mockito.ArgumentMatchers.eq(ACTOR), + org.mockito.ArgumentMatchers.eq(CHUNK_ID), + org.mockito.ArgumentMatchers.anyString())) + .thenReturn(citation); + + var response = + new CitationContentController(citations, actors) + .content(CHUNK_ID, authentication); + + assertEquals( + MediaType.TEXT_PLAIN, + response.getHeaders().getContentType()); + assertTrue(response.getHeaders() + .getFirst(HttpHeaders.CONTENT_DISPOSITION) + .startsWith("inline;")); + } + + @Test + void forcesUnknownTypesToDownloadAsBinary() { + CitationContent citation = citation( + new CloseTrackingInputStream(new byte[0]), + 0, + "text/html", + "leave-policy.html"); CitationContentService citations = mock(CitationContentService.class); CurrentActorProvider actors = mock(CurrentActorProvider.class); @@ -110,16 +141,31 @@ void fallsBackToBinaryForAnUntrustedStoredMediaType() { assertEquals( MediaType.APPLICATION_OCTET_STREAM, response.getHeaders().getContentType()); + assertTrue(response.getHeaders() + .getFirst(HttpHeaders.CONTENT_DISPOSITION) + .startsWith("attachment;")); } private static CitationContent citation( CloseTrackingInputStream stream, long contentLength, String mediaType) { + return citation( + stream, + contentLength, + mediaType, + "leave-policy.txt"); + } + + private static CitationContent citation( + CloseTrackingInputStream stream, + long contentLength, + String mediaType, + String fileName) { ObjectKey key = new ObjectKey("private/evidence/object-key"); return new CitationContent( CHUNK_ID, - "leave-policy.txt", + fileName, mediaType, contentLength, "sha256", diff --git a/apps/api/src/test/java/com/orgmemory/api/source/SourceUploadIntegrationTests.java b/apps/api/src/test/java/com/orgmemory/api/source/SourceUploadIntegrationTests.java index 8369ba2a..203c9df2 100644 --- a/apps/api/src/test/java/com/orgmemory/api/source/SourceUploadIntegrationTests.java +++ b/apps/api/src/test/java/com/orgmemory/api/source/SourceUploadIntegrationTests.java @@ -87,7 +87,6 @@ void uploadCreatesCanonicalSourceRevisionBlobAndDurableJob() throws Exception { new CreateUploadSourceCommand( ACTOR, "onboarding.txt", - "text/plain", content.length, KnowledgeClassification.CONFIDENTIAL, SALES_SPACE_ID), diff --git a/apps/worker/build.gradle.kts b/apps/worker/build.gradle.kts index ab0805e8..64940f22 100644 --- a/apps/worker/build.gradle.kts +++ b/apps/worker/build.gradle.kts @@ -8,9 +8,11 @@ dependencies { implementation(project(":integrations:authorization-openfga")) implementation(project(":integrations:connectors")) implementation(project(":integrations:graph-rag-postgres")) + implementation(project(":integrations:graph-rag-observability")) implementation(project(":integrations:graph-rag-spring-ai")) implementation(project(":integrations:object-storage-minio")) implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-opentelemetry") implementation(libs.spring.ai.starter.openai) implementation("org.springframework.ai:spring-ai-tika-document-reader") implementation("org.springframework.ai:spring-ai-pdf-document-reader") diff --git a/apps/worker/src/main/java/com/orgmemory/worker/OrgMemoryWorkerApplication.java b/apps/worker/src/main/java/com/orgmemory/worker/OrgMemoryWorkerApplication.java index 8a0691b7..42cfb467 100644 --- a/apps/worker/src/main/java/com/orgmemory/worker/OrgMemoryWorkerApplication.java +++ b/apps/worker/src/main/java/com/orgmemory/worker/OrgMemoryWorkerApplication.java @@ -1,6 +1,7 @@ package com.orgmemory.worker; import com.orgmemory.core.knowledge.KnowledgeRetrievalProperties; +import com.orgmemory.core.knowledge.GraphProcessingProperties; import com.orgmemory.core.knowledge.CanonicalHybridKnowledgeSearch; import com.orgmemory.core.knowledge.KnowledgeGraphExplorerConfiguration; import com.orgmemory.core.knowledge.SourceIngestionProperties; @@ -29,6 +30,7 @@ KnowledgeRetrievalProperties.class, ConnectorCrawlProperties.class, GraphIndexingProperties.class, + GraphProcessingProperties.class, KnowledgeAuthorizationConvergenceProperties.class }) @ComponentScan( diff --git a/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphIndexingProcessor.java b/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphIndexingProcessor.java index 1fa600b4..4328afed 100644 --- a/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphIndexingProcessor.java +++ b/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphIndexingProcessor.java @@ -9,35 +9,44 @@ import com.orgmemory.core.knowledge.GraphIndexingStoppedException; import com.orgmemory.graphrag.indexing.ExtractedChunk; import com.orgmemory.graphrag.indexing.GraphContributionAssembler; +import com.orgmemory.graphrag.indexing.LightRagEmbeddingPayloads; import com.orgmemory.graphrag.model.ContributionEmbedding; import com.orgmemory.graphrag.model.EntityContribution; import com.orgmemory.graphrag.model.ExtractionProfile; import com.orgmemory.graphrag.model.FloatVector; import com.orgmemory.graphrag.model.RelationContribution; +import com.orgmemory.graphrag.observability.GraphRagEventSink; import com.orgmemory.graphrag.port.EntityRelationExtractor; import com.orgmemory.graphrag.port.GraphRevisionEmbeddings; import com.orgmemory.graphrag.port.GraphRevisionProjection; +import com.orgmemory.graphrag.processing.GraphProcessingProfile; +import com.orgmemory.graphrag.processing.LightRagGraphProcessingProfiles; import com.orgmemory.integrations.graphrag.springai.GraphExtractionException; import com.orgmemory.integrations.graphrag.springai.SpringAiEntityRelationExtractor; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.UUID; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.function.ToIntFunction; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.embedding.TokenCountBatchingStrategy; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Component; @@ -52,20 +61,42 @@ class GraphIndexingProcessor { private final ObjectProvider embeddingModels; private final AiRouteResolver routes; private final GraphIndexingProperties properties; + private final GraphRagEventSink events; + @Autowired GraphIndexingProcessor( GraphIndexingCoordinator coordinator, GraphPublicationCommitter publications, GraphExtractorFactory extractors, ObjectProvider embeddingModels, AiRouteResolver routes, - GraphIndexingProperties properties) { + GraphIndexingProperties properties, + ObjectProvider eventSinks) { + this( + coordinator, + publications, + extractors, + embeddingModels, + routes, + properties, + eventSinks.getIfAvailable(() -> GraphRagEventSink.NO_OP)); + } + + GraphIndexingProcessor( + GraphIndexingCoordinator coordinator, + GraphPublicationCommitter publications, + GraphExtractorFactory extractors, + ObjectProvider embeddingModels, + AiRouteResolver routes, + GraphIndexingProperties properties, + GraphRagEventSink events) { this.coordinator = coordinator; this.publications = publications; this.extractors = extractors; this.embeddingModels = embeddingModels; this.routes = routes; this.properties = properties; + this.events = Objects.requireNonNull(events, "events"); } void processNext() { @@ -82,35 +113,61 @@ private void process(ClaimedGraphIndex claim) { claim.knowledgeAssetVersionId()); return; } - AiRoute extractionRoute = routes.resolve(AiWorkload.GRAPH_EXTRACTION); - ExtractionProfile extractionProfile = new ExtractionProfile( - extractionRoute.gatewayId(), - extractionRoute.modelId(), - SpringAiEntityRelationExtractor.PROMPT_VERSION, - properties.maximumEntitiesPerChunk(), - properties.maximumRelationsPerChunk(), - properties.entityTypeGuidance(), - properties.extractionExamples(), - properties.maximumGleaningRounds(), - properties.maximumGleaningInputTokens(), - properties.maximumSectionContextTokens()); + GraphProcessingProfile processingProfile = + claim.graphProcessingProfile().profile(); + ExtractionProfile extractionProfile = + processingProfile.extractionProfile(); + requireSupported(processingProfile); + AiRoute extractionRoute = new AiRoute( + extractionProfile.provider(), extractionProfile.model()); EntityRelationExtractor extractor = extractors.create(extractionRoute); - List extracted = extractChunks(claim, extractionProfile, extractor); - var contributions = GraphContributionAssembler.assemble( - claim.organizationId(), - claim.knowledgeAssetId(), - claim.sourceRevisionId(), - claim.aclSnapshotId(), - claim.aclGeneration(), - claim.projectionGeneration(), - Instant.now(), - extracted); - GraphRevisionEmbeddings embeddings = embed(claim, contributions.entities(), contributions.relations()); - publications.commit( + List extracted = observed( claim, - properties.workerId(), - properties.leaseDuration(), - new GraphRevisionProjection(contributions, embeddings)); + GraphRagEventSink.Stage.EXTRACT, + claim.chunks().size(), + () -> extractChunks(claim, extractionProfile, extractor), + List::size); + var contributions = observed( + claim, + GraphRagEventSink.Stage.MERGE, + extracted.size(), + () -> GraphContributionAssembler.assemble( + claim.organizationId(), + claim.knowledgeAssetId(), + claim.sourceRevisionId(), + claim.aclSnapshotId(), + claim.aclGeneration(), + claim.projectionGeneration(), + Instant.now(), + extracted), + value -> value.entities().size() + value.relations().size()); + GraphRevisionEmbeddings embeddings = observed( + claim, + GraphRagEventSink.Stage.EMBED, + contributions.entities().size() + contributions.relations().size(), + () -> embed( + claim, + contributions.entities(), + contributions.relations()), + value -> value.entityEmbeddings().size() + + value.relationEmbeddings().size()); + observed( + claim, + GraphRagEventSink.Stage.PUBLISH, + contributions.entities().size() + contributions.relations().size(), + () -> { + publications.commit( + claim, + properties.workerId(), + properties.leaseDuration(), + new GraphRevisionProjection( + contributions, + embeddings, + claim.graphProcessingProfile() + .canonicalSha256())); + return Boolean.TRUE; + }, + ignored -> 1); log.info( "Published graph generation {} for Knowledge Asset version {} with {} entities and {} relations", claim.projectionGeneration(), @@ -133,6 +190,79 @@ private void process(ClaimedGraphIndex claim) { } } + private static void requireSupported(GraphProcessingProfile profile) { + GraphProcessingProfile supported = LightRagGraphProcessingProfiles.current( + profile.extractionProfile()); + if (!supported.equals(profile)) { + throw new GraphExtractionException( + "The pinned graph processing profile is not supported by this worker"); + } + } + + private T observed( + ClaimedGraphIndex claim, + GraphRagEventSink.Stage stage, + int inputCount, + Callable action, + ToIntFunction outputCount) + throws Exception { + long startedAt = System.nanoTime(); + try { + T result = action.call(); + emit( + claim, + stage, + GraphRagEventSink.Outcome.SUCCEEDED, + startedAt, + inputCount, + outputCount.applyAsInt(result), + null); + return result; + } catch (Exception failure) { + GraphRagEventSink.Outcome outcome = + failure instanceof InterruptedException + || failure instanceof GraphIndexingStoppedException + ? GraphRagEventSink.Outcome.CANCELLED + : GraphRagEventSink.Outcome.FAILED; + emit( + claim, + stage, + outcome, + startedAt, + inputCount, + 0, + outcome == GraphRagEventSink.Outcome.FAILED + ? stage.name().toLowerCase(Locale.ROOT) + "_failed" + : null); + throw failure; + } + } + + private void emit( + ClaimedGraphIndex claim, + GraphRagEventSink.Stage stage, + GraphRagEventSink.Outcome outcome, + long startedAt, + int inputCount, + int outputCount, + String failureCode) { + try { + events.emit(new GraphRagEventSink.GraphRagEvent( + claim.jobId(), + claim.organizationId(), + stage, + outcome, + Duration.ofNanos(Math.max(0, System.nanoTime() - startedAt)), + inputCount, + outputCount, + null, + failureCode, + Instant.now())); + } catch (RuntimeException ignoredTelemetryFailure) { + // Telemetry must never control indexing availability or retries. + } + } + private static void logFailure(ClaimedGraphIndex claim, Exception failure) { GraphExtractionException extractionFailure = findExtractionFailure(failure); if (extractionFailure != null) { @@ -330,9 +460,8 @@ private GraphRevisionEmbeddings embed( } private static Document embeddingDocument(EntityContribution contribution) { - return new Document("%s\n%s\n%s".formatted( + return new Document(LightRagEmbeddingPayloads.entity( contribution.entity().normalizedName(), - contribution.type(), contribution.description())); } @@ -347,9 +476,8 @@ private static Document embeddingDocument( entitiesByEvidence, contribution.relation().targetEntityId(), contribution.provenance().chunkId()); - return new Document("%s\t%s\n%s\n%s\n%s".formatted( - String.join(", ", contribution.keywords()), - contribution.type(), + return new Document(LightRagEmbeddingPayloads.relation( + contribution.keywords(), source.entity().normalizedName(), target.entity().normalizedName(), contribution.description())); diff --git a/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphIndexingProperties.java b/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphIndexingProperties.java index ab692579..56044c27 100644 --- a/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphIndexingProperties.java +++ b/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphIndexingProperties.java @@ -1,7 +1,6 @@ package com.orgmemory.worker.graph; import java.time.Duration; -import java.util.List; import java.util.UUID; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.Assert; @@ -13,29 +12,7 @@ public record GraphIndexingProperties( String workerId, Duration leaseDuration, Duration extractionTimeout, - Integer maximumConcurrency, - Integer maximumEntitiesPerChunk, - Integer maximumRelationsPerChunk, - List entityTypeGuidance, - List extractionExamples, - Integer maximumGleaningRounds, - Integer maximumGleaningInputTokens, - Integer maximumSectionContextTokens) { - - private static final List DEFAULT_ENTITY_TYPES = List.of( - "PERSON", - "ORGANIZATION", - "TEAM", - "ROLE", - "POLICY", - "PROCESS", - "SYSTEM", - "PRODUCT", - "DOCUMENT", - "LOCATION", - "EVENT", - "CONCEPT", - "OTHER"); + Integer maximumConcurrency) { public GraphIndexingProperties { schedulingEnabled = schedulingEnabled == null || schedulingEnabled; @@ -47,21 +24,6 @@ public record GraphIndexingProperties( extractionTimeout = extractionTimeout == null ? Duration.ofMinutes(2) : extractionTimeout; maximumConcurrency = maximumConcurrency == null ? 4 : maximumConcurrency; - maximumEntitiesPerChunk = - maximumEntitiesPerChunk == null ? 40 : maximumEntitiesPerChunk; - maximumRelationsPerChunk = - maximumRelationsPerChunk == null ? 60 : maximumRelationsPerChunk; - entityTypeGuidance = entityTypeGuidance == null - ? DEFAULT_ENTITY_TYPES - : entityTypeGuidance.stream().map(String::strip).filter(value -> !value.isEmpty()).toList(); - extractionExamples = extractionExamples == null - ? List.of() - : extractionExamples.stream().map(String::strip).filter(value -> !value.isEmpty()).toList(); - maximumGleaningRounds = maximumGleaningRounds == null ? 1 : maximumGleaningRounds; - maximumGleaningInputTokens = - maximumGleaningInputTokens == null ? 24_000 : maximumGleaningInputTokens; - maximumSectionContextTokens = - maximumSectionContextTokens == null ? 256 : maximumSectionContextTokens; Assert.isTrue( !pollInterval.isNegative() && !pollInterval.isZero(), "graph indexing poll interval must be positive"); @@ -74,18 +36,5 @@ public record GraphIndexingProperties( Assert.isTrue( maximumConcurrency > 0 && maximumConcurrency <= 32, "graph extraction concurrency must be between 1 and 32"); - Assert.isTrue( - maximumEntitiesPerChunk > 0 && maximumRelationsPerChunk > 0, - "graph extraction limits must be positive"); - Assert.notEmpty(entityTypeGuidance, "graph entity type guidance must not be empty"); - Assert.isTrue( - maximumGleaningRounds >= 0 && maximumGleaningRounds <= 1, - "maximum gleaning rounds must be 0 or 1 for LightRAG v1.5.4 parity"); - Assert.isTrue( - maximumGleaningInputTokens >= 0, - "maximum gleaning input tokens must be non-negative"); - Assert.isTrue( - maximumSectionContextTokens > 0, - "maximum section context tokens must be positive"); } } diff --git a/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphPublicationCommitter.java b/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphPublicationCommitter.java index 7ab0b40c..6226cef0 100644 --- a/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphPublicationCommitter.java +++ b/apps/worker/src/main/java/com/orgmemory/worker/graph/GraphPublicationCommitter.java @@ -186,7 +186,7 @@ private static List contentRecords( ContentStore.ContentKind.CHUNK, chunk.content(), chunk.tokenCount(), - chunkMetadata(chunk))) + chunkMetadata(claim, chunk))) .toList(); } @@ -197,7 +197,7 @@ private static List lexicalDocuments( chunk.id().toString(), evidence(claim, chunk.id()), chunk.content(), - chunkMetadata(chunk))) + chunkMetadata(claim, chunk))) .toList(); } @@ -214,7 +214,7 @@ private static List vectorRecords( claim.embeddingProfile().id(), claim.embeddingProfile().model(), chunk.embedding(), - chunkMetadata(chunk))); + chunkMetadata(claim, chunk))); } Map entities = projection.contributions().entities().stream() @@ -235,7 +235,10 @@ private static List vectorRecords( embedding.vector(), Map.of( "contributionId", contribution.id().toString(), - "entityType", contribution.type()))); + "entityType", contribution.type(), + "graphProcessingProfileSha256", + claim.graphProcessingProfile() + .canonicalSha256()))); } Map relations = @@ -259,7 +262,10 @@ private static List vectorRecords( embedding.vector(), Map.of( "contributionId", contribution.id().toString(), - "relationType", contribution.type()))); + "relationType", contribution.type(), + "graphProcessingProfileSha256", + claim.graphProcessingProfile() + .canonicalSha256()))); } return List.copyOf(records); } @@ -288,9 +294,14 @@ private static EvidenceReference evidence( claim.aclGeneration()); } - private static Map chunkMetadata(GraphIndexChunk chunk) { + private static Map chunkMetadata( + ClaimedGraphIndex claim, + GraphIndexChunk chunk) { Map metadata = new LinkedHashMap<>(); metadata.put("chunkIndex", Integer.toString(chunk.index())); + metadata.put( + "graphProcessingProfileSha256", + claim.graphProcessingProfile().canonicalSha256()); if (chunk.heading() != null) { metadata.put("heading", chunk.heading()); } @@ -328,7 +339,9 @@ private static String manifestFingerprint( "embeddingProvider", claim.embeddingProfile().provider(), "embeddingModel", claim.embeddingProfile().model(), "embeddingDimensions", - Integer.toString(claim.embeddingProfile().dimensions()))); + Integer.toString(claim.embeddingProfile().dimensions()), + "graphProcessingProfileSha256", + claim.graphProcessingProfile().canonicalSha256())); } private static String vector(FloatVector vector) { diff --git a/apps/worker/src/main/java/com/orgmemory/worker/graph/SpringAiGraphExtractorFactory.java b/apps/worker/src/main/java/com/orgmemory/worker/graph/SpringAiGraphExtractorFactory.java index 4856c025..9d7d204f 100644 --- a/apps/worker/src/main/java/com/orgmemory/worker/graph/SpringAiGraphExtractorFactory.java +++ b/apps/worker/src/main/java/com/orgmemory/worker/graph/SpringAiGraphExtractorFactory.java @@ -19,6 +19,7 @@ final class SpringAiGraphExtractorFactory implements GraphExtractorFactory { @Override public EntityRelationExtractor create(AiRoute route) { return new SpringAiEntityRelationExtractor( - route.gatewayId(), chatModels.resolve(AiWorkload.GRAPH_EXTRACTION)); + route.gatewayId(), + chatModels.resolve(AiWorkload.GRAPH_EXTRACTION, route)); } } diff --git a/apps/worker/src/main/resources/application.yml b/apps/worker/src/main/resources/application.yml index 7168f4a0..1cd0bf13 100644 --- a/apps/worker/src/main/resources/application.yml +++ b/apps/worker/src/main/resources/application.yml @@ -18,6 +18,10 @@ spring: flyway: enabled: false ai: + chat: + observations: + log-prompt: false + log-completion: false model: embedding: ${ORGMEMORY_AI_MODEL_EMBEDDING:none} openai: @@ -77,18 +81,19 @@ orgmemory: semantic-embedding-batch-size: ${ORGMEMORY_SEMANTIC_EMBEDDING_BATCH_SIZE:64} maximum-chunks: ${ORGMEMORY_INGESTION_MAX_CHUNKS:500} graph-rag: - indexing: - scheduling-enabled: ${ORGMEMORY_GRAPH_INDEXING_ENABLED:true} - poll-interval: ${ORGMEMORY_GRAPH_INDEXING_POLL_INTERVAL:3s} - lease-duration: ${ORGMEMORY_GRAPH_INDEXING_LEASE_DURATION:10m} - extraction-timeout: ${ORGMEMORY_GRAPH_EXTRACTION_TIMEOUT:2m} - maximum-concurrency: ${ORGMEMORY_GRAPH_EXTRACTION_CONCURRENCY:4} + processing: maximum-entities-per-chunk: ${ORGMEMORY_GRAPH_MAX_ENTITIES_PER_CHUNK:40} maximum-relations-per-chunk: ${ORGMEMORY_GRAPH_MAX_RELATIONS_PER_CHUNK:60} entity-type-guidance: ${ORGMEMORY_GRAPH_ENTITY_TYPES:PERSON,ORGANIZATION,TEAM,ROLE,POLICY,PROCESS,SYSTEM,PRODUCT,DOCUMENT,LOCATION,EVENT,CONCEPT,OTHER} maximum-gleaning-rounds: ${ORGMEMORY_GRAPH_MAX_GLEANING_ROUNDS:1} maximum-gleaning-input-tokens: ${ORGMEMORY_GRAPH_MAX_GLEANING_INPUT_TOKENS:24000} maximum-section-context-tokens: ${ORGMEMORY_GRAPH_MAX_SECTION_CONTEXT_TOKENS:256} + indexing: + scheduling-enabled: ${ORGMEMORY_GRAPH_INDEXING_ENABLED:true} + poll-interval: ${ORGMEMORY_GRAPH_INDEXING_POLL_INTERVAL:3s} + lease-duration: ${ORGMEMORY_GRAPH_INDEXING_LEASE_DURATION:10m} + extraction-timeout: ${ORGMEMORY_GRAPH_EXTRACTION_TIMEOUT:2m} + maximum-concurrency: ${ORGMEMORY_GRAPH_EXTRACTION_CONCURRENCY:4} storage: object: endpoint: ${ORGMEMORY_OBJECT_STORAGE_ENDPOINT:http://localhost:9000} @@ -96,3 +101,8 @@ orgmemory: secret-key: ${ORGMEMORY_OBJECT_STORAGE_SECRET_KEY:orgmemory-local-secret} bucket: ${ORGMEMORY_OBJECT_STORAGE_BUCKET:orgmemory-evidence} maximum-object-size: ${ORGMEMORY_MAX_UPLOAD_SIZE:25MB} + +management: + tracing: + sampling: + probability: ${ORGMEMORY_TRACING_SAMPLING_PROBABILITY:0.1} diff --git a/apps/worker/src/test/java/com/orgmemory/worker/graph/GraphIndexingProcessorTests.java b/apps/worker/src/test/java/com/orgmemory/worker/graph/GraphIndexingProcessorTests.java index 0259bec6..ee27552c 100644 --- a/apps/worker/src/test/java/com/orgmemory/worker/graph/GraphIndexingProcessorTests.java +++ b/apps/worker/src/test/java/com/orgmemory/worker/graph/GraphIndexingProcessorTests.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -22,13 +23,18 @@ import com.orgmemory.core.knowledge.EmbeddingProfileRef; import com.orgmemory.core.knowledge.GraphIndexChunk; import com.orgmemory.core.knowledge.GraphIndexingCoordinator; +import com.orgmemory.core.knowledge.GraphProcessingProfileRef; +import com.orgmemory.graphrag.extraction.LightRagExtractionPrompt; import com.orgmemory.graphrag.model.ExtractedEntity; import com.orgmemory.graphrag.model.ExtractedRelation; import com.orgmemory.graphrag.model.ExtractionResult; +import com.orgmemory.graphrag.model.ExtractionProfile; import com.orgmemory.graphrag.model.FloatVector; import com.orgmemory.graphrag.model.RelationOrientation; +import com.orgmemory.graphrag.observability.GraphRagEventSink; import com.orgmemory.graphrag.port.EntityRelationExtractor; import com.orgmemory.graphrag.port.GraphRevisionProjection; +import com.orgmemory.graphrag.processing.LightRagGraphProcessingProfiles; import java.time.Duration; import java.util.List; import java.util.Optional; @@ -65,6 +71,7 @@ void publishesOneAtomicProjectionAndCompletesTheDurableJob() { EmbeddingModel embeddingModel = mock(EmbeddingModel.class); AiRouteResolver routes = mock(AiRouteResolver.class); GraphIndexingProperties properties = properties(); + GraphRagEventSink events = mock(GraphRagEventSink.class); ClaimedGraphIndex claim = claim(List.of( chunk( CHUNK_ID, @@ -102,17 +109,28 @@ void publishesOneAtomicProjectionAndCompletesTheDurableJob() { RelationOrientation.DIRECTED, 0.96))); }; - when(extractors.create(new AiRoute("openai", "gpt-5.6-sol"))) + when(extractors.create(new AiRoute("openai", "gpt-test"))) .thenReturn(extractor); when(embeddingModel.embed( anyList(), isNull(), any(TokenCountBatchingStrategy.class))) .thenAnswer(invocation -> { List documents = invocation.getArgument(0); assertEquals(6, documents.size()); - assertTrue(documents.getLast().getText().contains("orgmemory")); - assertTrue(documents.getLast().getText().contains("secure search")); - assertTrue(documents.getLast().getText().contains("retrieval")); - assertTrue(documents.getLast().getText().contains("security")); + assertTrue(documents.stream() + .map(Document::getText) + .anyMatch("orgmemory\nEnterprise memory platform"::equals)); + assertTrue(documents.stream() + .map(Document::getText) + .anyMatch("secure search\nPermission-aware retrieval"::equals)); + assertEquals( + "retrieval, security\torgmemory\nsecure search\n" + + "OrgMemory builds Secure Search", + documents.getLast().getText()); + assertFalse(documents.stream() + .map(Document::getText) + .anyMatch(text -> text.contains("\nproduct\n") + || text.contains("\ncapability\n") + || text.contains("\tbuilds\n"))); return documents.stream() .map(ignored -> new float[] {1.0f, 0.0f, 0.0f}) .toList(); @@ -124,7 +142,8 @@ void publishesOneAtomicProjectionAndCompletesTheDurableJob() { extractors, provider(embeddingModel), routes, - properties); + properties, + events); processor.processNext(); @@ -141,6 +160,18 @@ void publishesOneAtomicProjectionAndCompletesTheDurableJob() { assertEquals(2, projection.getValue().embeddings().relationEmbeddings().size()); verify(coordinator, never()).complete(any(), any()); verify(coordinator, never()).fail(any(), any(), any(), any()); + ArgumentCaptor emitted = + ArgumentCaptor.forClass(GraphRagEventSink.GraphRagEvent.class); + verify(events, times(4)).emit(emitted.capture()); + assertEquals( + List.of( + GraphRagEventSink.Stage.EXTRACT, + GraphRagEventSink.Stage.MERGE, + GraphRagEventSink.Stage.EMBED, + GraphRagEventSink.Stage.PUBLISH), + emitted.getAllValues().stream() + .map(GraphRagEventSink.GraphRagEvent::stage) + .toList()); } @Test @@ -165,7 +196,8 @@ void retriesWithoutPublishingWhenTheImmutableEmbeddingRouteDrifts() { extractors, new StaticListableBeanFactory().getBeanProvider(EmbeddingModel.class), routes, - properties); + properties, + GraphRagEventSink.NO_OP); processor.processNext(); @@ -211,7 +243,8 @@ void refreshesTheLeaseWhileAChunkExtractionIsStillRunning() throws Exception { extractors, new StaticListableBeanFactory().getBeanProvider(EmbeddingModel.class), routes, - properties); + properties, + GraphRagEventSink.NO_OP); Thread worker = Thread.ofVirtual().start(processor::processNext); try { @@ -260,7 +293,8 @@ void cancelsAnExtractionThatExceedsItsConfiguredDeadline() { extractors, new StaticListableBeanFactory().getBeanProvider(EmbeddingModel.class), routes, - properties); + properties, + GraphRagEventSink.NO_OP); processor.processNext(); @@ -302,7 +336,8 @@ void interruptionLeavesTheClaimedJobForLeaseBasedRetry() throws Exception { extractors, new StaticListableBeanFactory().getBeanProvider(EmbeddingModel.class), routes, - properties); + properties, + GraphRagEventSink.NO_OP); Thread worker = Thread.ofVirtual().start(() -> { processor.processNext(); @@ -346,7 +381,8 @@ void lostLeaseDoesNotRaiseASecondFailureWhileRecordingRetry() { extractors, new StaticListableBeanFactory().getBeanProvider(EmbeddingModel.class), routes, - properties); + properties, + GraphRagEventSink.NO_OP); assertDoesNotThrow(processor::processNext); @@ -359,6 +395,17 @@ private static ClaimedGraphIndex claim() { } private static ClaimedGraphIndex claim(List chunks) { + var graphProcessingProfile = + LightRagGraphProcessingProfiles.current(new ExtractionProfile( + "openai", + "gpt-test", + LightRagExtractionPrompt.VERSION, + 40, + 60)); + var graphProcessingProfileRef = new GraphProcessingProfileRef( + UUID.randomUUID(), + graphProcessingProfile.canonicalSha256(), + graphProcessingProfile); return new ClaimedGraphIndex( JOB_ID, ORGANIZATION_ID, @@ -369,7 +416,13 @@ private static ClaimedGraphIndex claim(List chunks) { ACL_SNAPSHOT_ID, 1, 1, - "graph:" + ORGANIZATION_ID + ":" + REVISION_ID + ":1", + graphProcessingProfileRef, + "graph:" + + ORGANIZATION_ID + + ":" + + REVISION_ID + + ":1:" + + graphProcessingProfile.canonicalSha256(), new EmbeddingProfileRef( EMBEDDING_PROFILE_ID, ORGANIZATION_ID, @@ -413,14 +466,7 @@ private static GraphIndexingProperties properties( "graph-worker-test", leaseDuration, extractionTimeout, - 2, - 40, - 60, - null, - null, - 1, - 24_000, - 256); + 2); } private static ObjectProvider provider(EmbeddingModel embeddingModel) { diff --git a/apps/worker/src/test/java/com/orgmemory/worker/graph/GraphPublicationCommitterTests.java b/apps/worker/src/test/java/com/orgmemory/worker/graph/GraphPublicationCommitterTests.java index e5391e2e..f5701979 100644 --- a/apps/worker/src/test/java/com/orgmemory/worker/graph/GraphPublicationCommitterTests.java +++ b/apps/worker/src/test/java/com/orgmemory/worker/graph/GraphPublicationCommitterTests.java @@ -13,12 +13,15 @@ import com.orgmemory.core.knowledge.EmbeddingProfileRef; import com.orgmemory.core.knowledge.GraphIndexChunk; import com.orgmemory.core.knowledge.GraphIndexingCoordinator; +import com.orgmemory.core.knowledge.GraphProcessingProfileRef; import com.orgmemory.graphrag.cache.ModelInvocationCache; import com.orgmemory.graphrag.cache.RetrievalResultCache; import com.orgmemory.graphrag.model.FloatVector; +import com.orgmemory.graphrag.model.ExtractionProfile; import com.orgmemory.graphrag.port.GraphRevisionContributions; import com.orgmemory.graphrag.port.GraphRevisionEmbeddings; import com.orgmemory.graphrag.port.GraphRevisionProjection; +import com.orgmemory.graphrag.processing.LightRagGraphProcessingProfiles; import com.orgmemory.graphrag.storage.ContentStore; import com.orgmemory.graphrag.storage.GraphStore; import com.orgmemory.graphrag.storage.LexicalIndex; @@ -197,6 +200,20 @@ private static Fixture fixture() { "text-embedding-3-large", 3, EmbeddingDistanceMetric.COSINE); + var processingProfile = LightRagGraphProcessingProfiles.current( + new ExtractionProfile("openai", "gpt-test", "orgmemory-lightrag-v1.5.4-json-v1", 4, 6)); + var processingProfileRef = new GraphProcessingProfileRef( + UUID.randomUUID(), + processingProfile.canonicalSha256(), + processingProfile); + String idempotencyKey = "graph:" + + organizationId + + ":" + + revisionId + + ":" + + projectionGeneration + + ":" + + processingProfile.canonicalSha256(); ClaimedGraphIndex claim = new ClaimedGraphIndex( UUID.randomUUID(), organizationId, @@ -207,8 +224,8 @@ private static Fixture fixture() { UUID.randomUUID(), 2, projectionGeneration, - "graph:" + organizationId + ":" + revisionId + ":" - + projectionGeneration, + processingProfileRef, + idempotencyKey, profile, "en", 1, @@ -235,7 +252,8 @@ private static Fixture fixture() { profileId, 3, List.of(), - List.of())); + List.of()), + processingProfile.canonicalSha256()); return new Fixture(claim, projection); } diff --git a/apps/worker/src/test/java/com/orgmemory/worker/ingestion/SourceIngestionPipelineIntegrationTests.java b/apps/worker/src/test/java/com/orgmemory/worker/ingestion/SourceIngestionPipelineIntegrationTests.java index fe729668..857bf28e 100644 --- a/apps/worker/src/test/java/com/orgmemory/worker/ingestion/SourceIngestionPipelineIntegrationTests.java +++ b/apps/worker/src/test/java/com/orgmemory/worker/ingestion/SourceIngestionPipelineIntegrationTests.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.isNull; @@ -184,7 +185,6 @@ void processesUploadedTextIntoVersionedLargeModelVectors() throws Exception { new CreateUploadSourceCommand( ACTOR, "support-resolution.txt", - "text/plain", content.length, KnowledgeClassification.CONFIDENTIAL, SALES_SPACE_ID), @@ -230,6 +230,22 @@ void processesUploadedTextIntoVersionedLargeModelVectors() throws Exception { "SELECT count(*) FROM knowledge_chunks WHERE source_object_id = ?", Integer.class, source.id())); + var graphProfile = jdbc.queryForMap( + """ + SELECT profile.canonical_sha256, profile.canonical_form + FROM graph_index_jobs job + JOIN graph_processing_profiles profile + ON profile.id = job.graph_processing_profile_id + JOIN source_revisions revision + ON revision.id = job.source_revision_id + AND revision.organization_id = job.organization_id + WHERE revision.source_object_id = ? + """, + source.id()); + assertTrue(graphProfile.get("canonical_sha256").toString() + .matches("[0-9a-f]{64}")); + assertTrue(graphProfile.get("canonical_form").toString() + .contains("schemaVersion=1")); assertEquals( 1, jdbc.queryForObject( @@ -427,7 +443,6 @@ void keepsPublicationPendingWhenAuthorizationProjectionIsUnavailable() throws Ex new CreateUploadSourceCommand( ACTOR, "authorization-pending.txt", - "text/plain", content.length, KnowledgeClassification.CONFIDENTIAL, SALES_SPACE_ID), @@ -554,10 +569,14 @@ static class UploadTestConfiguration { @Primary AiRouteResolver testAiRouteResolver() { return workload -> { - if (workload != AiWorkload.DOCUMENT_EMBEDDING) { - throw new IllegalArgumentException("Unsupported test workload: " + workload); - } - return new AiRoute("openai", "text-embedding-3-large"); + return switch (workload) { + case DOCUMENT_EMBEDDING -> + new AiRoute("openai", "text-embedding-3-large"); + case GRAPH_EXTRACTION -> + new AiRoute("openai", "gpt-test"); + default -> throw new IllegalArgumentException( + "Unsupported test workload: " + workload); + }; }; } } diff --git a/components/graph-rag-core/build.gradle.kts b/components/graph-rag-core/build.gradle.kts index 1f2c0c5a..9448f44f 100644 --- a/components/graph-rag-core/build.gradle.kts +++ b/components/graph-rag-core/build.gradle.kts @@ -2,8 +2,16 @@ plugins { id("orgmemory.java-library-conventions") } +sourceSets { + test { + resources.srcDir(rootProject.layout.projectDirectory.dir("evaluation/baselines")) + } +} + dependencies { testImplementation(project(":components:graph-rag-testkit")) + testImplementation(platform(libs.spring.boot.dependencies)) + testImplementation("tools.jackson.core:jackson-databind") testImplementation(platform(libs.junit.bom)) testImplementation(libs.junit.jupiter) testRuntimeOnly(libs.junit.platform.launcher) diff --git a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/extraction/LightRagExtractionPrompt.java b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/extraction/LightRagExtractionPrompt.java index d6435ae1..ef96fd9c 100644 --- a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/extraction/LightRagExtractionPrompt.java +++ b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/extraction/LightRagExtractionPrompt.java @@ -71,6 +71,18 @@ public final class LightRagExtractionPrompt { private LightRagExtractionPrompt() { } + /** Exact prompt templates included in immutable graph-processing provenance. */ + public static String templateSnapshot() { + return """ + [system] + %s + [initial] + %s + [continuation] + %s + """.formatted(SYSTEM_TEMPLATE, INITIAL_TEMPLATE, CONTINUATION_TEMPLATE); + } + public static RenderedPrompt render(ExtractionRequest request) { return render(request, request.sectionContext()); } diff --git a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/indexing/GraphContributionAssembler.java b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/indexing/GraphContributionAssembler.java index dd488d3c..56e1f76e 100644 --- a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/indexing/GraphContributionAssembler.java +++ b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/indexing/GraphContributionAssembler.java @@ -33,6 +33,9 @@ */ public final class GraphContributionAssembler { + public static final String MERGE_SEMANTICS_VERSION = + "orgmemory-evidence-scoped-merge-v1"; + private GraphContributionAssembler() { } diff --git a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/indexing/LightRagEmbeddingPayloads.java b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/indexing/LightRagEmbeddingPayloads.java new file mode 100644 index 00000000..a505057e --- /dev/null +++ b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/indexing/LightRagEmbeddingPayloads.java @@ -0,0 +1,52 @@ +package com.orgmemory.graphrag.indexing; + +import java.util.List; +import java.util.Objects; + +/** + * Byte-stable entity and relation embedding payloads from pinned LightRAG + * v1.5.4. + * + *

Contribution types remain metadata. Adding them to these strings changes + * vector semantics and therefore requires an explicitly versioned graph + * processing profile and a new projection publication. + */ +public final class LightRagEmbeddingPayloads { + + public static final String FORMAT_ID = "lightrag-v1.5.4"; + + private LightRagEmbeddingPayloads() { + } + + public static String entity(String normalizedName, String description) { + return required(normalizedName, "normalizedName") + + "\n" + + required(description, "description"); + } + + public static String relation( + List keywords, + String sourceName, + String targetName, + String description) { + Objects.requireNonNull(keywords, "keywords"); + String keywordText = String.join(", ", keywords.stream() + .map(keyword -> required(keyword, "keyword")) + .toList()); + return keywordText + + "\t" + + required(sourceName, "sourceName") + + "\n" + + required(targetName, "targetName") + + "\n" + + required(description, "description"); + } + + private static String required(String value, String field) { + String normalized = Objects.requireNonNull(value, field).strip(); + if (normalized.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return normalized; + } +} diff --git a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/observability/GraphRagEventSink.java b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/observability/GraphRagEventSink.java index 9b6cd8f9..bf67c5e5 100644 --- a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/observability/GraphRagEventSink.java +++ b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/observability/GraphRagEventSink.java @@ -4,6 +4,7 @@ import java.time.Instant; import java.util.Objects; import java.util.UUID; +import java.util.regex.Pattern; /** * Provider-neutral telemetry boundary. Events intentionally contain no query, @@ -12,6 +13,9 @@ @FunctionalInterface public interface GraphRagEventSink { + Pattern SHA_256 = Pattern.compile("[0-9a-f]{64}"); + Pattern FAILURE_CODE = Pattern.compile("[a-z0-9_]{1,64}"); + GraphRagEventSink NO_OP = event -> { }; void emit(GraphRagEvent event); @@ -42,10 +46,20 @@ record GraphRagEvent( } modelRouteFingerprint = normalizeOptional(modelRouteFingerprint); failureCode = normalizeOptional(failureCode); + if (modelRouteFingerprint != null + && !SHA_256.matcher(modelRouteFingerprint).matches()) { + throw new IllegalArgumentException( + "modelRouteFingerprint must be a lowercase SHA-256 value"); + } if (outcome == Outcome.FAILED && failureCode == null) { throw new IllegalArgumentException( "failureCode is required for a failed event"); } + if (failureCode != null + && !FAILURE_CODE.matcher(failureCode).matches()) { + throw new IllegalArgumentException( + "failureCode must be a bounded machine code"); + } Objects.requireNonNull(occurredAt, "occurredAt"); } } diff --git a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/port/GraphProjectionManifest.java b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/port/GraphProjectionManifest.java index 7b8f3c6e..e85453d8 100644 --- a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/port/GraphProjectionManifest.java +++ b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/port/GraphProjectionManifest.java @@ -25,6 +25,9 @@ public static String fingerprint(GraphRevisionProjection projection) { fields.put( "projectionGeneration", Long.toString(contributions.projectionGeneration())); + fields.put( + "graphProcessingProfileSha256", + projection.graphProcessingProfileSha256()); fields.put( "entities", contributions.entities().stream() @@ -61,14 +64,27 @@ public static String fingerprint(GraphRevisionProjection projection) { "orgmemory.graph-rag.projection-manifest.v1", fields); } - public static String idempotencyKey(GraphRevisionContributions contributions) { + public static String idempotencyKey( + GraphRevisionContributions contributions, + String graphProcessingProfileSha256) { Objects.requireNonNull(contributions, "contributions"); + String profileSha256 = + Objects.requireNonNull( + graphProcessingProfileSha256, + "graphProcessingProfileSha256") + .strip(); + if (!profileSha256.matches("[0-9a-f]{64}")) { + throw new IllegalArgumentException( + "graphProcessingProfileSha256 must be lowercase SHA-256 hex"); + } return "graph:" + contributions.organizationId() + ":" + contributions.sourceRevisionId() + ":" - + contributions.projectionGeneration(); + + contributions.projectionGeneration() + + ":" + + profileSha256; } private static String entity(EntityContribution contribution) { diff --git a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/port/GraphRevisionProjection.java b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/port/GraphRevisionProjection.java index 0f8eb7f9..c78c43b3 100644 --- a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/port/GraphRevisionProjection.java +++ b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/port/GraphRevisionProjection.java @@ -9,11 +9,14 @@ public record GraphRevisionProjection( GraphRevisionContributions contributions, - GraphRevisionEmbeddings embeddings) { + GraphRevisionEmbeddings embeddings, + String graphProcessingProfileSha256) { public GraphRevisionProjection { Objects.requireNonNull(contributions, "contributions"); Objects.requireNonNull(embeddings, "embeddings"); + graphProcessingProfileSha256 = + requireSha256(graphProcessingProfileSha256); if (!contributions.organizationId().equals(embeddings.organizationId()) || !contributions.knowledgeAssetId().equals(embeddings.knowledgeAssetId()) || !contributions.sourceRevisionId().equals(embeddings.sourceRevisionId()) @@ -41,6 +44,17 @@ public String manifestFingerprint() { } public String idempotencyKey() { - return GraphProjectionManifest.idempotencyKey(contributions); + return GraphProjectionManifest.idempotencyKey( + contributions, graphProcessingProfileSha256); + } + + private static String requireSha256(String value) { + String normalized = + Objects.requireNonNull(value, "graphProcessingProfileSha256").strip(); + if (!normalized.matches("[0-9a-f]{64}")) { + throw new IllegalArgumentException( + "graphProcessingProfileSha256 must be lowercase SHA-256 hex"); + } + return normalized; } } diff --git a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/processing/GraphProcessingProfile.java b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/processing/GraphProcessingProfile.java new file mode 100644 index 00000000..b5f17d67 --- /dev/null +++ b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/processing/GraphProcessingProfile.java @@ -0,0 +1,287 @@ +package com.orgmemory.graphrag.processing; + +import static com.orgmemory.graphrag.validation.TextValidation.requireText; + +import com.orgmemory.graphrag.model.ExtractionProfile; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Immutable identity of the algorithm that turns one pinned chunk projection into + * graph contributions and their vector payloads. + * + *

Embedding geometry is deliberately not part of this profile. Provider, model, + * dimensions and distance metric remain the independent {@code EmbeddingProfile} + * coordinate. + */ +public record GraphProcessingProfile( + int schemaVersion, + String algorithmVersion, + ExtractionProfile extractionProfile, + String promptTemplate, + String mergeSemanticsVersion, + String embeddingPayloadFormatVersion, + String canonicalSha256) { + + public static final int CURRENT_SCHEMA_VERSION = 1; + + public GraphProcessingProfile { + if (schemaVersion != CURRENT_SCHEMA_VERSION) { + throw new IllegalArgumentException( + "unsupported graph processing profile schema: " + schemaVersion); + } + algorithmVersion = requireText(algorithmVersion, "algorithmVersion"); + Objects.requireNonNull(extractionProfile, "extractionProfile"); + promptTemplate = requireText(promptTemplate, "promptTemplate"); + mergeSemanticsVersion = + requireText(mergeSemanticsVersion, "mergeSemanticsVersion"); + embeddingPayloadFormatVersion = requireText( + embeddingPayloadFormatVersion, "embeddingPayloadFormatVersion"); + canonicalSha256 = requireSha256(canonicalSha256); + String expected = ResolvedDocumentProcessingProfile.sha256(canonicalForm( + schemaVersion, + algorithmVersion, + extractionProfile, + promptTemplate, + mergeSemanticsVersion, + embeddingPayloadFormatVersion)); + if (!expected.equals(canonicalSha256)) { + throw new IllegalArgumentException( + "canonicalSha256 does not match the graph processing profile"); + } + } + + public static GraphProcessingProfile resolve( + String algorithmVersion, + ExtractionProfile extractionProfile, + String promptTemplate, + String mergeSemanticsVersion, + String embeddingPayloadFormatVersion) { + String canonical = canonicalForm( + CURRENT_SCHEMA_VERSION, + algorithmVersion, + extractionProfile, + promptTemplate, + mergeSemanticsVersion, + embeddingPayloadFormatVersion); + return new GraphProcessingProfile( + CURRENT_SCHEMA_VERSION, + algorithmVersion, + extractionProfile, + promptTemplate, + mergeSemanticsVersion, + embeddingPayloadFormatVersion, + ResolvedDocumentProcessingProfile.sha256(canonical)); + } + + public static GraphProcessingProfile restore( + String canonicalForm, + String canonicalSha256) { + Map fields = parseCanonicalForm(canonicalForm); + int schemaVersion = integer(take(fields, "schemaVersion"), "schemaVersion"); + String algorithmVersion = decoded(take(fields, "algorithmVersion")); + String provider = decoded(take(fields, "extraction.provider")); + String model = decoded(take(fields, "extraction.model")); + String promptVersion = decoded(take(fields, "extraction.promptVersion")); + int maxEntities = integer( + take(fields, "extraction.maxEntities"), "extraction.maxEntities"); + int maxRelations = integer( + take(fields, "extraction.maxRelations"), "extraction.maxRelations"); + List entityTypes = takeList(fields, "extraction.entityType"); + List examples = takeList(fields, "extraction.example"); + int maxGleaningRounds = integer( + take(fields, "extraction.maxGleaningRounds"), + "extraction.maxGleaningRounds"); + int maxGleaningInputTokens = integer( + take(fields, "extraction.maxGleaningInputTokens"), + "extraction.maxGleaningInputTokens"); + int maxSectionContextTokens = integer( + take(fields, "extraction.maxSectionContextTokens"), + "extraction.maxSectionContextTokens"); + String promptTemplate = decoded(take(fields, "promptTemplate")); + String mergeSemanticsVersion = + decoded(take(fields, "mergeSemanticsVersion")); + String embeddingPayloadFormatVersion = + decoded(take(fields, "embeddingPayloadFormatVersion")); + if (!fields.isEmpty()) { + throw new IllegalArgumentException( + "unknown graph processing profile fields: " + fields.keySet()); + } + return new GraphProcessingProfile( + schemaVersion, + algorithmVersion, + new ExtractionProfile( + provider, + model, + promptVersion, + maxEntities, + maxRelations, + entityTypes, + examples, + maxGleaningRounds, + maxGleaningInputTokens, + maxSectionContextTokens), + promptTemplate, + mergeSemanticsVersion, + embeddingPayloadFormatVersion, + canonicalSha256); + } + + public String canonicalForm() { + return canonicalForm( + schemaVersion, + algorithmVersion, + extractionProfile, + promptTemplate, + mergeSemanticsVersion, + embeddingPayloadFormatVersion); + } + + public String extractionFingerprint() { + return extractionProfile.fingerprint(); + } + + private static String canonicalForm( + int schemaVersion, + String algorithmVersion, + ExtractionProfile extractionProfile, + String promptTemplate, + String mergeSemanticsVersion, + String embeddingPayloadFormatVersion) { + Objects.requireNonNull(extractionProfile, "extractionProfile"); + StringBuilder value = new StringBuilder() + .append("schemaVersion=").append(schemaVersion).append('\n') + .append("algorithmVersion=").append(encoded(algorithmVersion)).append('\n') + .append("extraction.provider=") + .append(encoded(extractionProfile.provider())) + .append('\n') + .append("extraction.model=") + .append(encoded(extractionProfile.model())) + .append('\n') + .append("extraction.promptVersion=") + .append(encoded(extractionProfile.promptVersion())) + .append('\n') + .append("extraction.maxEntities=") + .append(extractionProfile.maxEntities()) + .append('\n') + .append("extraction.maxRelations=") + .append(extractionProfile.maxRelations()) + .append('\n'); + appendList(value, "extraction.entityType", extractionProfile.entityTypeGuidance()); + appendList(value, "extraction.example", extractionProfile.examples()); + value.append("extraction.maxGleaningRounds=") + .append(extractionProfile.maxGleaningRounds()) + .append('\n') + .append("extraction.maxGleaningInputTokens=") + .append(extractionProfile.maxGleaningInputTokens()) + .append('\n') + .append("extraction.maxSectionContextTokens=") + .append(extractionProfile.maxSectionContextTokens()) + .append('\n') + .append("promptTemplate=").append(encoded(promptTemplate)).append('\n') + .append("mergeSemanticsVersion=") + .append(encoded(mergeSemanticsVersion)) + .append('\n') + .append("embeddingPayloadFormatVersion=") + .append(encoded(embeddingPayloadFormatVersion)) + .append('\n'); + return value.toString(); + } + + private static void appendList( + StringBuilder target, + String field, + List values) { + target.append(field).append(".count=").append(values.size()).append('\n'); + for (int index = 0; index < values.size(); index++) { + target.append(field) + .append('.') + .append(index) + .append('=') + .append(encoded(values.get(index))) + .append('\n'); + } + } + + private static List takeList( + Map fields, + String field) { + int count = integer(take(fields, field + ".count"), field + ".count"); + if (count < 0 || count > 10_000) { + throw new IllegalArgumentException(field + " count is invalid"); + } + List values = new ArrayList<>(count); + for (int index = 0; index < count; index++) { + values.add(decoded(take(fields, field + "." + index))); + } + return List.copyOf(values); + } + + private static Map parseCanonicalForm(String canonicalForm) { + String normalized = requireText(canonicalForm, "canonicalForm"); + Map fields = new LinkedHashMap<>(); + for (String line : normalized.split("\\n")) { + int separator = line.indexOf('='); + if (separator <= 0) { + throw new IllegalArgumentException( + "invalid graph processing profile canonical form"); + } + String key = line.substring(0, separator); + String previous = fields.put(key, line.substring(separator + 1)); + if (previous != null) { + throw new IllegalArgumentException( + "duplicate graph processing profile field: " + key); + } + } + return fields; + } + + private static String take(Map fields, String key) { + String value = fields.remove(key); + if (value == null) { + throw new IllegalArgumentException( + "missing graph processing profile field: " + key); + } + return value; + } + + private static int integer(String value, String field) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException exception) { + throw new IllegalArgumentException(field + " must be an integer", exception); + } + } + + private static String encoded(String value) { + return Base64.getUrlEncoder() + .withoutPadding() + .encodeToString(requireText(value, "profile value") + .getBytes(StandardCharsets.UTF_8)); + } + + private static String decoded(String value) { + try { + return new String( + Base64.getUrlDecoder().decode(value), + StandardCharsets.UTF_8); + } catch (IllegalArgumentException exception) { + throw new IllegalArgumentException( + "invalid graph processing profile encoding", exception); + } + } + + private static String requireSha256(String value) { + String normalized = requireText(value, "canonicalSha256").toLowerCase(); + if (!normalized.matches("[0-9a-f]{64}")) { + throw new IllegalArgumentException( + "canonicalSha256 must be lowercase SHA-256 hex"); + } + return normalized; + } +} diff --git a/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/processing/LightRagGraphProcessingProfiles.java b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/processing/LightRagGraphProcessingProfiles.java new file mode 100644 index 00000000..0335440f --- /dev/null +++ b/components/graph-rag-core/src/main/java/com/orgmemory/graphrag/processing/LightRagGraphProcessingProfiles.java @@ -0,0 +1,26 @@ +package com.orgmemory.graphrag.processing; + +import com.orgmemory.graphrag.extraction.LightRagExtractionPrompt; +import com.orgmemory.graphrag.indexing.GraphContributionAssembler; +import com.orgmemory.graphrag.indexing.LightRagEmbeddingPayloads; +import com.orgmemory.graphrag.model.ExtractionProfile; +import java.util.Objects; + +/** Supported immutable graph-processing profiles derived from pinned LightRAG semantics. */ +public final class LightRagGraphProcessingProfiles { + + public static final String ALGORITHM_VERSION = + "lightrag-v1.5.4-orgmemory-secure-v1"; + + private LightRagGraphProcessingProfiles() { + } + + public static GraphProcessingProfile current(ExtractionProfile extractionProfile) { + return GraphProcessingProfile.resolve( + ALGORITHM_VERSION, + Objects.requireNonNull(extractionProfile, "extractionProfile"), + LightRagExtractionPrompt.templateSnapshot(), + GraphContributionAssembler.MERGE_SEMANTICS_VERSION, + LightRagEmbeddingPayloads.FORMAT_ID); + } +} diff --git a/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/indexing/LightRagEmbeddingPayloadsTests.java b/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/indexing/LightRagEmbeddingPayloadsTests.java new file mode 100644 index 00000000..675be6c7 --- /dev/null +++ b/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/indexing/LightRagEmbeddingPayloadsTests.java @@ -0,0 +1,30 @@ +package com.orgmemory.graphrag.indexing; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class LightRagEmbeddingPayloadsTests { + + @Test + void matchesPinnedEntityEmbeddingPayload() { + assertEquals( + "orgmemory\nPermission-aware enterprise knowledge.", + LightRagEmbeddingPayloads.entity( + "orgmemory", + "Permission-aware enterprise knowledge.")); + } + + @Test + void matchesPinnedRelationEmbeddingPayload() { + assertEquals( + "secure search, retrieval\torgmemory\nopenfga\n" + + "OrgMemory uses OpenFGA for relationship authorization.", + LightRagEmbeddingPayloads.relation( + List.of("secure search", "retrieval"), + "orgmemory", + "openfga", + "OrgMemory uses OpenFGA for relationship authorization.")); + } +} diff --git a/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/observability/GraphRagEventSinkTests.java b/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/observability/GraphRagEventSinkTests.java new file mode 100644 index 00000000..d11cb09f --- /dev/null +++ b/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/observability/GraphRagEventSinkTests.java @@ -0,0 +1,44 @@ +package com.orgmemory.graphrag.observability; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class GraphRagEventSinkTests { + + @Test + void rejectsRouteTextThatIsNotAnOpaqueFingerprint() { + assertThrows(IllegalArgumentException.class, () -> event( + GraphRagEventSink.Outcome.SUCCEEDED, + "openai:gpt-model:prompt-v1", + null)); + } + + @Test + void rejectsUnboundedFailureDiagnostics() { + assertThrows(IllegalArgumentException.class, () -> event( + GraphRagEventSink.Outcome.FAILED, + null, + "provider failed: raw payload")); + } + + private static GraphRagEventSink.GraphRagEvent event( + GraphRagEventSink.Outcome outcome, + String routeFingerprint, + String failureCode) { + return new GraphRagEventSink.GraphRagEvent( + UUID.randomUUID(), + UUID.randomUUID(), + GraphRagEventSink.Stage.RETRIEVE, + outcome, + Duration.ofMillis(12), + 1, + 2, + routeFingerprint, + failureCode, + Instant.now()); + } +} diff --git a/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/port/GraphProjectionManifestTests.java b/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/port/GraphProjectionManifestTests.java index 6bf3c635..15a5b01c 100644 --- a/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/port/GraphProjectionManifestTests.java +++ b/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/port/GraphProjectionManifestTests.java @@ -54,8 +54,30 @@ void outputChangeDoesChangeTheSemanticManifest() { assertNotEquals(first.manifestFingerprint(), changed.manifestFingerprint()); } + @Test + void processingProfileChangeCreatesANewManifestAndJobIdentity() { + GraphRevisionProjection first = projection( + "Evidence-backed policy", + Instant.parse("2026-07-24T00:00:00Z"), + "a".repeat(64)); + GraphRevisionProjection rebuilt = projection( + "Evidence-backed policy", + Instant.parse("2026-07-24T00:00:00Z"), + "b".repeat(64)); + + assertNotEquals(first.manifestFingerprint(), rebuilt.manifestFingerprint()); + assertNotEquals(first.idempotencyKey(), rebuilt.idempotencyKey()); + } + private static GraphRevisionProjection projection( String description, Instant extractedAt) { + return projection(description, extractedAt, "a".repeat(64)); + } + + private static GraphRevisionProjection projection( + String description, + Instant extractedAt, + String graphProcessingProfileSha256) { var provenance = new EvidenceProvenance( new EvidenceReference( ORGANIZATION_ID, @@ -94,6 +116,7 @@ private static GraphRevisionProjection projection( List.of(new ContributionEmbedding( CONTRIBUTION_ID, new FloatVector(new float[] {0.1F, 0.2F, 0.3F}))), List.of()); - return new GraphRevisionProjection(contributions, embeddings); + return new GraphRevisionProjection( + contributions, embeddings, graphProcessingProfileSha256); } } diff --git a/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/processing/GraphProcessingProfileTests.java b/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/processing/GraphProcessingProfileTests.java new file mode 100644 index 00000000..5e53cf94 --- /dev/null +++ b/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/processing/GraphProcessingProfileTests.java @@ -0,0 +1,115 @@ +package com.orgmemory.graphrag.processing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.orgmemory.graphrag.model.ExtractionProfile; +import java.util.List; +import org.junit.jupiter.api.Test; + +class GraphProcessingProfileTests { + + @Test + void canonicalSnapshotRoundTripsWithoutLosingPromptOrExamples() { + GraphProcessingProfile profile = profile(); + + GraphProcessingProfile restored = GraphProcessingProfile.restore( + profile.canonicalForm(), profile.canonicalSha256()); + + assertEquals(profile, restored); + assertEquals("Example\nwith newline", restored.extractionProfile().examples().getFirst()); + } + + @Test + void everyIndependentAlgorithmCoordinateChangesTheCanonicalHash() { + GraphProcessingProfile original = profile(); + ExtractionProfile extraction = original.extractionProfile(); + + assertNotEquals( + original.canonicalSha256(), + GraphProcessingProfile.resolve( + original.algorithmVersion() + "-next", + extraction, + original.promptTemplate(), + original.mergeSemanticsVersion(), + original.embeddingPayloadFormatVersion()) + .canonicalSha256()); + assertNotEquals( + original.canonicalSha256(), + GraphProcessingProfile.resolve( + original.algorithmVersion(), + new ExtractionProfile( + extraction.provider(), + extraction.model() + "-next", + extraction.promptVersion(), + extraction.maxEntities(), + extraction.maxRelations(), + extraction.entityTypeGuidance(), + extraction.examples(), + extraction.maxGleaningRounds(), + extraction.maxGleaningInputTokens(), + extraction.maxSectionContextTokens()), + original.promptTemplate(), + original.mergeSemanticsVersion(), + original.embeddingPayloadFormatVersion()) + .canonicalSha256()); + assertNotEquals( + original.canonicalSha256(), + GraphProcessingProfile.resolve( + original.algorithmVersion(), + extraction, + original.promptTemplate() + "\nchanged", + original.mergeSemanticsVersion(), + original.embeddingPayloadFormatVersion()) + .canonicalSha256()); + assertNotEquals( + original.canonicalSha256(), + GraphProcessingProfile.resolve( + original.algorithmVersion(), + extraction, + original.promptTemplate(), + original.mergeSemanticsVersion() + "-next", + original.embeddingPayloadFormatVersion()) + .canonicalSha256()); + assertNotEquals( + original.canonicalSha256(), + GraphProcessingProfile.resolve( + original.algorithmVersion(), + extraction, + original.promptTemplate(), + original.mergeSemanticsVersion(), + original.embeddingPayloadFormatVersion() + "-next") + .canonicalSha256()); + } + + @Test + void restoreRejectsCanonicalTextThatDoesNotMatchTheStoredHash() { + GraphProcessingProfile profile = profile(); + + assertThrows( + IllegalArgumentException.class, + () -> GraphProcessingProfile.restore( + profile.canonicalForm().replace("schemaVersion=1", "schemaVersion=2"), + profile.canonicalSha256())); + } + + private static GraphProcessingProfile profile() { + return GraphProcessingProfile.resolve( + "lightrag-v1.5.4-test", + new ExtractionProfile( + "openai", + "gpt-test", + "prompt-v1", + 4, + 6, + List.of("PERSON", "POLICY"), + List.of("Example\nwith newline"), + 1, + 2048, + 256), + "System %s\nUser %s", + "merge-v1", + "payload-v1"); + } +} diff --git a/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagUpstreamOracleTests.java b/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagUpstreamOracleTests.java new file mode 100644 index 00000000..97f2068c --- /dev/null +++ b/components/graph-rag-core/src/test/java/com/orgmemory/graphrag/query/LightRagUpstreamOracleTests.java @@ -0,0 +1,120 @@ +package com.orgmemory.graphrag.query; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.orgmemory.graphrag.chunking.ChunkingRequest; +import com.orgmemory.graphrag.chunking.FixedTokenChunker; +import com.orgmemory.graphrag.chunking.FixedTokenOptions; +import com.orgmemory.graphrag.indexing.LightRagEmbeddingPayloads; +import com.orgmemory.graphrag.parsing.CanonicalDocument; +import com.orgmemory.graphrag.testkit.CodePointTokenizer; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; + +class LightRagUpstreamOracleTests { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + void javaSemanticsMatchTheExecutablePinnedUpstreamOracle() throws IOException { + JsonNode oracle; + try (var stream = Objects.requireNonNull( + LightRagUpstreamOracleTests.class.getResourceAsStream( + "/lightrag-v1.5.4-oracle.json"))) { + oracle = mapper.readTree(stream); + } + assertEquals( + "9a45b64c2ee25b1d806e90db926a8af37480bb16", + oracle.at("/upstream/commit").asString()); + + JsonNode chunking = oracle.at("/fixture/chunking"); + var chunks = new FixedTokenChunker().chunk( + new ChunkingRequest( + CanonicalDocument.text(chunking.get("content").asString()), + new CodePointTokenizer(), + Optional.empty()), + new FixedTokenOptions( + chunking.get("chunkTokenSize").asInt(), + chunking.get("chunkOverlapTokenSize").asInt(), + null, + false)); + JsonNode expectedChunks = oracle.at("/expected/chunks"); + assertEquals( + values(expectedChunks, "content"), + chunks.stream().map(chunk -> chunk.content()).toList()); + assertEquals( + integers(expectedChunks, "tokens"), + chunks.stream().map(chunk -> chunk.tokenCount()).toList()); + assertEquals( + integers(expectedChunks, "_source_span", "start"), + chunks.stream() + .map(chunk -> chunk.provenance().startChar()) + .toList()); + assertEquals( + integers(expectedChunks, "_source_span", "end"), + chunks.stream() + .map(chunk -> chunk.provenance().endChar()) + .toList()); + + JsonNode polling = oracle.at("/fixture/weightedPolling"); + List> groups = polling.get("groups").valueStream() + .map(group -> group.valueStream() + .map(JsonNode::asString) + .map(LightRagUpstreamOracleTests::id) + .toList()) + .toList(); + assertEquals( + oracle.at("/expected/weightedPolling").valueStream() + .map(JsonNode::asString) + .map(LightRagUpstreamOracleTests::id) + .toList(), + LightRagQueryEngine.weightedPolling( + groups, + polling.get("maximumRelatedChunks").asInt(), + polling.get("minimumRelatedChunks").asInt())); + + JsonNode payloads = oracle.at("/fixture/embeddingPayloads"); + assertEquals( + oracle.at("/expected/entityEmbeddingPayload").asString(), + LightRagEmbeddingPayloads.entity( + payloads.get("entityName").asString(), + payloads.get("entityDescription").asString())); + assertEquals( + oracle.at("/expected/relationEmbeddingPayload").asString(), + LightRagEmbeddingPayloads.relation( + List.of("governs", "leave"), + payloads.get("relationSource").asString(), + payloads.get("relationTarget").asString(), + payloads.get("relationDescription").asString())); + } + + private static List values(JsonNode array, String field) { + return array.valueStream() + .map(node -> node.get(field).asString()) + .toList(); + } + + private static List integers( + JsonNode array, String objectField, String valueField) { + return array.valueStream() + .map(node -> node.get(objectField).get(valueField).asInt()) + .toList(); + } + + private static List integers(JsonNode array, String field) { + return array.valueStream() + .map(node -> node.get(field).asInt()) + .toList(); + } + + private static UUID id(String value) { + return UUID.nameUUIDFromBytes(value.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/contracts/openapi.json b/contracts/openapi.json index d787cb0f..db4dfba3 100644 --- a/contracts/openapi.json +++ b/contracts/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://127.0.0.1:8080","description":"Generated server url"}],"paths":{"/api/admin/source-principals/{principalId}/mapping":{"put":{"tags":["admin-source-access-controller"],"summary":"Confirm a principal maps to an internal user","operationId":"confirmAdminSourceMapping","parameters":[{"name":"principalId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmMappingRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminSourcePrincipalResponse"}}}}}},"delete":{"tags":["admin-source-access-controller"],"summary":"Revoke a principal's active mapping","operationId":"revokeAdminSourceMapping","parameters":[{"name":"principalId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminSourcePrincipalResponse"}}}}}}},"/api/admin/source-connections/identity-trust":{"put":{"tags":["admin-source-access-controller"],"summary":"Record the identity trust for a connection","operationId":"setAdminSourceConnectionTrust","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityTrustRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminSourceConnectionResponse"}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}":{"put":{"tags":["admin-connector-controller"],"summary":"Record how a connection is crawled","operationId":"configureAdminConnection","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigureConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminConnectionResponse"}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}/credential":{"put":{"tags":["admin-connector-controller"],"summary":"Store a credential for a connection","operationId":"setAdminConnectionCredential","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorCredentialRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}},"delete":{"tags":["admin-connector-controller"],"summary":"Forget a connection's stored credential","operationId":"forgetAdminConnectionCredential","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"}}}},"/api/sources":{"get":{"tags":["source-controller"],"summary":"List sources visible to the current user","operationId":"listSources","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SourceResponse"}}}}}}},"post":{"tags":["source-controller"],"summary":"Upload a source for asynchronous ingestion","operationId":"uploadSource","parameters":[{"name":"classification","in":"query","required":false,"schema":{"type":"string","default":"CONFIDENTIAL","enum":["PUBLIC","INTERNAL","CONFIDENTIAL","RESTRICTED"]}},{"name":"knowledgeSpaceId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SourceResponse"}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/curations/suppressions":{"post":{"tags":["knowledge-graph-management-controller"],"summary":"Delete an effective graph identity without deleting evidence","operationId":"suppressGraphIdentity","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuppressIdentityRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CuratedEntity"},{"$ref":"#/components/schemas/CuratedRelation"},{"$ref":"#/components/schemas/IdentityAlias"},{"$ref":"#/components/schemas/IdentitySuppression"}]}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/curations/relations":{"post":{"tags":["knowledge-graph-management-controller"],"summary":"Create or edit a governed graph relation","operationId":"curateGraphRelation","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurateRelationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CuratedEntity"},{"$ref":"#/components/schemas/CuratedRelation"},{"$ref":"#/components/schemas/IdentityAlias"},{"$ref":"#/components/schemas/IdentitySuppression"}]}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/curations/entities":{"post":{"tags":["knowledge-graph-management-controller"],"summary":"Create or edit a governed graph entity","operationId":"curateGraphEntity","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurateEntityRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CuratedEntity"},{"$ref":"#/components/schemas/CuratedRelation"},{"$ref":"#/components/schemas/IdentityAlias"},{"$ref":"#/components/schemas/IdentitySuppression"}]}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/curations/aliases":{"post":{"tags":["knowledge-graph-management-controller"],"summary":"Merge graph identities through a reversible alias","operationId":"mergeGraphIdentity","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AliasIdentityRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CuratedEntity"},{"$ref":"#/components/schemas/CuratedRelation"},{"$ref":"#/components/schemas/IdentityAlias"},{"$ref":"#/components/schemas/IdentitySuppression"}]}}}}}}},"/api/knowledge-assets/graph-jobs/{jobId}/resume":{"post":{"tags":["knowledge-asset-lifecycle-controller"],"summary":"Resume unfinished graph indexing","operationId":"resumeGraphIndexJob","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"Accepted","content":{"*/*":{"schema":{"$ref":"#/components/schemas/GraphIndexJobView"}}}}}}},"/api/knowledge-assets/graph-jobs/{jobId}/cancel":{"post":{"tags":["knowledge-asset-lifecycle-controller"],"summary":"Cancel queued or in-flight graph indexing","operationId":"cancelGraphIndexJob","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/GraphIndexJobView"}}}}}}},"/api/assistant/chat":{"post":{"tags":["assistant-controller"],"summary":"Stream an answer from permission-verified knowledge","operationId":"streamAssistantChat","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistantChatRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"text/event-stream":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ServerSentEventString"}}}}}}}},"/api/admin/roles/{role}/members":{"post":{"tags":["admin-role-controller"],"summary":"Assign a user to a role","operationId":"assignAdminRole","parameters":[{"name":"role","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}}},"/api/admin/invitations":{"get":{"tags":["admin-invitation-controller"],"summary":"List invited addresses and their status","operationId":"listAdminInvitations","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminInvitationResponse"}}}}}}},"post":{"tags":["admin-invitation-controller"],"summary":"Expect an address to sign in","operationId":"createAdminInvitation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvitationRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminInvitationResponse"}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}/test":{"post":{"tags":["admin-connector-controller"],"summary":"Check a connection's stored credential","operationId":"testAdminConnection","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminConnectorProbeResponse"}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}/crawl":{"post":{"tags":["admin-connector-controller"],"summary":"Ask for a content crawl on the next poll","operationId":"requestAdminConnectionCrawl","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Accepted"}}}},"/api/admin/connectors/{sourceSystem}/test":{"post":{"tags":["admin-connector-controller"],"summary":"Check a credential without storing it","operationId":"testAdminConnectorCredential","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorCredentialRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminConnectorProbeResponse"}}}}}}},"/api/admin/access/explain":{"post":{"tags":["admin-permission-controller"],"summary":"Answer whether a user holds a permission on one resource, and by which derivation","operationId":"explainAdminAccess","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExplainAccessRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ExplainAccessResponse"}}}}}}},"/api/admin/users/{userId}":{"patch":{"tags":["admin-user-controller"],"summary":"Change a user's role or activation","operationId":"updateAdminUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAdminUserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminUserResponse"}}}}}}},"/api/session":{"get":{"tags":["browser-session-controller"],"summary":"Read the current browser session","operationId":"getBrowserSession","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SessionResponse"}}}}}}},"/api/session/csrf":{"get":{"tags":["browser-session-controller"],"summary":"Issue a CSRF token for browser mutations","operationId":"getBrowserCsrfToken","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CsrfResponse"}}}}}}},"/api/organization/context":{"get":{"tags":["organization-context-controller"],"operationId":"context","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OrganizationContextResponse"}}}}}}},"/api/me":{"get":{"tags":["me-controller"],"operationId":"me","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}}}}},"/api/knowledge/search":{"get":{"tags":["knowledge-search-controller"],"summary":"Search permission-verified knowledge evidence","operationId":"searchKnowledge","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/KnowledgeSearchResponse"}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/export":{"get":{"tags":["knowledge-graph-management-controller"],"summary":"Export only graph evidence visible to the current user","operationId":"exportKnowledgeGraph","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","default":"JSON","enum":["JSON","CSV","MARKDOWN","TEXT"]}},{"name":"X-Request-Id","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/explorer":{"get":{"tags":["knowledge-graph-explorer-controller"],"summary":"Read a bounded permission-filtered graph view","operationId":"exploreKnowledgeGraph","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"entityLimit","in":"query","required":false,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/KnowledgeGraphView"}}}}}}},"/api/knowledge-spaces/visible":{"get":{"tags":["knowledge-space-controller"],"summary":"List Knowledge Spaces visible to the current user","operationId":"listVisibleKnowledgeSpaces","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeSpaceResponse"}}}}}}}},"/api/knowledge-spaces/upload-targets":{"get":{"tags":["knowledge-space-controller"],"summary":"List Knowledge Spaces where the current user may add knowledge","operationId":"listKnowledgeSpaceUploadTargets","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeSpaceResponse"}}}}}}}},"/api/knowledge-assets/graph-jobs/{jobId}":{"get":{"tags":["knowledge-asset-lifecycle-controller"],"summary":"Read graph indexing lifecycle status","operationId":"getGraphIndexJob","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/GraphIndexJobView"}}}}}}},"/api/health":{"get":{"tags":["health-controller"],"operationId":"health","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/api/citations/{chunkId}/content":{"get":{"tags":["citation-content-controller"],"summary":"Stream permission-verified source evidence","operationId":"readCitationContent","parameters":[{"name":"chunkId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StreamingResponseBody"}}}}}}},"/api/admin/users":{"get":{"tags":["admin-user-controller"],"summary":"List internal users with their sign-in and mapping status","operationId":"listAdminUsers","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminUserResponse"}}}}}}}},"/api/admin/users/{userId}/permissions":{"get":{"tags":["admin-permission-controller"],"summary":"Resolve a user's organization permissions as the engine currently answers them","operationId":"listAdminUserPermissions","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EffectivePermissionResponse"}}}}}}},"/api/admin/source-principals":{"get":{"tags":["admin-source-access-controller"],"summary":"List observed principals and their mapping","operationId":"listAdminSourcePrincipals","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminSourcePrincipalResponse"}}}}}}}},"/api/admin/source-groups":{"get":{"tags":["admin-source-access-controller"],"summary":"List source groups with their sealed membership","operationId":"listAdminSourceGroups","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminSourceGroupResponse"}}}}}}}},"/api/admin/source-connections":{"get":{"tags":["admin-source-access-controller"],"summary":"List observed connections and their trust level","operationId":"listAdminSourceConnections","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminSourceConnectionResponse"}}}}}}}},"/api/admin/roles":{"get":{"tags":["admin-role-controller"],"summary":"List roles and who is assigned to them","operationId":"listAdminRoles","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminRoleListResponse"}}}}}}},"/api/admin/connectors/{sourceSystem}":{"get":{"tags":["admin-connector-controller"],"summary":"List a source's connections and their crawl settings","operationId":"listAdminConnections","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminConnectionResponse"}}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}/scopes":{"get":{"tags":["admin-connector-controller"],"summary":"List what a connection can be pointed at","operationId":"listAdminConnectionScopes","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminConnectorScopeResponse"}}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}/activity":{"get":{"tags":["admin-connector-controller"],"summary":"Read what a connection has crawled and what went wrong","operationId":"getAdminConnectionActivity","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminConnectionActivityResponse"}}}}}}},"/api/admin/connectors/sources":{"get":{"tags":["admin-connector-controller"],"summary":"List the sources this deployment can ingest","operationId":"listAdminConnectorSources","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminConnectorSourceResponse"}}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/curations/{curationId}":{"delete":{"tags":["knowledge-graph-management-controller"],"summary":"Reverse a graph curation record","operationId":"deactivateGraphCuration","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"curationId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"authorizationGeneration","in":"query","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"reason","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/knowledge-assets/{knowledgeAssetId}":{"delete":{"tags":["knowledge-asset-lifecycle-controller"],"summary":"Retire a Knowledge Asset and remove its derived graph","operationId":"deleteKnowledgeAsset","parameters":[{"name":"knowledgeAssetId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/KnowledgeAssetRef"}}}}}}},"/api/admin/roles/{role}/members/{userId}":{"delete":{"tags":["admin-role-controller"],"summary":"Remove a user from a role","operationId":"revokeAdminRole","parameters":[{"name":"role","in":"path","required":true,"schema":{"type":"string"}},{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/admin/invitations/{invitationId}":{"delete":{"tags":["admin-invitation-controller"],"summary":"Withdraw an invitation that has not been used","operationId":"revokeAdminInvitation","parameters":[{"name":"invitationId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}}},"components":{"schemas":{"ConfirmMappingRequest":{"type":"object","properties":{"appUserId":{"type":"string","format":"uuid"}}},"AdminSourceMappingResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appUserId":{"type":"string","format":"uuid"},"appUserName":{"type":"string"},"appUserEmail":{"type":"string"},"method":{"type":"string","enum":["IDP_JOIN","SSO_EMAIL_JOIN","SELF_CLAIM","ADMIN_CONFIRMED"]},"status":{"type":"string","enum":["ACTIVE","REVOKED"]},"evidence":{"type":"string"},"verifiedAt":{"type":"string","format":"date-time"}}},"AdminSourcePrincipalResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"externalKey":{"type":"string"},"kind":{"type":"string","enum":["SOURCE_USER","SOURCE_GROUP"]},"observedEmail":{"type":"string"},"observedDisplayName":{"type":"string"},"ssoVerified":{"type":"boolean"},"lastSeenAt":{"type":"string","format":"date-time"},"mapping":{"$ref":"#/components/schemas/AdminSourceMappingResponse"}}},"IdentityTrustRequest":{"type":"object","properties":{"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"identityTrust":{"type":"string","enum":["UNTRUSTED","SSO_VERIFIED"]}}},"AdminSourceConnectionResponse":{"type":"object","properties":{"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"identityTrust":{"type":"string","enum":["UNTRUSTED","SSO_VERIFIED"]},"trustDecidedByUserId":{"type":"string","format":"uuid"},"trustDecidedAt":{"type":"string","format":"date-time"},"userCount":{"type":"integer","format":"int32"},"mappedUserCount":{"type":"integer","format":"int32"},"unmappedUserCount":{"type":"integer","format":"int32"},"groupCount":{"type":"integer","format":"int32"},"lastSeenAt":{"type":"string","format":"date-time"}}},"ConfigureConnectionRequest":{"type":"object","properties":{"crawlEnabled":{"type":"boolean"},"knowledgeSpaceId":{"type":"string","format":"uuid"},"actorUserId":{"type":"string","format":"uuid"},"sourceConfig":{"type":"object","additionalProperties":{}},"contentCrawlIntervalSeconds":{"type":"integer","format":"int64"}}},"AdminConnectionResponse":{"type":"object","properties":{"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"identityTrust":{"type":"string","enum":["UNTRUSTED","SSO_VERIFIED"]},"crawlEnabled":{"type":"boolean"},"knowledgeSpaceId":{"type":"string","format":"uuid"},"actorUserId":{"type":"string","format":"uuid"},"sourceConfig":{"type":"object","additionalProperties":{}},"contentCrawlIntervalSeconds":{"type":"integer","format":"int64"},"credentialSet":{"type":"boolean"},"credentialSetByUserId":{"type":"string","format":"uuid"},"credentialSetAt":{"type":"string","format":"date-time"},"configuredByUserId":{"type":"string","format":"uuid"},"configuredAt":{"type":"string","format":"date-time"}}},"ConnectorCredentialRequest":{"type":"object","properties":{"credential":{"type":"string"}}},"SourceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"sourceSystem":{"type":"string"},"aclAuthority":{"type":"string"},"status":{"type":"string"},"classification":{"type":"string"},"fileName":{"type":"string"},"mediaType":{"type":"string"},"contentLength":{"type":"integer","format":"int64"},"failureCode":{"type":"string"},"failureMessage":{"type":"string"},"embeddingProfileKey":{"type":"string"},"embeddingProvider":{"type":"string"},"embeddingModel":{"type":"string"},"embeddingDimensions":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SuppressIdentityRequest":{"type":"object","properties":{"idempotencyKey":{"type":"string"},"reason":{"type":"string"},"authorizationGeneration":{"type":"integer","format":"int64"},"kind":{"type":"string","enum":["ENTITY","RELATION"]},"identityId":{"type":"string","format":"uuid"}}},"CuratedEntity":{"allOf":[{"$ref":"#/components/schemas/GraphCurationRecord"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"namespace":{"$ref":"#/components/schemas/ProjectionNamespace"},"entity":{"$ref":"#/components/schemas/GraphIdentityRef"},"name":{"type":"string"},"type":{"type":"string"},"description":{"type":"string"},"governingEvidence":{"$ref":"#/components/schemas/EvidenceReference"},"provenance":{"$ref":"#/components/schemas/CurationProvenance"}}}]},"CuratedRelation":{"allOf":[{"$ref":"#/components/schemas/GraphCurationRecord"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"namespace":{"$ref":"#/components/schemas/ProjectionNamespace"},"relation":{"$ref":"#/components/schemas/GraphIdentityRef"},"sourceEntity":{"$ref":"#/components/schemas/GraphIdentityRef"},"targetEntity":{"$ref":"#/components/schemas/GraphIdentityRef"},"type":{"type":"string"},"keywords":{"type":"array","items":{"type":"string"}},"description":{"type":"string"},"weight":{"type":"number","format":"double"},"governingEvidence":{"$ref":"#/components/schemas/EvidenceReference"},"provenance":{"$ref":"#/components/schemas/CurationProvenance"}}}]},"CurationProvenance":{"type":"object","properties":{"actorUserId":{"type":"string","format":"uuid"},"authorizationModelId":{"type":"string"},"aclGeneration":{"type":"integer","format":"int64"},"curatedAt":{"type":"string","format":"date-time"},"reason":{"type":"string"}}},"EvidenceReference":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"knowledgeAssetId":{"type":"string","format":"uuid"},"sourceRevisionId":{"type":"string","format":"uuid"},"chunkId":{"type":"string","format":"uuid"},"aclSnapshotId":{"type":"string","format":"uuid"},"aclGeneration":{"type":"integer","format":"int64"},"chunk":{"type":"boolean"}}},"GraphCurationRecord":{},"GraphIdentityRef":{"type":"object","properties":{"kind":{"type":"string","enum":["ENTITY","RELATION"]},"id":{"type":"string","format":"uuid"}}},"IdentityAlias":{"allOf":[{"$ref":"#/components/schemas/GraphCurationRecord"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"namespace":{"$ref":"#/components/schemas/ProjectionNamespace"},"source":{"$ref":"#/components/schemas/GraphIdentityRef"},"target":{"$ref":"#/components/schemas/GraphIdentityRef"},"provenance":{"$ref":"#/components/schemas/CurationProvenance"}}}]},"IdentitySuppression":{"allOf":[{"$ref":"#/components/schemas/GraphCurationRecord"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"namespace":{"$ref":"#/components/schemas/ProjectionNamespace"},"identity":{"$ref":"#/components/schemas/GraphIdentityRef"},"provenance":{"$ref":"#/components/schemas/CurationProvenance"}}}]},"ProjectionNamespace":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"workspace":{"type":"string"},"collection":{"type":"string"}}},"CurateRelationRequest":{"type":"object","properties":{"idempotencyKey":{"type":"string"},"reason":{"type":"string"},"authorizationGeneration":{"type":"integer","format":"int64"},"relationId":{"type":"string","format":"uuid"},"sourceEntityId":{"type":"string","format":"uuid"},"targetEntityId":{"type":"string","format":"uuid"},"type":{"type":"string"},"keywords":{"type":"array","items":{"type":"string"}},"description":{"type":"string"},"weight":{"type":"number","format":"double"},"evidence":{"$ref":"#/components/schemas/EvidenceRequest"}}},"EvidenceRequest":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"knowledgeAssetId":{"type":"string","format":"uuid"},"sourceRevisionId":{"type":"string","format":"uuid"},"chunkId":{"type":"string","format":"uuid"},"aclSnapshotId":{"type":"string","format":"uuid"},"aclGeneration":{"type":"integer","format":"int64"}}},"CurateEntityRequest":{"type":"object","properties":{"idempotencyKey":{"type":"string"},"reason":{"type":"string"},"authorizationGeneration":{"type":"integer","format":"int64"},"entityId":{"type":"string","format":"uuid"},"name":{"type":"string"},"type":{"type":"string"},"description":{"type":"string"},"evidence":{"$ref":"#/components/schemas/EvidenceRequest"}}},"AliasIdentityRequest":{"type":"object","properties":{"idempotencyKey":{"type":"string"},"reason":{"type":"string"},"authorizationGeneration":{"type":"integer","format":"int64"},"kind":{"type":"string","enum":["ENTITY","RELATION"]},"sourceIdentityId":{"type":"string","format":"uuid"},"targetIdentityId":{"type":"string","format":"uuid"}}},"GraphIndexJobView":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"knowledgeAssetId":{"type":"string","format":"uuid"},"knowledgeAssetVersionId":{"type":"string","format":"uuid"},"sourceRevisionId":{"type":"string","format":"uuid"},"projectionGeneration":{"type":"integer","format":"int64"},"status":{"type":"string"},"attempt":{"type":"integer","format":"int32"},"cancellationRequested":{"type":"boolean"},"cancellationRequestedAt":{"type":"string","format":"date-time"},"lastErrorCode":{"type":"string"},"lastErrorMessage":{"type":"string"},"completedAt":{"type":"string","format":"date-time"}}},"AssistantChatRequest":{"type":"object","properties":{"message":{"type":"string","maxLength":4000,"minLength":0},"limit":{"type":"integer","format":"int32"}},"required":["message"]},"ServerSentEventString":{},"AssignRoleRequest":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"}}},"CreateInvitationRequest":{"type":"object","properties":{"email":{"type":"string"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]},"departmentId":{"type":"string","format":"uuid"}}},"AdminInvitationResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]},"departmentId":{"type":"string","format":"uuid"},"status":{"type":"string"},"invitedAt":{"type":"string","format":"date-time"},"acceptedAt":{"type":"string","format":"date-time"},"acceptedAppUserId":{"type":"string","format":"uuid"}}},"AdminConnectorProbeResponse":{"type":"object","properties":{"authenticated":{"type":"boolean"},"connectionKey":{"type":"string"},"accountName":{"type":"string"},"identityName":{"type":"string"},"canReadContent":{"type":"boolean"},"errorCode":{"type":"string"}}},"ExplainAccessRequest":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"},"permission":{"type":"string"},"resourceType":{"type":"string"},"resourceId":{"type":"string","format":"uuid"}}},"AccessBlockResponse":{"type":"object","properties":{"branch":{"type":"string"},"kind":{"type":"string"},"detail":{"type":"string"}}},"AccessStepResponse":{"type":"object","properties":{"object":{"type":"string"},"relation":{"type":"string"},"kind":{"type":"string"}}},"AclProvenanceResponse":{"type":"object","properties":{"authority":{"type":"string"},"origin":{"type":"string"},"generation":{"type":"integer","format":"int64"},"capturedAt":{"type":"string","format":"date-time"},"expired":{"type":"boolean"}}},"ExplainAccessResponse":{"type":"object","properties":{"state":{"type":"string","enum":["ALLOWED","DENIED","UNKNOWN"]},"reasonCode":{"type":"string"},"path":{"type":"array","items":{"$ref":"#/components/schemas/AccessStepResponse"}},"blockedBy":{"type":"array","items":{"$ref":"#/components/schemas/AccessBlockResponse"}},"provenance":{"$ref":"#/components/schemas/AclProvenanceResponse"},"policyVersion":{"type":"string"},"evaluatedAt":{"type":"string","format":"date-time"}}},"UpdateAdminUserRequest":{"type":"object","properties":{"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]},"active":{"type":"boolean"}}},"AdminUserResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]},"departmentId":{"type":"string","format":"uuid"},"active":{"type":"boolean"},"signInLinked":{"type":"boolean"},"mappedPrincipalCount":{"type":"integer","format":"int32"}}},"SessionResponse":{"type":"object","properties":{"authenticated":{"type":"boolean"},"name":{"type":"string"},"email":{"type":"string"},"userId":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"departmentId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]}}},"CsrfResponse":{"type":"object","properties":{"headerName":{"type":"string"},"parameterName":{"type":"string"},"token":{"type":"string"}}},"DepartmentResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"OrganizationContextResponse":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"departments":{"type":"array","items":{"$ref":"#/components/schemas/DepartmentResponse"}},"users":{"type":"array","items":{"$ref":"#/components/schemas/UserResponse"}}}},"UserResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"departmentId":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]}}},"MeResponse":{"type":"object","properties":{"authenticated":{"type":"boolean"},"subject":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"},"authorizationProvider":{"type":"string"},"userId":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"departmentId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]}}},"KnowledgeEvidenceResponse":{"type":"object","properties":{"citationId":{"type":"string","format":"uuid"},"knowledgeAssetId":{"type":"string","format":"uuid"},"title":{"type":"string"},"content":{"type":"string"},"sourceUri":{"type":"string"},"startPage":{"type":"integer","format":"int32"},"endPage":{"type":"integer","format":"int32"},"heading":{"type":"string"},"relevanceScore":{"type":"number","format":"double"}}},"KnowledgeSearchResponse":{"type":"object","properties":{"requestId":{"type":"string"},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeEvidenceResponse"}}}},"Entity":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"type":{"type":"string"},"description":{"type":"string"},"citationChunkIds":{"type":"array","items":{"type":"string","format":"uuid"}}}},"KnowledgeGraphView":{"type":"object","properties":{"knowledgeSpaceId":{"type":"string","format":"uuid"},"entities":{"type":"array","items":{"$ref":"#/components/schemas/Entity"}},"relations":{"type":"array","items":{"$ref":"#/components/schemas/Relation"}},"truncated":{"type":"boolean"}}},"Relation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"sourceEntityId":{"type":"string","format":"uuid"},"targetEntityId":{"type":"string","format":"uuid"},"type":{"type":"string"},"description":{"type":"string"},"weight":{"type":"number","format":"double"},"citationChunkIds":{"type":"array","items":{"type":"string","format":"uuid"}}}},"KnowledgeSpaceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key":{"type":"string"},"name":{"type":"string"},"departmentId":{"type":"string","format":"uuid"}}},"StreamingResponseBody":{},"EffectivePermissionResponse":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"},"permissions":{"type":"object","additionalProperties":{"type":"string","enum":["ALLOWED","DENIED","UNKNOWN"]}},"evaluatedAt":{"type":"string","format":"date-time"}}},"AdminSourceGroupMemberResponse":{"type":"object","properties":{"principalId":{"type":"string","format":"uuid"},"externalKey":{"type":"string"},"observedDisplayName":{"type":"string"},"observedEmail":{"type":"string"},"appUserId":{"type":"string","format":"uuid"},"appUserName":{"type":"string"}}},"AdminSourceGroupResponse":{"type":"object","properties":{"principalId":{"type":"string","format":"uuid"},"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"externalKey":{"type":"string"},"observedDisplayName":{"type":"string"},"sourceAclSnapshotId":{"type":"string","format":"uuid"},"aclGeneration":{"type":"integer","format":"int64"},"sealedAt":{"type":"string","format":"date-time"},"members":{"type":"array","items":{"$ref":"#/components/schemas/AdminSourceGroupMemberResponse"}}}},"AdminRoleListResponse":{"type":"object","properties":{"roles":{"type":"array","items":{"$ref":"#/components/schemas/AdminRoleResponse"}},"complete":{"type":"boolean"},"policyVersion":{"type":"string"}}},"AdminRoleResponse":{"type":"object","properties":{"role":{"type":"string"},"assignees":{"type":"array","items":{"type":"string"}}}},"AdminConnectorScopeResponse":{"type":"object","properties":{"key":{"type":"string"},"displayName":{"type":"string"},"reachable":{"type":"boolean"},"admissible":{"type":"boolean"},"instruction":{"type":"string"}}},"AdminConnectionActivityResponse":{"type":"object","properties":{"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"objectsTotal":{"type":"integer","format":"int64"},"objectsActive":{"type":"integer","format":"int64"},"objectsArchived":{"type":"integer","format":"int64"},"lastObjectAt":{"type":"string","format":"date-time"},"lastCrawlAt":{"type":"string","format":"date-time"},"recentAttempts":{"type":"array","items":{"$ref":"#/components/schemas/AdminCrawlAttemptResponse"}}}},"AdminCrawlAttemptResponse":{"type":"object","properties":{"outcome":{"type":"string"},"objectsMaterialized":{"type":"integer","format":"int32"},"objectsRotated":{"type":"integer","format":"int32"},"objectsRematerialized":{"type":"integer","format":"int32"},"objectsRetired":{"type":"integer","format":"int32"},"objectsFailed":{"type":"integer","format":"int32"},"errorCode":{"type":"string"},"errorMessage":{"type":"string"},"attemptedAt":{"type":"string","format":"date-time"}}},"AdminConnectorSourceResponse":{"type":"object","properties":{"sourceSystem":{"type":"string"},"displayName":{"type":"string"}}},"KnowledgeAssetRef":{"type":"object","properties":{"knowledgeAssetId":{"type":"string","format":"uuid"},"knowledgeAssetVersionId":{"type":"string","format":"uuid"},"normalizedRecordId":{"type":"string","format":"uuid"},"rawSourceObjectId":{"type":"string","format":"uuid"},"sourceAclSnapshotId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","ACTIVE","RETIRED"]}}}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://127.0.0.1:8080","description":"Generated server url"}],"paths":{"/api/admin/source-principals/{principalId}/mapping":{"put":{"tags":["admin-source-access-controller"],"summary":"Confirm a principal maps to an internal user","operationId":"confirmAdminSourceMapping","parameters":[{"name":"principalId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmMappingRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminSourcePrincipalResponse"}}}}}},"delete":{"tags":["admin-source-access-controller"],"summary":"Revoke a principal's active mapping","operationId":"revokeAdminSourceMapping","parameters":[{"name":"principalId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminSourcePrincipalResponse"}}}}}}},"/api/admin/source-connections/identity-trust":{"put":{"tags":["admin-source-access-controller"],"summary":"Record the identity trust for a connection","operationId":"setAdminSourceConnectionTrust","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityTrustRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminSourceConnectionResponse"}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}":{"put":{"tags":["admin-connector-controller"],"summary":"Record how a connection is crawled","operationId":"configureAdminConnection","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigureConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminConnectionResponse"}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}/credential":{"put":{"tags":["admin-connector-controller"],"summary":"Store a credential for a connection","operationId":"setAdminConnectionCredential","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorCredentialRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}},"delete":{"tags":["admin-connector-controller"],"summary":"Forget a connection's stored credential","operationId":"forgetAdminConnectionCredential","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"}}}},"/api/sources":{"get":{"tags":["source-controller"],"summary":"List sources visible to the current user","operationId":"listSources","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SourceResponse"}}}}}}},"post":{"tags":["source-controller"],"summary":"Upload a source for asynchronous ingestion","operationId":"uploadSource","parameters":[{"name":"classification","in":"query","required":false,"schema":{"type":"string","default":"CONFIDENTIAL","enum":["PUBLIC","INTERNAL","CONFIDENTIAL","RESTRICTED"]}},{"name":"knowledgeSpaceId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SourceResponse"}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/curations/suppressions":{"post":{"tags":["knowledge-graph-management-controller"],"summary":"Delete an effective graph identity without deleting evidence","operationId":"suppressGraphIdentity","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuppressIdentityRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CuratedEntity"},{"$ref":"#/components/schemas/CuratedRelation"},{"$ref":"#/components/schemas/IdentityAlias"},{"$ref":"#/components/schemas/IdentitySuppression"}]}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/curations/relations":{"post":{"tags":["knowledge-graph-management-controller"],"summary":"Create or edit a governed graph relation","operationId":"curateGraphRelation","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurateRelationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CuratedEntity"},{"$ref":"#/components/schemas/CuratedRelation"},{"$ref":"#/components/schemas/IdentityAlias"},{"$ref":"#/components/schemas/IdentitySuppression"}]}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/curations/entities":{"post":{"tags":["knowledge-graph-management-controller"],"summary":"Create or edit a governed graph entity","operationId":"curateGraphEntity","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurateEntityRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CuratedEntity"},{"$ref":"#/components/schemas/CuratedRelation"},{"$ref":"#/components/schemas/IdentityAlias"},{"$ref":"#/components/schemas/IdentitySuppression"}]}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/curations/aliases":{"post":{"tags":["knowledge-graph-management-controller"],"summary":"Merge graph identities through a reversible alias","operationId":"mergeGraphIdentity","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AliasIdentityRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CuratedEntity"},{"$ref":"#/components/schemas/CuratedRelation"},{"$ref":"#/components/schemas/IdentityAlias"},{"$ref":"#/components/schemas/IdentitySuppression"}]}}}}}}},"/api/knowledge-assets/{knowledgeAssetId}/graph-index":{"post":{"tags":["knowledge-asset-lifecycle-controller"],"summary":"Ensure graph indexing uses the current processing profile","operationId":"ensureKnowledgeAssetGraphIndex","parameters":[{"name":"knowledgeAssetId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"Accepted","content":{"*/*":{"schema":{"$ref":"#/components/schemas/GraphIndexJobView"}}}}}}},"/api/knowledge-assets/graph-jobs/{jobId}/resume":{"post":{"tags":["knowledge-asset-lifecycle-controller"],"summary":"Resume unfinished graph indexing","operationId":"resumeGraphIndexJob","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"Accepted","content":{"*/*":{"schema":{"$ref":"#/components/schemas/GraphIndexJobView"}}}}}}},"/api/knowledge-assets/graph-jobs/{jobId}/cancel":{"post":{"tags":["knowledge-asset-lifecycle-controller"],"summary":"Cancel queued or in-flight graph indexing","operationId":"cancelGraphIndexJob","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/GraphIndexJobView"}}}}}}},"/api/assistant/chat":{"post":{"tags":["assistant-controller"],"summary":"Stream an answer from permission-verified knowledge","operationId":"streamAssistantChat","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistantChatRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"text/event-stream":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ServerSentEventString"}}}}}}}},"/api/admin/roles/{role}/members":{"post":{"tags":["admin-role-controller"],"summary":"Assign a user to a role","operationId":"assignAdminRole","parameters":[{"name":"role","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRoleRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}}},"/api/admin/invitations":{"get":{"tags":["admin-invitation-controller"],"summary":"List invited addresses and their status","operationId":"listAdminInvitations","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminInvitationResponse"}}}}}}},"post":{"tags":["admin-invitation-controller"],"summary":"Expect an address to sign in","operationId":"createAdminInvitation","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvitationRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminInvitationResponse"}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}/test":{"post":{"tags":["admin-connector-controller"],"summary":"Check a connection's stored credential","operationId":"testAdminConnection","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminConnectorProbeResponse"}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}/crawl":{"post":{"tags":["admin-connector-controller"],"summary":"Ask for a content crawl on the next poll","operationId":"requestAdminConnectionCrawl","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Accepted"}}}},"/api/admin/connectors/{sourceSystem}/test":{"post":{"tags":["admin-connector-controller"],"summary":"Check a credential without storing it","operationId":"testAdminConnectorCredential","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectorCredentialRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminConnectorProbeResponse"}}}}}}},"/api/admin/access/explain":{"post":{"tags":["admin-permission-controller"],"summary":"Answer whether a user holds a permission on one resource, and by which derivation","operationId":"explainAdminAccess","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExplainAccessRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ExplainAccessResponse"}}}}}}},"/api/admin/users/{userId}":{"patch":{"tags":["admin-user-controller"],"summary":"Change a user's role or activation","operationId":"updateAdminUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAdminUserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminUserResponse"}}}}}}},"/api/session":{"get":{"tags":["browser-session-controller"],"summary":"Read the current browser session","operationId":"getBrowserSession","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SessionResponse"}}}}}}},"/api/session/csrf":{"get":{"tags":["browser-session-controller"],"summary":"Issue a CSRF token for browser mutations","operationId":"getBrowserCsrfToken","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CsrfResponse"}}}}}}},"/api/organization/context":{"get":{"tags":["organization-context-controller"],"operationId":"context","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OrganizationContextResponse"}}}}}}},"/api/me":{"get":{"tags":["me-controller"],"operationId":"me","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}}}}},"/api/knowledge/search":{"get":{"tags":["knowledge-search-controller"],"summary":"Search permission-verified knowledge evidence","operationId":"searchKnowledge","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/KnowledgeSearchResponse"}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/export":{"get":{"tags":["knowledge-graph-management-controller"],"summary":"Export only graph evidence visible to the current user","operationId":"exportKnowledgeGraph","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","default":"JSON","enum":["JSON","CSV","MARKDOWN","TEXT"]}},{"name":"X-Request-Id","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/explorer":{"get":{"tags":["knowledge-graph-explorer-controller"],"summary":"Read a bounded permission-filtered graph view","operationId":"exploreKnowledgeGraph","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"entityLimit","in":"query","required":false,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/KnowledgeGraphView"}}}}}}},"/api/knowledge-spaces/visible":{"get":{"tags":["knowledge-space-controller"],"summary":"List Knowledge Spaces visible to the current user","operationId":"listVisibleKnowledgeSpaces","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeSpaceResponse"}}}}}}}},"/api/knowledge-spaces/upload-targets":{"get":{"tags":["knowledge-space-controller"],"summary":"List Knowledge Spaces where the current user may add knowledge","operationId":"listKnowledgeSpaceUploadTargets","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeSpaceResponse"}}}}}}}},"/api/knowledge-assets/graph-jobs/{jobId}":{"get":{"tags":["knowledge-asset-lifecycle-controller"],"summary":"Read graph indexing lifecycle status","operationId":"getGraphIndexJob","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/GraphIndexJobView"}}}}}}},"/api/health":{"get":{"tags":["health-controller"],"operationId":"health","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/api/citations/{chunkId}/content":{"get":{"tags":["citation-content-controller"],"summary":"Stream permission-verified source evidence","operationId":"readCitationContent","parameters":[{"name":"chunkId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StreamingResponseBody"}}}}}}},"/api/admin/users":{"get":{"tags":["admin-user-controller"],"summary":"List internal users with their sign-in and mapping status","operationId":"listAdminUsers","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminUserResponse"}}}}}}}},"/api/admin/users/{userId}/permissions":{"get":{"tags":["admin-permission-controller"],"summary":"Resolve a user's organization permissions as the engine currently answers them","operationId":"listAdminUserPermissions","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/EffectivePermissionResponse"}}}}}}},"/api/admin/source-principals":{"get":{"tags":["admin-source-access-controller"],"summary":"List observed principals and their mapping","operationId":"listAdminSourcePrincipals","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminSourcePrincipalResponse"}}}}}}}},"/api/admin/source-groups":{"get":{"tags":["admin-source-access-controller"],"summary":"List source groups with their sealed membership","operationId":"listAdminSourceGroups","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminSourceGroupResponse"}}}}}}}},"/api/admin/source-connections":{"get":{"tags":["admin-source-access-controller"],"summary":"List observed connections and their trust level","operationId":"listAdminSourceConnections","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminSourceConnectionResponse"}}}}}}}},"/api/admin/roles":{"get":{"tags":["admin-role-controller"],"summary":"List roles and who is assigned to them","operationId":"listAdminRoles","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminRoleListResponse"}}}}}}},"/api/admin/connectors/{sourceSystem}":{"get":{"tags":["admin-connector-controller"],"summary":"List a source's connections and their crawl settings","operationId":"listAdminConnections","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminConnectionResponse"}}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}/scopes":{"get":{"tags":["admin-connector-controller"],"summary":"List what a connection can be pointed at","operationId":"listAdminConnectionScopes","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminConnectorScopeResponse"}}}}}}}},"/api/admin/connectors/{sourceSystem}/{connectionKey}/activity":{"get":{"tags":["admin-connector-controller"],"summary":"Read what a connection has crawled and what went wrong","operationId":"getAdminConnectionActivity","parameters":[{"name":"sourceSystem","in":"path","required":true,"schema":{"type":"string"}},{"name":"connectionKey","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AdminConnectionActivityResponse"}}}}}}},"/api/admin/connectors/sources":{"get":{"tags":["admin-connector-controller"],"summary":"List the sources this deployment can ingest","operationId":"listAdminConnectorSources","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminConnectorSourceResponse"}}}}}}}},"/api/knowledge-spaces/{knowledgeSpaceId}/graph/curations/{curationId}":{"delete":{"tags":["knowledge-graph-management-controller"],"summary":"Reverse a graph curation record","operationId":"deactivateGraphCuration","parameters":[{"name":"knowledgeSpaceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"curationId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"authorizationGeneration","in":"query","required":true,"schema":{"type":"integer","format":"int64"}},{"name":"reason","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/api/knowledge-assets/{knowledgeAssetId}":{"delete":{"tags":["knowledge-asset-lifecycle-controller"],"summary":"Retire a Knowledge Asset and remove its derived graph","operationId":"deleteKnowledgeAsset","parameters":[{"name":"knowledgeAssetId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/KnowledgeAssetRef"}}}}}}},"/api/admin/roles/{role}/members/{userId}":{"delete":{"tags":["admin-role-controller"],"summary":"Remove a user from a role","operationId":"revokeAdminRole","parameters":[{"name":"role","in":"path","required":true,"schema":{"type":"string"}},{"name":"userId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/admin/invitations/{invitationId}":{"delete":{"tags":["admin-invitation-controller"],"summary":"Withdraw an invitation that has not been used","operationId":"revokeAdminInvitation","parameters":[{"name":"invitationId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}}},"components":{"schemas":{"ConfirmMappingRequest":{"type":"object","properties":{"appUserId":{"type":"string","format":"uuid"}}},"AdminSourceMappingResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appUserId":{"type":"string","format":"uuid"},"appUserName":{"type":"string"},"appUserEmail":{"type":"string"},"method":{"type":"string","enum":["IDP_JOIN","SSO_EMAIL_JOIN","SELF_CLAIM","ADMIN_CONFIRMED"]},"status":{"type":"string","enum":["ACTIVE","REVOKED"]},"evidence":{"type":"string"},"verifiedAt":{"type":"string","format":"date-time"}}},"AdminSourcePrincipalResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"externalKey":{"type":"string"},"kind":{"type":"string","enum":["SOURCE_USER","SOURCE_GROUP"]},"observedEmail":{"type":"string"},"observedDisplayName":{"type":"string"},"ssoVerified":{"type":"boolean"},"lastSeenAt":{"type":"string","format":"date-time"},"mapping":{"$ref":"#/components/schemas/AdminSourceMappingResponse"}}},"IdentityTrustRequest":{"type":"object","properties":{"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"identityTrust":{"type":"string","enum":["UNTRUSTED","SSO_VERIFIED"]}}},"AdminSourceConnectionResponse":{"type":"object","properties":{"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"identityTrust":{"type":"string","enum":["UNTRUSTED","SSO_VERIFIED"]},"trustDecidedByUserId":{"type":"string","format":"uuid"},"trustDecidedAt":{"type":"string","format":"date-time"},"userCount":{"type":"integer","format":"int32"},"mappedUserCount":{"type":"integer","format":"int32"},"unmappedUserCount":{"type":"integer","format":"int32"},"groupCount":{"type":"integer","format":"int32"},"lastSeenAt":{"type":"string","format":"date-time"}}},"ConfigureConnectionRequest":{"type":"object","properties":{"crawlEnabled":{"type":"boolean"},"knowledgeSpaceId":{"type":"string","format":"uuid"},"actorUserId":{"type":"string","format":"uuid"},"sourceConfig":{"type":"object","additionalProperties":{}},"contentCrawlIntervalSeconds":{"type":"integer","format":"int64"}}},"AdminConnectionResponse":{"type":"object","properties":{"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"identityTrust":{"type":"string","enum":["UNTRUSTED","SSO_VERIFIED"]},"crawlEnabled":{"type":"boolean"},"knowledgeSpaceId":{"type":"string","format":"uuid"},"actorUserId":{"type":"string","format":"uuid"},"sourceConfig":{"type":"object","additionalProperties":{}},"contentCrawlIntervalSeconds":{"type":"integer","format":"int64"},"credentialSet":{"type":"boolean"},"credentialSetByUserId":{"type":"string","format":"uuid"},"credentialSetAt":{"type":"string","format":"date-time"},"configuredByUserId":{"type":"string","format":"uuid"},"configuredAt":{"type":"string","format":"date-time"}}},"ConnectorCredentialRequest":{"type":"object","properties":{"credential":{"type":"string"}}},"SourceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"sourceSystem":{"type":"string"},"aclAuthority":{"type":"string"},"status":{"type":"string"},"classification":{"type":"string"},"fileName":{"type":"string"},"mediaType":{"type":"string"},"contentLength":{"type":"integer","format":"int64"},"failureCode":{"type":"string"},"failureMessage":{"type":"string"},"embeddingProfileKey":{"type":"string"},"embeddingProvider":{"type":"string"},"embeddingModel":{"type":"string"},"embeddingDimensions":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SuppressIdentityRequest":{"type":"object","properties":{"idempotencyKey":{"type":"string"},"reason":{"type":"string"},"authorizationGeneration":{"type":"integer","format":"int64"},"kind":{"type":"string","enum":["ENTITY","RELATION"]},"identityId":{"type":"string","format":"uuid"}}},"CuratedEntity":{"allOf":[{"$ref":"#/components/schemas/GraphCurationRecord"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"namespace":{"$ref":"#/components/schemas/ProjectionNamespace"},"entity":{"$ref":"#/components/schemas/GraphIdentityRef"},"name":{"type":"string"},"type":{"type":"string"},"description":{"type":"string"},"governingEvidence":{"$ref":"#/components/schemas/EvidenceReference"},"provenance":{"$ref":"#/components/schemas/CurationProvenance"}}}]},"CuratedRelation":{"allOf":[{"$ref":"#/components/schemas/GraphCurationRecord"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"namespace":{"$ref":"#/components/schemas/ProjectionNamespace"},"relation":{"$ref":"#/components/schemas/GraphIdentityRef"},"sourceEntity":{"$ref":"#/components/schemas/GraphIdentityRef"},"targetEntity":{"$ref":"#/components/schemas/GraphIdentityRef"},"type":{"type":"string"},"keywords":{"type":"array","items":{"type":"string"}},"description":{"type":"string"},"weight":{"type":"number","format":"double"},"governingEvidence":{"$ref":"#/components/schemas/EvidenceReference"},"provenance":{"$ref":"#/components/schemas/CurationProvenance"}}}]},"CurationProvenance":{"type":"object","properties":{"actorUserId":{"type":"string","format":"uuid"},"authorizationModelId":{"type":"string"},"aclGeneration":{"type":"integer","format":"int64"},"curatedAt":{"type":"string","format":"date-time"},"reason":{"type":"string"}}},"EvidenceReference":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"knowledgeAssetId":{"type":"string","format":"uuid"},"sourceRevisionId":{"type":"string","format":"uuid"},"chunkId":{"type":"string","format":"uuid"},"aclSnapshotId":{"type":"string","format":"uuid"},"aclGeneration":{"type":"integer","format":"int64"},"chunk":{"type":"boolean"}}},"GraphCurationRecord":{},"GraphIdentityRef":{"type":"object","properties":{"kind":{"type":"string","enum":["ENTITY","RELATION"]},"id":{"type":"string","format":"uuid"}}},"IdentityAlias":{"allOf":[{"$ref":"#/components/schemas/GraphCurationRecord"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"namespace":{"$ref":"#/components/schemas/ProjectionNamespace"},"source":{"$ref":"#/components/schemas/GraphIdentityRef"},"target":{"$ref":"#/components/schemas/GraphIdentityRef"},"provenance":{"$ref":"#/components/schemas/CurationProvenance"}}}]},"IdentitySuppression":{"allOf":[{"$ref":"#/components/schemas/GraphCurationRecord"},{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"namespace":{"$ref":"#/components/schemas/ProjectionNamespace"},"identity":{"$ref":"#/components/schemas/GraphIdentityRef"},"provenance":{"$ref":"#/components/schemas/CurationProvenance"}}}]},"ProjectionNamespace":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"workspace":{"type":"string"},"collection":{"type":"string"}}},"CurateRelationRequest":{"type":"object","properties":{"idempotencyKey":{"type":"string"},"reason":{"type":"string"},"authorizationGeneration":{"type":"integer","format":"int64"},"relationId":{"type":"string","format":"uuid"},"sourceEntityId":{"type":"string","format":"uuid"},"targetEntityId":{"type":"string","format":"uuid"},"type":{"type":"string"},"keywords":{"type":"array","items":{"type":"string"}},"description":{"type":"string"},"weight":{"type":"number","format":"double"},"evidence":{"$ref":"#/components/schemas/EvidenceRequest"}}},"EvidenceRequest":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"knowledgeAssetId":{"type":"string","format":"uuid"},"sourceRevisionId":{"type":"string","format":"uuid"},"chunkId":{"type":"string","format":"uuid"},"aclSnapshotId":{"type":"string","format":"uuid"},"aclGeneration":{"type":"integer","format":"int64"}}},"CurateEntityRequest":{"type":"object","properties":{"idempotencyKey":{"type":"string"},"reason":{"type":"string"},"authorizationGeneration":{"type":"integer","format":"int64"},"entityId":{"type":"string","format":"uuid"},"name":{"type":"string"},"type":{"type":"string"},"description":{"type":"string"},"evidence":{"$ref":"#/components/schemas/EvidenceRequest"}}},"AliasIdentityRequest":{"type":"object","properties":{"idempotencyKey":{"type":"string"},"reason":{"type":"string"},"authorizationGeneration":{"type":"integer","format":"int64"},"kind":{"type":"string","enum":["ENTITY","RELATION"]},"sourceIdentityId":{"type":"string","format":"uuid"},"targetIdentityId":{"type":"string","format":"uuid"}}},"GraphIndexJobView":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"knowledgeAssetId":{"type":"string","format":"uuid"},"knowledgeAssetVersionId":{"type":"string","format":"uuid"},"sourceRevisionId":{"type":"string","format":"uuid"},"projectionGeneration":{"type":"integer","format":"int64"},"graphProcessingProfileId":{"type":"string","format":"uuid"},"graphProcessingProfileSha256":{"type":"string"},"status":{"type":"string"},"attempt":{"type":"integer","format":"int32"},"cancellationRequested":{"type":"boolean"},"cancellationRequestedAt":{"type":"string","format":"date-time"},"lastErrorCode":{"type":"string"},"lastErrorMessage":{"type":"string"},"completedAt":{"type":"string","format":"date-time"}}},"AssistantChatRequest":{"type":"object","properties":{"message":{"type":"string","maxLength":4000,"minLength":0},"limit":{"type":"integer","format":"int32"}},"required":["message"]},"ServerSentEventString":{},"AssignRoleRequest":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"}}},"CreateInvitationRequest":{"type":"object","properties":{"email":{"type":"string"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]},"departmentId":{"type":"string","format":"uuid"}}},"AdminInvitationResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]},"departmentId":{"type":"string","format":"uuid"},"status":{"type":"string"},"invitedAt":{"type":"string","format":"date-time"},"acceptedAt":{"type":"string","format":"date-time"},"acceptedAppUserId":{"type":"string","format":"uuid"}}},"AdminConnectorProbeResponse":{"type":"object","properties":{"authenticated":{"type":"boolean"},"connectionKey":{"type":"string"},"accountName":{"type":"string"},"identityName":{"type":"string"},"canReadContent":{"type":"boolean"},"errorCode":{"type":"string"}}},"ExplainAccessRequest":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"},"permission":{"type":"string"},"resourceType":{"type":"string"},"resourceId":{"type":"string","format":"uuid"}}},"AccessBlockResponse":{"type":"object","properties":{"branch":{"type":"string"},"kind":{"type":"string"},"detail":{"type":"string"}}},"AccessStepResponse":{"type":"object","properties":{"object":{"type":"string"},"relation":{"type":"string"},"kind":{"type":"string"}}},"AclProvenanceResponse":{"type":"object","properties":{"authority":{"type":"string"},"origin":{"type":"string"},"generation":{"type":"integer","format":"int64"},"capturedAt":{"type":"string","format":"date-time"},"expired":{"type":"boolean"}}},"ExplainAccessResponse":{"type":"object","properties":{"state":{"type":"string","enum":["ALLOWED","DENIED","UNKNOWN"]},"reasonCode":{"type":"string"},"path":{"type":"array","items":{"$ref":"#/components/schemas/AccessStepResponse"}},"blockedBy":{"type":"array","items":{"$ref":"#/components/schemas/AccessBlockResponse"}},"provenance":{"$ref":"#/components/schemas/AclProvenanceResponse"},"policyVersion":{"type":"string"},"evaluatedAt":{"type":"string","format":"date-time"}}},"UpdateAdminUserRequest":{"type":"object","properties":{"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]},"active":{"type":"boolean"}}},"AdminUserResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]},"departmentId":{"type":"string","format":"uuid"},"active":{"type":"boolean"},"signInLinked":{"type":"boolean"},"mappedPrincipalCount":{"type":"integer","format":"int32"}}},"SessionResponse":{"type":"object","properties":{"authenticated":{"type":"boolean"},"name":{"type":"string"},"email":{"type":"string"},"userId":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"departmentId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]}}},"CsrfResponse":{"type":"object","properties":{"headerName":{"type":"string"},"parameterName":{"type":"string"},"token":{"type":"string"}}},"DepartmentResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"OrganizationContextResponse":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"departments":{"type":"array","items":{"$ref":"#/components/schemas/DepartmentResponse"}},"users":{"type":"array","items":{"$ref":"#/components/schemas/UserResponse"}}}},"UserResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"departmentId":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]}}},"MeResponse":{"type":"object","properties":{"authenticated":{"type":"boolean"},"subject":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"},"authorizationProvider":{"type":"string"},"userId":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"departmentId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["EMPLOYEE","TEAM_LEAD","MANAGER","DIRECTOR","EXECUTIVE","ADMIN"]}}},"KnowledgeEvidenceResponse":{"type":"object","properties":{"citationId":{"type":"string","format":"uuid"},"knowledgeAssetId":{"type":"string","format":"uuid"},"title":{"type":"string"},"content":{"type":"string"},"sourceUri":{"type":"string"},"startPage":{"type":"integer","format":"int32"},"endPage":{"type":"integer","format":"int32"},"heading":{"type":"string"},"relevanceScore":{"type":"number","format":"double"}}},"KnowledgeSearchResponse":{"type":"object","properties":{"requestId":{"type":"string"},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/KnowledgeEvidenceResponse"}}}},"Entity":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"type":{"type":"string"},"description":{"type":"string"},"citationChunkIds":{"type":"array","items":{"type":"string","format":"uuid"}}}},"KnowledgeGraphView":{"type":"object","properties":{"knowledgeSpaceId":{"type":"string","format":"uuid"},"entities":{"type":"array","items":{"$ref":"#/components/schemas/Entity"}},"relations":{"type":"array","items":{"$ref":"#/components/schemas/Relation"}},"truncated":{"type":"boolean"}}},"Relation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"sourceEntityId":{"type":"string","format":"uuid"},"targetEntityId":{"type":"string","format":"uuid"},"type":{"type":"string"},"description":{"type":"string"},"weight":{"type":"number","format":"double"},"citationChunkIds":{"type":"array","items":{"type":"string","format":"uuid"}}}},"KnowledgeSpaceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key":{"type":"string"},"name":{"type":"string"},"departmentId":{"type":"string","format":"uuid"}}},"StreamingResponseBody":{},"EffectivePermissionResponse":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"},"permissions":{"type":"object","additionalProperties":{"type":"string","enum":["ALLOWED","DENIED","UNKNOWN"]}},"evaluatedAt":{"type":"string","format":"date-time"}}},"AdminSourceGroupMemberResponse":{"type":"object","properties":{"principalId":{"type":"string","format":"uuid"},"externalKey":{"type":"string"},"observedDisplayName":{"type":"string"},"observedEmail":{"type":"string"},"appUserId":{"type":"string","format":"uuid"},"appUserName":{"type":"string"}}},"AdminSourceGroupResponse":{"type":"object","properties":{"principalId":{"type":"string","format":"uuid"},"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"externalKey":{"type":"string"},"observedDisplayName":{"type":"string"},"sourceAclSnapshotId":{"type":"string","format":"uuid"},"aclGeneration":{"type":"integer","format":"int64"},"sealedAt":{"type":"string","format":"date-time"},"members":{"type":"array","items":{"$ref":"#/components/schemas/AdminSourceGroupMemberResponse"}}}},"AdminRoleListResponse":{"type":"object","properties":{"roles":{"type":"array","items":{"$ref":"#/components/schemas/AdminRoleResponse"}},"complete":{"type":"boolean"},"policyVersion":{"type":"string"}}},"AdminRoleResponse":{"type":"object","properties":{"role":{"type":"string"},"assignees":{"type":"array","items":{"type":"string"}}}},"AdminConnectorScopeResponse":{"type":"object","properties":{"key":{"type":"string"},"displayName":{"type":"string"},"reachable":{"type":"boolean"},"admissible":{"type":"boolean"},"instruction":{"type":"string"}}},"AdminConnectionActivityResponse":{"type":"object","properties":{"sourceSystem":{"type":"string"},"sourceConnectionKey":{"type":"string"},"objectsTotal":{"type":"integer","format":"int64"},"objectsActive":{"type":"integer","format":"int64"},"objectsArchived":{"type":"integer","format":"int64"},"lastObjectAt":{"type":"string","format":"date-time"},"lastCrawlAt":{"type":"string","format":"date-time"},"recentAttempts":{"type":"array","items":{"$ref":"#/components/schemas/AdminCrawlAttemptResponse"}}}},"AdminCrawlAttemptResponse":{"type":"object","properties":{"outcome":{"type":"string"},"objectsMaterialized":{"type":"integer","format":"int32"},"objectsRotated":{"type":"integer","format":"int32"},"objectsRematerialized":{"type":"integer","format":"int32"},"objectsRetired":{"type":"integer","format":"int32"},"objectsFailed":{"type":"integer","format":"int32"},"errorCode":{"type":"string"},"errorMessage":{"type":"string"},"attemptedAt":{"type":"string","format":"date-time"}}},"AdminConnectorSourceResponse":{"type":"object","properties":{"sourceSystem":{"type":"string"},"displayName":{"type":"string"}}},"KnowledgeAssetRef":{"type":"object","properties":{"knowledgeAssetId":{"type":"string","format":"uuid"},"knowledgeAssetVersionId":{"type":"string","format":"uuid"},"normalizedRecordId":{"type":"string","format":"uuid"},"rawSourceObjectId":{"type":"string","format":"uuid"},"sourceAclSnapshotId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","ACTIVE","RETIRED"]}}}}}} \ No newline at end of file diff --git a/core/src/main/java/com/orgmemory/core/assistant/AssistantCitation.java b/core/src/main/java/com/orgmemory/core/assistant/AssistantCitation.java new file mode 100644 index 00000000..91dcff03 --- /dev/null +++ b/core/src/main/java/com/orgmemory/core/assistant/AssistantCitation.java @@ -0,0 +1,21 @@ +package com.orgmemory.core.assistant; + +import com.orgmemory.core.knowledge.RetrievedKnowledgeEvidence; + +/** + * One prompt-scoped citation whose number is assigned by the server when the + * permission-verified evidence snapshot is rendered. + */ +public record AssistantCitation( + int number, + RetrievedKnowledgeEvidence evidence) { + + public AssistantCitation { + if (number < 1) { + throw new IllegalArgumentException("citation number must be positive"); + } + if (evidence == null) { + throw new IllegalArgumentException("citation evidence is required"); + } + } +} diff --git a/core/src/main/java/com/orgmemory/core/assistant/AssistantPromptFactory.java b/core/src/main/java/com/orgmemory/core/assistant/AssistantPromptFactory.java index 46eb63ed..9b417b89 100644 --- a/core/src/main/java/com/orgmemory/core/assistant/AssistantPromptFactory.java +++ b/core/src/main/java/com/orgmemory/core/assistant/AssistantPromptFactory.java @@ -39,7 +39,7 @@ private static PreparedPrompt render( StringBuilder value = new StringBuilder("Question:\n") .append(question.strip()) .append("\n\nPermission-verified evidence:\n"); - List includedEvidence = + List citations = new ArrayList<>(); int remaining = MAX_EVIDENCE_CHARACTERS; for (int index = 0; index < evidence.size() && remaining > 0; index++) { @@ -47,8 +47,8 @@ private static PreparedPrompt render( String content = truncate( source.content(), Math.min(MAX_EXCERPT_CHARACTERS, remaining)); - int sourceNumber = includedEvidence.size() + 1; - includedEvidence.add(source); + int sourceNumber = citations.size() + 1; + citations.add(new AssistantCitation(sourceNumber, source)); value.append("\n--- SOURCE ") .append(sourceNumber) .append(" BEGIN ---\nTitle: ") @@ -71,7 +71,7 @@ private static PreparedPrompt render( new ChatGenerationRequest( SYSTEM_INSTRUCTION, value.toString()), - includedEvidence); + citations); } private static String truncate(String content, int maximumCharacters) { @@ -90,10 +90,10 @@ private static String truncate(String content, int maximumCharacters) { record PreparedPrompt( ChatGenerationRequest request, - List evidence) { + List citations) { PreparedPrompt { - evidence = List.copyOf(evidence); + citations = List.copyOf(citations); } } } diff --git a/core/src/main/java/com/orgmemory/core/assistant/AssistantService.java b/core/src/main/java/com/orgmemory/core/assistant/AssistantService.java index 61d9623a..ca7dd1c3 100644 --- a/core/src/main/java/com/orgmemory/core/assistant/AssistantService.java +++ b/core/src/main/java/com/orgmemory/core/assistant/AssistantService.java @@ -5,6 +5,7 @@ import com.orgmemory.core.ai.ChatModelPort; import com.orgmemory.core.knowledge.PermissionAwareKnowledgeSearch; import com.orgmemory.core.organization.CurrentActor; +import java.util.List; import reactor.core.publisher.Flux; public class AssistantService { @@ -29,7 +30,7 @@ public AssistantTurn startTurn( String requestId) { var search = retrieval.search(actor, question, requestedLimit, requestId); if (search.evidence().isEmpty()) { - return new AssistantTurn(search.requestId(), search.evidence(), Flux.just(NO_ACCESSIBLE_EVIDENCE)); + return new AssistantTurn(search.requestId(), List.of(), Flux.just(NO_ACCESSIBLE_EVIDENCE)); } AssistantPromptFactory.PreparedPrompt prompt = @@ -54,7 +55,7 @@ public AssistantTurn startTurn( } return new AssistantTurn( search.requestId(), - prompt.evidence(), + prompt.citations(), content); } } diff --git a/core/src/main/java/com/orgmemory/core/assistant/AssistantTurn.java b/core/src/main/java/com/orgmemory/core/assistant/AssistantTurn.java index e9c82030..ecef31ae 100644 --- a/core/src/main/java/com/orgmemory/core/assistant/AssistantTurn.java +++ b/core/src/main/java/com/orgmemory/core/assistant/AssistantTurn.java @@ -1,19 +1,24 @@ package com.orgmemory.core.assistant; -import com.orgmemory.core.knowledge.RetrievedKnowledgeEvidence; import java.util.List; import reactor.core.publisher.Flux; public record AssistantTurn( String requestId, - List evidence, + List citations, Flux content) { public AssistantTurn { if (requestId == null || requestId.isBlank()) { throw new IllegalArgumentException("requestId is required"); } - evidence = List.copyOf(evidence); + citations = List.copyOf(citations); + for (int index = 0; index < citations.size(); index++) { + if (citations.get(index).number() != index + 1) { + throw new IllegalArgumentException( + "citation numbers must be consecutive and ordered"); + } + } if (content == null) { throw new IllegalArgumentException("content is required"); } diff --git a/core/src/main/java/com/orgmemory/core/knowledge/ClaimedGraphIndex.java b/core/src/main/java/com/orgmemory/core/knowledge/ClaimedGraphIndex.java index ce3c9088..5d8e9c85 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/ClaimedGraphIndex.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/ClaimedGraphIndex.java @@ -14,6 +14,7 @@ public record ClaimedGraphIndex( UUID aclSnapshotId, long aclGeneration, long projectionGeneration, + GraphProcessingProfileRef graphProcessingProfile, String idempotencyKey, EmbeddingProfileRef embeddingProfile, String language, @@ -28,6 +29,7 @@ public record ClaimedGraphIndex( Objects.requireNonNull(knowledgeAssetVersionId, "knowledgeAssetVersionId"); Objects.requireNonNull(sourceRevisionId, "sourceRevisionId"); Objects.requireNonNull(aclSnapshotId, "aclSnapshotId"); + Objects.requireNonNull(graphProcessingProfile, "graphProcessingProfile"); idempotencyKey = Objects.requireNonNull(idempotencyKey, "idempotencyKey").strip(); if (idempotencyKey.isEmpty()) { throw new IllegalArgumentException("idempotencyKey must not be blank"); diff --git a/core/src/main/java/com/orgmemory/core/knowledge/CreateUploadSourceCommand.java b/core/src/main/java/com/orgmemory/core/knowledge/CreateUploadSourceCommand.java index 44998398..85b6e4a3 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/CreateUploadSourceCommand.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/CreateUploadSourceCommand.java @@ -7,7 +7,6 @@ public record CreateUploadSourceCommand( CurrentActor actor, String fileName, - String mediaType, long contentLength, KnowledgeClassification classification, UUID knowledgeSpaceId) { diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJob.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJob.java index e72b4a8e..2716a42e 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJob.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJob.java @@ -29,6 +29,9 @@ class GraphIndexJob extends BaseEntity { @Column(name = "source_revision_id", nullable = false, updatable = false) private UUID sourceRevisionId; + @Column(name = "graph_processing_profile_id", nullable = false, updatable = false) + private UUID graphProcessingProfileId; + @Column(name = "projection_generation", nullable = false, updatable = false) private long projectionGeneration; @@ -84,6 +87,7 @@ protected GraphIndexJob() { UUID knowledgeAssetVersionId, UUID sourceRevisionId, long projectionGeneration, + GraphProcessingProfileRef graphProcessingProfile, int maxAttempts, Instant now) { super(UUID.randomUUID()); @@ -98,13 +102,18 @@ protected GraphIndexJob() { this.knowledgeAssetVersionId = Objects.requireNonNull(knowledgeAssetVersionId, "knowledgeAssetVersionId"); this.sourceRevisionId = Objects.requireNonNull(sourceRevisionId, "sourceRevisionId"); + this.graphProcessingProfileId = + Objects.requireNonNull(graphProcessingProfile, "graphProcessingProfile").id(); this.projectionGeneration = projectionGeneration; this.jobType = TYPE; this.status = GraphIndexJobStatus.PENDING; this.availableAt = Objects.requireNonNull(now, "now"); this.maxAttempts = maxAttempts; this.idempotencyKey = idempotencyKey( - organizationId, sourceRevisionId, projectionGeneration); + organizationId, + sourceRevisionId, + projectionGeneration, + graphProcessingProfile.canonicalSha256()); } void claim(String workerId, Instant now, Duration leaseDuration) { @@ -244,6 +253,10 @@ UUID getSourceRevisionId() { return sourceRevisionId; } + UUID getGraphProcessingProfileId() { + return graphProcessingProfileId; + } + long getProjectionGeneration() { return projectionGeneration; } @@ -291,9 +304,25 @@ private boolean isTerminal() { || status == GraphIndexJobStatus.CANCELLED; } - private static String idempotencyKey( - UUID organizationId, UUID sourceRevisionId, long generation) { - return "graph:" + organizationId + ":" + sourceRevisionId + ":" + generation; + static String idempotencyKey( + UUID organizationId, + UUID sourceRevisionId, + long generation, + String graphProcessingProfileSha256) { + String profileSha256 = Objects.requireNonNull( + graphProcessingProfileSha256, "graphProcessingProfileSha256"); + if (!profileSha256.matches("[0-9a-f]{64}")) { + throw new IllegalArgumentException( + "graphProcessingProfileSha256 must be lowercase SHA-256 hex"); + } + return "graph:" + + organizationId + + ":" + + sourceRevisionId + + ":" + + generation + + ":" + + profileSha256; } private static String requireFingerprint(String value) { diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobQueue.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobQueue.java index 6cdf8b9f..ba77c227 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobQueue.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobQueue.java @@ -1,7 +1,11 @@ package com.orgmemory.core.knowledge; +import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.UUID; +import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.stereotype.Service; /** @@ -14,17 +18,26 @@ class GraphIndexJobQueue { private final GraphIndexJobRepository jobs; private final KnowledgeAssetVersionRepository versions; private final SourceIngestionProperties ingestionProperties; + private final GraphProcessingProfileResolver processingProfiles; + private final GraphProcessingProfileRegistry profileRegistry; + private final JdbcClient jdbc; GraphIndexJobQueue( GraphIndexJobRepository jobs, KnowledgeAssetVersionRepository versions, - SourceIngestionProperties ingestionProperties) { + SourceIngestionProperties ingestionProperties, + GraphProcessingProfileResolver processingProfiles, + GraphProcessingProfileRegistry profileRegistry, + JdbcClient jdbc) { this.jobs = jobs; this.versions = versions; this.ingestionProperties = ingestionProperties; + this.processingProfiles = processingProfiles; + this.profileRegistry = profileRegistry; + this.jdbc = jdbc; } - void enqueue( + UUID enqueue( UUID organizationId, UUID sourceRevisionId, KnowledgeAssetRef assetRef, @@ -41,16 +54,56 @@ void enqueue( .filter(candidate -> sourceRevisionId.equals(candidate.getSourceRevisionId())) .orElseThrow(() -> new IllegalStateException( "Graph indexing target does not match the active Knowledge Asset version")); - if (jobs.findByKnowledgeAssetVersionId(version.getId()).isPresent()) { - return; - } - jobs.save(new GraphIndexJob( + GraphProcessingProfileRef profile = + profileRegistry.resolve(processingProfiles.current()); + String idempotencyKey = GraphIndexJob.idempotencyKey( organizationId, - version.getKnowledgeAssetId(), - version.getId(), sourceRevisionId, version.getVersionNumber(), - ingestionProperties.maximumAttempts(), - availableAt)); + profile.canonicalSha256()); + UUID jobId = UUID.nameUUIDFromBytes( + idempotencyKey.getBytes(StandardCharsets.UTF_8)); + OffsetDateTime createdAt = OffsetDateTime.now(ZoneOffset.UTC); + jdbc.sql(""" + INSERT INTO graph_index_jobs ( + id, organization_id, knowledge_asset_id, + knowledge_asset_version_id, source_revision_id, + graph_processing_profile_id, projection_generation, + job_type, status, available_at, attempt_count, + max_attempts, idempotency_key, cancellation_requested, + created_at, updated_at, version + ) VALUES ( + :id, :organizationId, :knowledgeAssetId, + :knowledgeAssetVersionId, :sourceRevisionId, + :graphProcessingProfileId, :projectionGeneration, + :jobType, 'PENDING', :availableAt, 0, + :maxAttempts, :idempotencyKey, false, + :createdAt, :createdAt, 0 + ) + ON CONFLICT ( + knowledge_asset_version_id, + graph_processing_profile_id + ) DO NOTHING + """) + .param("id", jobId) + .param("organizationId", organizationId) + .param("knowledgeAssetId", version.getKnowledgeAssetId()) + .param("knowledgeAssetVersionId", version.getId()) + .param("sourceRevisionId", sourceRevisionId) + .param("graphProcessingProfileId", profile.id()) + .param("projectionGeneration", version.getVersionNumber()) + .param("jobType", GraphIndexJob.TYPE) + .param( + "availableAt", + OffsetDateTime.ofInstant(availableAt, ZoneOffset.UTC)) + .param("maxAttempts", ingestionProperties.maximumAttempts()) + .param("idempotencyKey", idempotencyKey) + .param("createdAt", createdAt) + .update(); + return jobs.findByKnowledgeAssetVersionIdAndGraphProcessingProfileId( + version.getId(), profile.id()) + .orElseThrow(() -> new IllegalStateException( + "graph index job enqueue did not produce a durable job")) + .getId(); } } diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobRepository.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobRepository.java index dfb2d4dd..dc484494 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobRepository.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobRepository.java @@ -9,10 +9,12 @@ interface GraphIndexJobRepository extends JpaRepository { - Optional findByKnowledgeAssetVersionId(UUID knowledgeAssetVersionId); - Optional findByIdAndOrganizationId(UUID id, UUID organizationId); + Optional findByKnowledgeAssetVersionIdAndGraphProcessingProfileId( + UUID knowledgeAssetVersionId, + UUID graphProcessingProfileId); + @Query(value = """ SELECT * FROM graph_index_jobs diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobView.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobView.java index 79dd0acf..d0bd6d29 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobView.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexJobView.java @@ -9,6 +9,8 @@ public record GraphIndexJobView( UUID knowledgeAssetVersionId, UUID sourceRevisionId, long projectionGeneration, + UUID graphProcessingProfileId, + String graphProcessingProfileSha256, String status, int attempt, boolean cancellationRequested, diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexLifecycleService.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexLifecycleService.java index 4721ffef..a84e5770 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexLifecycleService.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexLifecycleService.java @@ -15,9 +15,10 @@ * Authorization boundary around status, cancellation and unfinished-work * recovery. * - *

A succeeded job is final. Re-extraction requires a new immutable source - * revision; delete rebuilds the effective graph by removing only the retired - * revision's contributions. + *

A succeeded job is final for its immutable graph-processing profile. A new + * profile may enqueue a new projection for the same immutable source revision; + * delete rebuilds the effective graph by removing only the retired revision's + * contributions. */ @Service public class GraphIndexLifecycleService { @@ -27,12 +28,21 @@ public class GraphIndexLifecycleService { private static final String RESOURCE_TYPE = "knowledge_asset"; private final GraphIndexingCoordinator coordinator; + private final GraphIndexJobQueue queue; + private final KnowledgeAssetRepository assets; + private final KnowledgeAssetVersionRepository versions; private final RelationshipAuthorizationPort authorization; GraphIndexLifecycleService( GraphIndexingCoordinator coordinator, + GraphIndexJobQueue queue, + KnowledgeAssetRepository assets, + KnowledgeAssetVersionRepository versions, RelationshipAuthorizationPort authorization) { this.coordinator = coordinator; + this.queue = queue; + this.assets = assets; + this.versions = versions; this.authorization = authorization; } @@ -63,6 +73,41 @@ public GraphIndexJobView resume(CurrentActor actor, UUID jobId) { return coordinator.resume(actor.organizationId(), jobId); } + @Transactional + public GraphIndexJobView ensureCurrentProfile( + CurrentActor actor, + UUID knowledgeAssetId) { + Objects.requireNonNull(actor, "actor"); + UUID assetId = Objects.requireNonNull( + knowledgeAssetId, "knowledgeAssetId"); + require(actor, CAN_REBUILD, assetId); + KnowledgeAsset asset = assets + .findByIdAndOrganizationId(assetId, actor.organizationId()) + .filter(candidate -> candidate.getArchivedAt() == null) + .orElseThrow(); + KnowledgeAssetVersion version = versions + .findByIdAndOrganizationId( + Objects.requireNonNull( + asset.getCurrentVersionId(), "currentVersionId"), + actor.organizationId()) + .filter(candidate -> + candidate.getStatus() == KnowledgeAssetVersionStatus.ACTIVE) + .orElseThrow(); + UUID jobId = queue.enqueue( + actor.organizationId(), + Objects.requireNonNull( + version.getSourceRevisionId(), "sourceRevisionId"), + new KnowledgeAssetRef( + assetId, + version.getId(), + version.getNormalizedRecordId(), + version.getRawSourceObjectId(), + version.getSourceAclSnapshotId(), + version.getStatus()), + java.time.Instant.now()); + return coordinator.status(actor.organizationId(), jobId); + } + private void require( CurrentActor actor, PermissionKey permission, UUID knowledgeAssetId) { var decision = authorization.check(new RelationshipAuthorizationQuery( diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexingCoordinator.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexingCoordinator.java index 60711f74..bc6f4b57 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexingCoordinator.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexingCoordinator.java @@ -17,6 +17,7 @@ public class GraphIndexingCoordinator { private final SourceRevisionRepository revisions; private final SourceAclSnapshotRepository aclSnapshots; private final EmbeddingProfileRepository embeddingProfiles; + private final GraphProcessingProfileRegistry graphProcessingProfiles; private final KnowledgeChunkProjectionStore chunks; GraphIndexingCoordinator( @@ -26,6 +27,7 @@ public class GraphIndexingCoordinator { SourceRevisionRepository revisions, SourceAclSnapshotRepository aclSnapshots, EmbeddingProfileRepository embeddingProfiles, + GraphProcessingProfileRegistry graphProcessingProfiles, KnowledgeChunkProjectionStore chunks) { this.jobs = jobs; this.assets = assets; @@ -33,6 +35,7 @@ public class GraphIndexingCoordinator { this.revisions = revisions; this.aclSnapshots = aclSnapshots; this.embeddingProfiles = embeddingProfiles; + this.graphProcessingProfiles = graphProcessingProfiles; this.chunks = chunks; } @@ -155,6 +158,8 @@ private Optional currentClaim(GraphIndexJob job) { .map(EmbeddingProfile::toRef) .orElseThrow(() -> new IllegalStateException( "Graph index embedding profile is missing")); + GraphProcessingProfileRef graphProcessingProfile = + graphProcessingProfiles.get(job.getGraphProcessingProfileId()); var activeChunks = chunks.loadActive( job.getOrganizationId(), job.getSourceRevisionId(), @@ -175,6 +180,7 @@ private Optional currentClaim(GraphIndexJob job) { snapshot.getId(), snapshot.getAclGeneration(), job.getProjectionGeneration(), + graphProcessingProfile, job.getIdempotencyKey(), embeddingProfile, version.getLanguage(), @@ -248,13 +254,17 @@ private GraphIndexJob tenantJob(UUID organizationId, UUID jobId) { .orElseThrow(); } - private static GraphIndexJobView view(GraphIndexJob job) { + private GraphIndexJobView view(GraphIndexJob job) { + GraphProcessingProfileRef profile = + graphProcessingProfiles.get(job.getGraphProcessingProfileId()); return new GraphIndexJobView( job.getId(), job.getKnowledgeAssetId(), job.getKnowledgeAssetVersionId(), job.getSourceRevisionId(), job.getProjectionGeneration(), + profile.id(), + profile.canonicalSha256(), job.getStatus().name(), job.getAttemptCount(), job.cancellationRequested(), diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRef.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRef.java new file mode 100644 index 00000000..7034f2f2 --- /dev/null +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRef.java @@ -0,0 +1,20 @@ +package com.orgmemory.core.knowledge; + +import com.orgmemory.graphrag.processing.GraphProcessingProfile; +import java.util.Objects; +import java.util.UUID; + +public record GraphProcessingProfileRef( + UUID id, + String canonicalSha256, + GraphProcessingProfile profile) { + + public GraphProcessingProfileRef { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(profile, "profile"); + if (!profile.canonicalSha256().equals(canonicalSha256)) { + throw new IllegalArgumentException( + "graph processing profile hash does not match the profile"); + } + } +} diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRegistry.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRegistry.java new file mode 100644 index 00000000..d68b28c5 --- /dev/null +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRegistry.java @@ -0,0 +1,65 @@ +package com.orgmemory.core.knowledge; + +import com.orgmemory.graphrag.processing.GraphProcessingProfile; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Objects; +import java.util.UUID; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** Insert-only registry for reproducible graph-processing profiles. */ +@Service +public class GraphProcessingProfileRegistry { + + private final GraphProcessingProfileRepository profiles; + private final JdbcClient jdbc; + + GraphProcessingProfileRegistry( + GraphProcessingProfileRepository profiles, + JdbcClient jdbc) { + this.profiles = profiles; + this.jdbc = jdbc; + } + + @Transactional + public GraphProcessingProfileRef resolve(GraphProcessingProfile profile) { + Objects.requireNonNull(profile, "profile"); + UUID id = UUID.nameUUIDFromBytes( + ("graph-processing:" + profile.canonicalSha256()) + .getBytes(StandardCharsets.UTF_8)); + jdbc.sql(""" + INSERT INTO graph_processing_profiles ( + id, canonical_sha256, canonical_form, created_at + ) VALUES ( + :id, :canonicalSha256, :canonicalForm, :createdAt + ) + ON CONFLICT (canonical_sha256) DO NOTHING + """) + .param("id", id) + .param("canonicalSha256", profile.canonicalSha256()) + .param("canonicalForm", profile.canonicalForm()) + .param("createdAt", OffsetDateTime.now(ZoneOffset.UTC)) + .update(); + GraphProcessingProfileRef resolved = profiles + .findByCanonicalSha256(profile.canonicalSha256()) + .orElseThrow(() -> new IllegalStateException( + "graph processing profile registration failed")) + .toRef(); + if (!resolved.profile().equals(profile)) { + throw new IllegalStateException( + "graph processing profile hash is bound to different settings"); + } + return resolved; + } + + @Transactional(readOnly = true) + public GraphProcessingProfileRef get(UUID profileId) { + return profiles.findById(Objects.requireNonNull(profileId, "profileId")) + .orElseThrow(() -> new IllegalStateException( + "graph processing profile was not found")) + .toRef(); + } +} diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRepository.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRepository.java new file mode 100644 index 00000000..122af068 --- /dev/null +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileRepository.java @@ -0,0 +1,11 @@ +package com.orgmemory.core.knowledge; + +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +interface GraphProcessingProfileRepository + extends JpaRepository { + + Optional findByCanonicalSha256(String canonicalSha256); +} diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileResolver.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileResolver.java new file mode 100644 index 00000000..6ce3f455 --- /dev/null +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProfileResolver.java @@ -0,0 +1,40 @@ +package com.orgmemory.core.knowledge; + +import com.orgmemory.core.ai.AiRoute; +import com.orgmemory.core.ai.AiRouteResolver; +import com.orgmemory.core.ai.AiWorkload; +import com.orgmemory.graphrag.extraction.LightRagExtractionPrompt; +import com.orgmemory.graphrag.model.ExtractionProfile; +import com.orgmemory.graphrag.processing.GraphProcessingProfile; +import com.orgmemory.graphrag.processing.LightRagGraphProcessingProfiles; +import org.springframework.stereotype.Service; + +/** Resolves the desired immutable profile before a graph job enters the queue. */ +@Service +class GraphProcessingProfileResolver { + + private final AiRouteResolver routes; + private final GraphProcessingProperties properties; + + GraphProcessingProfileResolver( + AiRouteResolver routes, + GraphProcessingProperties properties) { + this.routes = routes; + this.properties = properties; + } + + GraphProcessingProfile current() { + AiRoute route = routes.resolve(AiWorkload.GRAPH_EXTRACTION); + return LightRagGraphProcessingProfiles.current(new ExtractionProfile( + route.gatewayId(), + route.modelId(), + LightRagExtractionPrompt.VERSION, + properties.maximumEntitiesPerChunk(), + properties.maximumRelationsPerChunk(), + properties.entityTypeGuidance(), + properties.extractionExamples(), + properties.maximumGleaningRounds(), + properties.maximumGleaningInputTokens(), + properties.maximumSectionContextTokens())); + } +} diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProperties.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProperties.java new file mode 100644 index 00000000..188196bc --- /dev/null +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphProcessingProperties.java @@ -0,0 +1,79 @@ +package com.orgmemory.core.knowledge; + +import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Semantic GraphRAG settings shared by every process that can enqueue or execute + * a graph indexing job. + */ +@ConfigurationProperties("orgmemory.graph-rag.processing") +public record GraphProcessingProperties( + Integer maximumEntitiesPerChunk, + Integer maximumRelationsPerChunk, + List entityTypeGuidance, + List extractionExamples, + Integer maximumGleaningRounds, + Integer maximumGleaningInputTokens, + Integer maximumSectionContextTokens) { + + private static final List DEFAULT_ENTITY_TYPES = List.of( + "PERSON", + "ORGANIZATION", + "TEAM", + "ROLE", + "POLICY", + "PROCESS", + "SYSTEM", + "PRODUCT", + "DOCUMENT", + "LOCATION", + "EVENT", + "CONCEPT", + "OTHER"); + + public GraphProcessingProperties { + maximumEntitiesPerChunk = + maximumEntitiesPerChunk == null ? 40 : maximumEntitiesPerChunk; + maximumRelationsPerChunk = + maximumRelationsPerChunk == null ? 60 : maximumRelationsPerChunk; + entityTypeGuidance = normalized( + entityTypeGuidance == null ? DEFAULT_ENTITY_TYPES : entityTypeGuidance); + extractionExamples = + normalized(extractionExamples == null ? List.of() : extractionExamples); + maximumGleaningRounds = + maximumGleaningRounds == null ? 1 : maximumGleaningRounds; + maximumGleaningInputTokens = + maximumGleaningInputTokens == null ? 24_000 : maximumGleaningInputTokens; + maximumSectionContextTokens = + maximumSectionContextTokens == null ? 256 : maximumSectionContextTokens; + if (maximumEntitiesPerChunk <= 0 || maximumRelationsPerChunk <= 0) { + throw new IllegalArgumentException( + "graph extraction limits must be positive"); + } + if (entityTypeGuidance.isEmpty()) { + throw new IllegalArgumentException( + "graph entity type guidance must not be empty"); + } + if (maximumGleaningRounds < 0 || maximumGleaningRounds > 1) { + throw new IllegalArgumentException( + "maximum gleaning rounds must be 0 or 1 for LightRAG v1.5.4 parity"); + } + if (maximumGleaningInputTokens < 0) { + throw new IllegalArgumentException( + "maximum gleaning input tokens must be non-negative"); + } + if (maximumSectionContextTokens <= 0) { + throw new IllegalArgumentException( + "maximum section context tokens must be positive"); + } + } + + private static List normalized(List values) { + return values.stream() + .map(value -> value == null ? "" : value.strip()) + .filter(value -> !value.isEmpty()) + .distinct() + .toList(); + } +} diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalConfiguration.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalConfiguration.java index 21e6e157..a0ab6841 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalConfiguration.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalConfiguration.java @@ -2,8 +2,10 @@ import com.orgmemory.core.authorization.RelationshipAuthorizationSetPort; import com.orgmemory.core.permission.PermissionAuditService; +import com.orgmemory.graphrag.observability.GraphRagEventSink; import com.orgmemory.graphrag.query.LightRagQueryEngine; import com.orgmemory.graphrag.storage.ProjectionPublicationStore; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -24,7 +26,8 @@ GraphRagKnowledgeRetrievalService graphRagKnowledgeRetrievalService( LightRagQueryEngine engine, GraphRagRetrievalPolicy policy, PermissionAuditService audit, - KnowledgeRetrievalProperties retrievalProperties) { + KnowledgeRetrievalProperties retrievalProperties, + ObjectProvider eventSinks) { return new GraphRagKnowledgeRetrievalService( searchAuthorization, evidenceScopes, @@ -36,6 +39,7 @@ GraphRagKnowledgeRetrievalService graphRagKnowledgeRetrievalService( engine, policy, audit, - retrievalProperties); + retrievalProperties, + eventSinks.getIfAvailable(() -> GraphRagEventSink.NO_OP)); } } diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalService.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalService.java index 057e6a17..f4471326 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalService.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalService.java @@ -9,11 +9,14 @@ import com.orgmemory.core.permission.PermissionAuditDecision; import com.orgmemory.core.permission.PermissionAuditService; import com.orgmemory.graphrag.model.EvidenceReference; +import com.orgmemory.graphrag.observability.GraphRagEventSink; import com.orgmemory.graphrag.query.LightRagQueryEngine; import com.orgmemory.graphrag.query.LightRagQueryRequest; import com.orgmemory.graphrag.query.LightRagQueryResult; import com.orgmemory.graphrag.storage.ProjectionNamespace; import com.orgmemory.graphrag.storage.ProjectionPublicationStore; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; @@ -50,6 +53,7 @@ public class GraphRagKnowledgeRetrievalService private final GraphRagRetrievalPolicy policy; private final PermissionAuditService audit; private final KnowledgeRetrievalProperties retrievalProperties; + private final GraphRagEventSink events; public GraphRagKnowledgeRetrievalService( KnowledgeSearchAuthorizationService searchAuthorization, @@ -62,7 +66,8 @@ public GraphRagKnowledgeRetrievalService( LightRagQueryEngine engine, GraphRagRetrievalPolicy policy, PermissionAuditService audit, - KnowledgeRetrievalProperties retrievalProperties) { + KnowledgeRetrievalProperties retrievalProperties, + GraphRagEventSink events) { this.searchAuthorization = searchAuthorization; this.evidenceScopes = evidenceScopes; this.authorization = authorization; @@ -74,6 +79,7 @@ public GraphRagKnowledgeRetrievalService( this.policy = policy; this.audit = audit; this.retrievalProperties = retrievalProperties; + this.events = Objects.requireNonNull(events, "events"); } @Transactional(readOnly = true) @@ -83,20 +89,76 @@ public SecureKnowledgeSearchResult search( String query, Integer requestedLimit, String suppliedRequestId) { - String requestId = requestId(suppliedRequestId); - String normalizedQuery = normalizeQuery(query); - int limit = validateLimit(requestedLimit); - String authorizationModelId = searchAuthorization.require( - actor, - requestId, - normalizedQuery); - return search( - actor, - normalizedQuery, - limit, - requestId, - authorizationModelId, - 0); + Objects.requireNonNull(actor, "actor"); + UUID operationId = UUID.randomUUID(); + long startedAt = System.nanoTime(); + try { + String requestId = requestId(suppliedRequestId); + String normalizedQuery = normalizeQuery(query); + int limit = validateLimit(requestedLimit); + String authorizationModelId = searchAuthorization.require( + actor, + requestId, + normalizedQuery); + SecureKnowledgeSearchResult result = search( + actor, + normalizedQuery, + limit, + requestId, + authorizationModelId, + 0); + emit( + operationId, + actor.organizationId(), + GraphRagEventSink.Outcome.SUCCEEDED, + startedAt, + result.evidence().size(), + null); + return result; + } catch (RuntimeException failure) { + emit( + operationId, + actor.organizationId(), + GraphRagEventSink.Outcome.FAILED, + startedAt, + 0, + failureCode(failure)); + throw failure; + } + } + + private void emit( + UUID operationId, + UUID organizationId, + GraphRagEventSink.Outcome outcome, + long startedAt, + int outputCount, + String failureCode) { + try { + events.emit(new GraphRagEventSink.GraphRagEvent( + operationId, + organizationId, + GraphRagEventSink.Stage.RETRIEVE, + outcome, + Duration.ofNanos(Math.max(0, System.nanoTime() - startedAt)), + 1, + outputCount, + null, + failureCode, + Instant.now())); + } catch (RuntimeException ignoredTelemetryFailure) { + // Telemetry must never become a retrieval availability dependency. + } + } + + private static String failureCode(RuntimeException failure) { + if (failure instanceof IllegalArgumentException) { + return "invalid_request"; + } + if (failure instanceof KnowledgeRetrievalUnavailableException) { + return "retrieval_unavailable"; + } + return "retrieval_failed"; } private SecureKnowledgeSearchResult search( diff --git a/core/src/main/java/com/orgmemory/core/knowledge/PersistedGraphProcessingProfile.java b/core/src/main/java/com/orgmemory/core/knowledge/PersistedGraphProcessingProfile.java new file mode 100644 index 00000000..d03e7430 --- /dev/null +++ b/core/src/main/java/com/orgmemory/core/knowledge/PersistedGraphProcessingProfile.java @@ -0,0 +1,35 @@ +package com.orgmemory.core.knowledge; + +import com.orgmemory.graphrag.processing.GraphProcessingProfile; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "graph_processing_profiles") +class PersistedGraphProcessingProfile { + + @Id + private UUID id; + + @Column(name = "canonical_sha256", nullable = false, length = 64, updatable = false) + private String canonicalSha256; + + @Column(name = "canonical_form", nullable = false, columnDefinition = "text", updatable = false) + private String canonicalForm; + + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; + + protected PersistedGraphProcessingProfile() { + } + + GraphProcessingProfileRef toRef() { + GraphProcessingProfile profile = + GraphProcessingProfile.restore(canonicalForm, canonicalSha256); + return new GraphProcessingProfileRef(id, canonicalSha256, profile); + } +} diff --git a/core/src/main/java/com/orgmemory/core/knowledge/SourceUploadService.java b/core/src/main/java/com/orgmemory/core/knowledge/SourceUploadService.java index b34a63d9..e661bceb 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/SourceUploadService.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/SourceUploadService.java @@ -47,7 +47,7 @@ public SourceSummary upload(CreateUploadSourceCommand command, InputStream conte KnowledgeSpaceTarget targetSpace = knowledgeSpaces.requireUploadTarget( actor, command.knowledgeSpaceId()); String fileName = safeFileName(command.fileName()); - String mediaType = normalizedMediaType(command.mediaType(), fileName); + String mediaType = canonicalMediaType(fileName); KnowledgeClassification classification = command.classification() == null ? KnowledgeClassification.CONFIDENTIAL : command.classification(); @@ -132,12 +132,7 @@ private static String extension(String fileName) { return dot < 0 ? "" : fileName.substring(dot + 1).toLowerCase(Locale.ROOT); } - private static String normalizedMediaType(String claimedMediaType, String fileName) { - if (claimedMediaType != null - && !claimedMediaType.isBlank() - && !"application/octet-stream".equalsIgnoreCase(claimedMediaType)) { - return claimedMediaType.strip().toLowerCase(Locale.ROOT); - } + private static String canonicalMediaType(String fileName) { return switch (extension(fileName)) { case "pdf" -> "application/pdf"; case "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; diff --git a/core/src/main/resources/db/migration/V1__baseline.sql b/core/src/main/resources/db/migration/V1__baseline.sql index ebbc6d0c..fcc3df6f 100644 --- a/core/src/main/resources/db/migration/V1__baseline.sql +++ b/core/src/main/resources/db/migration/V1__baseline.sql @@ -360,6 +360,20 @@ CREATE TABLE public.graph_curation_records ( ); +-- +-- Name: graph_processing_profiles; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.graph_processing_profiles ( + id uuid NOT NULL, + canonical_sha256 character varying(64) NOT NULL, + canonical_form text NOT NULL, + created_at timestamp with time zone NOT NULL, + CONSTRAINT chk_graph_processing_profile_canonical_nonblank CHECK ((btrim(canonical_form) <> ''::text)), + CONSTRAINT chk_graph_processing_profile_sha CHECK (((canonical_sha256)::text ~ '^[0-9a-f]{64}$'::text)) +); + + -- -- Name: graph_index_jobs; Type: TABLE; Schema: public; Owner: - -- @@ -370,6 +384,7 @@ CREATE TABLE public.graph_index_jobs ( knowledge_asset_id uuid NOT NULL, knowledge_asset_version_id uuid NOT NULL, source_revision_id uuid NOT NULL, + graph_processing_profile_id uuid NOT NULL, projection_generation bigint NOT NULL, job_type character varying(64) NOT NULL, status character varying(32) NOT NULL, @@ -1458,6 +1473,14 @@ ALTER TABLE ONLY public.graph_curation_records ADD CONSTRAINT graph_curation_records_pkey PRIMARY KEY (id); +-- +-- Name: graph_processing_profiles graph_processing_profiles_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.graph_processing_profiles + ADD CONSTRAINT graph_processing_profiles_pkey PRIMARY KEY (id); + + -- -- Name: graph_index_jobs graph_index_jobs_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1850,6 +1873,14 @@ ALTER TABLE ONLY public.embedding_profiles ADD CONSTRAINT uq_embedding_profile_projection UNIQUE (id, organization_id, dimensions); +-- +-- Name: graph_processing_profiles uq_graph_processing_profile_sha; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.graph_processing_profiles + ADD CONSTRAINT uq_graph_processing_profile_sha UNIQUE (canonical_sha256); + + -- -- Name: evidence_blobs uq_evidence_blob_id_organization; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1883,11 +1914,11 @@ ALTER TABLE ONLY public.external_identities -- --- Name: graph_index_jobs uq_graph_index_job_version; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: graph_index_jobs uq_graph_index_job_version_profile; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.graph_index_jobs - ADD CONSTRAINT uq_graph_index_job_version UNIQUE (knowledge_asset_version_id); + ADD CONSTRAINT uq_graph_index_job_version_profile UNIQUE (knowledge_asset_version_id, graph_processing_profile_id); -- @@ -2817,6 +2848,14 @@ ALTER TABLE ONLY public.graph_index_jobs ADD CONSTRAINT fk_graph_index_job_asset_version FOREIGN KEY (knowledge_asset_version_id, organization_id, knowledge_asset_id) REFERENCES public.knowledge_asset_versions(id, organization_id, knowledge_asset_id); +-- +-- Name: graph_index_jobs fk_graph_index_job_processing_profile; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.graph_index_jobs + ADD CONSTRAINT fk_graph_index_job_processing_profile FOREIGN KEY (graph_processing_profile_id) REFERENCES public.graph_processing_profiles(id); + + -- -- Name: graph_index_jobs fk_graph_index_job_source_revision; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -3684,4 +3723,3 @@ ALTER TABLE ONLY public.user_invitations -- -- PostgreSQL database dump complete -- - diff --git a/core/src/test/java/com/orgmemory/core/assistant/AssistantServiceTests.java b/core/src/test/java/com/orgmemory/core/assistant/AssistantServiceTests.java index 5b848aa5..e0966423 100644 --- a/core/src/test/java/com/orgmemory/core/assistant/AssistantServiceTests.java +++ b/core/src/test/java/com/orgmemory/core/assistant/AssistantServiceTests.java @@ -51,7 +51,14 @@ void streamsOnlyPermissionVerifiedEvidenceToTheModel() { assertEquals(List.of("The probation period ", "is 60 days. [1]"), turn.content().collectList().block()); - assertEquals(List.of(evidence), turn.evidence()); + assertEquals(List.of(evidence), + turn.citations().stream() + .map(AssistantCitation::evidence) + .toList()); + assertEquals(List.of(1), + turn.citations().stream() + .map(AssistantCitation::number) + .toList()); ArgumentCaptor request = ArgumentCaptor.forClass(ChatGenerationRequest.class); verify(chat).stream(eq(AiWorkload.ASSISTANT_CHAT), request.capture()); assertEquals(true, request.getValue().userPrompt().contains(evidence.content())); @@ -80,7 +87,14 @@ void exposesCitationsOnlyForEvidenceIncludedInThePromptBudget() { 10, "request-budget"); - assertEquals(evidence.subList(0, 5), turn.evidence()); + assertEquals(evidence.subList(0, 5), + turn.citations().stream() + .map(AssistantCitation::evidence) + .toList()); + assertEquals(List.of(1, 2, 3, 4, 5), + turn.citations().stream() + .map(AssistantCitation::number) + .toList()); ArgumentCaptor request = ArgumentCaptor.forClass(ChatGenerationRequest.class); verify(chat).stream( diff --git a/core/src/test/java/com/orgmemory/core/knowledge/GraphIndexLifecycleServiceTests.java b/core/src/test/java/com/orgmemory/core/knowledge/GraphIndexLifecycleServiceTests.java new file mode 100644 index 00000000..31dbb85c --- /dev/null +++ b/core/src/test/java/com/orgmemory/core/knowledge/GraphIndexLifecycleServiceTests.java @@ -0,0 +1,97 @@ +package com.orgmemory.core.knowledge; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.orgmemory.core.authorization.AuthorizationDecision; +import com.orgmemory.core.authorization.RelationshipAuthorizationPort; +import com.orgmemory.core.organization.CurrentActor; +import com.orgmemory.core.organization.OrgMemoryAccessDeniedException; +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +class GraphIndexLifecycleServiceTests { + + private static final UUID ORGANIZATION_ID = UUID.randomUUID(); + private static final UUID USER_ID = UUID.randomUUID(); + private static final UUID ASSET_ID = UUID.randomUUID(); + private static final UUID VERSION_ID = UUID.randomUUID(); + private static final UUID REVISION_ID = UUID.randomUUID(); + private static final UUID JOB_ID = UUID.randomUUID(); + + private final GraphIndexingCoordinator coordinator = + mock(GraphIndexingCoordinator.class); + private final GraphIndexJobQueue queue = mock(GraphIndexJobQueue.class); + private final KnowledgeAssetRepository assets = + mock(KnowledgeAssetRepository.class); + private final KnowledgeAssetVersionRepository versions = + mock(KnowledgeAssetVersionRepository.class); + private final RelationshipAuthorizationPort authorization = + mock(RelationshipAuthorizationPort.class); + private final CurrentActor actor = + new CurrentActor(USER_ID, ORGANIZATION_ID, null, "User", "user@example.com"); + private final GraphIndexLifecycleService service = + new GraphIndexLifecycleService( + coordinator, queue, assets, versions, authorization); + + @Test + void ensureCurrentProfileAuthorizesAndEnqueuesTheActiveRevision() { + KnowledgeAsset asset = mock(KnowledgeAsset.class); + KnowledgeAssetVersion version = mock(KnowledgeAssetVersion.class); + GraphIndexJobView expected = mock(GraphIndexJobView.class); + when(authorization.check(any())) + .thenReturn(AuthorizationDecision.allow("model-v1")); + when(assets.findByIdAndOrganizationId(ASSET_ID, ORGANIZATION_ID)) + .thenReturn(Optional.of(asset)); + when(asset.getCurrentVersionId()).thenReturn(VERSION_ID); + when(versions.findByIdAndOrganizationId(VERSION_ID, ORGANIZATION_ID)) + .thenReturn(Optional.of(version)); + when(version.getStatus()).thenReturn(KnowledgeAssetVersionStatus.ACTIVE); + when(version.getSourceRevisionId()).thenReturn(REVISION_ID); + when(version.getId()).thenReturn(VERSION_ID); + when(queue.enqueue( + any(), + any(), + any(), + any())) + .thenReturn(JOB_ID); + when(coordinator.status(ORGANIZATION_ID, JOB_ID)).thenReturn(expected); + + GraphIndexJobView actual = service.ensureCurrentProfile(actor, ASSET_ID); + + assertEquals(expected, actual); + var reference = ArgumentCaptor.forClass(KnowledgeAssetRef.class); + verify(queue).enqueue( + org.mockito.ArgumentMatchers.eq(ORGANIZATION_ID), + org.mockito.ArgumentMatchers.eq(REVISION_ID), + reference.capture(), + any(Instant.class)); + assertEquals(ASSET_ID, reference.getValue().knowledgeAssetId()); + assertEquals(VERSION_ID, reference.getValue().knowledgeAssetVersionId()); + assertEquals( + KnowledgeAssetVersionStatus.ACTIVE, + reference.getValue().status()); + } + + @Test + void deniedRebuildDoesNotReadOrMutateGraphIndexState() { + when(authorization.check(any())) + .thenReturn(AuthorizationDecision.deny("DENIED", "model-v1")); + + assertThrows( + OrgMemoryAccessDeniedException.class, + () -> service.ensureCurrentProfile(actor, ASSET_ID)); + + verify(assets, never()).findByIdAndOrganizationId(any(), any()); + verify(queue, never()).enqueue(any(), any(), any(), any()); + verify(coordinator, never()).status(any(), any()); + } +} diff --git a/core/src/test/java/com/orgmemory/core/knowledge/GraphIndexingCoordinatorTests.java b/core/src/test/java/com/orgmemory/core/knowledge/GraphIndexingCoordinatorTests.java index 34dbb82a..b630401e 100644 --- a/core/src/test/java/com/orgmemory/core/knowledge/GraphIndexingCoordinatorTests.java +++ b/core/src/test/java/com/orgmemory/core/knowledge/GraphIndexingCoordinatorTests.java @@ -6,7 +6,10 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.orgmemory.graphrag.extraction.LightRagExtractionPrompt; +import com.orgmemory.graphrag.model.ExtractionProfile; import com.orgmemory.graphrag.model.FloatVector; +import com.orgmemory.graphrag.processing.LightRagGraphProcessingProfiles; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -25,6 +28,8 @@ class GraphIndexingCoordinatorTests { private static final UUID ACL_SNAPSHOT_ID = UUID.randomUUID(); private static final UUID EMBEDDING_PROFILE_ID = UUID.randomUUID(); private static final UUID CHUNK_ID = UUID.randomUUID(); + private static final GraphProcessingProfileRef GRAPH_PROCESSING_PROFILE = + graphProcessingProfile(); private final GraphIndexJobRepository jobs = mock(GraphIndexJobRepository.class); private final KnowledgeAssetRepository assets = mock(KnowledgeAssetRepository.class); @@ -35,10 +40,19 @@ class GraphIndexingCoordinatorTests { mock(SourceAclSnapshotRepository.class); private final EmbeddingProfileRepository embeddingProfiles = mock(EmbeddingProfileRepository.class); + private final GraphProcessingProfileRegistry graphProcessingProfiles = + mock(GraphProcessingProfileRegistry.class); private final KnowledgeChunkProjectionStore chunks = mock(KnowledgeChunkProjectionStore.class); private final GraphIndexingCoordinator coordinator = new GraphIndexingCoordinator( - jobs, assets, versions, revisions, aclSnapshots, embeddingProfiles, chunks); + jobs, + assets, + versions, + revisions, + aclSnapshots, + embeddingProfiles, + graphProcessingProfiles, + chunks); private GraphIndexJob job; private KnowledgeAsset asset; @@ -51,6 +65,7 @@ void setUpCurrentTarget() { VERSION_ID, REVISION_ID, 1, + GRAPH_PROCESSING_PROFILE, 5, Instant.parse("2026-07-23T00:00:00Z")); asset = mock(KnowledgeAsset.class); @@ -96,6 +111,8 @@ void setUpCurrentTarget() { "text-embedding-3-large", 1536, EmbeddingDistanceMetric.COSINE)); + when(graphProcessingProfiles.get(GRAPH_PROCESSING_PROFILE.id())) + .thenReturn(GRAPH_PROCESSING_PROFILE); when(chunks.loadActive( ORGANIZATION_ID, REVISION_ID, @@ -277,6 +294,7 @@ void failsAReclaimedFinalAttemptInsteadOfLeavingItProcessingForever() { VERSION_ID, REVISION_ID, 1, + GRAPH_PROCESSING_PROFILE, 1, Instant.parse("2026-07-23T00:00:00Z")); job.claim("lost-worker", Instant.parse("2026-07-23T00:00:00Z"), Duration.ofSeconds(1)); @@ -305,4 +323,33 @@ void retriesWhenPinnedProjectionInputsAreTemporarilyUnavailable() { assertEquals(GraphIndexJobStatus.PENDING, job.getStatus()); assertEquals(1, job.getAttemptCount()); } + + @Test + void processingProfileIsAnIndependentGraphJobIdentityCoordinate() { + String current = GraphIndexJob.idempotencyKey( + ORGANIZATION_ID, + REVISION_ID, + 1, + "a".repeat(64)); + String rebuilt = GraphIndexJob.idempotencyKey( + ORGANIZATION_ID, + REVISION_ID, + 1, + "b".repeat(64)); + + assertTrue(current.startsWith( + "graph:" + ORGANIZATION_ID + ":" + REVISION_ID + ":1:")); + assertTrue(!current.equals(rebuilt)); + } + + private static GraphProcessingProfileRef graphProcessingProfile() { + var profile = LightRagGraphProcessingProfiles.current(new ExtractionProfile( + "openai", + "gpt-test", + LightRagExtractionPrompt.VERSION, + 40, + 60)); + return new GraphProcessingProfileRef( + UUID.randomUUID(), profile.canonicalSha256(), profile); + } } diff --git a/core/src/test/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalServiceTests.java b/core/src/test/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalServiceTests.java index e6e6a00f..32918adb 100644 --- a/core/src/test/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalServiceTests.java +++ b/core/src/test/java/com/orgmemory/core/knowledge/GraphRagKnowledgeRetrievalServiceTests.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.assertNotNull; import com.orgmemory.core.authorization.AuthorizationDecision; import com.orgmemory.core.authorization.RelationshipAuthorizationPort; @@ -13,6 +14,7 @@ import com.orgmemory.core.organization.CurrentActor; import com.orgmemory.core.permission.PermissionAuditService; import com.orgmemory.graphrag.model.EvidenceReference; +import com.orgmemory.graphrag.observability.GraphRagEventSink; import com.orgmemory.graphrag.query.KeywordPlan; import com.orgmemory.graphrag.query.LightRagQueryEngine; import com.orgmemory.graphrag.query.LightRagQueryMode; @@ -27,6 +29,7 @@ import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; class GraphRagKnowledgeRetrievalServiceTests { @@ -107,6 +110,7 @@ void revocationBetweenRetrievalAndCitationCausesAFullRetryWithoutEgress() { mock(RelationshipAuthorizationSetPort.class); NeverRecheckedStore canonical = new NeverRecheckedStore(); + GraphRagEventSink events = mock(GraphRagEventSink.class); var service = new GraphRagKnowledgeRetrievalService( new KnowledgeSearchAuthorizationService(entry, audit), @@ -126,7 +130,8 @@ void revocationBetweenRetrievalAndCitationCausesAFullRetryWithoutEgress() { 20, 5, 5_000, - 1_000)); + 1_000), + events); SecureKnowledgeSearchResult result = service.search( actor, @@ -138,6 +143,13 @@ void revocationBetweenRetrievalAndCitationCausesAFullRetryWithoutEgress() { verify(engine).execute(any()); verify(finalAuthorization, never()).batchCheck(any()); assertEquals(0, canonical.recheckCount); + ArgumentCaptor captured = + ArgumentCaptor.forClass(GraphRagEventSink.GraphRagEvent.class); + verify(events).emit(captured.capture()); + assertEquals(GraphRagEventSink.Stage.RETRIEVE, captured.getValue().stage()); + assertEquals(GraphRagEventSink.Outcome.SUCCEEDED, captured.getValue().outcome()); + assertEquals(0, captured.getValue().outputCount()); + assertNotNull(captured.getValue().operationId()); } private static ResolvedKnowledgeEvidenceScope scope( diff --git a/core/src/test/java/com/orgmemory/core/knowledge/SourceUploadServiceTests.java b/core/src/test/java/com/orgmemory/core/knowledge/SourceUploadServiceTests.java index 61f4d000..9624d66d 100644 --- a/core/src/test/java/com/orgmemory/core/knowledge/SourceUploadServiceTests.java +++ b/core/src/test/java/com/orgmemory/core/knowledge/SourceUploadServiceTests.java @@ -1,11 +1,17 @@ package com.orgmemory.core.knowledge; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import com.orgmemory.core.knowledge.storage.ObjectKey; import com.orgmemory.core.knowledge.storage.ObjectStoragePort; +import com.orgmemory.core.knowledge.storage.ObjectWriteRequest; +import com.orgmemory.core.knowledge.storage.StoredObject; import com.orgmemory.core.organization.CurrentActor; import com.orgmemory.core.organization.OrgMemoryAccessDeniedException; import com.orgmemory.core.permission.KnowledgeClassification; @@ -13,10 +19,71 @@ import java.io.ByteArrayInputStream; import java.util.UUID; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.util.unit.DataSize; class SourceUploadServiceTests { + @Test + void derivesTheStoredMediaTypeFromTheAllowlistedExtension() { + UUID organizationId = UUID.randomUUID(); + UUID departmentId = UUID.randomUUID(); + UUID spaceId = UUID.randomUUID(); + CurrentActor actor = new CurrentActor( + UUID.randomUUID(), organizationId, departmentId, "Uploader", "uploader@example.com"); + ObjectStoragePort objects = mock(ObjectStoragePort.class); + SourceUploadRegistrationService registrations = + mock(SourceUploadRegistrationService.class); + KnowledgeSpaceService spaces = mock(KnowledgeSpaceService.class); + when(spaces.requireUploadTarget(actor, spaceId)).thenReturn( + new KnowledgeSpaceTarget( + spaceId, + "human-resources", + "Human Resources", + departmentId)); + when(objects.put(any(), any())).thenReturn(new StoredObject( + new ObjectKey("organizations/test/sources/workflow.txt"), + 4, + "text/plain", + "sha256", + "etag", + "version")); + when(registrations.register( + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any())) + .thenReturn(mock(SourceSummary.class)); + SourceUploadService service = new SourceUploadService( + objects, + registrations, + new KnowledgePermissionPolicy(), + new SourceIngestionProperties(DataSize.ofMegabytes(25), 5), + spaces); + + service.upload( + new CreateUploadSourceCommand( + actor, + "workflow.txt", + 4, + KnowledgeClassification.CONFIDENTIAL, + spaceId), + new ByteArrayInputStream( + new byte[] {1, 2, 3, 4})); + + ArgumentCaptor request = + ArgumentCaptor.forClass(ObjectWriteRequest.class); + verify(objects).put(request.capture(), any()); + assertEquals( + "text/plain", + request.getValue().mediaType()); + } + @Test void checksParentSpacePermissionBeforeWritingEvidence() { UUID organizationId = UUID.randomUUID(); @@ -39,7 +106,6 @@ void checksParentSpacePermissionBeforeWritingEvidence() { CreateUploadSourceCommand command = new CreateUploadSourceCommand( actor, "workflow.txt", - "text/plain", 4, KnowledgeClassification.CONFIDENTIAL, spaceId); diff --git a/docs/research/lightrag-v1.5.4-parity-manifest.md b/docs/research/lightrag-v1.5.4-parity-manifest.md index 684346db..dddd4ae4 100644 --- a/docs/research/lightrag-v1.5.4-parity-manifest.md +++ b/docs/research/lightrag-v1.5.4-parity-manifest.md @@ -80,12 +80,12 @@ exists. | Query | Streaming answer generation | Framework-neutral iterator and Spring AI streaming adapter; delivery shell wiring remains PR 11 | implemented | 7, 11 | | Runtime | Role-specific extraction/query/keyword/VLM/embedding routes | Some AI routes exist | partial | 2, 4, 5, 7 | | Runtime | Bounded concurrency and cancellation | Worker uses bounded virtual threads | partial | 5, 6 | -| Runtime | Worker/API/Assistant/MCP integration | Chunk Assistant exists; graph engine is not wired | partial | 11 | -| UI | Authorized citations and source preview | Planned source viewer | missing | 11 | -| UI | Permission-aware graph explorer | Disabled navigation target | missing | 11 | -| Evaluation | Upstream allow-all golden fixtures | No executable oracle yet | missing | 2, 12 | -| Evaluation | RAGAS quality evaluation | None | missing | 12 | -| Observability | Langfuse/OpenTelemetry-compatible tracing | General telemetry foundation only | partial | 12 | +| Runtime | Worker/API/Assistant/MCP integration | Shared-snapshot GraphRAG is wired through worker, API, Assistant and MCP permission-aware application use cases | implemented | 11 | +| UI | Authorized citations and source preview | Server-declared citation contract plus protected text/image/PDF preview and opaque revoke handling | implemented | 11, 12 | +| UI | Permission-aware graph explorer | Authorized read-only graph explorer is wired as a separate product surface | implemented | 11 | +| Evaluation | Upstream allow-all golden fixtures | Executable pinned-upstream chunking/weighted-polling/payload generator plus Java consumer; full query-channel oracle remains required | partial | 2, 12 | +| Evaluation | RAGAS quality evaluation | Locked RAGAS runner, strict sanitized dataset contract, repeated-trial variance summary and CI tests; representative live baseline remains required | partial | 12 | +| Observability | Langfuse/OpenTelemetry-compatible tracing | Payload-free stage events, OTLP adapter, closed attribute allowlist and API/worker runtime wiring; external collector ingestion remains to be verified | partial | 12 | | Security | Cross-tenant and denied contribution isolation | Strong graph/retrieval tests exist | implemented | every PR | | Performance | PostgreSQL/OpenSearch/Neo4j comparison | None | missing | 12 | diff --git a/docs/runbooks/graph-rag-production-hardening.md b/docs/runbooks/graph-rag-production-hardening.md new file mode 100644 index 00000000..4e34b46e --- /dev/null +++ b/docs/runbooks/graph-rag-production-hardening.md @@ -0,0 +1,123 @@ +# GraphRAG Production Hardening Runbook + +## Scope + +This runbook closes operational evidence; it does not turn an unexecuted +procedure into a passing result. Store raw outputs with the release evidence +and record the exact commit, container digests, corpus hash, embedding profile, +authorization model, hardware and timestamps. + +## Payload-Free Tracing + +`integrations/graph-rag-observability` converts completed GraphRAG stage events +to OpenTelemetry spans. API and worker use Spring Boot's OpenTelemetry starter. +Configure an OTLP-compatible collector, including Langfuse, with Spring Boot +4.1 properties under: + +```text +management.opentelemetry.tracing.export.otlp.endpoint +management.opentelemetry.tracing.export.otlp.headers +management.opentelemetry.tracing.export.otlp.transport +``` + +The application allowlist is limited to operation and organization UUIDs, +stage/outcome, monotonic duration, bounded input/output counts, an optional +lowercase SHA-256 model-route fingerprint, and a bounded machine failure code. +Never add query, prompt, completion, evidence/chunk text, document title/URI, +embedding values, actor identity, ACL subjects or exception messages. Spring AI +prompt and completion observation logging is explicitly disabled in API and +worker configuration. + +Before claiming Langfuse compatibility as verified: + +1. send one successful retrieval, one failed retrieval and one indexing job to + the configured collector; +2. confirm the stage names and original durations; +3. export the spans and scan every attribute/event for forbidden payload; +4. confirm error spans contain no exception event or stack trace; and +5. attach the sanitized export to release evidence. + +## Oracle And RAGAS + +The reference checkout must be LightRAG `v1.5.4` commit +`9a45b64c2ee25b1d806e90db926a8af37480bb16`. Use identical sanitized corpus, +recorded model responses, embedding profile and query set on both runtimes. +Compare normalized entities, relations, keywords, retrieval channel membership, +references, ordering invariants and token-allocation decisions. Do not compare +provider prose byte-for-byte. + +Regenerate the deterministic checked-in oracle and run its Java consumer from +the repository root: + +```powershell +evaluation\.venv\Scripts\python.exe ` + evaluation\oracle\generate_lightrag_v1_5_4.py ` + --upstream D:\OrgMemory\tmp\upstream-lightrag-v1.5.4 ` + --output evaluation\baselines\lightrag-v1.5.4-oracle.json + +.\gradlew.bat :components:graph-rag-core:test ` + --tests "com.orgmemory.graphrag.query.LightRagUpstreamOracleTests" +``` + +The generator verifies the upstream Git commit before executing upstream code. +The committed JSON is the only oracle copy consumed by Java tests; a changed +fixture must be reviewed as a semantic change. + +RAGAS is an external evaluation tool, never a runtime dependency. Pin its +environment and judge model, repeat the baseline to measure judge variance, +then fail only on a committed regression tolerance for faithfulness, context +precision and context recall. Never report a single stochastic judge run as an +absolute quality score. + +## Adapter Load Comparison + +PostgreSQL, OpenSearch and Neo4j comparisons are valid only when all runs use: + +- the same canonical corpus and authorization scopes; +- the same immutable embeddings and query set; +- pinned server/client versions and identical hardware limits; +- separate cold and warm phases; +- at least five measured repetitions after warm-up; and +- p50/p95 latency, throughput, error rate and recall-at-k together. + +A lower latency with lower recall is not a winner. PostgreSQL remains the +canonical evidence/ACL/publication authority regardless of which rebuildable +query adapter wins a workload. + +## Security And Failure Drills + +Run and retain evidence for: + +1. cross-tenant retrieval, graph, citation and export denial; +2. a denied contribution that shares an entity with an allowed contribution; +3. OpenFGA outage before retrieval and before citation open; +4. PostgreSQL, OpenSearch and Neo4j query-store outages; +5. deletion followed by rebuild, proving retired evidence is absent from + content, lexical, vector, graph and citation resolution; and +6. authorization generation/model changes during retrieval. + +Every unavailable authorization authority fails closed. A query accelerator may +fall back only to another path that applies the same pinned tenant, snapshot and +authorized-asset filters. + +## Backup And Restore Boundary + +Back up the canonical PostgreSQL `orgmemory` database, the PostgreSQL `openfga` +database, and the versioned object-storage bucket together. Keycloak realm +configuration is versioned in the repository; production identity state still +follows the identity provider's supported backup procedure. OpenSearch and +Neo4j are rebuildable projections and must not be the only copy of evidence or +ACL state. + +A restore drill passes only when: + +1. database and object snapshots come from the same recorded backup window; +2. Flyway validation succeeds without repair; +3. evidence blob hashes match the canonical ledger; +4. OpenFGA model and tuples match the recorded authorization version; +5. projections rebuild into a new generation; +6. allow, deny, revoke and cross-tenant tests pass after restore; and +7. the old projection generation never becomes visible during rebuild. + +Record recovery point and recovery time from the executed drill. Do not claim +backup/restore readiness from this procedure alone. diff --git a/docs/specs/domains/assistant-and-mcp.md b/docs/specs/domains/assistant-and-mcp.md index 461dd94a..6b14ee90 100644 --- a/docs/specs/domains/assistant-and-mcp.md +++ b/docs/specs/domains/assistant-and-mcp.md @@ -6,8 +6,23 @@ The in-app Assistant routes chat through the provider-neutral AI gateway and grounds every answer in `PermissionAwareKnowledgeSearch`. GraphRAG is the default retrieval engine; the canonical hybrid engine is an explicit configuration choice rather than an implicit fallback. Answers stream with -permission-verified citations, and citation content is read through an -authenticated backend endpoint instead of exposing object-storage URLs. +permission-verified citations. The server assigns each citation number at the +same time it renders the corresponding evidence into the model prompt and +streams that number as provider metadata. The browser makes only those declared +markers interactive; an undeclared `[n]` remains literal text. Citation content +is read through an authenticated backend endpoint instead of exposing +object-storage URLs. Every open performs one fresh canonical authorization and +integrity check; missing, changed, and denied citations are wire-equivalent +opaque `404` responses. +The citation response derives its media type from a closed extension allowlist, +never from upload metadata. Text, PDF, and known raster images may render +inline; Office and unknown formats are forced to download as binary content. + +The source panel treats citation content as server state. It deduplicates an +in-flight open, does not retry an authorization failure, does not show stale +content during a recheck, and discards the cached blob when the panel releases +the source. Text, image, and PDF previews use browser-local object URLs; the +external object-store address never reaches the browser. `apps/mcp` runs a stateless Spring AI MCP server with one read-only, closed-world `search_knowledge` tool. It validates the caller's bearer token and diff --git a/docs/specs/domains/knowledge-ingestion.md b/docs/specs/domains/knowledge-ingestion.md index 3bb827c2..0f1c7409 100644 --- a/docs/specs/domains/knowledge-ingestion.md +++ b/docs/specs/domains/knowledge-ingestion.md @@ -259,9 +259,9 @@ stall. The current path does not yet implement incremental webhooks or the Events API, credential rotation, a run of either adapter against a real workspace, Airbyte -staging, OCR, malware and DLP integrations, entity and relationship extraction, -graph publication, or hybrid retrieval extensions beyond the current secure FTS + -pgvector path. +staging, OCR, or malware and DLP integrations. Entity/relation extraction, +profile-versioned graph publication and secure hybrid GraphRAG retrieval are +implemented as rebuildable projections over this canonical ledger. ## Source Modules diff --git a/docs/specs/domains/secure-graph-rag.md b/docs/specs/domains/secure-graph-rag.md index 4a9f5215..d854b7e4 100644 --- a/docs/specs/domains/secure-graph-rag.md +++ b/docs/specs/domains/secure-graph-rag.md @@ -62,9 +62,18 @@ - A durable graph-index job is inserted only after the canonical source revision reaches `READY`. The job is unique per immutable Knowledge Asset - version and stores lease, attempts, retry time, and bounded failure evidence. + version and immutable `GraphProcessingProfile`, and stores lease, attempts, + retry time, and bounded failure evidence. +- `GraphProcessingProfile` and `EmbeddingProfile` are independent coordinates. + The former snapshots algorithm, complete extraction settings, exact prompt + templates, merge semantics and embedding-payload format; the latter owns + provider/model/vector geometry. Changing either produces a new rebuildable + generation without mutating a completed historical job. - Claims pin the current asset/version/revision, active chunk generation, ACL - snapshot/generation, language, and immutable embedding profile. + snapshot/generation, language, immutable embedding profile and exact + hash-addressed graph-processing profile. A retry reuses those coordinates; + an explicit rebuild resolves the current profile and enqueues a distinct job + only when its profile hash differs. - Chunk extraction uses bounded virtual-thread concurrency and renews the lease between batches. Model output remains untrusted and must satisfy the structured extraction contract before assembly. diff --git a/docs/tests/domains/assistant-and-mcp.md b/docs/tests/domains/assistant-and-mcp.md index ae529c3c..bfd2de97 100644 --- a/docs/tests/domains/assistant-and-mcp.md +++ b/docs/tests/domains/assistant-and-mcp.md @@ -6,6 +6,12 @@ | Assistant sends only permission-verified evidence to the model | `AssistantServiceTests#streamsOnlyPermissionVerifiedEvidenceToTheModel` | covered | | Empty authorized retrieval does not call the model | `AssistantServiceTests#doesNotCallTheModelWhenNoAccessibleEvidenceExists` | covered | | Provider failure is surfaced as unavailable | `AssistantServiceTests#asynchronousProviderFailureIsReportedAsUnavailable` | covered | +| Citation numbers are assigned with the exact prompt evidence order | `AssistantServiceTests#exposesCitationsOnlyForEvidenceIncludedInThePromptBudget`, `AssistantControllerStreamingTests`, `UiMessageStreamTests` | covered | +| Only server-declared citation markers become interactive | `assistant-pipeline.spec.ts#anchors only server-declared citations and opens the matching source` | covered | +| Text and PDF sources are fetched once through the protected endpoint | `assistant-pipeline.spec.ts` text and PDF preview scenarios | covered | +| Revoked citations produce one opaque 404 without leaking the backend detail | `assistant-pipeline.spec.ts#shows an opaque citation error after access is revoked`, `CitationContentWebMvcTests` | covered | +| Hostile upload media types cannot make citation content execute inline | `SourceUploadServiceTests#derivesTheStoredMediaTypeFromTheAllowlistedExtension`, `CitationContentControllerTests` | covered | +| Empty evidence, provider retry, and user abort are browser-tested | `assistant-pipeline.spec.ts` | covered | | GraphRAG is selected explicitly with no silent fallback | `AssistantConfigurationTests` | covered | | MCP publishes only the read-only permission-aware search tool | `OrgMemoryMcpContextTests` | covered | | MCP forwards caller bearer identity to canonical REST search | `KnowledgeSearchApiClientTests`, `KnowledgeSearchToolTests` | covered | diff --git a/evaluation/README.md b/evaluation/README.md new file mode 100644 index 00000000..afa1b822 --- /dev/null +++ b/evaluation/README.md @@ -0,0 +1,56 @@ +# OrgMemory Evaluation + +This directory contains offline quality and semantic-conformance tooling. It is +not part of the API or worker runtime. + +## Deterministic LightRAG oracle + +`oracle/generate_lightrag_v1_5_4.py` executes the pinned upstream fixed-token +chunker and weighted-polling function, and verifies the upstream +entity/relation embedding payload expressions. It refuses any checkout other +than commit `9a45b64c2ee25b1d806e90db926a8af37480bb16`. + +From the repository root: + +```powershell +evaluation\.venv\Scripts\python.exe ` + evaluation\oracle\generate_lightrag_v1_5_4.py ` + --upstream D:\OrgMemory\tmp\upstream-lightrag-v1.5.4 ` + --output evaluation\baselines\lightrag-v1.5.4-oracle.json + +.\gradlew.bat :components:graph-rag-core:test ` + --tests "com.orgmemory.graphrag.query.LightRagUpstreamOracleTests" +``` + +The committed JSON is the single source consumed by Java conformance tests. +Regenerate it only from the pinned checkout and review semantic changes rather +than accepting a changed file mechanically. + +## RAGAS + +RAGAS evaluates exported, sanitized Assistant cases. It is a stochastic +external judge and must be run in at least two trials; the runner reports mean, +spread, minima and maxima rather than presenting one score as truth. + +The runner uses the modern `ragas.metrics.collections` API end to end. Judge +calls use an asynchronous OpenAI client, while answer-relevancy embeddings use +a separate synchronous client because RAGAS 0.3.9 invokes that embedding +contract synchronously. Metric calls are concurrency-bounded and individually +timed out. OpenAI SDK retries cover transient transport, rate-limit and server +failures; a failed or non-finite metric aborts the run instead of producing a +partial quality report. + +```powershell +Set-Location evaluation +uv sync --frozen --dev +$env:OPENAI_API_KEY = "" +uv run orgmemory-ragas ` + --input fixtures\sample-evaluation.json ` + --output output\ragas-results.json ` + --trials 3 +``` + +The API key is read only from the environment. Prompts, retrieved contexts and +answers are not copied into the result artifact. Do not use production evidence +in evaluation fixtures. The included synthetic fixture proves evaluator wiring +only; it is not a product-quality benchmark. diff --git a/evaluation/baselines/lightrag-v1.5.4-oracle.json b/evaluation/baselines/lightrag-v1.5.4-oracle.json new file mode 100644 index 00000000..a25197e3 --- /dev/null +++ b/evaluation/baselines/lightrag-v1.5.4-oracle.json @@ -0,0 +1,92 @@ +{ + "schemaVersion": "orgmemory.lightrag-oracle.v1", + "upstream": { + "repository": "https://github.com/HKUDS/LightRAG", + "tag": "v1.5.4", + "commit": "9a45b64c2ee25b1d806e90db926a8af37480bb16" + }, + "fixtureSha256": "97d9b7f3cbbac619cd6e451c0f0ac38c8802714bd5a47a80b51a484450dd5681", + "fixture": { + "chunking": { + "content": "abcdefghij", + "chunkTokenSize": 4, + "chunkOverlapTokenSize": 1 + }, + "weightedPolling": { + "groups": [ + [ + "a1", + "a2", + "a3" + ], + [ + "b1" + ], + [ + "c1", + "c2" + ] + ], + "maximumRelatedChunks": 3, + "minimumRelatedChunks": 1 + }, + "embeddingPayloads": { + "entityName": "LEAVE POLICY", + "entityDescription": "Defines annual leave.", + "relationKeywords": "governs, leave", + "relationSource": "EMPLOYEE HANDBOOK", + "relationTarget": "LEAVE POLICY", + "relationDescription": "The handbook governs the policy." + } + }, + "expected": { + "chunks": [ + { + "tokens": 4, + "content": "abcd", + "chunk_order_index": 0, + "_source_span": { + "start": 0, + "end": 4 + } + }, + { + "tokens": 4, + "content": "defg", + "chunk_order_index": 1, + "_source_span": { + "start": 3, + "end": 7 + } + }, + { + "tokens": 4, + "content": "ghij", + "chunk_order_index": 2, + "_source_span": { + "start": 6, + "end": 10 + } + }, + { + "tokens": 1, + "content": "j", + "chunk_order_index": 3, + "_source_span": { + "start": 9, + "end": 10 + } + } + ], + "weightedPolling": [ + "a1", + "a2", + "a3", + "b1", + "c1", + "c2" + ], + "entityEmbeddingPayload": "LEAVE POLICY\nDefines annual leave.", + "relationEmbeddingPayload": "governs, leave\tEMPLOYEE HANDBOOK\nLEAVE POLICY\nThe handbook governs the policy." + } +} diff --git a/evaluation/fixtures/sample-evaluation.json b/evaluation/fixtures/sample-evaluation.json new file mode 100644 index 00000000..37e48dd5 --- /dev/null +++ b/evaluation/fixtures/sample-evaluation.json @@ -0,0 +1,20 @@ +{ + "schema_version": "orgmemory.rag-evaluation.v1", + "dataset_id": "synthetic-employee-policy-v1", + "system_under_test": "synthetic-contract-fixture", + "cases": [ + { + "case_id": "probation-policy", + "question": "What is the probation policy?", + "reference_answer": "The standard probation period is 60 days.", + "answer": "The standard probation period is 60 days. [1]", + "contexts": [ + "Employee Handbook: The standard probation period is 60 days." + ], + "citation_ids": [ + "synthetic-citation-1" + ], + "latency_ms": 120.5 + } + ] +} diff --git a/evaluation/oracle/generate_lightrag_v1_5_4.py b/evaluation/oracle/generate_lightrag_v1_5_4.py new file mode 100644 index 00000000..e34dcf6e --- /dev/null +++ b/evaluation/oracle/generate_lightrag_v1_5_4.py @@ -0,0 +1,211 @@ +from __future__ import annotations + +import argparse +import ast +import hashlib +import importlib.util +import json +import logging +import subprocess +import sys +import types +from collections.abc import Sequence +from pathlib import Path +from typing import Any + +UPSTREAM_COMMIT = "9a45b64c2ee25b1d806e90db926a8af37480bb16" + + +class CodePointTokenizer: + def encode(self, content: str) -> list[int]: + return [ord(character) for character in content] + + def decode(self, tokens: list[int]) -> str: + return "".join(chr(token) for token in tokens) + + +class ChunkTokenLimitExceededError(RuntimeError): + def __init__(self, **details: Any) -> None: + super().__init__(str(details)) + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Generate a deterministic semantic oracle from pinned LightRAG source.", + ) + parser.add_argument("--upstream", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + return parser.parse_args(argv) + + +def require_pinned_checkout(upstream: Path) -> None: + actual = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=upstream, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + if actual != UPSTREAM_COMMIT: + raise SystemExit( + f"LightRAG checkout must be {UPSTREAM_COMMIT}, found {actual}", + ) + + +def load_upstream_chunker(upstream: Path) -> Any: + lightrag = types.ModuleType("lightrag") + lightrag.__path__ = [str(upstream / "lightrag")] + exceptions = types.ModuleType("lightrag.exceptions") + exceptions.ChunkTokenLimitExceededError = ChunkTokenLimitExceededError + utils = types.ModuleType("lightrag.utils") + utils.Tokenizer = CodePointTokenizer + utils.logger = logging.getLogger("lightrag-oracle") + sys.modules["lightrag"] = lightrag + sys.modules["lightrag.exceptions"] = exceptions + sys.modules["lightrag.utils"] = utils + + module_path = upstream / "lightrag" / "chunker" / "token_size.py" + spec = importlib.util.spec_from_file_location( + "lightrag.chunker.token_size", + module_path, + ) + if spec is None or spec.loader is None: + raise RuntimeError(f"Cannot load upstream chunker from {module_path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module.chunking_by_token_size + + +def load_upstream_weighted_polling(upstream: Path) -> Any: + source_path = upstream / "lightrag" / "utils.py" + tree = ast.parse(source_path.read_text(encoding="utf-8"), filename=str(source_path)) + function = next( + ( + node + for node in tree.body + if isinstance(node, ast.FunctionDef) + and node.name == "pick_by_weighted_polling" + ), + None, + ) + if function is None: + raise RuntimeError("Pinned upstream weighted-polling function is missing") + module = ast.Module(body=[function], type_ignores=[]) + ast.fix_missing_locations(module) + namespace: dict[str, Any] = {} + exec(compile(module, str(source_path), "exec"), namespace) + return namespace["pick_by_weighted_polling"] + + +def require_embedding_payload_contract(upstream: Path) -> None: + source = (upstream / "lightrag" / "lightrag.py").read_text(encoding="utf-8") + required_fragments = ( + '"content": dp["entity_name"] + "\\n" + dp["description"]', + '"content": f"{dp[\'keywords\']}\\t{dp[\'src_id\']}\\n' + "{dp['tgt_id']}\\n{dp['description']}\"", + ) + missing = [fragment for fragment in required_fragments if fragment not in source] + if missing: + raise RuntimeError( + "Pinned upstream embedding payload contract changed: " + + repr(missing), + ) + + +def canonical_sha256(payload: Any) -> str: + encoded = json.dumps( + payload, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def generate(upstream: Path) -> dict[str, Any]: + require_pinned_checkout(upstream) + chunk = load_upstream_chunker(upstream) + weighted_polling = load_upstream_weighted_polling(upstream) + require_embedding_payload_contract(upstream) + + fixture = { + "chunking": { + "content": "abcdefghij", + "chunkTokenSize": 4, + "chunkOverlapTokenSize": 1, + }, + "weightedPolling": { + "groups": [ + ["a1", "a2", "a3"], + ["b1"], + ["c1", "c2"], + ], + "maximumRelatedChunks": 3, + "minimumRelatedChunks": 1, + }, + "embeddingPayloads": { + "entityName": "LEAVE POLICY", + "entityDescription": "Defines annual leave.", + "relationKeywords": "governs, leave", + "relationSource": "EMPLOYEE HANDBOOK", + "relationTarget": "LEAVE POLICY", + "relationDescription": "The handbook governs the policy.", + }, + } + chunk_input = fixture["chunking"] + chunks = chunk( + CodePointTokenizer(), + chunk_input["content"], + chunk_overlap_token_size=chunk_input["chunkOverlapTokenSize"], + chunk_token_size=chunk_input["chunkTokenSize"], + _emit_source_span=True, + ) + polling_input = fixture["weightedPolling"] + selected = weighted_polling( + [ + {"sorted_chunks": group} + for group in polling_input["groups"] + ], + polling_input["maximumRelatedChunks"], + polling_input["minimumRelatedChunks"], + ) + payload_input = fixture["embeddingPayloads"] + return { + "schemaVersion": "orgmemory.lightrag-oracle.v1", + "upstream": { + "repository": "https://github.com/HKUDS/LightRAG", + "tag": "v1.5.4", + "commit": UPSTREAM_COMMIT, + }, + "fixtureSha256": canonical_sha256(fixture), + "fixture": fixture, + "expected": { + "chunks": chunks, + "weightedPolling": selected, + "entityEmbeddingPayload": ( + f"{payload_input['entityName']}\n" + f"{payload_input['entityDescription']}" + ), + "relationEmbeddingPayload": ( + f"{payload_input['relationKeywords']}\t" + f"{payload_input['relationSource']}\n" + f"{payload_input['relationTarget']}\n" + f"{payload_input['relationDescription']}" + ), + }, + } + + +def main(argv: Sequence[str] | None = None) -> None: + args = parse_args(argv) + payload = generate(args.upstream.resolve()) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(payload, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + newline="\n", + ) + + +if __name__ == "__main__": + main() diff --git a/evaluation/pyproject.toml b/evaluation/pyproject.toml new file mode 100644 index 00000000..dcd5cbd9 --- /dev/null +++ b/evaluation/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "orgmemory-evaluation" +version = "0.1.0" +description = "Reproducible offline quality evaluation for OrgMemory retrieval and Assistant responses" +requires-python = ">=3.12,<3.14" +dependencies = [ + # Ragas 0.4.3 imports a VertexAI compatibility module removed from every + # current langchain-community release. Keep the latest working 0.3 line + # used by pinned LightRAG v1.5.4 until upstream publishes a compatible fix. + "instructor==1.15.4", + "langchain-community==0.3.31", + "openai==2.20.0", + "pydantic==2.12.5", + "ragas==0.3.9", +] + +[project.scripts] +orgmemory-ragas = "orgmemory_eval.ragas_runner:main" + +[dependency-groups] +dev = [ + "pytest==9.0.2", + "ruff==0.15.5", +] + +[build-system] +requires = ["hatchling==1.29.0"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/orgmemory_eval"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM"] diff --git a/evaluation/src/orgmemory_eval/__init__.py b/evaluation/src/orgmemory_eval/__init__.py new file mode 100644 index 00000000..341cdc67 --- /dev/null +++ b/evaluation/src/orgmemory_eval/__init__.py @@ -0,0 +1 @@ +"""Offline evaluation tools for OrgMemory.""" diff --git a/evaluation/src/orgmemory_eval/models.py b/evaluation/src/orgmemory_eval/models.py new file mode 100644 index 00000000..02d18744 --- /dev/null +++ b/evaluation/src/orgmemory_eval/models.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +class EvaluationCase(BaseModel): + model_config = ConfigDict(extra="forbid") + + case_id: str = Field(min_length=1, max_length=128) + question: str = Field(min_length=1) + reference_answer: str = Field(min_length=1) + answer: str = Field(min_length=1) + contexts: list[str] = Field(min_length=1) + citation_ids: list[str] = Field(default_factory=list) + latency_ms: float = Field(ge=0) + + @model_validator(mode="after") + def reject_blank_contexts(self) -> EvaluationCase: + if any(not context.strip() for context in self.contexts): + raise ValueError("contexts must not contain blank values") + return self + + +class EvaluationDataset(BaseModel): + model_config = ConfigDict(extra="forbid") + + schema_version: str = Field(pattern=r"^orgmemory\.rag-evaluation\.v1$") + dataset_id: str = Field(min_length=1, max_length=128) + system_under_test: str = Field(min_length=1, max_length=256) + cases: list[EvaluationCase] = Field(min_length=1) + + @model_validator(mode="after") + def require_unique_case_ids(self) -> EvaluationDataset: + case_ids = [case.case_id for case in self.cases] + if len(case_ids) != len(set(case_ids)): + raise ValueError("case_id values must be unique") + return self diff --git a/evaluation/src/orgmemory_eval/ragas_runner.py b/evaluation/src/orgmemory_eval/ragas_runner.py new file mode 100644 index 00000000..b5a14b0d --- /dev/null +++ b/evaluation/src/orgmemory_eval/ragas_runner.py @@ -0,0 +1,352 @@ +from __future__ import annotations + +import argparse +import asyncio +import hashlib +import json +import math +import os +import statistics +import tempfile +from collections.abc import Awaitable, Callable, Sequence +from dataclasses import dataclass +from datetime import UTC, datetime +from importlib.metadata import version +from pathlib import Path +from typing import Any + +from openai import AsyncOpenAI, OpenAI +from ragas.embeddings.base import BaseRagasEmbedding, embedding_factory +from ragas.llms import InstructorBaseRagasLLM, llm_factory +from ragas.metrics.collections import ( + AnswerRelevancy, + ContextPrecisionWithReference, + ContextRecall, + Faithfulness, +) +from ragas.metrics.result import MetricResult + +from orgmemory_eval.models import EvaluationCase, EvaluationDataset + +METRIC_NAMES = ( + "faithfulness", + "answer_relevancy", + "context_precision", + "context_recall", +) + + +@dataclass(frozen=True) +class MetricSuite: + faithfulness: Faithfulness + answer_relevancy: AnswerRelevancy + context_precision: ContextPrecisionWithReference + context_recall: ContextRecall + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Evaluate an exported OrgMemory Assistant dataset without retaining prompts.", + ) + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--trials", type=int, default=3) + parser.add_argument( + "--evaluator-model", + default=os.getenv("ORGMEMORY_EVAL_MODEL", "gpt-4.1-mini"), + ) + parser.add_argument( + "--embedding-model", + default=os.getenv("ORGMEMORY_EVAL_EMBEDDING_MODEL", "text-embedding-3-large"), + ) + parser.add_argument("--max-workers", type=int, default=4) + parser.add_argument("--timeout-seconds", type=int, default=180) + args = parser.parse_args(argv) + if args.trials < 2: + parser.error("--trials must be at least 2 so judge variance is measurable") + if args.max_workers < 1: + parser.error("--max-workers must be positive") + return args + + +def load_dataset(path: Path) -> tuple[EvaluationDataset, str]: + raw = path.read_bytes() + parsed = EvaluationDataset.model_validate_json(raw) + return parsed, hashlib.sha256(raw).hexdigest() + + +def evaluator( + *, + llm_client: AsyncOpenAI, + embedding_client: OpenAI, + evaluator_model: str, + embedding_model: str, +) -> tuple[InstructorBaseRagasLLM, BaseRagasEmbedding]: + llm = llm_factory(evaluator_model, client=llm_client) + embeddings = embedding_factory( + "openai", + model=embedding_model, + client=embedding_client, + interface="modern", + ) + if not isinstance(embeddings, BaseRagasEmbedding): + raise TypeError("modern embedding_factory returned a legacy embedding adapter") + return llm, embeddings + + +def metric_suite( + *, + llm: InstructorBaseRagasLLM, + embeddings: BaseRagasEmbedding, +) -> MetricSuite: + return MetricSuite( + faithfulness=Faithfulness(llm=llm), + answer_relevancy=AnswerRelevancy(llm=llm, embeddings=embeddings), + context_precision=ContextPrecisionWithReference(llm=llm), + context_recall=ContextRecall(llm=llm), + ) + + +def validated_score(metric_name: str, result: MetricResult) -> float: + score = float(result.value) + if not math.isfinite(score): + raise ValueError(f"{metric_name} returned a non-finite score") + if not 0.0 <= score <= 1.0: + raise ValueError(f"{metric_name} returned an out-of-range score: {score}") + return score + + +async def bounded_score( + *, + semaphore: asyncio.Semaphore, + timeout_seconds: int, + metric_name: str, + operation: Callable[[], Awaitable[MetricResult]], +) -> float: + async with semaphore: + result = await asyncio.wait_for(operation(), timeout=timeout_seconds) + return validated_score(metric_name, result) + + +async def score_case( + case: EvaluationCase, + *, + metrics: MetricSuite, + semaphore: asyncio.Semaphore, + timeout_seconds: int, +) -> dict[str, float]: + scores = await asyncio.gather( + bounded_score( + semaphore=semaphore, + timeout_seconds=timeout_seconds, + metric_name="faithfulness", + operation=lambda: metrics.faithfulness.ascore( + user_input=case.question, + response=case.answer, + retrieved_contexts=case.contexts, + ), + ), + bounded_score( + semaphore=semaphore, + timeout_seconds=timeout_seconds, + metric_name="answer_relevancy", + operation=lambda: metrics.answer_relevancy.ascore( + user_input=case.question, + response=case.answer, + ), + ), + bounded_score( + semaphore=semaphore, + timeout_seconds=timeout_seconds, + metric_name="context_precision", + operation=lambda: metrics.context_precision.ascore( + user_input=case.question, + reference=case.reference_answer, + retrieved_contexts=case.contexts, + ), + ), + bounded_score( + semaphore=semaphore, + timeout_seconds=timeout_seconds, + metric_name="context_recall", + operation=lambda: metrics.context_recall.ascore( + user_input=case.question, + retrieved_contexts=case.contexts, + reference=case.reference_answer, + ), + ), + ) + return dict(zip(METRIC_NAMES, scores, strict=True)) + + +async def run_trial( + dataset: EvaluationDataset, + *, + llm: InstructorBaseRagasLLM, + embeddings: BaseRagasEmbedding, + max_workers: int, + timeout_seconds: int, +) -> list[dict[str, float]]: + metrics = metric_suite(llm=llm, embeddings=embeddings) + semaphore = asyncio.Semaphore(max_workers) + return await asyncio.gather( + *[ + score_case( + case, + metrics=metrics, + semaphore=semaphore, + timeout_seconds=timeout_seconds, + ) + for case in dataset.cases + ] + ) + + +async def run_trials( + dataset: EvaluationDataset, + *, + llm: InstructorBaseRagasLLM, + embeddings: BaseRagasEmbedding, + trial_count: int, + max_workers: int, + timeout_seconds: int, +) -> list[list[dict[str, float]]]: + trials = [] + for _ in range(trial_count): + trials.append( + await run_trial( + dataset, + llm=llm, + embeddings=embeddings, + max_workers=max_workers, + timeout_seconds=timeout_seconds, + ) + ) + return trials + + +def summarize_trials( + dataset: EvaluationDataset, + trials: Sequence[Sequence[dict[str, float]]], + *, + dataset_sha256: str, + evaluator_model: str, + embedding_model: str, +) -> dict[str, Any]: + if not trials or any(len(trial) != len(dataset.cases) for trial in trials): + raise ValueError("every trial must contain one score row per evaluation case") + + case_results = [] + for index, case in enumerate(dataset.cases): + scores: dict[str, dict[str, float]] = {} + for metric in METRIC_NAMES: + values = [float(trial[index][metric]) for trial in trials] + scores[metric] = { + "mean": statistics.fmean(values), + "standard_deviation": statistics.pstdev(values), + "minimum": min(values), + "maximum": max(values), + } + case_results.append( + { + "case_id": case.case_id, + "latency_ms": case.latency_ms, + "citation_count": len(case.citation_ids), + "scores": scores, + } + ) + + aggregate: dict[str, dict[str, float]] = {} + for metric in METRIC_NAMES: + case_means = [case["scores"][metric]["mean"] for case in case_results] + aggregate[metric] = { + "mean": statistics.fmean(case_means), + "minimum_case_mean": min(case_means), + } + + return { + "schema_version": "orgmemory.ragas-results.v1", + "generated_at": datetime.now(UTC).isoformat(), + "dataset_id": dataset.dataset_id, + "dataset_sha256": dataset_sha256, + "system_under_test": dataset.system_under_test, + "evaluator_model": evaluator_model, + "embedding_model": embedding_model, + "ragas_version": version("ragas"), + "trial_count": len(trials), + "case_count": len(dataset.cases), + "aggregate": aggregate, + "cases": case_results, + } + + +def write_json_atomically(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + encoded = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + newline="\n", + dir=path.parent, + delete=False, + ) as temporary: + temporary.write(encoded) + temporary_path = Path(temporary.name) + temporary_path.replace(path) + + +async def async_main(args: argparse.Namespace, *, api_key: str) -> None: + dataset, dataset_sha256 = load_dataset(args.input) + llm_client = AsyncOpenAI( + api_key=api_key, + base_url=os.getenv("OPENAI_BASE_URL"), + timeout=args.timeout_seconds, + max_retries=3, + ) + embedding_client = OpenAI( + api_key=api_key, + base_url=os.getenv("OPENAI_BASE_URL"), + timeout=args.timeout_seconds, + max_retries=3, + ) + try: + llm, embeddings = evaluator( + llm_client=llm_client, + embedding_client=embedding_client, + evaluator_model=args.evaluator_model, + embedding_model=args.embedding_model, + ) + trials = await run_trials( + dataset, + llm=llm, + embeddings=embeddings, + trial_count=args.trials, + max_workers=args.max_workers, + timeout_seconds=args.timeout_seconds, + ) + finally: + await llm_client.close() + embedding_client.close() + + write_json_atomically( + args.output, + summarize_trials( + dataset, + trials, + dataset_sha256=dataset_sha256, + evaluator_model=args.evaluator_model, + embedding_model=args.embedding_model, + ), + ) + + +def main(argv: Sequence[str] | None = None) -> None: + args = parse_args(argv) + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + raise SystemExit("OPENAI_API_KEY is required; the value is never written to results") + asyncio.run(async_main(args, api_key=api_key)) + + +if __name__ == "__main__": + main() diff --git a/evaluation/tests/test_models.py b/evaluation/tests/test_models.py new file mode 100644 index 00000000..b3169088 --- /dev/null +++ b/evaluation/tests/test_models.py @@ -0,0 +1,45 @@ +import pytest +from pydantic import ValidationError + +from orgmemory_eval.models import EvaluationDataset + + +def dataset() -> dict: + return { + "schema_version": "orgmemory.rag-evaluation.v1", + "dataset_id": "employee-policy-v1", + "system_under_test": "orgmemory-local", + "cases": [ + { + "case_id": "probation-policy", + "question": "What is the probation policy?", + "reference_answer": "The probation period is 60 days.", + "answer": "The probation period is 60 days. [1]", + "contexts": ["The standard probation period is 60 days."], + "citation_ids": ["citation-1"], + "latency_ms": 120.5, + } + ], + } + + +def test_accepts_a_complete_dataset() -> None: + parsed = EvaluationDataset.model_validate(dataset()) + + assert parsed.cases[0].case_id == "probation-policy" + + +def test_rejects_duplicate_case_ids() -> None: + payload = dataset() + payload["cases"].append(payload["cases"][0].copy()) + + with pytest.raises(ValidationError, match="case_id values must be unique"): + EvaluationDataset.model_validate(payload) + + +def test_rejects_blank_contexts() -> None: + payload = dataset() + payload["cases"][0]["contexts"] = [" "] + + with pytest.raises(ValidationError, match="contexts must not contain blank"): + EvaluationDataset.model_validate(payload) diff --git a/evaluation/tests/test_ragas_runner.py b/evaluation/tests/test_ragas_runner.py new file mode 100644 index 00000000..9d7fbf64 --- /dev/null +++ b/evaluation/tests/test_ragas_runner.py @@ -0,0 +1,136 @@ +import asyncio + +import pytest +from ragas.embeddings.base import BaseRagasEmbedding +from ragas.llms import InstructorBaseRagasLLM +from ragas.metrics.result import MetricResult + +from orgmemory_eval.models import EvaluationDataset +from orgmemory_eval.ragas_runner import ( + METRIC_NAMES, + metric_suite, + run_trial, + summarize_trials, + validated_score, +) + + +class FakeInstructorLlm(InstructorBaseRagasLLM): + def generate(self, prompt: str, response_model: type): # noqa: ANN001 + return self._response(response_model) + + async def agenerate(self, prompt: str, response_model: type): # noqa: ANN001 + return self._response(response_model) + + @staticmethod + def _response(response_model: type): # noqa: ANN001 + payloads = { + "StatementGeneratorOutput": { + "statements": ["The standard probation period is 60 days."] + }, + "NLIStatementOutput": { + "statements": [ + { + "statement": "The standard probation period is 60 days.", + "reason": "The context states the same policy.", + "verdict": 1, + } + ] + }, + "AnswerRelevanceOutput": { + "question": "What is the probation policy?", + "noncommittal": 0, + }, + "ContextPrecisionOutput": { + "reason": "The context directly answers the question.", + "verdict": 1, + }, + "ContextRecallOutput": { + "classifications": [ + { + "statement": "The standard probation period is 60 days.", + "reason": "The retrieved context contains the statement.", + "attributed": 1, + } + ] + }, + } + return response_model.model_validate(payloads[response_model.__name__]) + + +class FakeEmbeddings(BaseRagasEmbedding): + def embed_text(self, text: str, **kwargs: object) -> list[float]: + return [1.0, 0.0] + + async def aembed_text(self, text: str, **kwargs: object) -> list[float]: + return self.embed_text(text, **kwargs) + + +def evaluation_dataset() -> EvaluationDataset: + return EvaluationDataset.model_validate( + { + "schema_version": "orgmemory.rag-evaluation.v1", + "dataset_id": "employee-policy-v1", + "system_under_test": "orgmemory-local", + "cases": [ + { + "case_id": "probation-policy", + "question": "What is the probation policy?", + "reference_answer": "The standard probation period is 60 days.", + "answer": "The standard probation period is 60 days.", + "contexts": [ + "Employee Handbook: The standard probation period is 60 days." + ], + "citation_ids": ["opaque-citation"], + "latency_ms": 80, + } + ], + } + ) + + +def test_modern_collections_metrics_accept_modern_adapters_and_score() -> None: + llm = FakeInstructorLlm() + embeddings = FakeEmbeddings() + + suite = metric_suite(llm=llm, embeddings=embeddings) + rows = asyncio.run( + run_trial( + evaluation_dataset(), + llm=llm, + embeddings=embeddings, + max_workers=2, + timeout_seconds=5, + ) + ) + + assert suite.faithfulness.name == "faithfulness" + assert rows[0].keys() == set(METRIC_NAMES) + assert all(score == pytest.approx(1.0) for score in rows[0].values()) + + +@pytest.mark.parametrize("score", [float("nan"), float("inf"), -0.01, 1.01]) +def test_non_finite_or_out_of_range_metric_scores_fail_closed(score: float) -> None: + with pytest.raises(ValueError): + validated_score("faithfulness", MetricResult(value=score)) + + +def test_summary_contains_scores_but_no_prompt_or_evidence_text() -> None: + dataset = evaluation_dataset() + row = {metric: 0.8 for metric in METRIC_NAMES} + + summary = summarize_trials( + dataset, + [[row], [row]], + dataset_sha256="a" * 64, + evaluator_model="judge-model", + embedding_model="embedding-model", + ) + + encoded = str(summary) + assert summary["trial_count"] == 2 + assert summary["cases"][0]["citation_count"] == 1 + assert "What is the probation policy?" not in encoded + assert "The standard probation period is 60 days." not in encoded + assert "Employee Handbook" not in encoded + assert "opaque-citation" not in encoded diff --git a/evaluation/tests/test_upstream_oracle.py b/evaluation/tests/test_upstream_oracle.py new file mode 100644 index 00000000..01c37cab --- /dev/null +++ b/evaluation/tests/test_upstream_oracle.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from orgmemory_eval.models import EvaluationDataset + +ROOT = Path(__file__).parents[2] + + +def test_checked_in_oracle_is_pinned_and_complete() -> None: + payload = json.loads( + (ROOT / "evaluation/baselines/lightrag-v1.5.4-oracle.json").read_text( + encoding="utf-8", + ), + ) + + assert payload["schemaVersion"] == "orgmemory.lightrag-oracle.v1" + assert ( + payload["upstream"]["commit"] + == "9a45b64c2ee25b1d806e90db926a8af37480bb16" + ) + assert len(payload["fixtureSha256"]) == 64 + assert payload["expected"]["chunks"] + assert payload["expected"]["weightedPolling"] + assert "\n" in payload["expected"]["entityEmbeddingPayload"] + assert "\t" in payload["expected"]["relationEmbeddingPayload"] + + +def test_sample_ragas_fixture_is_strictly_validated() -> None: + EvaluationDataset.model_validate_json( + (ROOT / "evaluation/fixtures/sample-evaluation.json").read_bytes(), + ) diff --git a/evaluation/uv.lock b/evaluation/uv.lock new file mode 100644 index 00000000..59751cc7 --- /dev/null +++ b/evaluation/uv.lock @@ -0,0 +1,2023 @@ +version = 1 +revision = 3 +requires-python = ">=3.12, <3.14" +resolution-markers = [ + "python_full_version >= '3.13' and sys_platform == 'win32'", + "python_full_version >= '3.13' and sys_platform == 'emscripten'", + "python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + +[[package]] +name = "datasets" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/85/ce4f780c32f7e36d71257f1c27e8ba898ebe379cb54f211f5f2013f2c219/datasets-5.0.0.tar.gz", hash = "sha256:83dbbbdb07a33b82192b8c419deb18739b138ee2ce1a322d55ce6b100954ec1a", size = 631708, upload-time = "2026-06-05T13:18:26.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/66/73034ad30b59f13439b75e620989dacba4c047256e358ba7c2e9ec98ea22/datasets-5.0.0-py3-none-any.whl", hash = "sha256:7dd34927a0fd7046e98aad5cb9430e699c373238a15befa7b9bf22b991a7fee6", size = 555084, upload-time = "2026-06-05T13:18:24.435Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/80/8232b582c4b318b817cf1274ba74976b07b34d35ef439b3eb948f98645a1/filelock-3.32.0.tar.gz", hash = "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", size = 213757, upload-time = "2026-07-21T13:17:42.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/79/b4c714bef36bc4ec2beeae1e0c124f0223888cd8c6feb1cdc56038116920/filelock-3.32.0-py3-none-any.whl", hash = "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3", size = 97732, upload-time = "2026-07-21T13:17:41.55Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +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" } +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" }, +] + +[[package]] +name = "gitpython" +version = "3.1.55" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/ab/ba0d29f2fa2277ed6256b2ac09003494045355f3a10bf32f351761287870/gitpython-3.1.55.tar.gz", hash = "sha256:781e3b1624dad81b24e9524bf0297b69786a0706db2cbceec1e2b05c38e5152f", size = 225071, upload-time = "2026-07-23T02:52:43.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/6a/d3b8208d2f8aac66abe8ccc1c23fa2c89464ec42cc71a601e95d05902428/gitpython-3.1.55-py3-none-any.whl", hash = "sha256:7c9ec1e69c158c081632ab35c41471e302c96db2ae42165036a5d2403378812e", size = 216590, upload-time = "2026-07-23T02:52:41.932Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/74/b13368064b09053253555d3f2839cc2684d22d5aed0d2ccffbf7a6736558/greenlet-3.5.4.tar.gz", hash = "sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20", size = 206538, upload-time = "2026-07-22T12:47:14.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/04/81bd731d6d1e3a469d9a4c36f5eb069bcf0cbb2d5d342c9fec22245b91fc/greenlet-3.5.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4", size = 295909, upload-time = "2026-07-22T11:38:09.261Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dd/f5f22903a6ae70f5ea328ed0beaec92ad903f0e3b7d2845133b354abc4b8/greenlet-3.5.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17", size = 612011, upload-time = "2026-07-22T12:26:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/8e/10/92a4a88d12b915d74ea5b6d288e4afefda4771647caa34442c156f7a454f/greenlet-3.5.4-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a", size = 624299, upload-time = "2026-07-22T12:29:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/0b14bb9db2989f32cd9fe7f76afedea01ee8bee3f87c07e69f24adfe7e63/greenlet-3.5.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f", size = 621541, upload-time = "2026-07-22T11:51:09.464Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/25e9a2d9eb6b2e8b7ca4e80a3a26cb887cce6c8e0a87c921164f11bc5574/greenlet-3.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d", size = 1581444, upload-time = "2026-07-22T12:25:03.818Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/4c9bf2e2c408dcc0556edce69efa9f802e82223573c53240136a086821f1/greenlet-3.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9", size = 1645842, upload-time = "2026-07-22T11:51:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/b5/41/303ecb26a3a56122c0f4d4073ee078881847bd6b6f463ae0ec57ec20223b/greenlet-3.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3", size = 247169, upload-time = "2026-07-22T11:38:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e3/ef56864b4c35fcb3eb3b41b869f6cc46f4cd3f5e2c68e74acde8ac433951/greenlet-3.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0", size = 245565, upload-time = "2026-07-22T11:38:27.061Z" }, + { url = "https://files.pythonhosted.org/packages/c0/9a/e51225dcd58713f16ccbdcc501a8da21098ea14515b7870f1f94459e5ff5/greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02", size = 294831, upload-time = "2026-07-22T11:38:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ea/de50a50fadf979713ab18b46f22ad5ff5f2dcfc637a3ebdecf669801e1a5/greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356", size = 614619, upload-time = "2026-07-22T12:26:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/2aae27fea41205b8650294c301f042a2a4bb6155eea48c995b890a92f2c1/greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef", size = 627021, upload-time = "2026-07-22T12:29:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/79fd826f9ccaae0b84e1b4ef68dabba5e105bb044ffcd448a0b782fcba9a/greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0", size = 624002, upload-time = "2026-07-22T11:51:11.391Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/27319f97e731298513dcba1a2e91b63e9d8811d9de22130f960b129b1bf1/greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd", size = 1581533, upload-time = "2026-07-22T12:25:05.322Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6d/24240bf562e9786dd2799ee0a4a4dadb4ded22510f41b20245099159ac8c/greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f", size = 1645781, upload-time = "2026-07-22T11:51:14.805Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5a/442ab1a9ef7ca6bf7210e5397a95972206a91a31033a03c8900866a10039/greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c", size = 247133, upload-time = "2026-07-22T11:39:20.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/9160210222386b1a378ff94db846b9508ca24a121cf684991561fdb69280/greenlet-3.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f", size = 245500, upload-time = "2026-07-22T11:40:22.185Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/39/67be8d71f900d9a55761b6022821d6679fb56c64f1b6063d5af2c2606727/hf_xet-1.5.2.tar.gz", hash = "sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47", size = 903674, upload-time = "2026-07-16T17:29:56.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/ba/2b70603c7552db82baeb2623e2336898304a17328845151be4fe1f48d420/hf_xet-1.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799", size = 4033760, upload-time = "2026-07-16T17:29:43.884Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/b097a86a1e4a6098f3a79382643ab09d5733d87ccc864877ad1e12b49b70/hf_xet-1.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed", size = 3841438, upload-time = "2026-07-16T17:29:45.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/35/db860aa3a0780660324a506ad4b3d322ddc6ecbba4b9340aed0942cbf21c/hf_xet-1.5.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c", size = 4428006, upload-time = "2026-07-16T17:29:46.996Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/832dd980af4b0c3ae0660e309285f2ffcdff2faa38129390dbb47aa4a3f9/hf_xet-1.5.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025", size = 4221099, upload-time = "2026-07-16T17:29:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/9e/05/ae50f0d34e3254e6c3e208beb2519f6b8673016fc4b3643badaf6450d186/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f", size = 4420766, upload-time = "2026-07-16T17:29:50.092Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/c050bc2743a2bcd68928bfee157b08681667a164a24ec95fbfcfcd717e08/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097", size = 4636716, upload-time = "2026-07-16T17:29:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/68b01c5c2edb56ac9a67b3d076ffddcb90867abaee923923eb34e7a14e76/hf_xet-1.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65", size = 3988373, upload-time = "2026-07-16T17:29:53.395Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/988383e9dc17294d536fcbcd6fd16eed882e411ad16c954984a53e47b09c/hf_xet-1.5.2-cp38-abi3-win_arm64.whl", hash = "sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d", size = 3816957, upload-time = "2026-07-16T17:29:54.991Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/9b/d3bb4e7d792835daf34dd7091bbc7d7b4e0437d9388f1ea7239cce49f478/huggingface_hub-1.24.0.tar.gz", hash = "sha256:18431ff4daae0749aa9ba102fc952e314c98e1d30ebdec5319d85ca0a83e1ae5", size = 921848, upload-time = "2026-07-17T09:54:01.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/c3/aeaaf3911d2529614be18d1c8b5496afc185560e76568063d517283318af/huggingface_hub-1.24.0-py3-none-any.whl", hash = "sha256:6ed4120a84a6beec900640aa7e346bd766a6b7341e41526fef5dc8bd81fb7d59", size = 771904, upload-time = "2026-07-17T09:53:59.106Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "instructor" +version = "1.15.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "docstring-parser" }, + { name = "jinja2" }, + { name = "jiter" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/24/f6b28e83b3194c6223ed7c6eed5724687f6ecd378ec2ff24044f0cbf1f09/instructor-1.15.4.tar.gz", hash = "sha256:ea2280c3678d0f6891c4d826104f95624b680e69877113a6345b1d7c9027ba0f", size = 70049678, upload-time = "2026-06-28T07:36:43.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/8d/f668a30fff4d25b36533355e23aeb0b5724df4628eb974124ed64b7bcf8d/instructor-1.15.4-py3-none-any.whl", hash = "sha256:00e0ecda80fd9746fb6d082d3f9641e193adb1d8849f0775f91519a82aeff968", size = 252522, upload-time = "2026-06-28T07:36:36.863Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, + { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, + { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + +[[package]] +name = "langchain" +version = "1.3.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/68/a6dbad9c22df4087a0f9e79ddd46226c442b30128bfeee538d5889492a73/langchain-1.3.14.tar.gz", hash = "sha256:1b6696c72ba3bbbce54d745e0180742c9f6ece8bbc59ed5a46c3e20b9a435929", size = 645181, upload-time = "2026-07-16T13:28:18.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ec/0f942e78a621f8e3162ff1ed24284f469aaf51fb4607ee5831c626f2b2bc/langchain-1.3.14-py3-none-any.whl", hash = "sha256:4d10dbe91005952cddd56d0dc77aa108964da6bae90ab20063653957e901f782", size = 139560, upload-time = "2026-07-16T13:28:16.498Z" }, +] + +[[package]] +name = "langchain-community" +version = "0.3.31" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "dataclasses-json" }, + { name = "httpx-sse" }, + { name = "langchain" }, + { name = "langchain-core" }, + { name = "langsmith" }, + { name = "numpy" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/49/2ff5354273809e9811392bc24bcffda545a196070666aef27bc6aacf1c21/langchain_community-0.3.31.tar.gz", hash = "sha256:250e4c1041539130f6d6ac6f9386cb018354eafccd917b01a4cff1950b80fd81", size = 33241237, upload-time = "2025-10-07T20:17:57.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/0a/b8848db67ad7c8d4652cb6f4cb78d49b5b5e6e8e51d695d62025aa3f7dbc/langchain_community-0.3.31-py3-none-any.whl", hash = "sha256:1c727e3ebbacd4d891b07bd440647668001cea3e39cbe732499ad655ec5cb569", size = 2532920, upload-time = "2025-10-07T20:17:54.91Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/fc/84e23e8adff5adcd7792273ad610c4972ec534658a52698fb8db6defa87a/langchain_core-1.5.1.tar.gz", hash = "sha256:b0df382704c6403c1e0c9603415bce09290455d4aeeb38f350d15f78ca597483", size = 972219, upload-time = "2026-07-23T20:13:22.282Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/1e/1eb8833dc59b9c1f49da89a4a1ad7510440c9eb57fb539ea2203294a1acd/langchain_core-1.5.1-py3-none-any.whl", hash = "sha256:c5ec8f51dd05124f950c9afd0fd8bb3f7be4e405eeb868d481b2f8ff652cb9d2", size = 561634, upload-time = "2026-07-23T20:13:20.717Z" }, +] + +[[package]] +name = "langchain-openai" +version = "1.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "openai" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/0f/01147f842499338ae3b0dd0a351fb83006d9ed623cf3a999bd68ba5bbe2d/langchain_openai-1.1.10.tar.gz", hash = "sha256:ca6fae7cf19425acc81814efed59c7d205ec9a1f284fd1d08aae9bda85d6501b", size = 1059755, upload-time = "2026-02-17T18:03:44.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/17/3785cbcdc81c451179247e4176d2697879cb4f45ab2c59d949ca574e072d/langchain_openai-1.1.10-py3-none-any.whl", hash = "sha256:d91b2c09e9fbc70f7af45345d3aa477744962d41c73a029beb46b4f83b824827", size = 87205, upload-time = "2026-02-17T18:03:43.502Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" }, +] + +[[package]] +name = "langgraph" +version = "1.2.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/4b/0d1130e26b41a99dcc88353bbe7162a1f255c4db746bd94024268e6af27b/langgraph-1.2.9.tar.gz", hash = "sha256:385f87bc1802c35af7e0aa479278ecba8582d103515eb48256cb2ddcd42d0bd4", size = 722869, upload-time = "2026-07-10T01:30:14.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/16/0b8dc48823f1326f3e0c8012a3c07a40da6f194299e2ec080df236287baf/langgraph-1.2.9-py3-none-any.whl", hash = "sha256:c2d98ad94333937922ba04148641c1da2bfe45b5b8e55d7b6dcb0bb2df809e76", size = 247473, upload-time = "2026-07-10T01:30:13.733Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, + { name = "orjson" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, +] + +[[package]] +name = "langsmith" +version = "0.10.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, + { name = "websockets" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/38/c709ff119172e490a8e8bccd10deeda8da239f15f11521009a58c7b904ac/langsmith-0.10.10.tar.gz", hash = "sha256:e0e175e9dd8de6d96dbb55244482e20590a2691ec7c5e087afc1f302e33d98fe", size = 4739726, upload-time = "2026-07-23T04:18:56.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/ec/0b2348bea502e07cb3c2a6e5109423debd82aef9fcef6f807c159ceb5297/langsmith-0.10.10-py3-none-any.whl", hash = "sha256:eb5e9da635f5853fc657cddd1c27f40a8e8b2f00c38051555d608c8e57eb605d", size = 675232, upload-time = "2026-07-23T04:18:54.399Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, +] + +[[package]] +name = "marshmallow" +version = "3.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "multiprocess" +version = "0.70.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" }, + { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl", hash = "sha256:8d5eb4ec5017ba2fab4e34a747c6d2c2b6fecfe9e7236e77988db91580ada952", size = 156414, upload-time = "2026-01-19T06:47:35.915Z" }, + { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +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" }, +] + +[[package]] +name = "numpy" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, +] + +[[package]] +name = "openai" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/5a/f495777c02625bfa18212b6e3b73f1893094f2bf660976eb4bc6f43a1ca2/openai-2.20.0.tar.gz", hash = "sha256:2654a689208cd0bf1098bb9462e8d722af5cbe961e6bba54e6f19fb843d88db1", size = 642355, upload-time = "2026-02-10T19:02:54.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/a0/cf4297aa51bbc21e83ef0ac018947fa06aea8f2364aad7c96cbf148590e6/openai-2.20.0-py3-none-any.whl", hash = "sha256:38d989c4b1075cd1f76abc68364059d822327cf1a932531d429795f4fc18be99", size = 1098479, upload-time = "2026-02-10T19:02:52.157Z" }, +] + +[[package]] +name = "orgmemory-evaluation" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "instructor" }, + { name = "langchain-community" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "ragas" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "instructor", specifier = "==1.15.4" }, + { name = "langchain-community", specifier = "==0.3.31" }, + { name = "openai", specifier = "==2.20.0" }, + { name = "pydantic", specifier = "==2.12.5" }, + { name = "ragas", specifier = "==0.3.9" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = "==9.0.2" }, + { name = "ruff", specifier = "==0.15.5" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "pyarrow" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/f3/95428098d1fa7d04432fb750eed06b41304c2f6a5d3319985e64db2d9d41/pyarrow-25.0.0.tar.gz", hash = "sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4", size = 1199181, upload-time = "2026-07-10T08:29:50.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/44/fdd3a4377807b7dcabe2d4b5aa99dbbc98e2e5df3f1ca4e7f0aec492d987/pyarrow-25.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e", size = 35850884, upload-time = "2026-07-10T08:26:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/bf/71/9f053177a7709b8c90abb00a2375b916286f9f0d6cfb21a5cadd4ef811e8/pyarrow-25.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec", size = 37616197, upload-time = "2026-07-10T08:26:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/1a/22bfb6597dcdc861fa83c39c06e1457cb56f698940eff42fbb25de30e8e5/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f", size = 46841966, upload-time = "2026-07-10T08:27:07.685Z" }, + { url = "https://files.pythonhosted.org/packages/55/0e/cd705c042bc4fe7022478db577fcab4abdcfabb9bc37ab7a75556b3fcb2b/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778", size = 50088993, upload-time = "2026-07-10T08:27:14.268Z" }, + { url = "https://files.pythonhosted.org/packages/98/ee/d822e1ee31fe31ec5d057210e0605c950b975dcd8d9a332976cc859a9df8/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537", size = 49941005, upload-time = "2026-07-10T08:27:21.274Z" }, + { url = "https://files.pythonhosted.org/packages/33/1b/207a90cc64619a095eb75a263ae069735f2810056d43c667befd573ec083/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b", size = 53112355, upload-time = "2026-07-10T08:27:27.911Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fe/81d1e5f8beed15c01e98649d5c6e2167b67fd395884a2488f18bf1cf0dba/pyarrow-25.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5", size = 27945954, upload-time = "2026-07-10T08:27:32.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c8/098ce17d778fd9d29e40bb8c5f19a40cc90c3f0b46c9057b0d7993f42f54/pyarrow-25.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc", size = 35844549, upload-time = "2026-07-10T08:27:37.956Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/24c28877219abf6263d909b1592c97ff82c59f13a59acbed11fc87c0654f/pyarrow-25.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e", size = 37610397, upload-time = "2026-07-10T08:27:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/53/55/6d1d5f5aff317ec5de9421594679ed51ed828fe7e2ce209327f819d801e4/pyarrow-25.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be", size = 46841701, upload-time = "2026-07-10T08:27:49.741Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5d/f790fb6965ab54c9da0dda7856abc75fd0d7648d865f8d603c111d203a64/pyarrow-25.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9", size = 50090118, upload-time = "2026-07-10T08:27:56.051Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8c/faf025357ebf31bc96777f234277aa31e2aeca6dd4ecaa391f29085473c2/pyarrow-25.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d", size = 49945559, upload-time = "2026-07-10T08:28:01.927Z" }, + { url = "https://files.pythonhosted.org/packages/07/a1/bd051871708ea99a5e0fc711926c26c6f2c6d0130c7aaac8093e34998af6/pyarrow-25.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1", size = 53114238, upload-time = "2026-07-10T08:28:08.594Z" }, + { url = "https://files.pythonhosted.org/packages/7c/31/737f0c3cffcd6af647849477d1dd68045deac2e3963c3f9f211bedc48540/pyarrow-25.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18", size = 27861162, upload-time = "2026-07-10T08:28:12.975Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +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" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "ragas" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "datasets" }, + { name = "diskcache" }, + { name = "gitpython" }, + { name = "instructor" }, + { name = "langchain" }, + { name = "langchain-community" }, + { name = "langchain-core" }, + { name = "langchain-openai" }, + { name = "nest-asyncio" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "openai" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "rich" }, + { name = "scikit-network" }, + { name = "tiktoken" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/cd/456c0a0de093636683cfe61c0db7865005f00f1a1d38d3c964c0b6e70c73/ragas-0.3.9.tar.gz", hash = "sha256:7807cfc3f01c0297ef99ab3e6a5f53e7cac0a73267d273273b17871437c55786", size = 43865644, upload-time = "2025-11-11T17:25:19.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e7/4b76c1d5826481fe6bd0aaa4527e7a98a17164559c41b5fe0663a236a1dd/ragas-0.3.9-py3-none-any.whl", hash = "sha256:1be77af14c7101f26125cc025ff1026f49e2d564162471ae590c3d832527f9c5", size = 366710, upload-time = "2025-11-11T17:25:16.374Z" }, +] + +[[package]] +name = "regex" +version = "2026.7.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" }, + { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" }, + { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" }, + { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" }, + { url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" }, + { url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" }, + { url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" }, + { url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" }, + { url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" }, + { url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" }, + { url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" }, + { url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" }, + { url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" }, + { url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" }, + { url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" }, + { url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" }, + { url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rich" +version = "14.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" }, + { url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" }, + { url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" }, + { url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" }, + { url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" }, +] + +[[package]] +name = "scikit-network" +version = "0.33.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/6d/28b00fbef9ff7d8ba31861bf16705a1a74a1696fb65aab2a7c584f966bec/scikit_network-0.33.5.tar.gz", hash = "sha256:ae2149d9a280fdc4bbadd5f8a7b17c8af61c054bc3f834792bc61483e6783c12", size = 1784205, upload-time = "2025-11-19T09:45:14.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/32/f092fca9a3ae256e0608ec6a4d8830023ea4ae478c2e27e94cf5802824f0/scikit_network-0.33.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ced625228be1632595d11adaf22d61d1d4c788909cd4d8c364e720b2814aac7f", size = 2874698, upload-time = "2025-11-19T09:44:52.765Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/5b2ce72f93b422af48a2b755fbcbab271bf980a6b46484f754d63978f1ff/scikit_network-0.33.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:808b625d28005c24b47cbdf65f780a3a355aa4080992e6b78f03434e873d06e6", size = 2854224, upload-time = "2025-11-19T09:44:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/86/02/974ae67f493ccf988108894e8a9dedfd00ca5113d2848e0b9fc2a4d18824/scikit_network-0.33.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9f2d98059cd79bdb935ff6a638f1c3a12b0b1cb7ace9e1d3fb35476eeeaabaa", size = 7924650, upload-time = "2025-11-19T09:44:57.704Z" }, + { url = "https://files.pythonhosted.org/packages/8d/34/b67e48e111916a6f09fe29a971a9716c14f78525e0ab7c46e6a6538cf2f6/scikit_network-0.33.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aac2d3bc14214f02dac300624f5ec2650af9a98b52f304d300f0ec2813a0e544", size = 8012322, upload-time = "2025-11-19T09:45:00.079Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a2/49293b53b837b3248d19fffd41a9fd73dcb2eeabbab890cc4c8aa237b545/scikit_network-0.33.5-cp312-cp312-win_amd64.whl", hash = "sha256:2866b16aed9ef25ba42cb2f2e44ef2ad079337f336ce48d0604b55fa4af87688", size = 2746491, upload-time = "2025-11-19T09:45:01.997Z" }, + { url = "https://files.pythonhosted.org/packages/d6/cd/0069244e970d27fa0ab0512394295a106605f00c271e85618182460d2c92/scikit_network-0.33.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa8b490a777e081dd6f69627a26b50cc4012f27fff683a1e4828819a88a5dcf2", size = 2862564, upload-time = "2025-11-19T09:45:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/51/37/85454864e50a65e528fe3b15eb3b41eb68b0c7f6d5c51c220b3198622ede/scikit_network-0.33.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3615d073ba9ae1ae30dda2de747474cd23c86cededa82b317471ee9f9bebd1b2", size = 2842521, upload-time = "2025-11-19T09:45:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b9/4023f35e430b51020f17b0f1d8933768cf1ed7cd1623cc089f5543048983/scikit_network-0.33.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2408d3f4c81256a3193d536aad4a6ffcfbb05d096abe6a9cc0b6b5e275df876d", size = 7871695, upload-time = "2025-11-19T09:45:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ec/e50755f7459130ba745c42c37665c5ae9a7c7e357f43400b5b8b966f902e/scikit_network-0.33.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:526490a1e0e8e49ad0f4cca193f581d60082a2fa8c9a825eb0b6936050b0d02b", size = 7968762, upload-time = "2025-11-19T09:45:10.347Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/616974a0adb9d04a791570e9371caaef14c54f8806b04dd59c80a7b60289/scikit_network-0.33.5-cp313-cp313-win_amd64.whl", hash = "sha256:722c15fcede5e07ac008354bbd6ef375e0f5bf1fd52bd40271775997be2fb715", size = 2742482, upload-time = "2025-11-19T09:45:12.843Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, + { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, +] + +[[package]] +name = "tqdm" +version = "4.69.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/84/da0e5038228fa34dfd77c5026b173ed035d2a3ba31f1077590c013de2bff/tqdm-4.69.1.tar.gz", hash = "sha256:2be21080a0ce17e902c2f1baeb6a74bf551b67bbdfa4bc0109fad471d0b4cb0d", size = 793046, upload-time = "2026-07-24T14:22:02.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/50/5817619a0fca56aff06383dbfde7ae017b3ca383915b3f1e4713164273cf/tqdm-4.69.1-py3-none-any.whl", hash = "sha256:0a654b96f7a2660cceb615b56f307ec2bef96c515409014a429a561981ab52b4", size = 675452, upload-time = "2026-07-24T14:22:00.048Z" }, +] + +[[package]] +name = "typer" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/91/63938e0e7e7876658e5e40178e7c0735b53527886fe11797a11699c55edd/uuid_utils-0.17.0.tar.gz", hash = "sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2", size = 43220, upload-time = "2026-07-09T13:49:58.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/80/a7e685968e3cec99d6fe2fb25d0f5726310e1bba356da68c13dfd8b7d140/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9205068badf453d2f0821fd5d340389b4679992d7ff79d4f3e5608996dd1b287", size = 556403, upload-time = "2026-07-09T13:48:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/56/47/3102d93bcb7b0bfe6bede63ff8f221a7f91348e10a37f682773be27c56d9/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0fcca4e838af9ac9243b3358d7c14afa4dca286a87781124c272d6c4cad9c968", size = 285608, upload-time = "2026-07-09T13:48:28.769Z" }, + { url = "https://files.pythonhosted.org/packages/55/fb/d59695f0f8db065b93c63316eaafa05a22d75a0486978a33736c52c646d5/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f3729e839209f3457d0d8b6a35a376fdf65577a5aecaf4cc3587d3305759ba6", size = 319926, upload-time = "2026-07-09T13:48:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/5a/03/62fabcd1e990e07a0e220e8d552af45bc16f107fa8e55c2014a706bb1a1e/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dac0ad0cd9a2818d1775215365a4e8c2f8ada215529dd26f3f8cceeb67a6988", size = 327172, upload-time = "2026-07-09T13:48:31.187Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/a5081391338b459e2f8d8b12581f00f8caa6317fab510e0e85c18c59e938/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e671b2322ef09106ecb1ca0f4c398b134d5e2c1f80d7a4f3336847a3072c0e94", size = 439075, upload-time = "2026-07-09T13:48:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/59/30/91795bd01e17a13661280d4899fbf38fb05e3f38e873f9aaec106ec30aa0/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb3e5caca8d3a6f72ea4cce024583f989f6f2e9186f98800213fff0176e8bcc", size = 320247, upload-time = "2026-07-09T13:48:33.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/11/09102b78303e4eb62069d6d88ef9fd661dc523e8f429e1fd67eaa78a6f44/uuid_utils-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b72c2002202038666bf647f9a790906214c7c11cd0d6efef77b7d07bef3034a", size = 344738, upload-time = "2026-07-09T13:48:34.786Z" }, + { url = "https://files.pythonhosted.org/packages/74/f9/be95bad6954b60328878c3800258f01a6accd24fd75112d13f023462d53f/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e2ac1c0b56f2c91b6f158e29ed96b1503223fe8aa6e79b1be1dc55bd8a5131c", size = 496845, upload-time = "2026-07-09T13:48:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/02/8a19a34e0530d987488a068a71576a236f5c8c746630b870b57f71eb24ef/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6c142bd0cb4dba31c10babe00d59f7ef6460f0ef55eaa9c1a9da270684af996a", size = 603233, upload-time = "2026-07-09T13:48:37.512Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a8/b1abab36ff73b0248d82179816467f6d39a2e80fd64329a895ca94f3508e/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e252db239eb41c32248e096e0d170bce5896a4fd3405556362bc3dd83d912206", size = 561401, upload-time = "2026-07-09T13:48:38.977Z" }, + { url = "https://files.pythonhosted.org/packages/61/91/70e7b528b351cc03a9ca43e6116371cdde31bb12bcead7ca2ca1367366cc/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:237722b6581bb5b4eb4cefbcbe5c6e2980a440aabe781fbe50ebf1cb71eee4cc", size = 525314, upload-time = "2026-07-09T13:48:40.599Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/9167e90cf9937d6558f92d022ff3024a69d938a514d9c8faa4080f73b001/uuid_utils-0.17.0-cp312-cp312-win32.whl", hash = "sha256:46a73cacdf512f473a81f65dbf84186e08cfe6e9118fa582b6c6b33a8288a30d", size = 166831, upload-time = "2026-07-09T13:48:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7d/0b889654d9ee3413f810cf4685e241285f650d98a4103ac9f3c6bcc95f29/uuid_utils-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:e59b60a0a4cb7541480e02090d37dc2df3b72df4c2e776fff64ce3a4e3dd4637", size = 172944, upload-time = "2026-07-09T13:48:42.992Z" }, + { url = "https://files.pythonhosted.org/packages/be/35/8c6e1bf65e4d400352885dadc656ad6d0af96e89231e3f04686bc2197128/uuid_utils-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:d561a4c5747a1e6c7fa7c49a0292e78b4e8c456332caa084fc7abad8de828652", size = 172459, upload-time = "2026-07-09T13:48:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/d2/dd/614fb9912157ac0128e6050859ccf06d9f13df9a944a803e8f80f6157e38/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63", size = 557259, upload-time = "2026-07-09T13:48:45.664Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d072711704de3d21bec08b6c2f36a215200ca1d5e01a390ea1ac434080a0/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73", size = 286271, upload-time = "2026-07-09T13:48:47.018Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/8a63e5eb2d5a6ba69a6c2036e305075bd6f5a022e7ea25fc6ce0eb7c51d2/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9", size = 320025, upload-time = "2026-07-09T13:48:48.208Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/bdc2caf9719d9090d7c46043242ae6136cba4f7a7ee384992ab905ad9aa1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1", size = 327931, upload-time = "2026-07-09T13:48:49.673Z" }, + { url = "https://files.pythonhosted.org/packages/b6/33/9219d09d51ead282b578b2a4e0a515c2cce3ec52076cada8bfb7e35727d5/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098", size = 438537, upload-time = "2026-07-09T13:48:50.842Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/e8e0f8b3955f2081c116157119d87659937893242eb834aa170da04d660b/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869", size = 320656, upload-time = "2026-07-09T13:48:52.164Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/d1ceddc430ff04b6e21704b2030d4438074a2f478b265dab43da957791c1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131", size = 345310, upload-time = "2026-07-09T13:48:54.076Z" }, + { url = "https://files.pythonhosted.org/packages/d5/62/89438e12f389a843e626b7e37691319a057b3d6b80914609106891faadda/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb", size = 496771, upload-time = "2026-07-09T13:48:55.365Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/eedcd99f522d60e238ead03844f0d51743ba84d33044959e230b756bf212/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3", size = 603631, upload-time = "2026-07-09T13:48:56.746Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a8/bb1b38aaddd7243b6e562c6694f499bf094800918316192fd8cb2cdc2620/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64", size = 562008, upload-time = "2026-07-09T13:48:58.241Z" }, + { url = "https://files.pythonhosted.org/packages/b4/77/5f7ed930dc105e293845c09e4d5bd84076318a12f45a46783e1af64906d7/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89", size = 525527, upload-time = "2026-07-09T13:48:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/fd/25/1b55697adf6811a6f92cff6340e6b03e31fd6bc51066a5c10698c29b3679/uuid_utils-0.17.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e", size = 97965, upload-time = "2026-07-09T13:49:01.217Z" }, + { url = "https://files.pythonhosted.org/packages/26/bf/cd729343de4684230be8a966bad7bfc2cf10ce3e643b1189a8b5370dbe35/uuid_utils-0.17.0-cp313-cp313-win32.whl", hash = "sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c", size = 167316, upload-time = "2026-07-09T13:49:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/76/f0/e602ae0a1b139a7826e5189b93d91902564def06d5006324fd2faf82c8fc/uuid_utils-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff", size = 173630, upload-time = "2026-07-09T13:49:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/024ebece265b387154115dc4f1d9727174ef82623069f4bec8b7ed7e73f7/uuid_utils-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f", size = 173214, upload-time = "2026-07-09T13:49:04.836Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "xxhash" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/63/71aa56b151a1b28770037a61bd4e461c2619cfc8866a4fcaf1548605e325/xxhash-3.8.1.tar.gz", hash = "sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46", size = 86223, upload-time = "2026-07-06T10:49:58.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/91/f65c34a7aa7b4e7cf4854f8e6ef3f7ee32ceac41d4f008da0780db0612f6/xxhash-3.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e6e49370822c1f4d8d90e678b06dbcb08b51a026a7c4b55479e7d467f2e813bc", size = 34680, upload-time = "2026-07-06T10:44:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/57/04/b10a245a4c09a9cfa88f8e9ae755029413ad1ac17047f9a61906e5ae0799/xxhash-3.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:220d68130f83f7cc86d6edfdeab176adc73d7200bf3a8ec10c629e8cf605c215", size = 32397, upload-time = "2026-07-06T10:44:42.196Z" }, + { url = "https://files.pythonhosted.org/packages/3a/75/45ab795b5945b6388583bd75202106af505537935566c15a1577797a0e08/xxhash-3.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d365ee1892c1fa803536f8c6ce21d24b29c9718ec75eb856095c07830f8c478", size = 220549, upload-time = "2026-07-06T10:44:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/13/44/5ba2bd0a14ddf4193fc7d8ec29625f659f22c06d60b28f04bf46305d8330/xxhash-3.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:852bfe059720632e2f16a6a4745e41d20937b2bf2a42a401e2412046bb6971cc", size = 241186, upload-time = "2026-07-06T10:44:45.534Z" }, + { url = "https://files.pythonhosted.org/packages/23/32/c4147def4d1e4538b906f82731e0ba23424377fc50a7cddd03cd284c8f63/xxhash-3.8.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2f8c25a7061d952de589bd0ea0eaadee32378ff83dd6a677b267f9cd86f401f8", size = 264852, upload-time = "2026-07-06T10:44:47.199Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bd/71ed14f4f0318bb7fd7b2ec51999413487fa8da8d41208e84d50d1ef0f98/xxhash-3.8.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:868a8dcaff1a84ba78038e1cef14fc88ccf84d9b4d12ea604696e0693296aa56", size = 242663, upload-time = "2026-07-06T10:44:48.846Z" }, + { url = "https://files.pythonhosted.org/packages/91/09/70af22c565a8473b3f2ae73f88e7721af281bc4a575236dbd1970c9f76f6/xxhash-3.8.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6536d8677d2fff7e64cd0b98b976df9de7aee0e69590044c2af5f51b76b7a170", size = 473510, upload-time = "2026-07-06T10:44:50.695Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/34db781c8f0cf99c544ca1f2bc2e5bf55426e1eb4ca6de8ea5da56a9f352/xxhash-3.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82c0cedd280eab2e8291270e6c04894dbc096f8159a39dcf1807429f026ca3cc", size = 220469, upload-time = "2026-07-06T10:44:52.422Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/9a184f615fa5a4dce30c01534f62946ce5a11ce40f73785cbd356ccabaa9/xxhash-3.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daa86e4b68221d38e669bb236ba112d0335353829fb627c82e5909e4bbe8694c", size = 310290, upload-time = "2026-07-06T10:44:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/a9/dc/9b9a9789011ee153723a5eb9e7dd7fcbae2ba9b3fe7a729249ca7c252056/xxhash-3.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2bc7113e6f2b6b3922dd61796ca9f36af09da3773898e7003038dc992fc83b8d", size = 238173, upload-time = "2026-07-06T10:44:55.693Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/71c6005ada9dcb608a4e1902e8475ecadb5f3fbfa04e1e244d276a2d0c43/xxhash-3.8.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5eed32dad81d6ba8e62dc7b9ffa0500199385d7810a8dd9d4eafaceb8c6e20bb", size = 269026, upload-time = "2026-07-06T10:44:57.424Z" }, + { url = "https://files.pythonhosted.org/packages/2f/87/d6c036ba25dfbd9c8633be5aa86fc9474bbb9e2c68212a841d090abe7344/xxhash-3.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:83697b0ea1f10e7f5d8b26a4906fa851393c61546c63839643a2b7fe2d868061", size = 224970, upload-time = "2026-07-06T10:44:59.085Z" }, + { url = "https://files.pythonhosted.org/packages/48/62/4c1f035a41c5752aa05e195b6c904c07b94fe9061a16de61e72a6e6b135f/xxhash-3.8.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:36fc69160465ae75c6ec4ac9f781bb2aa16ae7ff869e73c26fee85fbb11b9887", size = 240820, upload-time = "2026-07-06T10:45:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/d39d565069b87e86d21a2af2a31d04db79249d25aa8d5b62959056a89857/xxhash-3.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:445e0f5a31f2f3546ae0895d4811e159518cdc9d824c11419898d40cfadb677e", size = 300619, upload-time = "2026-07-06T10:45:02.716Z" }, + { url = "https://files.pythonhosted.org/packages/13/22/75467acc887edc8cf71c97ab1708feb3df7a88bda589b9f399765c6387d2/xxhash-3.8.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:dfe0580fbfd5e4af87d0cc52d2044f155d55ebd8c8a93568758a2ea7d8e15975", size = 443267, upload-time = "2026-07-06T10:45:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b6/1da3baa5fa6ef705e3425fddd382be7dfc4dfba2686df90a20f16e9c7b1b/xxhash-3.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:095e1323fa108be1292c54c86da3ef3c7a7dc015b105a52133973bc07a6ad11a", size = 217338, upload-time = "2026-07-06T10:45:06.304Z" }, + { url = "https://files.pythonhosted.org/packages/78/dd/b5295a9f97484e7a1c2b283a742ca45e3104991c55a1ef670dde161829ba/xxhash-3.8.1-cp312-cp312-win32.whl", hash = "sha256:bf28f55e427e0483acb1f666bd0d869b6d5e5a716680c216ad7befe3d4cfba2e", size = 31970, upload-time = "2026-07-06T10:45:07.823Z" }, + { url = "https://files.pythonhosted.org/packages/ec/31/3fa0b807d7e21515cd975e7fe5c039d52ac3e9401a96d6ad68dae6305215/xxhash-3.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:2256e80e4960ee282f63428adb349cb7f8bd8efe4db770d88eb815f4b9860724", size = 32741, upload-time = "2026-07-06T10:45:09.42Z" }, + { url = "https://files.pythonhosted.org/packages/b8/05/86feada74e239600e6875aa507afb40482a89b92700aa74a92da83bdcb77/xxhash-3.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:9df56e6df96a60590935e22373041cccc91fd55858763dcffb55bf63b3a2b396", size = 29234, upload-time = "2026-07-06T10:45:10.809Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8c/446bb782cd0d27007a917b5569a08dd73219c3e8d6e459014db104b27bdb/xxhash-3.8.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:3c682fcd96eb4bf64be32a4d95f96107e1588005831bd8a741b324fdda01b913", size = 38562, upload-time = "2026-07-06T10:45:12.425Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/c0c45627eaa6be7a5d6117423adf8f7a15b17ee74b4b17072cca5959a225/xxhash-3.8.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:036a024d8b9c01f70782e09ed98d532e76fd23f950ae7154bd950fe94e90ebec", size = 36656, upload-time = "2026-07-06T10:45:13.932Z" }, + { url = "https://files.pythonhosted.org/packages/f6/94/8324c04cc7597154caaeba6c094e01fbd2e7601d01e7a13eea9f5420e77b/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d6a5c0bce213b23b0166fe0d35bcbbe23ce4b968f257cc7eb6fd57cb8e1e6297", size = 31169, upload-time = "2026-07-06T10:45:15.687Z" }, + { url = "https://files.pythonhosted.org/packages/40/a4/beb6bb26e1184e126dbe7a5682330214ef54dcfbf882078aa9f4b5428d42/xxhash-3.8.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5177aa44eddaa97c6ef0cc00c6d540edb64d51781d2f8fb941612ec61a92c9ed", size = 32177, upload-time = "2026-07-06T10:45:17.035Z" }, + { url = "https://files.pythonhosted.org/packages/56/0f/fc4c92a5a528f839b34b6419b2e53c8597f2a629d5a1f5d721f65bfa1fd6/xxhash-3.8.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7801b7223db017b9c0c9ccf37e44524edb35a1544a1c032add22c061c6af0276", size = 34642, upload-time = "2026-07-06T10:45:18.39Z" }, + { url = "https://files.pythonhosted.org/packages/d4/58/edbfb141d4000767ac6a9694f8ac0763e2c2e983e65c9e31620ba56e2667/xxhash-3.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e80238259655bf69d7bcd08226a970d7f42605f3157786bfa76dd13472d7fa0", size = 34684, upload-time = "2026-07-06T10:45:20.033Z" }, + { url = "https://files.pythonhosted.org/packages/07/3f/5072f1f0f5714186f0ac2a0b5a4929ce30d4b845e94886b6c01b6ebda0be/xxhash-3.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bcab50a389cc04d87f90092af78a6adba2ab3deca63175a3344ca83514045315", size = 32401, upload-time = "2026-07-06T10:45:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/49/c7/802ea2f9c2ed59219934d6d65c470d502b1788043eae277a52af8658bda6/xxhash-3.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a2489d3a776fa380cb8e71f54c7fda268a9baf3de9b1395093fd280f95735907", size = 220617, upload-time = "2026-07-06T10:45:23.234Z" }, + { url = "https://files.pythonhosted.org/packages/99/a8/e10488efd31fcb13fcd6acbc6e788f10c6f8e3a0cc4ae3eb89dc19c55a12/xxhash-3.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ab1e5432690276e71192be7401b55f96db2d0eedea5d44eb1f164505669cc0", size = 241295, upload-time = "2026-07-06T10:45:25.364Z" }, + { url = "https://files.pythonhosted.org/packages/18/cc/14180b17d44892a631f8ae7323c30bfbb1328efc8209e528a480293528ac/xxhash-3.8.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b30e01a0b97a4bc3f519a4d7a82da3dc53251fb0de5eeea8660dcd4ff094c0c2", size = 264688, upload-time = "2026-07-06T10:45:27.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/72/a14019d0c5f6c41ee407a503036ae32787c91325ca218a96a9b5627be651/xxhash-3.8.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f44275ddb0978b67a58a951501903f04d49335a91f7681c9ce122ecb8ccb329", size = 242740, upload-time = "2026-07-06T10:45:28.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/92550e556c6fcfcb96c6a336945eb53a431ed43120ed749636debb16c5cf/xxhash-3.8.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3b87cbd974512c0c5fc7b469c36b2cdc9ee6d76e4ec78bccb2c7184611c49b0", size = 473599, upload-time = "2026-07-06T10:45:30.524Z" }, + { url = "https://files.pythonhosted.org/packages/29/83/e361d3c1acd1b21e1d489616de6fa4aaf843365d8179f612e3743eac20a9/xxhash-3.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98ee81b4b7f3023c9cb04a78cc67610baffcb5812d92f2096cb5a5efc6f19437", size = 220559, upload-time = "2026-07-06T10:45:32.979Z" }, + { url = "https://files.pythonhosted.org/packages/05/01/006a4243c2c2a6831827f9999f6d1c23feeef100eb023c1f886022a00bf3/xxhash-3.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2666f059a1588a99267e33605365ed89cea92f424b3522806a9f4bd8ad2e3d62", size = 310383, upload-time = "2026-07-06T10:45:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d8/20/af388e8bf9f9a0f89eeef7d2a1935d176ee1c20bc6adeda05035879379cf/xxhash-3.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0093cf7eeb91b84776e8742113afa4bdf47533d36cf719179aaaf1f56f6f8bf", size = 238228, upload-time = "2026-07-06T10:45:38.02Z" }, + { url = "https://files.pythonhosted.org/packages/63/6b/4666579a87eebd1744663c404297355fa0658617b015cedfa58810ee7036/xxhash-3.8.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3a800912a2e5e975d4128969d645c4a2a80aa886ccd6c9b1c6f44529e327e8cf", size = 269137, upload-time = "2026-07-06T10:45:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/de/d3/e963a8a46f900a137d91b02144d8ea07a8f812971b138204a3b2f8b8e55c/xxhash-3.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0fe37f72a207223d22a4eddc3149d4298993385aa9daef25c039246ca5a309f3", size = 225068, upload-time = "2026-07-06T10:45:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/aa/80/9d181dbcde4b0fe48375f48833a5832d4b8cd2b349b15110c92ee472d874/xxhash-3.8.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5db43f249b4be9f99ef4b967863f37094fb40e67effafb78ba4f0356b6396104", size = 240874, upload-time = "2026-07-06T10:45:43.414Z" }, + { url = "https://files.pythonhosted.org/packages/39/15/ce3ab5a1cd27ead25a5196e55a7284220f6ad6e316da494ffd900b2b600f/xxhash-3.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c4ed42965c2cd9081f011be22f69d0e65d3b6165fe7734072fd0c232840bbd4e", size = 300702, upload-time = "2026-07-06T10:45:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/96/c0/2281a8ab5f2a62dbf57a23c58a01ccc1d98abf40f71193c8a81f59e759b5/xxhash-3.8.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3557bec8fcb11738a8920eeb68974bc76b75262f6947998d3147954ce0a4b893", size = 443351, upload-time = "2026-07-06T10:45:47.188Z" }, + { url = "https://files.pythonhosted.org/packages/81/2e/071a58c1a53a52d4f7a3aa0987be0c396dffd40da8204805fe1b130a81f4/xxhash-3.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00de40f3b42240db23a82a5c682b55d7263d84a26a953240c1aee463409660e3", size = 217396, upload-time = "2026-07-06T10:45:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/68/44/36ab58134badd9d3433fc7b53c4ca8d113d8e807782885628640f8297a4d/xxhash-3.8.1-cp313-cp313-win32.whl", hash = "sha256:b5196cc2574cfec572a5f3fb7cfa5ade27305ae3d06516a082132441aff4c83a", size = 31974, upload-time = "2026-07-06T10:45:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/2a0b84798448e766f7b89ceed073cb0cb5a43fc9ebbacbdea74a38de18e3/xxhash-3.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:538f5f865df6cd8c32dd63158a0e5b4f5dd08d732a7da8b7228a5a0776c8ce55", size = 32739, upload-time = "2026-07-06T10:45:52.221Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/bb51dbf7c363ff88a7cbd50b7959718219577ef44d7cf255929ffc4a2194/xxhash-3.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:a6617f30641ba0d8baa1635fbefb1dffc5165ec36d26921bd5cee13497cd937a", size = 29239, upload-time = "2026-07-06T10:45:53.714Z" }, + { url = "https://files.pythonhosted.org/packages/56/d3/827ca123c2ee5443a6aaed3c5dd199237dc2f010e2bebd7ec09ef36f3a5f/xxhash-3.8.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:bfcd82852c62a60e314670a9602de354c4460f8adad916e2e42a20860c7870bc", size = 34964, upload-time = "2026-07-06T10:45:55.535Z" }, + { url = "https://files.pythonhosted.org/packages/05/67/67ae2a3ccdeb8b8ef025d35aee9edd1d26c3abe5051d47da9286232afbf8/xxhash-3.8.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:08ea2081f5e88615fec8622a9f87fbe21b8ea58d88cfc02163ca11026ee62a92", size = 32697, upload-time = "2026-07-06T10:45:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/38/5a/3d3994346e1f45493679cb5c1ffc2bf454e410e9d1e8a662d253becee91e/xxhash-3.8.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2e32855b6f9e5b18f449e59d45e3d5778bdeb660632ef2693cca267a11246c75", size = 225954, upload-time = "2026-07-06T10:45:58.897Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2c/53169270309b7cd8e05504e07fe123bac053b89d00ac63617faacf0a2ec0/xxhash-3.8.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6e088bd7870775624256a0d84c2a6714afd223b2eeb56b0ca58398e52a32fda", size = 249776, upload-time = "2026-07-06T10:46:00.977Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/5c551d8d592f944506f7c5185e210255c15e672a3c6008c156a1bd9b775e/xxhash-3.8.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:72eb5ae575cc7ae2b23f6f8064a8b10f638c7149819ae9cc6d20ebd4d37a1629", size = 274776, upload-time = "2026-07-06T10:46:02.869Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/d3a762270cee2d7bcd0e25e28c623e5f3f5c0dc637b66e3e47dd5b0bb3f0/xxhash-3.8.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d0b48cdf690a64cedf7258c3dc9506cc41fc86edd7739c40e3098952265dc068", size = 252056, upload-time = "2026-07-06T10:46:04.688Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/b78e4373b2cb6d1c42af60ea2d7e9146ad0710b239ac7f706d5d31d5bb98/xxhash-3.8.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb9e256a357dfcede7818c6d34e70db2d6b664394803d1de4b6984d2de76c0f1", size = 482108, upload-time = "2026-07-06T10:46:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/642d923336ea61a15f8ce64fc7e078729e6e06c3a026e517fa79b2c23b7a/xxhash-3.8.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51f71a6e2ad071e70c937e41fcb6c19f82c3f9f49831eba850ed4a106ffbb647", size = 226739, upload-time = "2026-07-06T10:46:08.598Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0a/a37d6da6427d45a8d23e3ee3a0ca9c9d4a90364849c6637fe2963a755f9b/xxhash-3.8.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4a6443968c4e8dc69967e12776776a5952c119cc1bd94168ad1c5ad667c2be1", size = 319658, upload-time = "2026-07-06T10:46:10.504Z" }, + { url = "https://files.pythonhosted.org/packages/4a/51/ebbd40da8a3f1bc53b4b7a9a87f8e28bd95c5f21bc14b8a57860cf367d1b/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:714503083a1f2065c9ad15340dd49ac8a8e948a505a705ffa1750cb951519113", size = 246059, upload-time = "2026-07-06T10:46:12.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/4c/d9014030147e1f0bb26e7da47aa240dd9ec61c763c573e558111d869f8e1/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:77f74e45a1e5574bbbf80181c8027b3a4c65c2248fffbd557bd596fff13102f9", size = 275535, upload-time = "2026-07-06T10:46:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/caee2db41fadcd5a25aa4323213f9afec5a8586d4e419241e3d659362bd7/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e0e1b0fb0259c1b75d1251ac0bb4d7ab675d36f7a6bf4ba6aa630dae94f9ffa", size = 231292, upload-time = "2026-07-06T10:46:16.452Z" }, + { url = "https://files.pythonhosted.org/packages/0b/60/f52f08bcdc904c4514ea5c25caa19e9f3214144434a6ff96dc82dc1cbddd/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:10e4393ec33633c2f05ad01869e546ad080b1a18f2650503731f153774608b31", size = 250490, upload-time = "2026-07-06T10:46:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/24/a0/94dc7ae310838f250669c6ad7168e6d6fca17d49dac1053f06dc232c4a56/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b3ba794c3d885803db6c3116686923f1ec13bc86e621e169a375282b63ea1cc6", size = 309861, upload-time = "2026-07-06T10:46:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/adeead7d0eb28cdfc2832544ea639ffbc6749ccde47a8e228d667459182e/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:57189a69c0891e4818853feaa521c972d22c880a001453addea015f48e3c3398", size = 448739, upload-time = "2026-07-06T10:46:22.79Z" }, + { url = "https://files.pythonhosted.org/packages/04/a4/22ec0e07db57d901c9298ae98aa3cf2be45bafded6f07c13131e85b89032/xxhash-3.8.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d59e71153fe9ff85648d00e18649b07e9b22c797291abb7e27274fa06df8b838", size = 223657, upload-time = "2026-07-06T10:46:24.831Z" }, + { url = "https://files.pythonhosted.org/packages/94/32/8a9531f37b59e5a013003db7cb7414baf4ce7e0e1268e0d5947cd3d6a2df/xxhash-3.8.1-cp313-cp313t-win32.whl", hash = "sha256:5b96f0024e9840f449bd91b2d005c921a4b666055a0d1b6492463799f32aae22", size = 32377, upload-time = "2026-07-06T10:46:26.86Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/2ca45fd7f671de5f81fc297ef1c95080b40c86ec6be0cc6034b8f7707ac8/xxhash-3.8.1-cp313-cp313t-win_amd64.whl", hash = "sha256:37d5a56c36dcc0b9a87b814cd992598d33863ff683749de6c86081f278d5e629", size = 33274, upload-time = "2026-07-06T10:46:28.39Z" }, + { url = "https://files.pythonhosted.org/packages/5a/54/20d7163463ddb6438b73a427d1655a77a502cf9b9b0c3ada3599629d9c0a/xxhash-3.8.1-cp313-cp313t-win_arm64.whl", hash = "sha256:6696c8752aded28ff3b16f33ef28ce28fb5d209b80c206746f943199fcf5fd65", size = 29375, upload-time = "2026-07-06T10:46:29.962Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, + { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, + { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, + { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, + { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, +] diff --git a/integrations/ai-openai-compatible/src/main/java/com/orgmemory/integrations/ai/openai/OpenAiCompatibleChatModelProvider.java b/integrations/ai-openai-compatible/src/main/java/com/orgmemory/integrations/ai/openai/OpenAiCompatibleChatModelProvider.java index e0c1fd98..557b9af4 100644 --- a/integrations/ai-openai-compatible/src/main/java/com/orgmemory/integrations/ai/openai/OpenAiCompatibleChatModelProvider.java +++ b/integrations/ai-openai-compatible/src/main/java/com/orgmemory/integrations/ai/openai/OpenAiCompatibleChatModelProvider.java @@ -21,7 +21,10 @@ public final class OpenAiCompatibleChatModelProvider { } public ChatModel resolve(AiWorkload workload) { - AiRoute route = gateways.resolve(workload); + return resolve(workload, gateways.resolve(workload)); + } + + public ChatModel resolve(AiWorkload workload, AiRoute route) { return models.computeIfAbsent(route, ignored -> { AiGatewayProperties.Gateway gateway = gateways.definition(workload, route); return OpenAiChatModel.builder() diff --git a/integrations/graph-rag-observability/build.gradle.kts b/integrations/graph-rag-observability/build.gradle.kts new file mode 100644 index 00000000..2f45dc9d --- /dev/null +++ b/integrations/graph-rag-observability/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("orgmemory.spring-library-conventions") +} + +dependencies { + api(project(":components:graph-rag-core")) + implementation("io.opentelemetry:opentelemetry-api") + implementation("org.springframework.boot:spring-boot-autoconfigure") + + testImplementation("io.opentelemetry:opentelemetry-sdk") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} diff --git a/integrations/graph-rag-observability/src/main/java/com/orgmemory/integrations/graphrag/observability/GraphRagObservabilityAutoConfiguration.java b/integrations/graph-rag-observability/src/main/java/com/orgmemory/integrations/graphrag/observability/GraphRagObservabilityAutoConfiguration.java new file mode 100644 index 00000000..408a3c9d --- /dev/null +++ b/integrations/graph-rag-observability/src/main/java/com/orgmemory/integrations/graphrag/observability/GraphRagObservabilityAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.orgmemory.integrations.graphrag.observability; + +import com.orgmemory.graphrag.observability.GraphRagEventSink; +import io.opentelemetry.api.OpenTelemetry; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +@ConditionalOnClass(OpenTelemetry.class) +@ConditionalOnBean(OpenTelemetry.class) +public class GraphRagObservabilityAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(GraphRagEventSink.class) + GraphRagEventSink graphRagEventSink(OpenTelemetry openTelemetry) { + return new OpenTelemetryGraphRagEventSink(openTelemetry); + } +} diff --git a/integrations/graph-rag-observability/src/main/java/com/orgmemory/integrations/graphrag/observability/OpenTelemetryGraphRagEventSink.java b/integrations/graph-rag-observability/src/main/java/com/orgmemory/integrations/graphrag/observability/OpenTelemetryGraphRagEventSink.java new file mode 100644 index 00000000..0b12f5db --- /dev/null +++ b/integrations/graph-rag-observability/src/main/java/com/orgmemory/integrations/graphrag/observability/OpenTelemetryGraphRagEventSink.java @@ -0,0 +1,95 @@ +package com.orgmemory.integrations.graphrag.observability; + +import com.orgmemory.graphrag.observability.GraphRagEventSink; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import java.time.Instant; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * Payload-free OpenTelemetry adapter for completed GraphRAG stages. + * + *

The attribute set is deliberately closed. Prompt, query, completion, + * evidence, document, actor, ACL and exception data cannot enter this adapter. + */ +public final class OpenTelemetryGraphRagEventSink implements GraphRagEventSink { + + static final String INSTRUMENTATION_SCOPE = "com.orgmemory.graph-rag"; + static final AttributeKey OPERATION_ID = + AttributeKey.stringKey("orgmemory.graph_rag.operation_id"); + static final AttributeKey ORGANIZATION_ID = + AttributeKey.stringKey("orgmemory.graph_rag.organization_id"); + static final AttributeKey STAGE = + AttributeKey.stringKey("orgmemory.graph_rag.stage"); + static final AttributeKey OUTCOME = + AttributeKey.stringKey("orgmemory.graph_rag.outcome"); + static final AttributeKey DURATION_NANOS = + AttributeKey.longKey("orgmemory.graph_rag.duration_nanos"); + static final AttributeKey INPUT_COUNT = + AttributeKey.longKey("orgmemory.graph_rag.input_count"); + static final AttributeKey OUTPUT_COUNT = + AttributeKey.longKey("orgmemory.graph_rag.output_count"); + static final AttributeKey MODEL_ROUTE_FINGERPRINT = + AttributeKey.stringKey("orgmemory.graph_rag.model_route_fingerprint"); + static final AttributeKey FAILURE_CODE = + AttributeKey.stringKey("orgmemory.graph_rag.failure_code"); + + private final Tracer tracer; + + public OpenTelemetryGraphRagEventSink(OpenTelemetry openTelemetry) { + this.tracer = Objects.requireNonNull(openTelemetry, "openTelemetry") + .getTracer(INSTRUMENTATION_SCOPE); + } + + @Override + public void emit(GraphRagEvent event) { + Objects.requireNonNull(event, "event"); + long endEpochNanos = epochNanos(event.occurredAt()); + long startEpochNanos = Math.subtractExact( + endEpochNanos, + event.duration().toNanos()); + Span span = tracer.spanBuilder(spanName(event)) + .setSpanKind(SpanKind.INTERNAL) + .setStartTimestamp(startEpochNanos, TimeUnit.NANOSECONDS) + .startSpan(); + span.setAttribute(OPERATION_ID, event.operationId().toString()); + span.setAttribute(ORGANIZATION_ID, event.organizationId().toString()); + span.setAttribute(STAGE, enumValue(event.stage())); + span.setAttribute(OUTCOME, enumValue(event.outcome())); + span.setAttribute(DURATION_NANOS, event.duration().toNanos()); + span.setAttribute(INPUT_COUNT, event.inputCount()); + span.setAttribute(OUTPUT_COUNT, event.outputCount()); + if (event.modelRouteFingerprint() != null) { + span.setAttribute( + MODEL_ROUTE_FINGERPRINT, + event.modelRouteFingerprint()); + } + if (event.failureCode() != null) { + span.setAttribute(FAILURE_CODE, event.failureCode()); + } + if (event.outcome() == Outcome.FAILED) { + span.setStatus(StatusCode.ERROR); + } + span.end(endEpochNanos, TimeUnit.NANOSECONDS); + } + + private static String spanName(GraphRagEvent event) { + return "orgmemory.graph_rag." + enumValue(event.stage()); + } + + private static String enumValue(Enum value) { + return value.name().toLowerCase(Locale.ROOT); + } + + private static long epochNanos(Instant instant) { + return Math.addExact( + Math.multiplyExact(instant.getEpochSecond(), 1_000_000_000L), + instant.getNano()); + } +} diff --git a/integrations/graph-rag-observability/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/integrations/graph-rag-observability/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..38153248 --- /dev/null +++ b/integrations/graph-rag-observability/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.orgmemory.integrations.graphrag.observability.GraphRagObservabilityAutoConfiguration diff --git a/integrations/graph-rag-observability/src/test/java/com/orgmemory/integrations/graphrag/observability/OpenTelemetryGraphRagEventSinkTests.java b/integrations/graph-rag-observability/src/test/java/com/orgmemory/integrations/graphrag/observability/OpenTelemetryGraphRagEventSinkTests.java new file mode 100644 index 00000000..ae256f51 --- /dev/null +++ b/integrations/graph-rag-observability/src/test/java/com/orgmemory/integrations/graphrag/observability/OpenTelemetryGraphRagEventSinkTests.java @@ -0,0 +1,85 @@ +package com.orgmemory.integrations.graphrag.observability; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.orgmemory.graphrag.observability.GraphRagEventSink; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import java.time.Duration; +import java.time.Instant; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class OpenTelemetryGraphRagEventSinkTests { + + @Test + void exportsOnlyTheClosedPayloadFreeAttributeSetWithOriginalTiming() { + InMemorySpanExporter exporter = InMemorySpanExporter.create(); + try (SdkTracerProvider provider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .build()) { + var telemetry = OpenTelemetrySdk.builder() + .setTracerProvider(provider) + .build(); + var sink = new OpenTelemetryGraphRagEventSink(telemetry); + UUID operationId = UUID.randomUUID(); + UUID organizationId = UUID.randomUUID(); + Instant endedAt = Instant.parse("2026-07-24T10:15:30.123456789Z"); + Duration duration = Duration.ofMillis(125); + + sink.emit(new GraphRagEventSink.GraphRagEvent( + operationId, + organizationId, + GraphRagEventSink.Stage.RETRIEVE, + GraphRagEventSink.Outcome.FAILED, + duration, + 4, + 2, + "a".repeat(64), + "retrieval_failed", + endedAt)); + + var span = exporter.getFinishedSpanItems().getFirst(); + assertEquals("orgmemory.graph_rag.retrieve", span.getName()); + assertEquals(StatusCode.ERROR, span.getStatus().getStatusCode()); + assertEquals(epochNanos(endedAt), span.getEndEpochNanos()); + assertEquals(duration.toNanos(), span.getEndEpochNanos() - span.getStartEpochNanos()); + assertEquals(operationId.toString(), span.getAttributes().get( + OpenTelemetryGraphRagEventSink.OPERATION_ID)); + assertEquals(organizationId.toString(), span.getAttributes().get( + OpenTelemetryGraphRagEventSink.ORGANIZATION_ID)); + + Set keys = span.getAttributes().asMap().keySet().stream() + .map(key -> key.getKey()) + .collect(Collectors.toSet()); + assertEquals(Set.of( + "orgmemory.graph_rag.operation_id", + "orgmemory.graph_rag.organization_id", + "orgmemory.graph_rag.stage", + "orgmemory.graph_rag.outcome", + "orgmemory.graph_rag.duration_nanos", + "orgmemory.graph_rag.input_count", + "orgmemory.graph_rag.output_count", + "orgmemory.graph_rag.model_route_fingerprint", + "orgmemory.graph_rag.failure_code"), keys); + assertFalse(keys.stream().anyMatch(key -> + key.contains("query") + || key.contains("prompt") + || key.contains("evidence") + || key.contains("user") + || key.contains("exception"))); + } + } + + private static long epochNanos(Instant instant) { + return Math.addExact( + Math.multiplyExact(instant.getEpochSecond(), 1_000_000_000L), + instant.getNano()); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 5b157731..a68e0b7a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,6 +27,7 @@ include(":integrations:ai-openai-compatible") include(":integrations:graph-rag-postgres") include(":integrations:graph-rag-opensearch") include(":integrations:graph-rag-neo4j") +include(":integrations:graph-rag-observability") include(":integrations:graph-rag-sidecar-json") include(":integrations:graph-rag-spring-ai") include(":integrations:object-storage-minio") diff --git a/web/package.json b/web/package.json index 7c6200d1..3510bb8f 100644 --- a/web/package.json +++ b/web/package.json @@ -9,6 +9,7 @@ "check:api": "corepack pnpm gen:api && git diff --exit-code -- src/lib/hey-api", "build": "corepack pnpm lint && tsc -b && vite build", "lint": "oxlint", + "test:e2e": "playwright test", "typecheck": "tsc -b", "preview": "vite preview" }, @@ -46,6 +47,7 @@ "zustand": "^5.0.14" }, "devDependencies": { + "@playwright/test": "1.61.1", "@tailwindcss/vite": "^4.3.3", "@tanstack/react-query-devtools": "^5.101.2", "@tanstack/router-plugin": "1.168.23", diff --git a/web/playwright.config.ts b/web/playwright.config.ts new file mode 100644 index 00000000..8f648927 --- /dev/null +++ b/web/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from "@playwright/test" + +const port = 4173 + +export default defineConfig({ + testDir: "./test/e2e", + fullyParallel: true, + forbidOnly: Boolean(process.env.CI), + failOnFlakyTests: Boolean(process.env.CI), + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI + ? [["line"], ["html", { open: "never", outputFolder: "../output/playwright/report" }]] + : "list", + outputDir: "../output/playwright/test-results", + use: { + baseURL: `http://127.0.0.1:${port}`, + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: { + command: `pnpm dev --host 127.0.0.1 --port ${port} --strictPort`, + url: `http://127.0.0.1:${port}`, + reuseExistingServer: !process.env.CI, + stdout: "ignore", + stderr: "pipe", + }, +}) diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 58b665c4..4cfb7108 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: specifier: ^5.0.14 version: 5.0.14(@types/react@19.2.17)(immer@11.1.11)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) devDependencies: + '@playwright/test': + specifier: 1.61.1 + version: 1.61.1 '@tailwindcss/vite': specifier: ^4.3.3 version: 4.3.3(vite@8.1.5(@types/node@26.1.1)(jiti@2.7.0)) @@ -428,6 +431,11 @@ packages: cpu: [x64] os: [win32] + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@radix-ui/number@1.1.2': resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} @@ -2174,6 +2182,11 @@ packages: picomatch: optional: true + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2707,6 +2720,16 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + points-on-curve@0.2.0: resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} @@ -3470,6 +3493,10 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.73.0': optional: true + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@radix-ui/number@1.1.2': {} '@radix-ui/primitive@1.1.4': {} @@ -5200,6 +5227,9 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -6008,6 +6038,14 @@ snapshots: pkce-challenge@5.0.1: {} + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + points-on-curve@0.2.0: {} points-on-path@0.2.1: diff --git a/web/src/components/ai-elements/message-response.tsx b/web/src/components/ai-elements/message-response.tsx index 96ae2e52..aa44a4da 100644 --- a/web/src/components/ai-elements/message-response.tsx +++ b/web/src/components/ai-elements/message-response.tsx @@ -24,10 +24,7 @@ export const MessageResponse = memo( plugins={streamdownPlugins} {...props} /> - ), - (prevProps, nextProps) => - prevProps.children === nextProps.children && - nextProps.isAnimating === prevProps.isAnimating + ) ); MessageResponse.displayName = "MessageResponse"; diff --git a/web/src/features/assistant/components/assistant-answer.tsx b/web/src/features/assistant/components/assistant-answer.tsx new file mode 100644 index 00000000..20446fca --- /dev/null +++ b/web/src/features/assistant/components/assistant-answer.tsx @@ -0,0 +1,131 @@ +import type { ReactNode } from "react" +import { useMemo } from "react" +import { defaultRemarkPlugins } from "streamdown" + +import { MessageResponse } from "@/components/ai-elements/message-response" +import type { AssistantSourceRef } from "@/features/assistant/components/assistant-sources-panel" + +const CITATION_TAG = "orgmemory-citation" +const CITATION_MARKER = /\[(\d{1,3})]/g + +interface MarkdownNode { + type: string + value?: string + children?: MarkdownNode[] +} + +interface CitationElementProps { + children?: ReactNode + "data-number"?: string +} + +export function AssistantAnswer({ + content, + sources, + onOpenSource, +}: { + content: string + sources: AssistantSourceRef[] + onOpenSource: (sourceId: string) => void +}) { + const sourceByNumber = useMemo( + () => new Map(sources.map((source) => [source.citationNumber, source])), + [sources], + ) + const citationContractKey = useMemo( + () => + sources + .map((source) => `${source.citationNumber}:${source.id}:${source.title}`) + .join("|"), + [sources], + ) + const remarkPlugins = useMemo( + () => [ + ...Object.values(defaultRemarkPlugins), + citationRemarkPlugin, + ], + [], + ) + const components = useMemo( + () => ({ + [CITATION_TAG]: ({ children, "data-number": rawNumber }: CitationElementProps) => { + const citationNumber = Number(rawNumber) + const source = sourceByNumber.get(citationNumber) + if (!source) return <>{children} + + return ( + + ) + }, + }), + [onOpenSource, sourceByNumber], + ) + + return ( + + {content} + + ) +} + +function citationRemarkPlugin() { + return (tree: MarkdownNode) => { + replaceCitationMarkers(tree) + } +} + +function replaceCitationMarkers(node: MarkdownNode) { + if (!node.children || node.type === "link" || node.type === "linkReference") { + return + } + + const children: MarkdownNode[] = [] + for (const child of node.children) { + if (child.type !== "text" || child.value === undefined) { + replaceCitationMarkers(child) + children.push(child) + continue + } + + let cursor = 0 + CITATION_MARKER.lastIndex = 0 + for (const match of child.value.matchAll(CITATION_MARKER)) { + const matchIndex = match.index + const citationNumber = Number(match[1]) + if (matchIndex > cursor) { + children.push({ + type: "text", + value: child.value.slice(cursor, matchIndex), + }) + } + children.push({ + type: "html", + value: `<${CITATION_TAG} data-number="${citationNumber}">[${citationNumber}]`, + }) + cursor = matchIndex + match[0].length + } + if (cursor === 0) { + children.push(child) + } else if (cursor < child.value.length) { + children.push({ + type: "text", + value: child.value.slice(cursor), + }) + } + } + node.children = children +} diff --git a/web/src/features/assistant/components/assistant-page.tsx b/web/src/features/assistant/components/assistant-page.tsx index fb99ead7..8425dad5 100644 --- a/web/src/features/assistant/components/assistant-page.tsx +++ b/web/src/features/assistant/components/assistant-page.tsx @@ -1,11 +1,7 @@ import { useChat } from "@ai-sdk/react" -import { - type SourceDocumentUIPart, - type SourceUrlUIPart, - type UIMessage, -} from "ai" +import { type SourceUrlUIPart, type UIMessage } from "ai" import { Copy, LoaderCircle, RotateCcw, ShieldCheck } from "lucide-react" -import { useMemo, useRef, useState } from "react" +import { useCallback, useMemo, useRef, useState } from "react" import { toast } from "sonner" import { @@ -19,7 +15,6 @@ import { MessageActions, MessageContent, } from "@/components/ai-elements/message" -import { MessageResponse } from "@/components/ai-elements/message-response" import { PromptInput, PromptInputBody, @@ -33,6 +28,7 @@ import { Source, Sources, SourcesContent, SourcesTrigger } from "@/components/ai import { Suggestion, Suggestions } from "@/components/ai-elements/suggestion" import { Button } from "@/components/ui/button" import { createAssistantTransport } from "@/features/assistant/api/chat-transport" +import { AssistantAnswer } from "@/features/assistant/components/assistant-answer" import { type AssistantSourceRef, AssistantSourcesPanel, @@ -44,8 +40,6 @@ const SUGGESTIONS = [ "What is the product release process?", ] -type SourcePart = SourceUrlUIPart | SourceDocumentUIPart - function textFor(message: UIMessage) { return message.parts .filter((part) => part.type === "text") @@ -54,37 +48,62 @@ function textFor(message: UIMessage) { } function sourcesFor(message: UIMessage) { - const sources = message.parts.filter( - (part): part is SourcePart => part.type === "source-url" || part.type === "source-document", - ) - return [...new Map(sources.map((source) => [source.sourceId, source])).values()] + const sources: AssistantSourceRef[] = [] + const seenNumbers = new Set() + const seenIds = new Set() + for (const part of message.parts) { + if (part.type !== "source-url") continue + const citationNumber = citationNumberFor(part) + const url = citationUrl(part.url) + if ( + citationNumber === null || + url === null || + seenNumbers.has(citationNumber) || + seenIds.has(part.sourceId) + ) { + continue + } + seenNumbers.add(citationNumber) + seenIds.add(part.sourceId) + sources.push({ + id: part.sourceId, + citationNumber, + title: part.title ?? "Company knowledge", + url, + }) + } + return sources.sort((left, right) => left.citationNumber - right.citationNumber) } function hasVisibleOutput(message: UIMessage) { return textFor(message).trim().length > 0 || sourcesFor(message).length > 0 } -function sourceHref(source: SourcePart) { - if (source.type === "source-url") return source.url +function citationNumberFor(source: SourceUrlUIPart) { + const metadata = source.providerMetadata?.orgmemory + if (!metadata || Array.isArray(metadata)) return null + const number = metadata.citationNumber + return typeof number === "number" && Number.isSafeInteger(number) && number > 0 + ? number + : null +} + +function citationUrl(rawUrl: string) { try { const baseUrl = new URL("https://orgmemory.invalid") - const sourceUrl = new URL(source.sourceId, baseUrl) - if (sourceUrl.origin === baseUrl.origin && sourceUrl.pathname === "/sources") { - return `${sourceUrl.pathname}${sourceUrl.search}${sourceUrl.hash}` + const sourceUrl = new URL(rawUrl, baseUrl) + if ( + sourceUrl.origin === baseUrl.origin && + /^\/api\/citations\/[0-9a-f-]{36}\/content$/i.test(sourceUrl.pathname) && + sourceUrl.search === "" && + sourceUrl.hash === "" + ) { + return sourceUrl.pathname } } catch { - // Fall through to the validated Documents search route. + return null } - const query = source.title?.trim() - return query ? `/sources?q=${encodeURIComponent(query)}` : "/sources" -} - -function sourceRefs(sources: SourcePart[]): AssistantSourceRef[] { - return sources.map((source) => ({ - id: source.sourceId, - title: source.title ?? "Company knowledge", - url: sourceHref(source), - })) + return null } function greeting() { @@ -115,6 +134,13 @@ export function AssistantPage() { (latestMessage === undefined || latestMessage.role === "user" || !hasVisibleOutput(latestMessage)) + const openSources = useCallback((messageId: string, sources: AssistantSourceRef[], sourceId: string) => { + setSourcePanel({ + messageId, + sources, + selectedSourceId: sourceId, + }) + }, []) function send(rawMessage: string) { const message = rawMessage.trim() @@ -201,7 +227,11 @@ export function AssistantPage() { {content.trim() ? ( - {content} + openSources(message.id, sources, sourceId)} + /> ) : null} {sources.length > 0 ? ( @@ -209,32 +239,25 @@ export function AssistantPage() { { - const refs = sourceRefs(sources) - setSourcePanel({ - messageId: message.id, - sources: refs, - selectedSourceId: refs[0].id, - }) + openSources(message.id, sources, sources[0].id) }} /> {sources.map((source) => ( { - const refs = sourceRefs(sources) event.preventDefault() - setSourcePanel({ - messageId: message.id, - sources: refs, - selectedSourceId: source.sourceId, - }) + openSources(message.id, sources, source.id) }} className="inline-flex items-center gap-1.5 rounded-md border border-border-subtle bg-surface-subtle px-2.5 py-1.5 text-supporting text-content-secondary transition-colors hover:bg-action-ghost-hover hover:text-content-primary" - /> + > + {source.citationNumber} + {source.title} + ))} diff --git a/web/src/features/assistant/components/assistant-sources-panel.tsx b/web/src/features/assistant/components/assistant-sources-panel.tsx index 49018f67..1bdc2a2c 100644 --- a/web/src/features/assistant/components/assistant-sources-panel.tsx +++ b/web/src/features/assistant/components/assistant-sources-panel.tsx @@ -1,3 +1,4 @@ +import { useQuery } from "@tanstack/react-query" import { BookOpen, Download, @@ -21,19 +22,16 @@ import { cn } from "@/lib/utils" export interface AssistantSourceRef { id: string + citationNumber: number title: string url: string } -type PreviewState = - | { status: "idle" | "loading" } - | { status: "error" } - | { - status: "ready" - blobUrl: string - mediaType: string - text?: string - } +interface PreviewPayload { + blob: Blob + mediaType: string + text?: string +} interface AssistantSourcesPanelProps { open: boolean @@ -80,49 +78,48 @@ function SourcesPanelContent({ onSelect, }: AssistantSourcesPanelProps) { const selected = sources.find((source) => source.id === selectedSourceId) ?? sources[0] ?? null - const [preview, setPreview] = useState({ status: "idle" }) const canPreview = selected?.url.startsWith("/api/citations/") ?? false + const selectedUrl = canPreview ? selected?.url : undefined + const previewQuery = useQuery({ + queryKey: ["assistant-citation-preview", selectedUrl], + enabled: selectedUrl !== undefined, + queryFn: async (): Promise => { + if (!selectedUrl) throw new Error("Citation URL is unavailable") + const response = await fetch(selectedUrl, { + credentials: "same-origin", + }) + if (!response.ok) throw new Error("Source is unavailable") + const blob = await response.blob() + const mediaType = blob.type || "application/octet-stream" + const text = isTextPreview(mediaType) ? await blob.text() : undefined + return { blob, mediaType, text } + }, + gcTime: 0, + staleTime: 0, + retry: false, + refetchOnMount: "always", + refetchOnWindowFocus: false, + }) + const [blobUrl, setBlobUrl] = useState(null) useEffect(() => { - if (!selected || !canPreview) { - setPreview({ status: "idle" }) + if (!previewQuery.data || previewQuery.isFetching) { + setBlobUrl(null) return } - const controller = new AbortController() - let objectUrl: string | undefined - setPreview({ status: "loading" }) - - void fetch(selected.url, { - credentials: "same-origin", - signal: controller.signal, - }) - .then(async (response) => { - if (!response.ok) throw new Error("Source is unavailable") - const blob = await response.blob() - objectUrl = URL.createObjectURL(blob) - const mediaType = blob.type || "application/octet-stream" - const text = isTextPreview(mediaType) ? await blob.text() : undefined - if (!controller.signal.aborted) { - setPreview({ status: "ready", blobUrl: objectUrl, mediaType, text }) - } - }) - .catch((error: unknown) => { - if ( - !controller.signal.aborted && - !(error instanceof DOMException && error.name === "AbortError") - ) { - setPreview({ status: "error" }) - } - }) + const objectUrl = URL.createObjectURL(previewQuery.data.blob) + setBlobUrl(objectUrl) return () => { - controller.abort() - if (objectUrl) URL.revokeObjectURL(objectUrl) + URL.revokeObjectURL(objectUrl) } - }, [canPreview, selected]) + }, [previewQuery.data, previewQuery.isFetching]) - const ready = preview.status === "ready" ? preview : null + const ready = + previewQuery.data && blobUrl && !previewQuery.isFetching + ? { ...previewQuery.data, blobUrl } + : null return (

@@ -139,7 +136,7 @@ function SourcesPanelContent({
- {sources.map((source, index) => ( + {sources.map((source) => ( @@ -177,13 +174,16 @@ function SourcesPanelContent({
) : null} - {preview.status === "loading" ? ( + {selected && + canPreview && + !previewQuery.isError && + (previewQuery.isPending || previewQuery.isFetching || !blobUrl) ? (
) : null} - {preview.status === "error" ? ( + {selected && canPreview && previewQuery.isError ? ( ) : null} {ready && selected ? ( @@ -209,7 +209,7 @@ function CitationPreviewContent({ preview, title, }: { - preview: Extract + preview: PreviewPayload & { blobUrl: string } title: string }) { if (preview.mediaType === "application/pdf") { diff --git a/web/test/e2e/assistant-pipeline.spec.ts b/web/test/e2e/assistant-pipeline.spec.ts new file mode 100644 index 00000000..4f1f79c9 --- /dev/null +++ b/web/test/e2e/assistant-pipeline.spec.ts @@ -0,0 +1,341 @@ +import { expect, test, type Page, type Route } from "@playwright/test" + +const FIRST_CHUNK_ID = "43000000-0000-0000-0000-000000000003" +const SECOND_CHUNK_ID = "43000000-0000-0000-0000-000000000004" + +interface AssistantHarnessOptions { + chatFrames?: string[] + citationResponses?: Record< + string, + { status: number; contentType?: string; body?: string | Buffer } + > + holdChat?: boolean +} + +async function assistantHarness(page: Page, options: AssistantHarnessOptions = {}) { + const requests: string[] = [] + const unexpectedRequests: string[] = [] + const browserErrors: string[] = [] + let releaseChat: (() => void) | undefined + const chatRelease = new Promise((resolve) => { + releaseChat = resolve + }) + + page.on("pageerror", (error) => browserErrors.push(error.message)) + page.on("console", (message) => { + if (message.type() === "error") browserErrors.push(message.text()) + }) + + await page.route("**/api/**", async (route) => { + const request = route.request() + const url = new URL(request.url()) + if (!url.pathname.startsWith("/api/")) { + await route.continue() + return + } + requests.push(`${request.method()} ${url.pathname}`) + + if (url.pathname === "/api/session") { + await json(route, { + authenticated: true, + name: "Playwright User", + email: "playwright@example.test", + userId: "41000000-0000-0000-0000-000000000001", + organizationId: "41000000-0000-0000-0000-000000000002", + departmentId: "41000000-0000-0000-0000-000000000003", + role: "EMPLOYEE", + }) + return + } + + if (url.pathname === "/api/session/csrf") { + await route.fulfill({ + status: 200, + contentType: "application/json", + headers: { "set-cookie": "XSRF-TOKEN=playwright-token; Path=/" }, + body: JSON.stringify({ + headerName: "X-XSRF-TOKEN", + parameterName: "_csrf", + token: "playwright-token", + }), + }) + return + } + + if (url.pathname === "/api/assistant/chat") { + if (options.holdChat) await chatRelease + await route.fulfill({ + status: 200, + contentType: "text/event-stream", + headers: { "x-vercel-ai-ui-message-stream": "v1" }, + body: sse(options.chatFrames ?? citedAnswerFrames()), + }) + return + } + + const citation = options.citationResponses?.[url.pathname] + if (citation) { + await route.fulfill({ + status: citation.status, + contentType: citation.contentType, + body: citation.body, + }) + return + } + + unexpectedRequests.push(`${request.method()} ${url.pathname}`) + await json(route, { message: "Unexpected E2E request" }, 500) + }) + + return { + requests, + unexpectedRequests, + browserErrors, + releaseChat: () => releaseChat?.(), + } +} + +test("anchors only server-declared citations and opens the matching source", async ({ page }) => { + const secondPath = `/api/citations/${SECOND_CHUNK_ID}/content` + const harness = await assistantHarness(page, { + citationResponses: { + [secondPath]: { + status: 200, + contentType: "text/plain", + body: "Expense claims require the original receipt.", + }, + }, + }) + await page.goto("/") + await submit(page, "How do I submit an expense claim?") + + await expect(page.getByText("Use the approved form")).toBeVisible() + await expect(page.getByRole("button", { name: "Open source 1: Employee Handbook" })).toHaveCount(1) + await expect(page.getByRole("button", { name: "Open source 2: Expense Policy" })).toHaveCount(2) + await expect(page.getByRole("button", { name: /Open source 9/ })).toHaveCount(0) + await expect(page.getByText(/verify exceptions \[9]\./)).toBeVisible() + + await page.getByRole("button", { name: "Open source 2: Expense Policy" }).first().click() + await expect(page.getByRole("complementary", { name: "Answer sources" })).toBeVisible() + await expect(page.getByText("Expense claims require the original receipt.")).toBeVisible() + expect(harness.requests.filter((request) => request === `GET ${secondPath}`)).toHaveLength(1) + expect(harness.unexpectedRequests).toEqual([]) + expect(harness.browserErrors).toEqual([]) +}) + +test("shows an opaque citation error after access is revoked", async ({ page }) => { + const firstPath = `/api/citations/${FIRST_CHUNK_ID}/content` + const harness = await assistantHarness(page, { + chatFrames: singleSourceFrames(), + citationResponses: { + [firstPath]: { + status: 404, + contentType: "application/problem+json", + body: JSON.stringify({ title: "Not Found", detail: "opaque" }), + }, + }, + }) + await page.goto("/") + await submit(page, "What is the probation policy?") + + await page.getByRole("button", { name: "Open source 1: Employee Handbook" }).click() + await expect( + page.getByText("The source changed or you no longer have access."), + ).toBeVisible() + await expect(page.getByText("opaque")).toHaveCount(0) + expect(harness.unexpectedRequests).toEqual([]) + expect(unexpectedBrowserErrors(harness.browserErrors, [404])).toEqual([]) +}) + +test("previews an authorized PDF through the protected citation endpoint", async ({ page }) => { + const firstPath = `/api/citations/${FIRST_CHUNK_ID}/content` + const harness = await assistantHarness(page, { + chatFrames: singleSourceFrames(), + citationResponses: { + [firstPath]: { + status: 200, + contentType: "application/pdf", + body: minimalPdf(), + }, + }, + }) + await page.goto("/") + await submit(page, "What is the probation policy?") + + await page.getByRole("button", { name: "Open source 1: Employee Handbook" }).click() + await expect(page.locator('iframe[title="Employee Handbook"]')).toBeVisible() + expect(harness.requests.filter((request) => request === `GET ${firstPath}`)).toHaveLength(1) + expect(harness.unexpectedRequests).toEqual([]) + expect(harness.browserErrors).toEqual([]) +}) + +test("renders a safe answer without source UI when no evidence is available", async ({ page }) => { + const harness = await assistantHarness(page, { + chatFrames: textOnlyFrames( + "I could not find enough accessible company knowledge to answer that question.", + ), + }) + await page.goto("/") + await submit(page, "Show me the financial forecast") + + await expect( + page.getByText("I could not find enough accessible company knowledge to answer that question."), + ).toBeVisible() + await expect(page.getByText(/Used \d+ sources/)).toHaveCount(0) + await expect(page.getByRole("button", { name: /Open source/ })).toHaveCount(0) + expect(harness.unexpectedRequests).toEqual([]) + expect(harness.browserErrors).toEqual([]) +}) + +test("reports provider failure and retries with exactly one new request", async ({ page }) => { + const harness = await assistantHarness(page, { + chatFrames: [ + frame({ type: "start", messageId: "assistant-error" }), + frame({ type: "error", errorText: "The assistant stream failed." }), + "data: [DONE]", + ], + }) + await page.goto("/") + await submit(page, "What is the probation policy?") + + await expect(page.getByRole("alert")).toContainText("OrgMemory could not complete this turn.") + expect(harness.requests.filter((request) => request === "POST /api/assistant/chat")).toHaveLength(1) + await page.getByRole("button", { name: "Retry" }).click() + await expect + .poll( + () => harness.requests.filter((request) => request === "POST /api/assistant/chat").length, + ) + .toBe(2) + expect(harness.unexpectedRequests).toEqual([]) + expect(harness.browserErrors).toEqual([]) +}) + +test("stop aborts one in-flight assistant request", async ({ page }) => { + const harness = await assistantHarness(page, { holdChat: true }) + await page.goto("/") + await submit(page, "What is the probation policy?", false) + + await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() + await page.getByRole("button", { name: "Stop" }).click() + await expect(page.getByRole("button", { name: "Submit" })).toBeVisible() + harness.releaseChat() + expect(harness.requests.filter((request) => request === "POST /api/assistant/chat")).toHaveLength(1) + expect(harness.unexpectedRequests).toEqual([]) + expect(harness.browserErrors).toEqual([]) +}) + +async function submit(page: Page, message: string, awaitDispatch = true) { + const composer = page.getByPlaceholder("Ask OrgMemory…") + await composer.waitFor({ state: "visible" }) + await composer.fill(message) + await composer.press("Enter") + if (awaitDispatch) { + await expect(page.getByText(message, { exact: true })).toBeVisible() + } +} + +function citedAnswerFrames() { + return [ + frame({ type: "start", messageId: "assistant-cited" }), + frame({ type: "start-step" }), + sourceFrame(1, FIRST_CHUNK_ID, "Employee Handbook"), + sourceFrame(2, SECOND_CHUNK_ID, "Expense Policy"), + frame({ type: "text-start", id: "answer" }), + frame({ + type: "text-delta", + id: "answer", + delta: "Use the approved form [1], attach receipts [2][2], and verify exceptions [9].", + }), + frame({ type: "text-end", id: "answer" }), + frame({ type: "finish-step" }), + frame({ type: "finish", finishReason: "stop" }), + "data: [DONE]", + ] +} + +function singleSourceFrames() { + return [ + frame({ type: "start", messageId: "assistant-revoked" }), + frame({ type: "start-step" }), + sourceFrame(1, FIRST_CHUNK_ID, "Employee Handbook"), + frame({ type: "text-start", id: "answer" }), + frame({ type: "text-delta", id: "answer", delta: "The probation period is 60 days [1]." }), + frame({ type: "text-end", id: "answer" }), + frame({ type: "finish-step" }), + frame({ type: "finish", finishReason: "stop" }), + "data: [DONE]", + ] +} + +function textOnlyFrames(text: string) { + return [ + frame({ type: "start", messageId: "assistant-empty" }), + frame({ type: "start-step" }), + frame({ type: "text-start", id: "answer" }), + frame({ type: "text-delta", id: "answer", delta: text }), + frame({ type: "text-end", id: "answer" }), + frame({ type: "finish-step" }), + frame({ type: "finish", finishReason: "stop" }), + "data: [DONE]", + ] +} + +function sourceFrame(number: number, chunkId: string, title: string) { + return frame({ + type: "source-url", + sourceId: `urn:orgmemory:citation:${number}:${chunkId}`, + url: `/api/citations/${chunkId}/content`, + title, + providerMetadata: { + orgmemory: { citationNumber: number }, + }, + }) +} + +function frame(value: Record) { + return `data: ${JSON.stringify(value)}` +} + +function sse(frames: string[]) { + return `${frames.join("\n\n")}\n\n` +} + +async function json(route: Route, body: unknown, status = 200) { + await route.fulfill({ + status, + contentType: "application/json", + body: JSON.stringify(body), + }) +} + +function unexpectedBrowserErrors(errors: string[], allowedHttpStatuses: number[] = []) { + return errors.filter( + (error) => + !allowedHttpStatuses.some((status) => + error.includes(`server responded with a status of ${status}`), + ), + ) +} + +function minimalPdf() { + const objects = [ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>", + ] + let pdf = "%PDF-1.4\n" + const offsets = objects.map((object, index) => { + const offset = Buffer.byteLength(pdf) + pdf += `${index + 1} 0 obj\n${object}\nendobj\n` + return offset + }) + const xrefOffset = Buffer.byteLength(pdf) + pdf += `xref\n0 ${objects.length + 1}\n` + pdf += "0000000000 65535 f \n" + for (const offset of offsets) { + pdf += `${offset.toString().padStart(10, "0")} 00000 n \n` + } + pdf += `trailer\n<< /Root 1 0 R /Size ${objects.length + 1} >>\n` + pdf += `startxref\n${xrefOffset}\n%%EOF\n` + return Buffer.from(pdf, "ascii") +} From 3cef1803b489417b51dbdedc2d9c089b21634d29 Mon Sep 17 00:00:00 2001 From: kl3inIT Date: Sat, 25 Jul 2026 05:51:32 +0700 Subject: [PATCH 2/3] Address assistant pipeline review findings --- .../knowledge/CitationContentController.java | 56 ++++-------- .../knowledge/GraphIndexLifecycleService.java | 6 +- .../core/knowledge/KnowledgeContentType.java | 90 +++++++++++++++++++ .../core/knowledge/SourceUploadService.java | 30 ++----- .../knowledge/KnowledgeContentTypeTests.java | 31 +++++++ evaluation/src/orgmemory_eval/models.py | 55 +++++++----- evaluation/src/orgmemory_eval/ragas_runner.py | 4 + evaluation/tests/test_models.py | 38 +++++++- evaluation/tests/test_ragas_runner.py | 30 +++++++ 9 files changed, 257 insertions(+), 83 deletions(-) create mode 100644 core/src/main/java/com/orgmemory/core/knowledge/KnowledgeContentType.java create mode 100644 core/src/test/java/com/orgmemory/core/knowledge/KnowledgeContentTypeTests.java diff --git a/apps/api/src/main/java/com/orgmemory/api/knowledge/CitationContentController.java b/apps/api/src/main/java/com/orgmemory/api/knowledge/CitationContentController.java index af280a20..1f50e7e6 100644 --- a/apps/api/src/main/java/com/orgmemory/api/knowledge/CitationContentController.java +++ b/apps/api/src/main/java/com/orgmemory/api/knowledge/CitationContentController.java @@ -2,9 +2,8 @@ import com.orgmemory.api.security.CurrentActorProvider; import com.orgmemory.core.knowledge.CitationContentService; +import com.orgmemory.core.knowledge.KnowledgeContentType; import io.swagger.v3.oas.annotations.Operation; -import java.nio.file.Path; -import java.util.Locale; import java.util.UUID; import org.springframework.http.CacheControl; import org.springframework.http.ContentDisposition; @@ -49,16 +48,16 @@ ResponseEntity content( citation.stream().transferTo(output); } }; - MediaType responseMediaType = safeMediaType( - citation.fileName()); + SafeRepresentation representation = + safeRepresentation(citation.fileName()); return ResponseEntity.ok() - .contentType(responseMediaType) + .contentType(representation.mediaType()) .contentLength(citation.contentLength()) .cacheControl(CacheControl.noStore()) .header("X-Request-ID", requestId) .header( HttpHeaders.CONTENT_DISPOSITION, - contentDisposition(responseMediaType) + representation.contentDisposition() .filename( citation.fileName(), java.nio.charset.StandardCharsets.UTF_8) @@ -68,37 +67,20 @@ ResponseEntity content( .body(body); } - private static MediaType safeMediaType(String fileName) { - String normalized = Path.of(fileName) - .getFileName() - .toString(); - int separator = normalized.lastIndexOf('.'); - String extension = separator < 0 - ? "" - : normalized.substring(separator + 1) - .toLowerCase(Locale.ROOT); - return switch (extension) { - case "pdf" -> MediaType.APPLICATION_PDF; - case "txt", "md" -> MediaType.TEXT_PLAIN; - case "png" -> MediaType.IMAGE_PNG; - case "jpg", "jpeg" -> MediaType.IMAGE_JPEG; - case "gif" -> MediaType.IMAGE_GIF; - case "webp" -> MediaType.parseMediaType("image/webp"); - case "docx" -> MediaType.parseMediaType( - "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); - case "pptx" -> MediaType.parseMediaType( - "application/vnd.openxmlformats-officedocument.presentationml.presentation"); - default -> MediaType.APPLICATION_OCTET_STREAM; - }; + private static SafeRepresentation safeRepresentation(String fileName) { + return KnowledgeContentType.fromFileName(fileName) + .map(contentType -> new SafeRepresentation( + MediaType.parseMediaType( + contentType.browserSafeMediaType()), + contentType.inlinePreviewAllowed() + ? ContentDisposition.inline() + : ContentDisposition.attachment())) + .orElseGet(() -> new SafeRepresentation( + MediaType.APPLICATION_OCTET_STREAM, + ContentDisposition.attachment())); } - private static ContentDisposition.Builder contentDisposition( - MediaType mediaType) { - if (MediaType.APPLICATION_PDF.equals(mediaType) - || MediaType.TEXT_PLAIN.equals(mediaType) - || "image".equals(mediaType.getType())) { - return ContentDisposition.inline(); - } - return ContentDisposition.attachment(); - } + private record SafeRepresentation( + MediaType mediaType, + ContentDisposition.Builder contentDisposition) {} } diff --git a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexLifecycleService.java b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexLifecycleService.java index a84e5770..1fb4b9c0 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexLifecycleService.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/GraphIndexLifecycleService.java @@ -84,7 +84,8 @@ public GraphIndexJobView ensureCurrentProfile( KnowledgeAsset asset = assets .findByIdAndOrganizationId(assetId, actor.organizationId()) .filter(candidate -> candidate.getArchivedAt() == null) - .orElseThrow(); + .orElseThrow(() -> new IllegalStateException( + "Graph indexing requires an active, non-archived Knowledge Asset")); KnowledgeAssetVersion version = versions .findByIdAndOrganizationId( Objects.requireNonNull( @@ -92,7 +93,8 @@ public GraphIndexJobView ensureCurrentProfile( actor.organizationId()) .filter(candidate -> candidate.getStatus() == KnowledgeAssetVersionStatus.ACTIVE) - .orElseThrow(); + .orElseThrow(() -> new IllegalStateException( + "Graph indexing requires an active Knowledge Asset version")); UUID jobId = queue.enqueue( actor.organizationId(), Objects.requireNonNull( diff --git a/core/src/main/java/com/orgmemory/core/knowledge/KnowledgeContentType.java b/core/src/main/java/com/orgmemory/core/knowledge/KnowledgeContentType.java new file mode 100644 index 00000000..908507c2 --- /dev/null +++ b/core/src/main/java/com/orgmemory/core/knowledge/KnowledgeContentType.java @@ -0,0 +1,90 @@ +package com.orgmemory.core.knowledge; + +import java.util.Locale; +import java.util.Optional; +import java.util.Set; + +/** + * Closed content-type policy shared by evidence ingestion and browser delivery. + * + *

The canonical media type describes stored evidence. Browser delivery has + * a separate safe media type and disposition so source metadata cannot turn + * active or unsupported content into same-origin executable content. + */ +public enum KnowledgeContentType { + PDF(Set.of("pdf"), "application/pdf", "application/pdf", true, true), + WORD( + Set.of("docx"), + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + true, + false), + POWERPOINT( + Set.of("pptx"), + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + true, + false), + MARKDOWN(Set.of("md"), "text/markdown", "text/plain", true, true), + TEXT(Set.of("txt"), "text/plain", "text/plain", true, true), + PNG(Set.of("png"), "image/png", "image/png", false, true), + JPEG(Set.of("jpg", "jpeg"), "image/jpeg", "image/jpeg", false, true), + GIF(Set.of("gif"), "image/gif", "image/gif", false, true), + WEBP(Set.of("webp"), "image/webp", "image/webp", false, true); + + private final Set extensions; + private final String canonicalMediaType; + private final String browserSafeMediaType; + private final boolean uploadAllowed; + private final boolean inlinePreviewAllowed; + + KnowledgeContentType( + Set extensions, + String canonicalMediaType, + String browserSafeMediaType, + boolean uploadAllowed, + boolean inlinePreviewAllowed) { + this.extensions = Set.copyOf(extensions); + this.canonicalMediaType = canonicalMediaType; + this.browserSafeMediaType = browserSafeMediaType; + this.uploadAllowed = uploadAllowed; + this.inlinePreviewAllowed = inlinePreviewAllowed; + } + + public static Optional fromFileName(String fileName) { + if (fileName == null) { + return Optional.empty(); + } + String normalized = fileName.replace('\\', '/'); + int slash = normalized.lastIndexOf('/'); + String baseName = slash < 0 ? normalized : normalized.substring(slash + 1); + int dot = baseName.lastIndexOf('.'); + if (dot < 0 || dot == baseName.length() - 1) { + return Optional.empty(); + } + String extension = + baseName.substring(dot + 1).toLowerCase(Locale.ROOT); + for (KnowledgeContentType candidate : values()) { + if (candidate.extensions.contains(extension)) { + return Optional.of(candidate); + } + } + return Optional.empty(); + } + + public String canonicalMediaType() { + return canonicalMediaType; + } + + public String browserSafeMediaType() { + return browserSafeMediaType; + } + + public boolean uploadAllowed() { + return uploadAllowed; + } + + public boolean inlinePreviewAllowed() { + return inlinePreviewAllowed; + } +} diff --git a/core/src/main/java/com/orgmemory/core/knowledge/SourceUploadService.java b/core/src/main/java/com/orgmemory/core/knowledge/SourceUploadService.java index e661bceb..d2a155ee 100644 --- a/core/src/main/java/com/orgmemory/core/knowledge/SourceUploadService.java +++ b/core/src/main/java/com/orgmemory/core/knowledge/SourceUploadService.java @@ -11,17 +11,13 @@ import com.orgmemory.core.permission.KnowledgePermissionPolicy; import java.io.InputStream; import java.nio.file.Path; -import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.UUID; import org.springframework.stereotype.Service; @Service public class SourceUploadService { - private static final Set ALLOWED_EXTENSIONS = Set.of("pdf", "docx", "pptx", "txt", "md"); - private final ObjectStoragePort objects; private final SourceUploadRegistrationService registrations; private final KnowledgePermissionPolicy permissionPolicy; @@ -47,7 +43,7 @@ public SourceSummary upload(CreateUploadSourceCommand command, InputStream conte KnowledgeSpaceTarget targetSpace = knowledgeSpaces.requireUploadTarget( actor, command.knowledgeSpaceId()); String fileName = safeFileName(command.fileName()); - String mediaType = canonicalMediaType(fileName); + String mediaType = requiredUploadType(fileName).canonicalMediaType(); KnowledgeClassification classification = command.classification() == null ? KnowledgeClassification.CONFIDENTIAL : command.classification(); @@ -104,10 +100,7 @@ private void validate(CreateUploadSourceCommand command, InputStream content) { throw new IllegalArgumentException("file size must be within the configured upload limit"); } String fileName = safeFileName(command.fileName()); - String extension = extension(fileName); - if (!ALLOWED_EXTENSIONS.contains(extension)) { - throw new IllegalArgumentException("file type is not supported"); - } + requiredUploadType(fileName); KnowledgeClassification classification = command.classification() == null ? KnowledgeClassification.CONFIDENTIAL : command.classification(); @@ -127,19 +120,10 @@ private static String safeFileName(String value) { return fileName.replaceAll("\\p{Cntrl}", "_"); } - private static String extension(String fileName) { - int dot = fileName.lastIndexOf('.'); - return dot < 0 ? "" : fileName.substring(dot + 1).toLowerCase(Locale.ROOT); - } - - private static String canonicalMediaType(String fileName) { - return switch (extension(fileName)) { - case "pdf" -> "application/pdf"; - case "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - case "pptx" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation"; - case "md" -> "text/markdown"; - case "txt" -> "text/plain"; - default -> "application/octet-stream"; - }; + private static KnowledgeContentType requiredUploadType(String fileName) { + return KnowledgeContentType.fromFileName(fileName) + .filter(KnowledgeContentType::uploadAllowed) + .orElseThrow(() -> + new IllegalArgumentException("file type is not supported")); } } diff --git a/core/src/test/java/com/orgmemory/core/knowledge/KnowledgeContentTypeTests.java b/core/src/test/java/com/orgmemory/core/knowledge/KnowledgeContentTypeTests.java new file mode 100644 index 00000000..1e71455d --- /dev/null +++ b/core/src/test/java/com/orgmemory/core/knowledge/KnowledgeContentTypeTests.java @@ -0,0 +1,31 @@ +package com.orgmemory.core.knowledge; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class KnowledgeContentTypeTests { + + @Test + void keepsCanonicalAndBrowserSafeMarkdownTypesExplicit() { + KnowledgeContentType markdown = + KnowledgeContentType.fromFileName("policy.MD").orElseThrow(); + + assertEquals("text/markdown", markdown.canonicalMediaType()); + assertEquals("text/plain", markdown.browserSafeMediaType()); + assertTrue(markdown.uploadAllowed()); + assertTrue(markdown.inlinePreviewAllowed()); + } + + @Test + void recognizesPreviewOnlyImagesWithoutAllowingImageUploads() { + KnowledgeContentType image = + KnowledgeContentType.fromFileName("evidence.jpeg").orElseThrow(); + + assertEquals("image/jpeg", image.browserSafeMediaType()); + assertFalse(image.uploadAllowed()); + assertTrue(image.inlinePreviewAllowed()); + } +} diff --git a/evaluation/src/orgmemory_eval/models.py b/evaluation/src/orgmemory_eval/models.py index 02d18744..787f629b 100644 --- a/evaluation/src/orgmemory_eval/models.py +++ b/evaluation/src/orgmemory_eval/models.py @@ -1,32 +1,47 @@ from __future__ import annotations -from pydantic import BaseModel, ConfigDict, Field, model_validator +from typing import Annotated + +from pydantic import BaseModel, ConfigDict, Field, StringConstraints, model_validator + +Identifier = Annotated[ + str, + StringConstraints(strip_whitespace=True, min_length=1, max_length=128), +] +NonBlankText = Annotated[ + str, + StringConstraints(strip_whitespace=True, min_length=1), +] +SchemaVersion = Annotated[ + str, + StringConstraints( + strip_whitespace=True, + pattern=r"^orgmemory\.rag-evaluation\.v1$", + ), +] class EvaluationCase(BaseModel): - model_config = ConfigDict(extra="forbid") - - case_id: str = Field(min_length=1, max_length=128) - question: str = Field(min_length=1) - reference_answer: str = Field(min_length=1) - answer: str = Field(min_length=1) - contexts: list[str] = Field(min_length=1) - citation_ids: list[str] = Field(default_factory=list) + model_config = ConfigDict(extra="forbid", allow_inf_nan=False) + + case_id: Identifier + question: NonBlankText + reference_answer: NonBlankText + answer: NonBlankText + contexts: list[NonBlankText] = Field(min_length=1) + citation_ids: list[Identifier] = Field(default_factory=list) latency_ms: float = Field(ge=0) - @model_validator(mode="after") - def reject_blank_contexts(self) -> EvaluationCase: - if any(not context.strip() for context in self.contexts): - raise ValueError("contexts must not contain blank values") - return self - class EvaluationDataset(BaseModel): - model_config = ConfigDict(extra="forbid") - - schema_version: str = Field(pattern=r"^orgmemory\.rag-evaluation\.v1$") - dataset_id: str = Field(min_length=1, max_length=128) - system_under_test: str = Field(min_length=1, max_length=256) + model_config = ConfigDict(extra="forbid", allow_inf_nan=False) + + schema_version: SchemaVersion + dataset_id: Identifier + system_under_test: Annotated[ + str, + StringConstraints(strip_whitespace=True, min_length=1, max_length=256), + ] cases: list[EvaluationCase] = Field(min_length=1) @model_validator(mode="after") diff --git a/evaluation/src/orgmemory_eval/ragas_runner.py b/evaluation/src/orgmemory_eval/ragas_runner.py index b5a14b0d..1162197e 100644 --- a/evaluation/src/orgmemory_eval/ragas_runner.py +++ b/evaluation/src/orgmemory_eval/ragas_runner.py @@ -66,6 +66,10 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser.error("--trials must be at least 2 so judge variance is measurable") if args.max_workers < 1: parser.error("--max-workers must be positive") + if args.timeout_seconds < 1: + parser.error("--timeout-seconds must be positive") + if args.input.resolve() == args.output.resolve(): + parser.error("--output must not overwrite --input") return args diff --git a/evaluation/tests/test_models.py b/evaluation/tests/test_models.py index b3169088..741f407a 100644 --- a/evaluation/tests/test_models.py +++ b/evaluation/tests/test_models.py @@ -1,3 +1,5 @@ +import math + import pytest from pydantic import ValidationError @@ -41,5 +43,39 @@ def test_rejects_blank_contexts() -> None: payload = dataset() payload["cases"][0]["contexts"] = [" "] - with pytest.raises(ValidationError, match="contexts must not contain blank"): + with pytest.raises(ValidationError, match="string_too_short"): + EvaluationDataset.model_validate(payload) + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("case_id", " "), + ("question", "\t"), + ("reference_answer", "\n"), + ("answer", " "), + ], +) +def test_rejects_blank_semantic_case_fields(field: str, value: str) -> None: + payload = dataset() + payload["cases"][0][field] = value + + with pytest.raises(ValidationError): + EvaluationDataset.model_validate(payload) + + +def test_rejects_blank_citation_ids() -> None: + payload = dataset() + payload["cases"][0]["citation_ids"] = [" "] + + with pytest.raises(ValidationError): + EvaluationDataset.model_validate(payload) + + +@pytest.mark.parametrize("latency", [math.nan, math.inf, -math.inf]) +def test_rejects_non_finite_latency(latency: float) -> None: + payload = dataset() + payload["cases"][0]["latency_ms"] = latency + + with pytest.raises(ValidationError): EvaluationDataset.model_validate(payload) diff --git a/evaluation/tests/test_ragas_runner.py b/evaluation/tests/test_ragas_runner.py index 9d7fbf64..bdbee149 100644 --- a/evaluation/tests/test_ragas_runner.py +++ b/evaluation/tests/test_ragas_runner.py @@ -9,6 +9,7 @@ from orgmemory_eval.ragas_runner import ( METRIC_NAMES, metric_suite, + parse_args, run_trial, summarize_trials, validated_score, @@ -134,3 +135,32 @@ def test_summary_contains_scores_but_no_prompt_or_evidence_text() -> None: assert "The standard probation period is 60 days." not in encoded assert "Employee Handbook" not in encoded assert "opaque-citation" not in encoded + + +def test_rejects_output_that_resolves_to_input(tmp_path) -> None: + dataset_path = tmp_path / "dataset.json" + + with pytest.raises(SystemExit): + parse_args( + [ + "--input", + str(dataset_path), + "--output", + str(tmp_path / "." / "dataset.json"), + ] + ) + + +@pytest.mark.parametrize("timeout_seconds", ["0", "-1"]) +def test_rejects_non_positive_timeout(timeout_seconds: str, tmp_path) -> None: + with pytest.raises(SystemExit): + parse_args( + [ + "--input", + str(tmp_path / "input.json"), + "--output", + str(tmp_path / "output.json"), + "--timeout-seconds", + timeout_seconds, + ] + ) From 7e84c5c7529abb258d6f80661a58e6971a66d7d0 Mon Sep 17 00:00:00 2001 From: kl3inIT Date: Sat, 25 Jul 2026 05:52:21 +0700 Subject: [PATCH 3/3] Align review policy with pre-release workflow --- .coderabbit.yaml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 042cdd7e..7dbbbf44 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -57,9 +57,11 @@ reviews: - path: "core/src/main/resources/db/migration/*.sql" instructions: | - Flyway migrations are immutable after release. Check tenant isolation, - foreign keys, uniqueness, indexes, append-only evidence semantics, safe - defaults, and compatibility with PostgreSQL 18 plus pgvector. + The repository is pre-release: V1 is the intentionally resettable clean + baseline and development data carries no migration cost. Once a release + baseline is frozen, later Flyway migrations are immutable. Check tenant + isolation, foreign keys, uniqueness, indexes, append-only evidence + semantics, safe defaults, and PostgreSQL 18 plus pgvector compatibility. - path: "integrations/authorization-openfga/**/*" instructions: | @@ -77,10 +79,10 @@ reviews: - path: ".github/**/*.{yml,yaml}" instructions: | - Require least-privilege permissions, immutable action SHAs, bounded job - timeouts, concurrency cancellation, frozen lockfiles, and no secrets in - pull-request workflows. Keep Dependabot comments beside pinned action - SHAs so action updates remain discoverable. + Require least-privilege permissions, explicit release tags for actions, + bounded job timeouts, concurrency cancellation, frozen lockfiles, and no + secrets in pull-request workflows. GitHub Actions are intentionally not + pinned to commit SHAs; Dependabot owns their scheduled version updates. tools: github-checks: