diff --git a/.env.example b/.env.example index 38b7d9f..f10dd92 100644 --- a/.env.example +++ b/.env.example @@ -13,10 +13,10 @@ OLLAMA_BASE_URL=http://host.docker.internal:11434 # Only required if quality_preset is set to "cloud" in config.yaml. # INSTALL_CLOUD_DEPS=true -# OpenAI (Used for embeddings: text-embedding-3-large) -# OPENAI_API_KEY=your_openai_api_key_here +# OPENAI_API_KEY= +# ANTHROPIC_API_KEY= -# AWS Bedrock (Used for Claude 3.5 Sonnet and Llama 3.1) +# AWS Bedrock (Used for Claude 4.6 Sonnet and Llama 3.1) # AWS_ACCESS_KEY_ID=your_aws_access_key_here # AWS_SECRET_ACCESS_KEY=your_aws_secret_key_here # AWS_DEFAULT_REGION=us-east-1 @@ -28,8 +28,3 @@ OLLAMA_BASE_URL=http://host.docker.internal:11434 # Custom MongoDB Connection (e.g., external Atlas cluster or local host) # MONGO_URI=mongodb://your-custom-db-host:27017/?directConnection=true - -# Custom Embedding Settings (Must match your config.yaml quality preset) -# EMBED_PROVIDER=ollama -# EMBED_MODEL=mxbai-embed-large -# EMBED_DIMS=1024 \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 00b2c25..505c7bd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,21 +12,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Python 3.13 uses: actions/setup-python@v6 with: python-version: "3.13" - cache: "pip" + + - name: Install uv + uses: astral-sh/setup-uv@v8 - name: Install dependencies - run: | - python -m pip install --upgrade pip - # Install the main requirements - pip install -r requirements-test.txt + run: uv sync --dev - name: Run Pytest - run: | - # Runs all tests in the tests/ directory - pytest tests/ -v + run: uv run pytest tests/ -v diff --git a/.gitignore b/.gitignore index 6c5f57f..b16b67e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ simulation_snapshot.json results/ leaderboard.csv leaderboard.json +leaderboard/ +eval_results.json # ── Environments & Secrets ── .env diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/CHANGELOG.md b/CHANGELOG.md index b84fe70..d8b2010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,56 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [v2.0.0] — 2026-06-22 + +### Added + +- **Multi-provider LLM support via CrewAI**: The project now uses CrewAI's `LLM` class with auto-detection for provider from model string. Supports OpenAI, Anthropic, AWS Bedrock, Google Gemini, Groq, and Azure out of the box. New presets added: `openai`, `anthropic`, `gemini`. +- **Alias vocabulary extraction**: All Confluence page generation now emits domain-level alias terms alongside Markdown content, indexed via Atlas Search for BM25 retrieval. +- **Stage-based incident advancement with realistic stall probabilities**: Incidents now advance through stages (detected → investigating → fix_in_progress → review_pending → resolved) with configurable per-stage probabilities influenced by knowledge gaps, recurrence, and system health. Incidents can stall for days, creating realistic resolution timelines. +- **Domain ownership claiming**: Engineers who repeatedly work in orphaned knowledge domains (via PR authorship, Confluence writing, or incident resolution) are automatically promoted to `primary_owner` in the domain registry, driving a knowledge recovery arc. +- **Post-simulation evaluation questions**: The HF dataset export now includes `questions/eval_questions.jsonl` for benchmark evaluation. +- **Department-level average stress tracking**: `DepartmentDayPlan` now carries `avg_stress` for visibility into team-wide morale. +- **Deterministic gap detection**: Knowledge gap detection in Confluence writing and PR reviews is now fully deterministic, driven by domain registry lookups and BM25 text search over persona skill records — no LLM involvement. +- **Ad-hoc Confluence page authorship tracking**: Ad-hoc page generation now returns `(conf_id, author, title)` for downstream lifecycle credit. + +### Changed + +- **Migration from `requirements.txt` to `uv`/`pyproject.toml`**: The project now uses `uv` for dependency management with a `pyproject.toml` declaring all dependencies. Removed `requirements.txt`, `requirements-cloud.txt`, and `requirements-test.txt`. +- **Embedding pipeline replaced with structured text retrieval**: Vector embeddings removed entirely. Memory now uses Atlas Search with alias-weighted BM25 for artifact retrieval and MongoDB `$text` search for event retrieval. Removed `embed_worker.py`, all embedder classes (Ollama, OpenAI, Bedrock, Infinity), and fallback hash embeddings. +- **Dockerfile overhaul**: Now uses multi-stage `uv` copy, `pyproject.toml`-based install, and `uv run` execution. Removed separate `requirements-cloud.txt` install step. +- **Recurrence detection simplified to text-only**: Recurrence detector now uses MongoDB `$text` index exclusively (text-only), removing the vector/hybrid fusion pipeline, RRF scoring, and all vector-specific thresholds. +- **Confluence pages moved to dedicated `confluence_pages` collection**: No longer stored in the generic `artifacts` collection. Zoom transcripts similarly moved to `zoom_transcripts`. `embed_artifact()` is now a routing function dispatching to type-specific stores. +- **Ticket assignment uses BM25 term overlap instead of cosine similarity**: `TicketAssigner` now computes skill scores via normalized term overlap between ticket title tokens and engineer domain-registry expertise tokens. +- **Config overhaul**: Expanded provider presets, added `base_url` for non-standard OpenAI endpoints, removed `embed_provider`/`embed_model`/`embed_dims` fields from presets. Simulation defaults updated (60-day runs, higher incident probability, adjusted morale decay). +- **Persona config updates**: New departed employee "Sharon" added to knowledge gaps. Morgan's departure moved to day 34, Reese's hire to day 26. Mobile engineer stress increased to 72. +- **LLM task outputs now structured JSON**: All Confluence writing tasks now require JSON responses with `markdown_doc` and `aliases` keys, replacing freeform Markdown output. +- **Knowledge gap detection moved from LLM self-audit to engine computation**: The LLM emits raw observations (`topics_in_doc`, `topics_outside_my_expertise`, `claims_i_approximated`, `sections_i_left_thin`) and the engine classifies gaps deterministically using domain registry coverage thresholds. +- **`context_for_prompt()` replaced with tiered context builder**: Three tiers — pinned tech stack + active gaps, alias-weighted BM25 search, and open knowledge gap questions. Noise-stripping applied before BM25 search. +- **PR review knowledge audit removed**: Reviewer audit metadata (author_domain_fit, gap_classification) removed from PR review task. Gap detection now runs via `scan_for_knowledge_gaps` on the PR text. +- **Dataset export rebranded**: HuggingFace dataset card now uses "OrgForge EpistemicBench" naming, includes questions config, and updated citation format. +- **`dotenv` auto-loaded**: `.env` is now loaded automatically in `config_loader.py` on import. +- **Postmortem writer selection**: Now uses incident actors rather than a static `postmortem_writer` role. + +### Removed + +- **All vector/embedding infrastructure**: `embed_worker.py` (242 lines), all embedder classes (`OllamaEmbedder`, `OpenAIEmbedder`, `BedrockEmbedder`, `InfinityEmbedder`), `build_embedder()` factory, `_embed()` method, `_init_vector_indexes()`, and all vector search aggregation pipelines. +- **HyDE query rewriting**: `_rewrite_query()` removed. Noise-stripping in `_strip_query_noise()` replaces the HyDE pattern. +- **LLM-authored knowledge gap self-assessments**: The `author_domain_fit`, `gap_classification`, `topics_beyond_author_expertise`, `hedged_claims`, and `deferred_or_incomplete` fields removed from PR review and design doc JSON output schemas. +- **`_SKIP_EMBED_TYPES`**: Removed the event type embedding skip list since no events are embedded. +- **`find_confluence_experts()`**: Replaced by `search_persona_skills_text()` and `domain_context_for_topic()`. +- **`recall()` and `recall_events()` vector search methods**: Replaced by `search_artifacts_text()` and structured MongoDB queries. +- **Postmortem causal chain log line**: Removed the verbose causal chain append log message. + +### Fixed + +- **Postmortem artifact timestamp**: `_write_postmortem` now correctly uses incident actors to determine the writer, fixing timestamp attribution. +- **Ad-hoc Confluence return type**: `write_adhoc_page()` now returns `(conf_id, author, title)` tuple instead of `None`, enabling callers in `NormalDayHandler` to credit domain knowledge. +- **Test suite updated**: Removed embedding-dependent tests, patched mongomock for Atlas Search index methods, and fixed incident advancement tests to account for stochastic stage progression. +- **`docker-compose.yaml` version field**: Removed deprecated `version: "3.9"` top-level key. + +--- + ## [v1.3.3] — 2026-04-05 ### Added diff --git a/Dockerfile b/Dockerfile index 98c4ab8..b19222d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,50 +1,19 @@ -# ── OrgForge — App Container ───────────────────────────────── -# Runs flow.py (simulation) and email_gen.py (emails). -# -# Build args: -# INSTALL_CLOUD_DEPS=true installs boto3, langchain-aws, openai -# required for quality_preset: "cloud" -# default: false (keeps image lean for local presets) -# -# Examples: -# Local preset: docker build -t orgforge . -# Cloud preset: docker build --build-arg INSTALL_CLOUD_DEPS=true -t orgforge . -# ───────────────────────────────────────────────────────────── - FROM python:3.11-slim ARG INSTALL_CLOUD_DEPS=false -# ── System deps ─────────────────────────────────────────────── -# curl: Ollama connection probing -# gcc + libffi-dev: required by crewai/pydantic native extensions -RUN apt-get update && apt-get install -y --no-install-recommends \ - curl \ - gcc \ - libffi-dev \ - && rm -rf /var/lib/apt/lists/* +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv WORKDIR /app -COPY requirements.txt . -COPY requirements-cloud.txt . - -RUN pip install --no-cache-dir -r requirements.txt +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-install-project RUN if [ "$INSTALL_CLOUD_DEPS" = "true" ]; then \ - pip install --no-cache-dir -r requirements-cloud.txt; \ + uv add boto3; \ fi -# ── Application code ────────────────────────────────────────── -# Copy only source files — not export, .env, or local config overrides. -# config.yaml is bind-mounted at runtime (see docker-compose.yaml) so edits -# on the host are picked up without rebuilding the image. COPY src/ /app/src/ - -# Output directory — created here so the bind mount has a target -# even if ./export doesn't exist on the host yet. RUN mkdir -p /app/export -# ── Default command ─────────────────────────────────────────── -# Run flow.py from its new home in the src directory -CMD ["python", "src/flow.py"] +CMD ["uv", "run", "python", "src/flow.py"] \ No newline at end of file diff --git a/README.md b/README.md index 0afbf99..dd594cf 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,10 @@ ![OrgForge corpus overview](orgforge_hero.png) -OrgForge simulates weeks of realistic enterprise activity — Confluence pages, JIRA tickets, Slack threads, Git PRs, Zoom transcripts, Zendesk tickets, Salesforce records, emails, and server telemetry — grounded in an event-driven state machine so LLMs can't hallucinate facts out of sequence. +OrgForge simulates weeks of realistic enterprise activity: Confluence pages, JIRA tickets, Slack threads, Git PRs, Zoom transcripts, Zendesk tickets, Salesforce records, emails, and server telemetry. It is grounded in an event-driven state machine so LLMs cannot hallucinate facts out of sequence. The dataset is the exhaust of a living simulation. Engineers leave mid-sprint, forcing deterministic incident handoffs, ticket reassignments, and CRM ownership lapses. Knowledge gaps surface when under-documented systems break. New hires build their internal network through simulated collaboration. Stress propagates through a live, weighted social graph. Every artifact reflects the exact state of the org at the moment it was written. ---- ## Table of Contents @@ -31,7 +30,7 @@ The dataset is the exhaust of a living simulation. Engineers leave mid-sprint, f - [Option 3 — Cloud Preset](#option-3--cloud-preset-aws-bedrock--openai) - [Running on AWS EC2](#running-on-aws-ec2) - [Configuration](#configuration) - - [Quality Presets](#quality-presets) + - [Quality Presets & Multi-Provider Support](#quality-presets--multi-provider-support) - [Key Config Fields](#key-config-fields) - [Dynamic Org Lifecycle](#dynamic-org-lifecycle) - [How the Event Bus Works](#how-the-event-bus-works) @@ -43,17 +42,15 @@ The dataset is the exhaust of a living simulation. Engineers leave mid-sprint, f - [Citation](#citation) - [License](#license) ---- ## Why Does This Exist? -When building AI agents that reason over institutional knowledge, you need a realistic corpus to test against. The only widely-used corporate dataset is the Enron email corpus — 25 years old, legally sensitive, and covering one company in crisis. +When building AI agents that reason over institutional knowledge, you need a realistic corpus to test against. The only widely-used corporate dataset is the Enron email corpus: 25 years old, legally sensitive, and covering one company in crisis. -OrgForge generates that corpus from scratch, parameterized to any company, industry, or org structure. LLMs write the prose, but the facts — who was on-call, which ticket was open, when the incident resolved, who just left the team, and which customer SLA was breached — are strictly controlled by the state machine. +OrgForge generates that corpus from scratch, parameterized to any company, industry, or org structure. LLMs write the prose, but the facts (who was on-call, which ticket was open, when the incident resolved, who just left the team, and which customer SLA was breached) are strictly controlled by the state machine. -**The central design bet:** grounding LLM output in a deterministic event log makes the dataset actually useful for evaluating retrieval systems. You have ground truth about what happened, when, who was involved, and what the org's state was — so you can measure whether an agent surfaces the right context, not just plausible-sounding context. +**The central design bet:** grounding LLM output in a deterministic event log makes the dataset actually useful for evaluating retrieval systems. You have ground truth about what happened, when, who was involved, and what the org's state was, so you can measure whether an agent surfaces the right context, not just plausible-sounding context. ---- ## What the Output Looks Like @@ -86,14 +83,10 @@ Here's what a slice of a real simulation produces. An incident fires on Day 8: **`confluence/postmortems/IT-108.md`** — written the next day, linking the same root cause and PR: -> _This incident was triggered by connection pool exhaustion under sustained load, first surfaced in IT-108. The fix landed in PR \#47 (merged by Sarah). A prior knowledge gap in TitanDB connection management — stemming from Jordan's departure on Day 12 — contributed to the delayed diagnosis._ +> *This incident was triggered by connection pool exhaustion under sustained load, first surfaced in IT-108. The fix landed in PR #47 (merged by Sarah). A prior knowledge gap in TitanDB connection management — stemming from Jordan's departure on Day 12 — contributed to the delayed diagnosis.* Meanwhile, the `datadog/metrics.jsonl` time-series data reflects the exact latency spike, Zendesk support tickets from affected customers are automatically escalated to 'Urgent', Salesforce opportunities are flagged as 'at-risk', and end-of-month customer invoices (`invoices/`) automatically apply SLA credits based on the incident's duration. -None of this is coincidence — it all traces back to one SimEvent that every downstream artifact reads from. - ---- - ## What Gets Generated A default 22-day simulation produces: @@ -107,14 +100,13 @@ A default 22-day simulation produces: | `zoom/` | Verbatim meeting transcripts from sync design discussions, capturing undocumented verbal decisions | | `salesforce/` | CRM accounts and active sales opportunities, including risk flags propagated from active incidents | | `zendesk/` | Customer support tickets and comments, automatically escalated during system outages | -| `emails/` | External inbound/outbound emails — customer complaints, vendor messages, HR communications, sales updates | +| `emails/` | External inbound/outbound emails: customer complaints, vendor messages, HR communications, sales updates | | `datadog/` | Time-series system metrics (`metrics.jsonl`) and alert payloads (`alerts.jsonl`) reflecting incident degradation & recovery | | `nps/` | Post-simulation customer satisfaction surveys, scored deterministically based on SLA breaches and support ticket resolution | | `invoices/` | End-of-month customer invoices featuring SLA credit line items calculated directly from incident duration | | `simulation_snapshot.json` | Full state: incidents, morale curve, system health, relationship graph, departed employees, new hires, knowledge gap events | | `simulation.log` | Complete chronological system and debug logs for the entire run | ---- ## Architecture & Mechanics @@ -122,11 +114,10 @@ OrgForge is not an LLM wrapper. Four interlocking systems enforce correctness. 👉 **[Read the full Architecture Deep-Dive here.](ARCHITECTURE.md)** ---- ## The Departure Cascade -The most complex behaviour in the simulation. When an engineer departs mid-sprint, the following fires in order before that day's planning runs: +The most complex behavior in the simulation. When an engineer departs mid-sprint, the following fires in order before that day's planning runs: 1. **Incident handoff** — active incidents assigned to the departing engineer are rerouted via Dijkstra escalation routing (while the node is still in the graph) to the next available person in the chain. 2. **Ticket & CRM reassignment** — orphaned JIRA tickets go to the dept lead. Salesforce accounts and open opportunities owned by the departed employee are flagged for reassignment, maintaining cross-domain ground truth. @@ -134,13 +125,12 @@ The most complex behaviour in the simulation. When an engineer departs mid-sprin 4. **Knowledge gap propagation** — if the departed engineer owned undocumented domains (configured via `documented_pct`), those gaps are registered in the SimEvent log and surface in subsequent incidents as contributing factors. 5. **`employee_departed` SimEvent** — emitted with edge snapshot, centrality at departure, reassigned tickets, and incident handoffs. Full ground truth for retrieval evaluation. -So when Jordan leaves on Day 12, the postmortem on Day 9's incident doesn't mention her. But the postmortem on Day 15 might: _"A prior knowledge gap in auth-service, stemming from a recent departure, contributed to the delayed diagnosis."_ That sentence is grounded in a real SimEvent, not LLM inference. +So when Jordan leaves on Day 12, the postmortem on Day 9's incident doesn't mention her. But the postmortem on Day 15 might: *"A prior knowledge gap in auth-service, stemming from a recent departure, contributed to the delayed diagnosis."* That sentence is grounded in a real SimEvent, not LLM inference. ---- ## Insider Threat Simulation -OrgForge includes an optional insider threat module that layers adversarial behavior on top of the normal simulation — without touching any of the clean simulation paths. When disabled (the default), it is completely inert: no overhead, no additional output, no altered code paths. +OrgForge includes an optional insider threat module that layers adversarial behavior on top of the normal simulation, without touching any of the clean simulation paths. When disabled (the default), it is completely inert: no overhead, no additional output, no altered code paths. When enabled, designated employees exhibit configurable threat behaviors across multiple surfaces: anomalous git activity, off-hours access, sentiment drift in Slack, data staging on their workstation, and IDP authentication anomalies. All threat telemetry is written to a separate `security_telemetry/` directory in industry-standard log formats (JSONL, CEF, ECS, LEEF), keeping it cleanly isolated from the normal simulation output so detection agents must work to find it. @@ -148,7 +138,6 @@ The module is designed for building and evaluating insider threat detection syst 👉 **[Read the full Insider Threat reference here.](INSIDER_THREAT.md)** ---- ## Quickstart @@ -160,7 +149,7 @@ The module is designed for building and evaluating insider threat detection syst | ---------------------------------- | ------------------------------------ | -------------------------------------- | | Everything in Docker | `docker compose up` | Recommended for first run | | Local Ollama + Docker for the rest | `docker compose up mongodb orgforge` | Set `OLLAMA_BASE_URL` in `.env` | -| Cloud preset (AWS Bedrock) | `docker compose up mongodb orgforge` | Set credentials in `.env`, skip Ollama | +| Cloud preset (any provider) | `docker compose up mongodb orgforge` | Set credentials in `.env`, skip Ollama | ### Option 1 — Everything in Docker (Recommended) @@ -170,12 +159,9 @@ cd orgforge docker compose up ``` -First run pulls models automatically (\~5–8 min depending on your connection). Subsequent runs start in seconds — models are cached in a named volume. - When the simulation finishes, run the post-processing artifact generators: ```bash -python email_gen.py python post_sim_artifacts.py ``` @@ -197,35 +183,60 @@ docker compose up mongodb orgforge > **Linux note:** `host.docker.internal` requires Docker Desktop, or the `extra_hosts: host-gateway` entry in `docker-compose.yaml` (already included). -### Option 3 — Cloud Preset (AWS Bedrock + OpenAI) +### Option 3 — Cloud Preset (Any Provider) -Best output quality. Uses Claude Sonnet for document generation, Llama 3.1 8B on Bedrock for high-volume worker calls, and OpenAI `text-embedding-3-large` for embeddings. +Best output quality. Uses CrewAI's multi-provider `LLM` class, which auto-detects the provider from the model string. Supported providers include OpenAI, Anthropic, AWS Bedrock, Google Gemini, Groq, and Azure. Set `quality_preset: "cloud"` in `config.yaml`, then: ```bash -# .env -AWS_ACCESS_KEY_ID=... +# .env — set the env vars for whichever provider(s) you configure +AWS_ACCESS_KEY_ID=... # for Bedrock AWS_SECRET_ACCESS_KEY=... AWS_DEFAULT_REGION=us-east-1 -OPENAI_API_KEY=... +OPENAI_API_KEY=... # for OpenAI +ANTHROPIC_API_KEY=... # for Anthropic ``` ```bash -pip install boto3 langchain-aws openai +pip install crewai boto3 openai docker compose up mongodb orgforge ``` +**Using OpenAI directly** — add an `openai` preset to `config/config.yaml`: + +```yaml +quality_presets: + cloud: + provider: openai + planner: gpt-4o + worker: gpt-4o-mini +``` + +Then set `quality_preset: "openai"` and ensure `OPENAI_API_KEY` is in `.env`. No code changes needed. + +**Using Anthropic directly** — same pattern: + +```yaml +quality_presets: + cloud: + provider: anthropic + planner: claude-sonnet-4-20250514 + worker: claude-haiku-3-5-20241022 +``` + +Set `ANTHROPIC_API_KEY` in `.env` and you're done. + ### Running on AWS EC2 -**Cheap EC2 + Bedrock/OpenAI (no GPU required)** +**Cheap EC2 + Cloud API (no GPU required)** -A `t3.small` works fine — the cloud APIs do all the heavy lifting. +A `t3.small` works fine: the cloud APIs do all the heavy lifting. 1. Launch an EC2 instance (Ubuntu or Amazon Linux) and install Docker 2. `git clone https://github.com/aeriesec/orgforge.git && cd orgforge` 3. `cp .env.example .env` and fill in your credentials -4. Set `quality_preset: "cloud"` in `config/config.yaml` +4. Set `quality_preset: "cloud"` (or `openai`, `anthropic`, `gemini`) in `config/config.yaml` 5. `docker compose up --build -d mongodb orgforge` **GPU Instance + 70B Local Models** @@ -238,16 +249,44 @@ For `Llama 3.3 70B` entirely locally, use a `g5.2xlarge` or `g5.12xlarge` with t `config/config.yaml` is the single source of truth. No Python changes are needed for most customizations. -### Quality Presets +### Quality Presets & Multi-Provider Support + +OrgForge uses CrewAI's `LLM` class, which auto-detects the provider from the model string. This means you can use any supported provider by simply adding a preset to your config and setting the corresponding API key environment variable. + +| Provider | Model Example | Env Var Required | +| ---------- | ----------------------------------- | --------------------- | +| Ollama | `llama3.3:70b-instruct-q4_KM` | `OLLAMA_BASE_URL` | +| AWS Bedrock | `us.anthropic.claude-sonnet-4-20250514-v1:0` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` | +| OpenAI | `gpt-4o` | `OPENAI_API_KEY` | +| Anthropic | `claude-sonnet-4-20250514` | `ANTHROPIC_API_KEY` | +| Gemini | `gemini-2.5-pro` | `GEMINI_API_KEY` | +| Groq | `groq/llama-3.1-70b-versatile` | `GROQ_API_KEY` | +| Azure | `azure/gpt-4o` | Azure credentials | + +**Built-in presets:** ```yaml -quality_preset: "local_gpu" # local_gpu | cloud +quality_preset: "local_gpu" # local_gpu | cloud | openai | anthropic ``` -| Preset | Planner | Worker | Embeddings | Best For | -| ----------- | --------------------------- | --------------------- | ---------------------- | ------------------------ | -| `local_gpu` | llama3.3:70b-instruct-q4_KM | llama3.1:8b-instruct | mxbai-embed-large | High-fidelity local runs | -| `cloud` | Claude Sonnet (Bedrock) | llama3.1:8b (Bedrock) | text-embedding-3-large | Best output quality | +| Preset | Planner | Worker | Best For | +| ----------- | --------------------------- | --------------------- | ------------------------ | +| `local_gpu` | llama3.3:70b-instruct-q4_KM | llama3.1:8b-instruct | High-fidelity local runs | +| `cloud` | Claude Sonnet (Bedrock) | llama3.1:8b (Bedrock) | Best output quality | +| `openai` | gpt-4o | gpt-4o-mini | Fastest cloud setup | +| `anthropic` | claude-sonnet-4-20250514 | claude-haiku-3-5-20241022 | Anthropic-native access | + +**Adding your own preset**: just add a block under `quality_presets` in `config/config.yaml`: + +```yaml +quality_presets: + gemini: + provider: gemini + planner: gemini-2.5-pro + worker: gemini-2.0-flash +``` + +Then set the corresponding API key in `.env` and switch `quality_preset: "gemini"`. That's it — CrewAI handles the routing. ### Key Config Fields @@ -281,7 +320,7 @@ org_lifecycle: knowledge_domains: - "auth-service" - "redis-cache" - documented_pct: 0.25 # fraction written down — drives gap severity + documented_pct: 0.25 # fraction written down: drives gap severity scheduled_hires: - name: "Taylor" @@ -338,58 +377,26 @@ facts={ } ``` -This is what makes the dataset useful for RAG evaluation: you have ground truth about what happened, when, who was involved, and what the org's state was — so you can measure whether a retrieval system actually surfaces the right context. ---- ## Memory Requirements | Preset | RAM Required | Notes | | ----------- | ------------ | ---------------------------------------- | -| `local_gpu` | \~48 GB VRAM | Llama 3.3 70B — requires A100 or 2× A10G | -| `cloud` | \~500 MB | Only MongoDB + Python run locally | +| `local_gpu` | ~48 GB VRAM | Llama 3.3 70B: requires A100 or 2x A10G | +| Cloud presets (openai, anthropic, cloud, gemini) | ~500 MB | Only MongoDB + Python run locally | -For `local_gpu` on AWS, a `g5.2xlarge` (A10G 24GB) runs 70B at q4 quantization. At \~$0.50/hour spot pricing a full 22-day simulation costs roughly $3–5. - ---- - -## Project Structure - -``` -orgforge/ -├── .github/workflows/ # CI/CD pipelines -├── src/ -│ ├── flow.py # State machine and simulation engine -│ ├── day_planner.py # LLM-driven per-department daily planning -│ ├── normal_day.py # Agenda dispatcher — produces typed artifacts per activity -│ ├── crm_system.py # Salesforce & Zendesk integration and propagation rules -│ ├── planner_models.py # Dataclasses for plans, events, and validation results -│ ├── plan_validator.py # Integrity boundary between LLM proposals and execution -│ ├── org_lifecycle.py # Dynamic hiring, firing, and knowledge gap propagation -│ ├── graph_dynamics.py # Social graph: stress propagation, edge decay, escalation -│ ├── memory.py # Vector DB and SimEvent bus -│ ├── email_gen.py # Reflective post-processing email artifacts -│ └── post_sim_artifacts.py # Deterministic post-processing (NPS, invoices, Datadog) -├── config/ # YAML configurations -├── tests/ # Pytest suite -├── scripts/ # Entrypoint and helper scripts -├── export/ # Output directory for generated dataset -├── README.md -├── ARCHITECTURE.md -└── CONTRIBUTING.md -``` - ---- +For `local_gpu` on AWS, a `g5.2xlarge` (A10G 24GB) runs 70B at q4 quantization. At ~$0.50/hour spot pricing a full 22-day simulation costs roughly $3–5. ## Roadmap - [x] Native integrations for Zoom, Zendesk, and Salesforce CRM +- [x] Multi-provider LLM support via CrewAI (OpenAI, Anthropic, Bedrock, Gemini, Groq, Azure) - [ ] Plugin architecture for additional integrations (PagerDuty, Workday, etc.) -- [ ] Domain packs — pre-configured `config.yaml` templates for healthcare, fintech, legal +- [ ] Domain packs: pre-configured `config.yaml` templates for healthcare, fintech, legal - [x] Export to HuggingFace dataset format -- [x] Evaluation harness — benchmark RAG retrieval against SimEvent ground truth +- [x] Evaluation harness: benchmark RAG retrieval against SimEvent ground truth ---- ## Adding a New Artifact Type @@ -398,13 +405,11 @@ orgforge/ A formal plugin architecture is on the roadmap. Open an issue before starting so we can align on the interface. ---- ## Contributing Contributions are welcome. Please read **[CONTRIBUTING.md](CONTRIBUTING.md)** before opening a PR. For new domain configs or artifact types, open an Issue first. ---- ## Citation @@ -419,8 +424,7 @@ If you use this work, please cite: } ``` ---- ## License -MIT — see **[LICENSE](https://www.google.com/search?q=LICENSE)**. +MIT — see **[LICENSE](LICENSE)**. diff --git a/config/config.yaml b/config/config.yaml index 81b38b7..f4c65df 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -21,23 +21,16 @@ quality_preset: "cloud" model_presets: local_gpu: provider: "ollama" - planner: "ollama/llama3.3:70b" - worker: "ollama/llama3.1:8b-instruct" + planner: "ollama/gemma4:e4b" + worker: "ollama/gemma4:e4b" base_url: "http://localhost:11434" - embed_provider: "ollama" - embed_model: "mxbai-embed-large" - embed_dims: 1024 cloud: - provider: "bedrock" - # Claude 4.6 Sonnet on Bedrock — best for long-form corporate doc generation - planner: "bedrock/openai.gpt-oss-120b-1:0" - # Llama 3.1 8B on Bedrock — fast, cheap for high-volume worker calls - worker: "bedrock/openai.gpt-oss-120b-1:0" - aws_region: "us-east-2" - embed_provider: "ollama" - embed_model: "mxbai-embed-large" - embed_dims: 1024 + provider: "openai" + planner: "DeepSeek-V4-Pro" + worker: "DeepSeek-V4-Pro" + aws_region: "us-east-2" # ← only needed for bedrock + base_url: "https://tenure-foundry.services.ai.azure.com/openai/v1" # ── SIMULATION ──────────────────────────────────────────────── simulation: @@ -47,26 +40,26 @@ simulation: domain: "apexathletics.io" output_dir: "./export" db_name: "orgforge" - max_days: 30 - start_date: "2026-03-02" + max_days: 60 + start_date: "2026-01-01" adhoc_confluence_prob: 0.2 aws_alert_prob: 0.4 snyk_alert_prob: 0.2 sprint_tickets_per_planning: 4 - max_incident_email_threads: 3 + max_incident_email_threads: 4 new_hire: "Elena" new_hire_dept: "Product" sprint_length_days: 10 - incident_base_prob: 0.15 - incident_cooldown_days: 3 + incident_base_prob: 0.22 + incident_cooldown_days: 2 # ── MORALE ──────────────────────────────────────────────────── morale: - initial: 0.75 - daily_decay: 0.97 - good_day_recovery: 0.02 - intervention_threshold: 0.55 + initial: 0.70 + daily_decay: 0.96 + good_day_recovery: 0.015 + intervention_threshold: 0.60 # ── LEGACY SYSTEMS ──────────────────────────────────────────── legacy_system: @@ -122,6 +115,16 @@ knowledge_gaps: - "AWS cost structure" - "Project Titan" documented_pct: 0.20 + - name: "Sharon" + left: "2025-03" + dept: "Engineering_Mobile" + role: "iOS Engineer" + knew_about: + - "mobile analytics" + - "legacy auth service" + - "AWS cost structure" + - "Project Titan" + documented_pct: 0.32 # ── ORG CHART ───────────────────────────────────────────────── org_chart: @@ -160,9 +163,8 @@ org_lifecycle: - "redis-cache" - "oauth2-flow" documented_pct: 0.25 # only 25% written down — high gap risk - - name: "Morgan" - day: 24 + day: 34 reason: "layoff" role: "DevOps Engineer" dept: "Engineering_Backend" @@ -190,7 +192,7 @@ org_lifecycle: tenure: "new" - name: "Reese" - day: 20 + day: 26 dept: "Engineering_Backend" role: "DevOps Engineer" expertise: ["Kubernetes", "Terraform", "AWS", "Infrastructure"] @@ -396,7 +398,7 @@ personas: she says 'no, that's wrong for iOS' and drops a link. She's not hostile, she's just precise and tired of relitigating platform basics. pet_peeves: "web-first design thinking, Android-parity delays" - stress: 65 + stress: 72 expertise: [ "iOS mobile development", diff --git a/docker-compose.yaml b/docker-compose.yaml index f7989b0..d7ef9a1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: "3.9" - # ═══════════════════════════════════════════════════════════════ # OrgForge — Docker Compose # ═══════════════════════════════════════════════════════════════ diff --git a/eval/agentic_eval_harness-v2.py b/eval/agentic_eval_harness-v2.py new file mode 100644 index 0000000..e20c8f1 --- /dev/null +++ b/eval/agentic_eval_harness-v2.py @@ -0,0 +1,3938 @@ +""" +agentic_eval_harness.py +======================= +OrgForge Agentic Evaluation Harness — v2 + +Evaluates AI agents on three novel tracks that require the deterministic +state machine to exist. No retrieval scoring. Each track has its own +trajectory model and scorer because the reasoning structure is fundamentally +different for each. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TRACK 1 — PERSPECTIVE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Temporal gate: as_of_time from question (actor's knowledge horizon) +Actor gate: tool calls filtered to actor_visible_artifacts +Trajectory: did the agent stay within the actor's visibility cone? + did it correctly identify what was and wasn't accessible? +Score penalty: using artifacts outside the actor's cone, even to reach + the correct answer. The point is epistemic discipline. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TRACK 2 — COUNTERFACTUAL +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Temporal gate: as_of_time of the effect event (agent can see everything up to + and including the effect to understand what happened) +No actor gate: agent has read access to all subsystems +Trajectory: did the agent identify the correct causal mechanism? + did it trace cause → effect correctly? +Answer scoring: structured extraction of (outcome_changed, mechanism, actors) + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TRACK 3 — SILENCE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Temporal gate: end of simulation (agent can see the full corpus) +No actor gate: agent has read access to all subsystems +Trajectory: CRITICAL — did the agent search expected_search_space before + concluding absence? A correct "no" without checking the right + places is scored as a trajectory failure even if the boolean is right. +Answer scoring: boolean only — did the agent correctly conclude absence? + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Score weights (per track) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Track Answer Trajectory Notes +─────────────── ─────── ────────── ───────────────────────── +PERSPECTIVE 0.40 0.60 Trajectory is primary — epistemic discipline matters +COUNTERFACTUAL 0.50 0.50 Both matter — wrong mechanism, wrong answer +SILENCE 0.30 0.70 Can't score a "no" without proof of search +""" + +from __future__ import annotations + +from collections import Counter +import json +import logging +import re +from statistics import mean +import time +from dataclasses import dataclass, field, asdict +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any, Dict, List, Optional, Set, Tuple +import argparse +from config_loader import CONFIG +import yaml + +import requests as _requests + +from eval_harness import _ARTIFACT_SUBSYSTEM + +from graph_eval_track import ( + register_graph_tool, + score_graph_trajectory, + score_graph_answer, +) + +logger = logging.getLogger("orgforge.agentic_eval") + +with open(Path(__file__).resolve().parent.parent / "config" / "config.yaml") as f: + _CFG = yaml.safe_load(f) + +_SIM_CFG = _CFG.get("simulation", {}) +BASE = Path(_SIM_CFG.get("output_dir", "./export")) +EVAL_DIR = BASE / "eval" +_SIM_START = datetime.strptime(CONFIG["simulation"]["start_date"], "%Y-%m-%d") + +_TRACK_WEIGHTS = { + "PERSPECTIVE": {"answer": 0.40, "trajectory": 0.60}, + "COUNTERFACTUAL": {"answer": 0.50, "trajectory": 0.50}, + "SILENCE": {"answer": 0.30, "trajectory": 0.70}, + "GRAPH": {"answer": 0.50, "trajectory": 0.50}, +} + +_DOCTYPE_TO_TOOL = { + "jira": "get_ticket", + "confluence": "get_confluence_page", + "slack": "get_slack_thread", + "email": "get_email", + "pr": "get_pr", + "zd_ticket": "get_zd_ticket", + "sf_opp": "get_sf_opportunity", + "sf_account": "get_sf_account", + "zoom": "get_zoom_transcript", + "datadog": "get_datadog_alert", + "invoice": "get_invoice", + "nps": "get_nps_response", +} + + +_TOOL_SUBSYSTEM = { + "get_ticket": "jira", + "get_confluence_page": "confluence", + "get_slack_thread": "slack", + "get_email": "email", + "get_pr": "git", + "get_zd_ticket": "zendesk", + "get_sf_opportunity": "salesforce", + "get_sf_account": "salesforce", + "get_zoom_transcript": "zoom", + "get_datadog_alert": "datadog", + "get_invoice": "invoice", + "get_nps_response": "salesforce", + "get_events_for_day": None, + "search_artifacts": None, +} + +_SUBSYSTEM_EVENT_TYPES: Dict[str, Set[str]] = { + "jira": { + "incident_opened", + "incident_resolved", + "ticket_progress", + "pr_review", + "sprint_planned", + "sprint_goal_updated", + "postmortem_created", + }, + "slack": { + "standup", + "normal_day_slack", + "watercooler_chat", + "farewell_message", + "onboarding_session", + "warmup_1on1", + "morale_intervention", + "1on1_scheduled", + }, + "confluence": { + "confluence_created", + "design_discussion", + "retrospective", + "leadership_sync", + }, + "git": { + "pr_review", + "code_review_comment", + }, + "email": { + "inbound_external_email", + "customer_email_routed", + "vendor_email_routed", + "hr_outbound_email", + "sales_outbound_email", + "email_dropped", + "hr_checkin", + }, + "zoom": { + "zoom_meeting", + "design_discussion", + "vendor_meeting", + "async_question", + "deep_work_session", + }, + "salesforce": { + "crm_touchpoint", + "crm_account_at_risk", + "customer_health_briefing", + "feature_request_from_sales", + "stability_update_to_sales", + "proactive_outreach_initiated", + "sf_deals_risk_flagged", + }, + "zendesk": { + "zd_ticket_opened", + "zd_tickets_escalated", + "zd_tickets_resolved", + "customer_escalation", + }, + "datadog": { + "dlp_alert", + "secret_detected", + }, +} + +# Sim-internal types never exposed to any actor +_INTERNAL_EVENT_TYPES = { + "knowledge_gap_detected", + "escalation_chain", + "assignment_domain_mismatch", + "sf_ownership_lapsed", + "fix_in_progress", + "day_summary", + "employee_departed", + "employee_hired", + "external_contact_summarized", + "vendor_email_routed", + "secret_detected", + "deep_work_session", + "watercooler_chat", + "ticket_progress", + "standup", + "dept_plan_created", + "mentoring", + "inbound_external_email", + "customer_email_routed", +} + +KNOWN_EVENT_TYPES = { + "incident_opened", + "incident_resolved", + "escalation_chain", + "fix_in_progress", + "postmortem_created", + "knowledge_gap_detected", + "standup", + "pr_review", + "ticket_progress", + "design_discussion", + "async_question", + "code_review_comment", + "deep_work_session", + "sprint_planned", + "retrospective", + "sprint_goal_updated", + "leadership_sync", + "feature_request_from_sales", + "stability_update_to_sales", + "hr_checkin", + "morale_intervention", + "1on1_scheduled", + "external_contact_summarized", + "vendor_meeting", + "customer_escalation", + "normal_day_slack", + "confluence_created", + "day_summary", + "employee_departed", + "employee_hired", + "onboarding_session", + "farewell_message", + "warmup_1on1", + "watercooler_chat", + "inbound_external_email", + "customer_email_routed", + "customer_escalation", + "vendor_email_routed", + "hr_outbound_email", + "email_dropped", + "dlp_alert", + "secret_detected", + "zoom_meeting", + "sales_outbound_email", + "proactive_outreach_initiated", + "zd_ticket_opened", + "zd_tickets_escalated", + "zd_tickets_resolved", + "sf_deals_risk_flagged", + "sf_ownership_lapsed", + "crm_touchpoint", + "crm_account_at_risk", + "customer_health_briefing", + "assignment_domain_mismatch", +} + +_JIRA_PROJECT_ACCESS: Dict[str, Set[str]] = { + "ENG": {"engineering_backend", "engineering_mobile", "ceo"}, + "HR": {"hr_ops", "ceo"}, + "SALES": {"sales_marketing", "ceo"}, + "PROD": {"product", "ceo"}, + "DES": {"design", "product", "ceo"}, + "QA": {"qa_support", "ceo"}, + "ORG": { + "engineering_backend", + "engineering_mobile", + "product", + "ceo", + "hr_ops", + "design", + "sales_marketing", + "qa_support", + }, +} + +_TEMPORAL_DRIFT_THRESHOLD_DAYS = 5 + + +def _jira_project_visible(ticket_id: str, role: str) -> bool: + prefix = ticket_id.split("-")[0] if "-" in ticket_id else "" + allowed_roles = _JIRA_PROJECT_ACCESS.get(prefix) + if allowed_roles is None: + return True # unknown prefix — don't restrict + return role in allowed_roles + + +def _business_day_to_date(start: datetime, n: int) -> datetime: + """Convert a 1-based business day counter to a calendar date.""" + current = start + days_counted = 0 + while days_counted < n: + current += timedelta(days=1) + if current.weekday() < 5: + days_counted += 1 + return current + + +def _date_to_business_day(start: datetime, target: datetime) -> int: + count = 0 + current = start + while current < target: + current += timedelta(days=1) + if current.weekday() < 5: + count += 1 + return count + + +# ───────────────────────────────────────────────────────────────────────────── +# DATA CLASSES +# ───────────────────────────────────────────────────────────────────────────── + + +@dataclass +class ToolCall: + tool_name: str + arguments: Dict[str, Any] + result_ids: List[str] + result_types: List[str] + timestamp_requested: Optional[str] + timestamp_applied: Optional[str] + temporal_drift_days: Optional[float] + temporal_drift_violation: bool + horizon_violation: bool + actor_gate_violation: bool + subsystem_violation: bool + returned_empty: bool + latency_ms: float + + +@dataclass +class AgentTrajectory: + question_id: str + question_type: str + tool_calls: List[ToolCall] = field(default_factory=list) + final_answer: Dict[str, Any] = field(default_factory=dict) + total_latency_ms: float = 0.0 + horizon_violations: int = 0 + actor_gate_violations: int = 0 # PERSPECTIVE track + subsystem_violations: int = 0 # PERSPECTIVE track + search_space_coverage: float = 0.0 # SILENCE track + causal_mechanism_found: bool = False # COUNTERFACTUAL track + graph_tool_called: bool = False # GRAPH track + graph_correct_day_queried: bool = False + dead_ends_hit: int = 0 + dead_ends_recovered: int = 0 + budget_exceeded: bool = False + prompt_tokens: int = 0 + completion_tokens: int = 0 + total_tokens: int = 0 + + +@dataclass +class PerspectiveTrajectoryScore: + epistemic_discipline: float # 1.0 - (cone violations / total calls) + subsystem_discipline: float # 1.0 - (subsystem violations / total calls) + horizon_discipline: float # 1.0 - (horizon violations / total calls) + temporal_precision: float + temporal_drift_discipline: float + conclusion_grounding: float # did final answer cite in-cone artifacts? + dead_end_recovery: float + composite: float + + +@dataclass +class CounterfactualTrajectoryScore: + cause_identified: float # did agent retrieve the cause event? + effect_identified: float # did agent retrieve the effect event? + mechanism_correct: float # did agent name the correct link_type? + causal_chain_complete: float # did agent traverse cause → effect in order? + horizon_discipline: float + composite: float + + +@dataclass +class SilenceTrajectoryScore: + search_space_coverage: float # fraction of expected_search_space the agent checked + correct_absence_conclusion: float # did agent explicitly conclude "does not exist"? + premature_conclusion: float # did agent conclude before searching? (penalty) + horizon_discipline: float + composite: float + + +@dataclass +class EvalResult: + question_id: str + question_type: str + difficulty: str + answer_score: float + answer_correct: bool + trajectory_score: float + combined_score: float + failure_reason: Optional[str] + tool_call_count: int + meta: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict: + return asdict(self) + + +class GatedTools: + """ + Wraps the document corpus and enforces gates per question type. + + PERSPECTIVE: temporal gate (as_of_time) + actor gate (visibility cone) + COUNTERFACTUAL: temporal gate only (as_of_time = effect event timestamp) + SILENCE: no gate (agent sees full corpus — the absence must be real) + + Violations are logged but results are still returned (the agent should + observe them and self-correct). Violations penalize trajectory score. + """ + + def __init__( + self, + mem, + question: dict, + as_of_time: str, + actor_visible_artifacts: Optional[Set[str]] = None, + actor_visible_by_subsystem: Optional[Dict[str, Set[str]]] = None, + actor_subsystem_access: Optional[Set[str]] = None, + ): + self._mem = mem + self._question = question + self._as_of_time = as_of_time + self._actor_visible = actor_visible_artifacts or set() + self._actor_visible_by_subsystem = actor_visible_by_subsystem or {} + self._actor_subsystems = actor_subsystem_access + self._question_type = question.get("question_type", "") + self._call_log: List[ToolCall] = [] + self._confluence_seen: set = set() + + def _gate_ts(self) -> str: + if self._question_type == "SILENCE": + if not hasattr(self, "_silence_gate_ts"): + events = self._mem.get_event_log(from_db=True) + max_day = max((e.day for e in events), default=1) if events else 1 + self._silence_gate_ts = _business_day_to_date( + _SIM_START, max_day + ).isoformat() + return self._silence_gate_ts + return self._as_of_time + + @property + def call_log(self) -> List[ToolCall]: + return self._call_log + + def _temporal_gate(self, doc: dict) -> bool: + ts = doc.get("timestamp") or doc.get("created") or doc.get("date") + if not ts: + return True + try: + return datetime.fromisoformat(str(ts)) <= datetime.fromisoformat( + self._gate_ts() + ) + except (ValueError, TypeError): + return True + + def _check_actor_gate(self, doc_id: str, doc_type: str) -> Tuple[bool, bool]: + """ + Returns (actor_gate_violation, subsystem_violation). + Only meaningful for PERSPECTIVE questions. + """ + if self._question_type != "PERSPECTIVE": + return False, False + + if doc_type in ("jira", "jira_tickets"): + if not _jira_project_visible(doc_id, self._question.get("actor_role", "")): + return True, False + + subsystem = _ARTIFACT_SUBSYSTEM.get(doc_type, "default") + + subsystem_violation = ( + bool(self._actor_subsystems) + and subsystem not in self._actor_subsystems + and subsystem != "default" + ) + + actor_gate_violation = ( + bool(self._actor_visible) and doc_id not in self._actor_visible + ) + + return actor_gate_violation, subsystem_violation + + def _record( + self, + tool_name: str, + arguments: Dict, + results: List[dict], + t0: float, + horizon_violation: bool = False, + timestamp_applied: Optional[str] = None, + ) -> List[dict]: + latency = (time.time() - t0) * 1000 + filtered = [r for r in results if self._temporal_gate(r)] + horizon_violation = horizon_violation or len(filtered) < len(results) + + result_ids = [str(r.get("id", r.get("_id", ""))) for r in filtered] + result_types = [str(r.get("doc_type", r.get("type", ""))) for r in filtered] + + actor_gate_violation = False + subsystem_violation = False + for rid, rtype in zip(result_ids, result_types): + agv, sv = self._check_actor_gate(rid, rtype) + if agv: + actor_gate_violation = True + if sv: + subsystem_violation = True + + # Check subsystem from tool name too + tool_subsystem = _TOOL_SUBSYSTEM.get(tool_name) + if ( + self._question_type == "PERSPECTIVE" + and tool_subsystem + and self._actor_subsystems + and tool_subsystem not in self._actor_subsystems + ): + subsystem_violation = True + + requested = arguments.get("as_of_time") + drift = None + if requested and timestamp_applied: + try: + drift = ( + datetime.fromisoformat(timestamp_applied) + - datetime.fromisoformat(requested) + ).days + except (ValueError, TypeError): + pass + + temporal_drift_violation = ( + drift is not None and drift < -_TEMPORAL_DRIFT_THRESHOLD_DAYS + ) + + self._call_log.append( + ToolCall( + tool_name=tool_name, + arguments=arguments, + result_ids=result_ids, + result_types=result_types, + timestamp_requested=requested, + timestamp_applied=timestamp_applied, + temporal_drift_days=drift, + temporal_drift_violation=temporal_drift_violation, + horizon_violation=horizon_violation, + actor_gate_violation=actor_gate_violation, + subsystem_violation=subsystem_violation, + returned_empty=len(filtered) == 0, + latency_ms=latency, + ) + ) + return filtered + + _COLLECTION_TS_FIELD = { + "jira": "created_at", + "jira_tickets": "created_at", + "confluence": "timestamp", + "slack": "timestamp", + "email": "timestamp", + "pr": "created_at", + "zd_ticket": "timestamp", + "sf_opp": "timestamp", + "sf_account": "timestamp", + "zoom": "timestamp", + "datadog": "timestamp", + "invoice": "timestamp", + "nps": "timestamp", + } + + def _build_query( + self, + base: dict, + doc_type: str = "", + id_field: str = "id", + agent_as_of_time: Optional[str] = None, + ) -> Tuple[Optional[dict], str]: + """ + Constructs a MongoDB filter with temporal and actor gates applied. + base: the caller's own filter fields e.g. {"id": ticket_id} + doc_type: the artifact type for subsystem gate checking + """ + ceiling = self._gate_ts() + if agent_as_of_time: + effective_ts = min(agent_as_of_time, ceiling) + else: + effective_ts = ceiling + + query = {**base} + + ts_field = self._COLLECTION_TS_FIELD.get(doc_type, "timestamp") + query[ts_field] = {"$lte": effective_ts} + + if self._question_type == "PERSPECTIVE" and doc_type: + subsystem = _ARTIFACT_SUBSYSTEM.get(doc_type, "default") + + if ( + self._actor_subsystems + and subsystem not in self._actor_subsystems + and subsystem != "default" + ): + return None, effective_ts + + if self._actor_visible: + query[id_field] = {"$in": list(self._actor_visible)} + + if "id" in base: + query[id_field] = ( + base["id"] + if base["id"] in self._actor_visible + else "__blocked__" + ) + + return query, effective_ts + + def get_ticket(self, ticket_id: str) -> dict: + t0 = time.time() + gate = self._gate_ts() + query, effective_ts = self._build_query({"id": ticket_id}, doc_type="jira") + if query is None: + self._record( + "get_ticket", + {"ticket_id": ticket_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + doc = ( + self._mem._db["jira_tickets"].find_one( + query, + { + "_id": 0, + "causal_chain": 0, + "dept_type": 0, + "story_points": 0, + "bot_threads": 0, + "escalation_narrative": 0, + "gap_areas": 0, + "recurrence_chain_depth": 0, + "recurrence_chain_root": 0, + "recurrence_gap_days": 0, + "recurrence_of": 0, + "prior_postmortem": 0, + "sprint": 0, + }, + ) + or {} + ) + + if doc: + comments = doc.get("comments", []) + doc["comments"] = [c for c in comments if c.get("created", "9999") <= gate] + + created = doc.get("created_at", "9999") + in_progress_day = doc.get("in_progress_since") + in_review_day = doc.get("in_review_since") + + def day_to_iso(day): + return ( + (_SIM_START + timedelta(days=day - 1)).isoformat() + if day + else "9999" + ) + + in_progress_dt = day_to_iso(in_progress_day) + in_review_dt = day_to_iso(in_review_day) + completed = ( + doc.get("updated_at", "9999") if doc.get("status") == "Done" else "9999" + ) + + if completed <= gate: + derived_status = "Done" + elif in_review_dt <= gate: + derived_status = "In Review" + elif in_progress_dt <= gate: + derived_status = "In Progress" + else: + derived_status = "To Do" + + doc["status"] = derived_status + if derived_status != "Done": + doc.pop("completion_artifact", None) + + if doc.get("linked_prs"): + visible_prs = [] + for pr_id in doc["linked_prs"]: + pr = self._mem._db["prs"].find_one( + {"id": pr_id, "created_at": {"$lte": gate}}, {"id": 1} + ) + if pr: + visible_prs.append(pr_id) + doc["linked_prs"] = visible_prs + + if in_progress_day: + if in_progress_dt > gate: + doc.pop("in_progress_since", None) + if in_review_day: + if in_review_dt > gate: + doc.pop("in_review_since", None) + doc.pop("last_review_requested_day", None) + + doc["comments"] = [ + {k: v for k, v in c.items() if k not in ("day", "updated")} + for c in comments + if c.get("created", "9999") <= gate + ] + + results = self._record( + "get_ticket", + {"ticket_id": ticket_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + + return results[0] if results else {} + + def get_confluence_page(self, page_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query({"_id": page_id}, doc_type="confluence") + if query is None: + self._record("get_confluence_page", {"page_id": page_id}, [], t0) + return {} + query["type"] = "confluence" + + doc = ( + self._mem._db["artifacts"].find_one( + query, + { + "_id": 0, + "embedding": 0, + "type": 0, + "metadata": 0, + "date": 0, + "timestamp": 0, + }, + ) + or {} + ) + + results = self._record( + "get_confluence_page", + {"page_id": page_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + + return results[0] if results else {} + + def get_slack_thread(self, thread_id: str) -> List[dict]: + t0 = time.time() + query, effective_ts = self._build_query( + {"_id": thread_id}, + doc_type="slack", + id_field="_id", + ) + if query is None: + return self._record("get_slack_thread", {"thread_id": thread_id}, [], t0) + query["type"] = "slack_thread" + doc = ( + self._mem._db["artifacts"].find_one(query, {"_id": 0, "embedding": 0}) or {} + ) + docs = [doc] if doc else [] + return self._record( + "get_slack_thread", + {"thread_id": thread_id}, + docs, + t0, + timestamp_applied=effective_ts, + ) + + def get_email(self, email_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query({"_id": email_id}, doc_type="email") + if query is None: + self._record("get_email", {"email_id": email_id}, [], t0) + return {} + query["type"] = "email" + doc = ( + self._mem._db["artifacts"].find_one(query, {"_id": 0, "embedding": 0}) or {} + ) + results = self._record( + "get_email", + {"email_id": email_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_pr(self, pr_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query({"pr_id": pr_id}, doc_type="pr") + if query is None: + self._record("get_pr", {"pr_id": pr_id}, [], t0) + return {} + doc = ( + self._mem._db["pull_requests"].find_one( + query, {"_id": 0, "author_email": 0} + ) + or {} + ) + + results = self._record( + "get_pr", + {"pr_id": pr_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + + return results[0] if results else {} + + def get_zd_ticket(self, ticket_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query({"id": ticket_id}, doc_type="zd_ticket") + if query is None: + self._record("get_zd_ticket", {"ticket_id": ticket_id}, [], t0) + return {} + doc = self._mem._db["zd_tickets"].find_one(query, {"_id": 0}) or {} + results = self._record( + "get_zd_ticket", + {"ticket_id": ticket_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_sf_opportunity(self, opp_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query( + {"id": opp_id}, doc_type="sf_opportunity" + ) + if query is None: + self._record("get_sf_opportunity", {"opp_id": opp_id}, [], t0) + return {} + doc = self._mem._db["salesforce_opps"].find_one(query, {"_id": 0}) or {} + results = self._record( + "get_sf_opportunity", + {"opp_id": opp_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_sf_account(self, account_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query( + {"id": account_id}, doc_type="sf_account" + ) + if query is None: + self._record("get_sf_account", {"account_id": account_id}, [], t0) + return {} + doc = self._mem._db["salesforce_accounts"].find_one(query, {"_id": 0}) or {} + results = self._record( + "get_sf_account", + {"account_id": account_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_zoom_transcript(self, transcript_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query({"id": transcript_id}, doc_type="zoom") + if query is None: + self._record( + "get_zoom_transcript", {"transcript_id": transcript_id}, [], t0 + ) + return {} + + doc = ( + self._mem._db["artifacts"].find_one(query, {"_id": 0, "embedding": 0}) or {} + ) + if not doc: + self._record( + "get_zoom_transcript", + {"transcript_id": transcript_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + date_str = doc.get("date", "") + md_path = BASE / "zoom" / date_str / f"{transcript_id}.md" + try: + doc["transcript"] = md_path.read_text(encoding="utf-8") + except FileNotFoundError: + logger.warning( + f"[get_zoom_transcript] Transcript file not found: {md_path}" + ) + doc["transcript"] = doc.get("content", "") + + results = self._record( + "get_zoom_transcript", + {"transcript_id": transcript_id}, + [doc], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_datadog_alert(self, alert_id: str) -> dict: + t0 = time.time() + effective_ts = self._gate_ts() + + path = BASE / "datadog" / "alerts.jsonl" + if not path.exists(): + self._record( + "get_datadog_alert", + {"alert_id": alert_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + doc = None + with open(path) as f: + for line in f: + try: + alert = json.loads(line) + if alert.get("id") == alert_id: + doc = alert + break + except json.JSONDecodeError: + continue + + if not doc: + self._record( + "get_datadog_alert", + {"alert_id": alert_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + if self._question_type != "SILENCE": + date_happened = doc.get("date_happened", 0) + if date_happened: + doc_ts = datetime.fromtimestamp(date_happened).isoformat() + if doc_ts > self._gate_ts(): + self._record( + "get_datadog_alert", + {"alert_id": alert_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + results = self._record("get_datadog_alert", {"alert_id": alert_id}, [doc], t0) + return results[0] if results else {} + + def get_invoice(self, invoice_id: str) -> dict: + t0 = time.time() + effective_ts = self._gate_ts() + + path = BASE / "invoices" / f"{invoice_id}.json" + if not path.exists(): + self._record( + "get_invoice", + {"invoice_id": invoice_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + doc = json.loads(path.read_text()) + + ts = doc.get("timestamp") or doc.get("date") or doc.get("created_at", "") + if self._question_type != "SILENCE" and ts and ts > effective_ts: + self._record( + "get_invoice", + {"invoice_id": invoice_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + results = self._record( + "get_invoice", + {"invoice_id": invoice_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_nps_response(self, account_name: str) -> dict: + t0 = time.time() + effective_ts = self._gate_ts() + + fname = ( + account_name.lower().replace(" ", "_").replace(".", "").replace(",", "") + + ".json" + ) + path = BASE / "nps" / "responses" / fname + if not path.exists(): + self._record( + "get_nps_response", + {"account_name": account_name}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + doc = json.loads(path.read_text()) + + ts = doc.get("timestamp") or doc.get("date") or doc.get("created_at", "") + if self._question_type != "SILENCE" and ts and ts > effective_ts: + self._record( + "get_nps_response", + {"account_name": account_name}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + results = self._record( + "get_nps_response", + {"account_name": account_name}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + _ARTIFACT_ID_BLOCKLIST = { + "eml_path", + "artifact_path", + "slack_path", + "jira", + "slack", + } + + def _project_event(self, event: dict, actor_visible: Set[str]) -> dict: + drop_keys = {"tags", "date", "artifact_path", "eml_path", "slack_path"} + doc = {k: v for k, v in event.items() if k not in drop_keys} + + if "artifact_ids" in doc: + doc["artifact_ids"] = { + k: v + for k, v in doc["artifact_ids"].items() + if v + and v != "[]" + and k not in self._ARTIFACT_ID_BLOCKLIST + and ( + (isinstance(v, list) and any(item in actor_visible for item in v)) + or (isinstance(v, str) and v in actor_visible) + ) + } + + return doc + + def get_events_for_day( + self, day: int, event_type: Optional[str] = None + ) -> List[dict]: + t0 = time.time() + + if self._question_type == "SILENCE": + gate_day = self._question.get("trigger_day", 30) + gate_ts = (_SIM_START + timedelta(days=gate_day)).isoformat() + else: + gate_day = (datetime.fromisoformat(self._as_of_time) - _SIM_START).days + 1 + gate_ts = self._as_of_time + + if day > gate_day: + logger.warning( + f"[get_events_for_day] Day {day} requested but gate is Day {gate_day} — blocked" + ) + return self._record( + "get_events_for_day", {"day": day, "event_type": event_type}, [], t0 + ) + + query: Dict = { + "day": day, + } + + allowed_types: Set[str] = set() + if self._actor_subsystems is None: + allowed_types = set(KNOWN_EVENT_TYPES) + else: + for subsystem in self._actor_subsystems: + allowed_types.update(_SUBSYSTEM_EVENT_TYPES.get(subsystem, set())) + + if self._question_type != "COUNTERFACTUAL": + allowed_types -= _INTERNAL_EVENT_TYPES + + if event_type: + if ( + event_type in _INTERNAL_EVENT_TYPES + and self._question_type != "COUNTERFACTUAL" + ): + logger.warning( + f"[get_events_for_day] Internal event type requested: {event_type}" + ) + return self._record( + "get_events_for_day", {"day": day, "event_type": event_type}, [], t0 + ) + query["type"] = event_type + else: + query["type"] = {"$in": list(allowed_types)} + + if self._question_type == "PERSPECTIVE": + actor = self._question.get("actor", "") + visible = self._actor_visible_by_subsystem + query["$or"] = [ + {"actors": actor}, + {"artifact_ids.email": {"$in": list(visible.get("email", set()))}}, + {"artifact_ids.jira": {"$in": list(visible.get("jira", set()))}}, + { + "artifact_ids.confluence": { + "$in": list(visible.get("confluence", set())) + } + }, + { + "artifact_ids.slack_thread": { + "$in": list(visible.get("slack", set())) + } + }, + {"artifact_ids.pr": {"$in": list(visible.get("git", set()))}}, + { + "artifact_ids.zoom_transcript": { + "$in": list(visible.get("zoom", set())) + } + }, + ] + + # logger.info(f"[get_events_for_day] - query: {query}") + + docs = list( + self._mem._db["events"].find( + query, + { + "_id": 0, + "event_id": 1, + "type": 1, + "day": 1, + "actors": 1, + "summary": 1, + "artifact_ids": 1, + }, + ) + ) + + # logger.info(f"[get_events_for_day] - results before project: {docs}") + + if self._actor_visible is not None: + docs = [self._project_event(d, self._actor_visible) for d in docs] + + # logger.info(f"[get_events_for_day] - results: {docs}") + + return self._record( + "get_events_for_day", {"day": day, "event_type": event_type}, docs, t0 + ) + + def search_artifacts( + self, + query: str, + doc_type: str, + actor: Optional[str] = None, + after_day: Optional[int] = None, + limit: int = 6, + ) -> List[dict]: + t0 = time.time() + + if after_day is not None: + try: + after_day = int(after_day) + except (ValueError, TypeError): + after_day = None + + effective_ts = self._gate_ts() + MAX_SEARCH_LIMIT = 15 + limit = min(limit, MAX_SEARCH_LIMIT) + + actor_id_filter = None + if self._question_type == "PERSPECTIVE" and self._actor_visible: + actor_id_filter = list(self._actor_visible) + + exact_doc = self._mem._db["artifacts"].find_one( + {"_id": query}, + {"embedding": 0, "timestamp": 0, "created_at": 0, "date": 0}, + ) + if exact_doc: + # logger.info(f"[search_artifacts] - exact_doc: {exact_doc}") + + ts_filter = {"timestamp": {"$lte": effective_ts}} + if after_day is not None: + floor_ts = _business_day_to_date(_SIM_START, after_day).isoformat() + ts_filter["timestamp"]["$gte"] = floor_ts + if actor_id_filter is not None and query not in self._actor_visible: + return self._record( + "search_artifacts", + { + "query": query, + "doc_type": doc_type, + "actor": actor, + "after_day": after_day, + }, + [], + t0, + timestamp_applied=effective_ts, + ) + exact_doc_ts = exact_doc.get("timestamp", "") + if exact_doc_ts <= effective_ts and ( + after_day is None or exact_doc_ts >= floor_ts + ): + return self._record( + "search_artifacts", + { + "query": query, + "doc_type": doc_type, + "actor": actor, + "after_day": after_day, + }, + [exact_doc], + t0, + timestamp_applied=effective_ts, + ) + return self._record( + "search_artifacts", + { + "query": query, + "doc_type": doc_type, + "actor": actor, + "after_day": after_day, + }, + [], + t0, + timestamp_applied=effective_ts, + ) + + text_filter: dict = { + "$text": {"$search": query}, + "timestamp": {"$lte": effective_ts}, + } + if actor_id_filter is not None: + text_filter["_id"] = {"$in": actor_id_filter} + if after_day is not None: + floor_ts = _business_day_to_date(_SIM_START, after_day).isoformat() + text_filter["timestamp"] = { + "$gte": floor_ts, + "$lte": effective_ts, + } + if doc_type: + text_filter["type"] = doc_type + if actor: + text_filter["metadata.author"] = actor + + # logger.info(f"[search_artifacts] - query: {text_filter}") + # logger.info(f"[search_artifacts] - limit: {limit}") + + results = list( + self._mem._db["artifacts"] + .find( + text_filter, + { + "content": 0, + "embedding": 0, + "timestamp": 0, + "created_at": 0, + "date": 0, + "metadata.tags": 0, + "metadata.parent_id": 0, + "metadata.is_chunk": 0, + "score": {"$meta": "textScore"}, + }, + ) + .sort([("score", {"$meta": "textScore"})]) + .limit(limit) + ) + + # logger.info(f"[search_artifacts] - results length: {len(results)}") + + # logger.info(f"[search_artifacts] - results: {results}") + + return self._record( + "search_artifacts", + { + "query": query, + "doc_type": doc_type, + "actor": actor, + "after_day": after_day, + }, + results, + t0, + timestamp_applied=effective_ts, + ) + + def get_graph_snapshot(self, day: int) -> dict: + """ + Returns the social graph snapshot for the given day: + nodes (actor names), edges (source, target, weight), and org state. + Registered dynamically for GRAPH-track questions via register_graph_tool(). + Calling this on a non-GRAPH question logs a tool call but returns {}. + """ + + t0 = time.time() + if self._question_type != "GRAPH": + self._record("get_graph_snapshot", {"day": day}, [], t0) + return {} + + logger.warning( + "[get_graph_snapshot] stub reached for GRAPH question — register_graph_tool() may not have run" + ) + self._record("get_graph_snapshot", {"day": day}, [], t0) + return {} + + def get_stress_snapshot(self, day: int) -> dict: + """ + Returns {day, stress: {name: int}} for the given day. + Registered dynamically for GRAPH-track questions. + """ + + t0 = time.time() + if self._question_type != "GRAPH": + self._record("get_stress_snapshot", {"day": day}, [], t0) + return {} + self._record("get_stress_snapshot", {"day": day}, [], t0) + return {} + + +# ───────────────────────────────────────────────────────────────────────────── +# SCORERS +# ───────────────────────────────────────────────────────────────────────────── + + +class PerspectiveScorer: + """ + Scores a PERSPECTIVE trajectory. + + Answer scoring: + - Exact match on ground_truth.could_actor_have_known (boolean) + - Partial credit for correctly identifying blocked_subsystems + - Partial credit for citing in-cone evidence + + Trajectory scoring: + - Epistemic discipline: fraction of tool calls that stayed within cone + - Subsystem discipline: fraction of tool calls to accessible subsystems + - Conclusion grounding: did the final answer cite in-cone artifacts? + """ + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + gt_bool = ground_truth.get("could_actor_have_known", False) + + # Extract boolean from agent answer — schema violation scores 0, not 0.1 + agent_bool = self._extract_boolean(final_answer) + if agent_bool is None: + return 0.0, False + + correct = agent_bool == gt_bool + if not correct: + return 0.0, False + + # Partial credit for explaining the mechanism correctly + score = 0.6 # base for correct boolean + + gt_blocked = set(ground_truth.get("blocked_subsystems", [])) + agent_blocked = set(final_answer.get("blocked_subsystems", [])) + if gt_blocked and agent_blocked: + overlap = len(gt_blocked & agent_blocked) / len(gt_blocked) + score += 0.2 * overlap + + gt_evidence = set(ground_truth.get("evidence_artifacts", [])) + agent_evidence = set(final_answer.get("evidence_artifacts", [])) + if gt_evidence and agent_evidence: + overlap = len(gt_evidence & agent_evidence) / len(gt_evidence) + score += 0.2 * overlap + elif not gt_evidence: + score += 0.2 # no evidence required — agent doesn't need to cite any + + return min(score, 1.0), True + + def score_trajectory( + self, + trajectory: AgentTrajectory, + question: dict, + ) -> PerspectiveTrajectoryScore: + calls = trajectory.tool_calls + if not calls: + return PerspectiveTrajectoryScore( + epistemic_discipline=0.0, + subsystem_discipline=0.0, + horizon_discipline=0.0, + temporal_precision=0.0, + temporal_drift_discipline=1.0, + conclusion_grounding=0.0, + dead_end_recovery=0.0, + composite=0.0, + ) + + n = len(calls) + actor_cone_violations = sum(1 for c in calls if c.actor_gate_violation) + subsystem_violations = sum(1 for c in calls if c.subsystem_violation) + horizon_violations = sum(1 for c in calls if c.horizon_violation) + dead_ends = sum(1 for c in calls if c.returned_empty) + dead_ends_recovered = trajectory.dead_ends_recovered + + epistemic_discipline = 1.0 - (actor_cone_violations / n) + subsystem_discipline = 1.0 - (subsystem_violations / n) + horizon_discipline = 1.0 - (horizon_violations / n) + + # Conclusion grounding: continuous coverage fraction of required evidence + # cited within the actor's cone. Binary hit/miss loses resolution at the + # top of the leaderboard where frontier models will be differentiating. + actor_visible = set(question.get("actor_visible_artifacts", [])) + gt_evidence = set( + question.get("ground_truth", {}).get("evidence_artifacts", []) + ) + cited = set(trajectory.final_answer.get("evidence_artifacts", [])) + + if gt_evidence: + in_cone_required = gt_evidence & actor_visible + if in_cone_required: + conclusion_grounding = len(cited & in_cone_required) / len( + in_cone_required + ) + else: + conclusion_grounding = 1.0 # nothing in-cone required + elif cited: + conclusion_grounding = 1.0 if (cited & actor_visible) else 0.3 + else: + conclusion_grounding = 0.0 + + dead_end_recovery = dead_ends_recovered / dead_ends if dead_ends > 0 else 1.0 + + sim_days = CONFIG["simulation"].get("num_days", 60) + drifts = [ + c.temporal_drift_days for c in calls if c.temporal_drift_days is not None + ] + temporal_precision = ( + max(0.0, 1.0 - mean(abs(d) / sim_days for d in drifts)) if drifts else 1.0 + ) + + drift_violations = sum(1 for c in calls if c.temporal_drift_violation) + temporal_drift_discipline = 1.0 - (drift_violations / n) + + composite = ( + 0.30 * epistemic_discipline + + 0.25 * subsystem_discipline + + 0.20 * conclusion_grounding + + 0.10 * horizon_discipline + + 0.05 * temporal_precision + + 0.05 * temporal_drift_discipline + + 0.05 * dead_end_recovery + ) + + return PerspectiveTrajectoryScore( + epistemic_discipline=round(epistemic_discipline, 4), + subsystem_discipline=round(subsystem_discipline, 4), + horizon_discipline=round(horizon_discipline, 4), + temporal_precision=round(temporal_precision, 4), + temporal_drift_discipline=round(temporal_drift_discipline, 4), + conclusion_grounding=round(conclusion_grounding, 4), + dead_end_recovery=round(dead_end_recovery, 4), + composite=round(composite, 4), + ) + + def _extract_boolean(self, answer: Dict) -> Optional[bool]: + val = answer.get("could_actor_have_known") + if isinstance(val, bool): + return val + # Schema violation — model did not return the required boolean field. + # Log and return None so the caller scores this as 0. + logger.warning( + " PERSPECTIVE schema violation: 'could_actor_have_known' missing or " + f"non-boolean (got {val!r})" + ) + return None + + +class CounterfactualScorer: + """ + Scores a COUNTERFACTUAL trajectory. + + Answer scoring: + - outcome_changed: boolean match (0.4) + - mechanism: correct link_type identified (0.35) + - actors: at least one correct actor identified (0.25) + + Trajectory scoring: + - cause_identified: agent retrieved the cause event + - effect_identified: agent retrieved the effect event + - mechanism_correct: agent named the right link_type + - causal_chain_complete: agent traversed cause → effect in order + """ + + _MECHANISM_ALIASES = { + "involves_gap": { + "knowledge gap", + "gap", + "undocumented", + "missing documentation", + "knowledge_gap", + }, + "recurrence_of": { + "recurrence", + "repeat incident", + "recurred", + "same issue", + "recurring", + }, + "spawned_doc": { + "spawned", + "documentation", + "confluence", + "design discussion", + "produced doc", + }, + "email_dropped": { + "dropped", + "unactioned", + "routing failure", + "missed email", + "no response", + }, + "sf_ownership_lapsed": { + "ownership lapsed", + "crm gap", + "salesforce", + "account owner", + "orphaned", + }, + "blocker_flagged": { + "blocker", + "blocked", + "delay", + "progress", + "technical blocker", + "blocker_flagged", + }, + "incident_coordination": { + "coordination", + "external contact", + "external party", + "incident_coordination", + "coordinated with", + }, + "departure_reassignment": { + "reassignment", + "departed", + "departure", + "reassigned", + "departure_reassignment", + "not reassigned", + }, + } + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + score = 0.0 + gt_outcome = ground_truth.get("outcome_changed", True) + agent_outcome = self._extract_boolean(final_answer, "outcome_changed") + + if agent_outcome is None: + return 0.0, False + if agent_outcome == gt_outcome: + score += 0.4 + + gt_mechanism = ground_truth.get("causal_mechanism", "") + agent_mechanism_prose = str(final_answer.get("mechanism", "")).lower() + agent_mechanism_structured = str( + final_answer.get("causal_mechanism", "") + ).lower() + agent_mechanism = agent_mechanism_prose + " " + agent_mechanism_structured + aliases = self._MECHANISM_ALIASES.get(gt_mechanism, {gt_mechanism}) + if any(alias in agent_mechanism for alias in aliases): + score += 0.35 + + gt_actors = set(ground_truth.get("actors", [])) + agent_actors_raw = final_answer.get( + "actors", final_answer.get("involved_actors", []) + ) + agent_actors = ( + set(agent_actors_raw) if isinstance(agent_actors_raw, list) else set() + ) + if gt_actors and agent_actors and (gt_actors & agent_actors): + score += 0.25 + elif not gt_actors: + score += 0.25 + + is_correct = score >= 0.75 + return round(min(score, 1.0), 4), is_correct + + def score_trajectory( + self, + trajectory: AgentTrajectory, + question: dict, + ground_truth: Dict, + ) -> CounterfactualTrajectoryScore: + calls = trajectory.tool_calls + if not calls: + return CounterfactualTrajectoryScore(0, 0, 0, 0, 1.0, 0.0) + + n = len(calls) + retrieved_ids = set() + for call in calls: + retrieved_ids.update(call.result_ids) + + evidence_artifacts = ground_truth.get("evidence_chain_artifacts", {}) + cause_artifacts = set(evidence_artifacts.get("cause", [])) + effect_artifacts = set(evidence_artifacts.get("effect", [])) + + cause_identified = ( + 1.0 if (cause_artifacts and cause_artifacts & retrieved_ids) else 0.0 + ) + effect_identified = ( + 1.0 if (effect_artifacts and effect_artifacts & retrieved_ids) else 0.0 + ) + + gt_mechanism = ground_truth.get("causal_mechanism", "") + aliases = self._MECHANISM_ALIASES.get(gt_mechanism, {gt_mechanism}) + agent_text = " ".join( + [str(c.arguments) for c in calls] + [str(trajectory.final_answer)] + ).lower() + mechanism_correct = ( + 1.0 if any(alias in agent_text for alias in aliases) else 0.0 + ) + + cause_only_artifacts = cause_artifacts - effect_artifacts + effect_only_artifacts = effect_artifacts - cause_artifacts + shared_artifacts = cause_artifacts & effect_artifacts + + cause_call_idx = next( + ( + i + for i, c in enumerate(calls) + if cause_only_artifacts & set(c.result_ids) + ), + next( + ( + i + for i, c in enumerate(calls) + if shared_artifacts & set(c.result_ids) + ), + None, + ), + ) + effect_call_idx = next( + ( + i + for i, c in enumerate(calls) + if effect_only_artifacts & set(c.result_ids) + ), + next( + ( + i + for i, c in enumerate(calls) + if shared_artifacts & set(c.result_ids) + ), + None, + ), + ) + if cause_call_idx is not None and effect_call_idx is not None: + if cause_call_idx == effect_call_idx and not ( + cause_only_artifacts or effect_only_artifacts + ): + causal_chain_complete = ( + 0.5 # single shared artifact, no ordered traversal + ) + elif cause_call_idx <= effect_call_idx: + causal_chain_complete = 1.0 + else: + causal_chain_complete = 0.5 + elif cause_call_idx is not None or effect_call_idx is not None: + causal_chain_complete = 0.5 + else: + causal_chain_complete = 0.0 + + horizon_violations = sum(1 for c in calls if c.horizon_violation) + horizon_discipline = 1.0 - (horizon_violations / n) + + composite = ( + 0.25 * cause_identified + + 0.25 * effect_identified + + 0.25 * mechanism_correct + + 0.15 * causal_chain_complete + + 0.10 * horizon_discipline + ) + + return CounterfactualTrajectoryScore( + cause_identified=cause_identified, + effect_identified=effect_identified, + mechanism_correct=mechanism_correct, + causal_chain_complete=causal_chain_complete, + horizon_discipline=round(horizon_discipline, 4), + composite=round(composite, 4), + ) + + def _extract_boolean(self, answer: Dict, key: str) -> Optional[bool]: + val = answer.get(key) + if isinstance(val, bool): + return val + # Schema violation — model did not return the required boolean field. + # Log and return None so the caller scores this as 0. + logger.warning( + f" COUNTERFACTUAL schema violation: '{key}' missing or " + f"non-boolean (got {val!r})" + ) + return None + + +class SilenceScorer: + """ + Scores a SILENCE trajectory. + + The key insight: absence is only meaningful if the agent searched the + right places. A lucky "no" without evidence of search is scored as a + trajectory failure. + + Answer scoring: + - Boolean correct (did agent conclude the artifact does not exist): 1.0 + - If agent concluded "yes" (artifact exists): 0.0 + + Trajectory scoring: + - search_space_coverage: fraction of expected_search_space the agent checked + - correct_absence_conclusion: did agent say "does not exist" explicitly? + - premature_conclusion: penalty if agent concluded before searching + """ + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + gt_answer = ground_truth.get("answer", False) # Always False for SILENCE + agent_answer = self._extract_absence_conclusion(final_answer) + + if agent_answer is None: + return 0.0, False + + correct = agent_answer == gt_answer + return (1.0, True) if correct else (0.0, False) + + _PATH_SUBSYSTEM_MAP: List[Tuple[str, str]] = [ + ("slack_", "slack"), + ("slack/", "slack"), + ("confluence/", "confluence"), + ("CONF-", "confluence"), + ("zendesk/", "zendesk"), + ("customer_reply_", "zendesk"), + ("ext_email_", "email"), + ("export/emails/", "email"), + ("ENG-", "jira"), + ("IT-", "jira"), + ("ORG-", "jira"), + ("QA-", "jira"), + ("HR-", "jira"), + ("PR-", "git"), + ("salesforce/", "salesforce"), + ("SF-", "salesforce"), + ("zoom_", "zoom"), + ("datadog/", "datadog"), + ] + + _TOOL_SUBSYSTEM_MAP: Dict[str, str] = { + "get_ticket": "jira", + "get_confluence_page": "confluence", + "get_slack_thread": "slack", + "get_email": "email", + "get_pr": "git", + "get_zd_ticket": "zendesk", + "get_sf_opportunity": "salesforce", + "get_sf_account": "salesforce", + "get_zoom_transcript": "zoom", + "get_datadog_alert": "datadog", + } + + _SEARCH_DOCTYPE_MAP: Dict[str, str] = { + "jira": "jira", + "confluence": "confluence", + "slack": "slack", + "email": "email", + "pr": "git", + "zd_ticket": "zendesk", + "zoom": "zoom", + } + + _QUERY_STOPWORDS: frozenset = frozenset( + { + "the", + "a", + "an", + "of", + "in", + "for", + "on", + "to", + "at", + "by", + "is", + "was", + "and", + "or", + "not", + "be", + "it", + "its", + } + ) + + def _infer_artifact_subsystem(self, entry: str) -> Optional[str]: + for prefix, sub in self._PATH_SUBSYSTEM_MAP: + if entry.startswith(prefix): + return sub + return None + + def _semantic_coverage_check( + self, + expected_entry: str, + calls: List[ToolCall], + question: dict, + normalized_tool_args: List[str], + ) -> bool: + """ + Returns True when the agent's search behavior would plausibly surface + the expected artifact without exactly naming its path segment. + + Requires BOTH to pass: + 1. Subsystem coverage — at least one call reached the artifact's subsystem. + search_artifacts with no doc_type counts as covering all subsystems. + 2. Query relevance — at least one query contains trigger actor names, + the link_value domain, or event-type keywords from the question. + """ + target_sub = self._infer_artifact_subsystem(expected_entry) + + subsystem_covered = False + for call in calls: + direct_sub = self._TOOL_SUBSYSTEM_MAP.get(call.tool_name) + if direct_sub is not None: + if target_sub is None or direct_sub == target_sub: + subsystem_covered = True + break + elif call.tool_name == "search_artifacts": + doc_type = call.arguments.get("doc_type", "") + mapped_sub = self._SEARCH_DOCTYPE_MAP.get(doc_type) + if not doc_type or mapped_sub == target_sub or target_sub is None: + subsystem_covered = True + break + + if not subsystem_covered: + return False + + gt = question.get("ground_truth", {}) + semantic_keywords: Set[str] = set() + + for actor in question.get("trigger_actors", gt.get("trigger_actors", [])): + semantic_keywords.update( + w.lower() for w in str(actor).split() if len(w) > 2 + ) + for term in str(gt.get("link_value", "")).split(): + if len(term) > 2: + semantic_keywords.add(term.lower()) + for term in question.get("trigger_event_type", "").replace("_", " ").split(): + if len(term) > 2: + semantic_keywords.add(term.lower()) + + semantic_keywords -= self._QUERY_STOPWORDS + + if not semantic_keywords: + return subsystem_covered + + return any( + kw in arg for kw in semantic_keywords for arg in normalized_tool_args + ) + + def score_trajectory( + self, + trajectory: AgentTrajectory, + question: dict, + ) -> SilenceTrajectoryScore: + calls = trajectory.tool_calls + expected_space = set(question.get("expected_search_space", [])) + if not calls: + return SilenceTrajectoryScore(0.0, 0.0, 0.0, 1.0, 0.0) + + n = len(calls) + + # What did the agent search? + searched_ids: Set[str] = set() + searched_tool_args: List[str] = [] + for call in calls: + searched_ids.update(call.result_ids) + searched_tool_args.append(str(call.arguments).lower()) + + def _normalize_search_term(s: str) -> str: + """ + Extract the terminal component of a path-style search space entry. + e.g. "confluence/postmortems/IT-108" → "it-108" + "slack/channels/incidents" → "incidents" + "IT-108" → "it-108" + """ + return s.strip("/").split("/")[-1].lower() + + normalized_expected: Dict[str, str] = { + _normalize_search_term(e): e for e in expected_space + } + + # Also normalize all tool arg strings and result IDs for matching. + normalized_tool_args: List[str] = [arg.lower() for arg in searched_tool_args] + normalized_result_ids: Set[str] = { + _normalize_search_term(rid) for rid in searched_ids + } + + covered = set() + for norm_term, original in normalized_expected.items(): + if any(norm_term in arg for arg in normalized_tool_args): + covered.add(original) + elif norm_term in normalized_result_ids: + covered.add(original) + elif any(original.lower() in arg for arg in normalized_tool_args): + covered.add(original) + elif self._semantic_coverage_check( # ← new semantic tier + original, calls, question, normalized_tool_args + ): + covered.add(original) + + search_space_coverage = ( + len(covered) / len(expected_space) if expected_space else 1.0 + ) + + conclusion_text = str(trajectory.final_answer.get("reasoning", "")).lower() + conclusion_text += str(trajectory.final_answer.get("answer", "")).lower() + explicit_negative = any( + phrase in conclusion_text + for phrase in ( + "does not exist", + "was not created", + "no postmortem", + "not found", + "could not find", + "no record", + "never created", + "not in the corpus", + "no evidence", + "not present", + "absent", + ) + ) + correct_absence_conclusion = 1.0 if explicit_negative else 0.3 + + premature_conclusion = 1.0 - max(0.0, min(1.0, (n - 1) / 3)) + + horizon_violations = sum(1 for c in calls if c.horizon_violation) + horizon_discipline = ( + 1.0 # SILENCE has no temporal gate, so no violations possible + ) + + composite = ( + 0.50 * search_space_coverage + + 0.30 * correct_absence_conclusion + - 0.10 * premature_conclusion + + 0.10 * horizon_discipline + ) + + return SilenceTrajectoryScore( + search_space_coverage=round(search_space_coverage, 4), + correct_absence_conclusion=round(correct_absence_conclusion, 4), + premature_conclusion=round(premature_conclusion, 4), + horizon_discipline=round(horizon_discipline, 4), + composite=round(max(0.0, composite), 4), + ) + + def _extract_absence_conclusion(self, answer: Dict) -> Optional[bool]: + """True = artifact exists, False = artifact does not exist.""" + val = answer.get("exists", answer.get("found", answer.get("answer"))) + if isinstance(val, bool): + return val + # Schema violation — model did not return the required boolean field. + # Log and return None so the caller scores this as 0. + logger.warning( + f" SILENCE schema violation: 'exists' missing or non-boolean (got {val!r})" + ) + return None + + +# ───────────────────────────────────────────────────────────────────────────── +# AGENT RUNNER +# ───────────────────────────────────────────────────────────────────────────── + + +class AgenticEvalRunner: + """ + Runs the agent on each question and scores the result. + + For each question: + 1. Sets up the gated tool layer appropriate for the track + 2. Runs the agent with the typed tool surface + 3. Collects the trajectory + 4. Scores answer + trajectory with the track-specific scorer + 5. Combines scores with track-specific weights + """ + + def __init__( + self, + model: str = "claude-sonnet-4-6", + max_steps: int = 6, + ungated: bool = False, + zero_shot: bool = False, + call_delay: float = 1.0, + service_tier: str = "default", + backend: str = "bedrock", + ollama_url: str = "http://localhost:11434", + ollama_model: str = "gemma4", + ): + self._model = model + self._max_steps = max_steps + self._call_delay = call_delay + self._service_tier = service_tier + self._backend = backend + + from memory import Memory + + self._mem = Memory() + + if backend == "bedrock": + import boto3 + + self._bedrock = boto3.client("bedrock-runtime") + elif backend == "ollama": + self._ollama_base_url = ollama_url.rstrip("/") + self._ollama_model = ollama_model + self._model = ollama_model # override model name for logging/output + else: + raise ValueError( + f"Unknown backend: {backend!r}. Choose 'bedrock' or 'ollama'." + ) + + self._perspective_scorer = PerspectiveScorer() + self._counterfactual_scorer = CounterfactualScorer() + self._silence_scorer = SilenceScorer() + + # --ungated: all actor/subsystem gates disabled regardless of question type. + # Establishes the "god-mode" information ceiling for the Epistemic Tax. + self._ungated = ungated + + # --zero-shot: agent receives no tools at all (no corpus access). + # Establishes the hallucination / prior-knowledge floor. + # Mutually exclusive with --ungated; zero_shot takes precedence if both set. + self._zero_shot = zero_shot + + vis_path = EVAL_DIR / "actor_visibility.json" + if vis_path.exists(): + with open(vis_path) as f: + self._actor_visibility = json.load(f) + else: + logger.warning( + "actor_visibility.json not found — PERSPECTIVE gates will not be enforced" + ) + self._actor_visibility = {} + self._artifact_timestamps = { + doc["_id"]: doc.get("timestamp", "") + for doc in self._mem._db["artifacts"].find({}, {"_id": 1, "timestamp": 1}) + } + + def run( + self, + questions_path: Path, + out_path: Path, + question_types: Optional[List[str]] = None, + max_questions: Optional[int] = None, + ) -> None: + with open(questions_path) as f: + data = json.load(f) + + questions = data["questions"] + if question_types: + questions = [q for q in questions if q["question_type"] in question_types] + if max_questions: + questions = questions[:max_questions] + + logger.info( + f"Running agentic eval on {len(questions)} questions " + f"(model={self._model}, max_steps={self._max_steps})" + ) + + results: List[EvalResult] = [] + per_question: List[dict] = [] + token_counter = Counter() + + for i, question in enumerate(questions): + qtype = question["question_type"] + logger.info( + f"[{i + 1}/{len(questions)}] {qtype} — {question['question_id']}" + ) + + try: + result = self._run_question(question) + except Exception as exc: + logger.error(f" Failed: {exc}") + result = EvalResult( + question_id=question["question_id"], + question_type=qtype, + difficulty=question.get("difficulty", "unknown"), + answer_score=0.0, + answer_correct=False, + trajectory_score=0.0, + combined_score=0.0, + failure_reason=str(exc), + tool_call_count=0, + meta={"error": str(exc)}, + ) + + results.append(result) + per_question.append(result.to_dict()) + + token_counter["prompt"] += result.meta.get("prompt_tokens", 0) + token_counter["completion"] += result.meta.get("completion_tokens", 0) + token_counter["total"] += result.meta.get("total_tokens", 0) + + logger.info( + f" answer={result.answer_score:.3f} " + f"trajectory={result.trajectory_score:.3f} " + f"combined={result.combined_score:.3f} " + f"tools={result.tool_call_count} " + f"total tokens={result.meta.get('total_tokens', 0):,} " + f"input tokens={result.meta.get('prompt_tokens', 0):,} " + f"[total input tokens: {token_counter['prompt']:,}]" + f"[running: {token_counter['total']:,}]" + ) + + summary = self._aggregate(results) + out_path.parent.mkdir(parents=True, exist_ok=True) + output = { + "meta": { + "model": self._model, + "max_steps": self._max_steps, + "n_questions": len(results), + "track_weights": _TRACK_WEIGHTS, + }, + "summary": summary, + "per_question": per_question, + } + with open(out_path, "w") as f: + json.dump(output, f, indent=2, default=str) + + logger.info(f"Results written to {out_path}") + logger.info( + f"Overall — answer: {summary['overall']['answer_score']:.3f} " + f"trajectory: {summary['overall']['trajectory_score']:.3f} " + f"combined: {summary['overall']['combined_score']:.3f}" + + ( + f" | violation_adjusted: " + f"{summary['overall'].get('violation_adjusted_combined_score', 'n/a')}" + ) + ) + + def _run_question(self, question: dict) -> EvalResult: + qtype = question["question_type"] + ground_truth = question["ground_truth"] + + # Set up gated tools. + # --ungated: strip all actor/subsystem gates by passing None for both, + # regardless of question type. Temporal gate still applies. + # --zero-shot: GatedTools is still constructed (for consistent call + # logging infrastructure) but _tool_list() returns [] so the agent + # never actually invokes any tool. + as_of_time = self._infer_as_of_time(question) + actor_visible_by_subsystem = {} + + if self._ungated: + actor_visible = None + actor_subsystems = None + else: + if qtype == "PERSPECTIVE" and not self._ungated: + actor = question.get("actor", "") + as_of_time = question.get("as_of_time", "") + cone_data = self._actor_visibility.get(actor, {}) + if cone_data: + actor_visible = set( + aid + for ids in cone_data.get("visible_artifacts", {}).values() + for aid in ids + if self._artifact_timestamps.get(aid, "9999") <= as_of_time + ) + for subsystem, ids in cone_data.get( + "visible_artifacts", {} + ).items(): + filtered = { + aid + for aid in ids + if self._artifact_timestamps.get(aid, "9999") <= as_of_time + } + actor_visible_by_subsystem[subsystem] = filtered + actor_visible.update(filtered) + else: + logger.warning(f"No visibility cone found for {actor}") + actor_visible = set() + actor_subsystems = set(question.get("subsystem_access", [])) + else: + actor_visible = None + actor_subsystems = None + + tools = GatedTools( + mem=self._mem, + question=question, + as_of_time=as_of_time, + actor_visible_artifacts=actor_visible, + actor_visible_by_subsystem=actor_visible_by_subsystem, + actor_subsystem_access=actor_subsystems, + ) + + if question.get("question_type") == "GRAPH": + register_graph_tool(tools, self._mem, question) + + # Run agent + trajectory = self._run_agent(question, tools) + + # Score + if qtype == "PERSPECTIVE": + answer_score, answer_correct = self._perspective_scorer.score_answer( + trajectory.final_answer, ground_truth + ) + question_with_cone = { + **question, + "actor_visible_artifacts": list(actor_visible or []), + } + traj = self._perspective_scorer.score_trajectory( + trajectory, question_with_cone + ) + traj_score = traj.composite + traj_detail = asdict(traj) + + elif qtype == "COUNTERFACTUAL": + answer_score, answer_correct = self._counterfactual_scorer.score_answer( + trajectory.final_answer, ground_truth + ) + traj = self._counterfactual_scorer.score_trajectory( + trajectory, question, ground_truth + ) + traj_score = traj.composite + traj_detail = asdict(traj) + + elif qtype == "SILENCE": + answer_score, answer_correct = self._silence_scorer.score_answer( + trajectory.final_answer, ground_truth + ) + traj = self._silence_scorer.score_trajectory(trajectory, question) + traj_score = traj.composite + traj_detail = asdict(traj) + + elif qtype == "GRAPH": + graph_subtype = question.get("graph_subtype", "") + traj_score = score_graph_trajectory( + trajectory=trajectory, + question=question, + ground_truth=question.get("ground_truth", {}), + ) + answer_score, answer_correct = score_graph_answer( + agent_answer=trajectory.final_answer, + ground_truth=ground_truth, + graph_subtype=graph_subtype, + ) + traj_detail = {"traj_score": traj_score} # ← add this line + + else: + raise ValueError(f"Unknown question type: {qtype}") + + weights = _TRACK_WEIGHTS[qtype] + combined = weights["answer"] * answer_score + weights["trajectory"] * traj_score + + failure_reason = None + if trajectory.budget_exceeded: + failure_reason = "step_budget_exceeded" + + return EvalResult( + question_id=question["question_id"], + question_type=qtype, + difficulty=question.get("difficulty", "unknown"), + answer_score=round(answer_score, 4), + answer_correct=answer_correct, + trajectory_score=round(traj_score, 4), + combined_score=round(combined, 4), + failure_reason=failure_reason, + tool_call_count=len(trajectory.tool_calls), + meta={ + "model": self._model, + "eval_mode": ( + "zero_shot" + if self._zero_shot + else "ungated" + if self._ungated + else "gated" + ), + "as_of_time": as_of_time, + "budget_exceeded": trajectory.budget_exceeded, + "trajectory_detail": traj_detail, + "horizon_violations": trajectory.horizon_violations, + "actor_gate_violations": trajectory.actor_gate_violations, + "subsystem_violations": trajectory.subsystem_violations, + "dead_ends_hit": trajectory.dead_ends_hit, + "dead_ends_recovered": trajectory.dead_ends_recovered, + "total_latency_ms": round(trajectory.total_latency_ms, 1), + "tool_calls": [asdict(tc) for tc in trajectory.tool_calls], + "final_answer": trajectory.final_answer, + "prompt_tokens": trajectory.prompt_tokens, + "completion_tokens": trajectory.completion_tokens, + "total_tokens": trajectory.total_tokens, + }, + ) + + # ── Converse API tool definitions ──────────────────────────────────────── + + _CONVERSE_TOOL_DEFS: Dict[str, dict] = { + "get_ticket": { + "toolSpec": { + "name": "get_ticket", + "description": "Retrieve a Jira ticket by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "ticket_id": { + "type": "string", + "description": "Jira ticket ID, e.g. 'ENG-42'", + } + }, + "required": ["ticket_id"], + } + }, + } + }, + "get_confluence_page": { + "toolSpec": { + "name": "get_confluence_page", + "description": "Retrieve a Confluence page by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "page_id": { + "type": "string", + "description": "Confluence page ID", + } + }, + "required": ["page_id"], + } + }, + } + }, + "get_slack_thread": { + "toolSpec": { + "name": "get_slack_thread", + "description": "Retrieve a Slack thread by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "thread_id": { + "type": "string", + "description": "Slack thread ID", + } + }, + "required": ["thread_id"], + } + }, + } + }, + "get_email": { + "toolSpec": { + "name": "get_email", + "description": "Retrieve an email by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "email_id": { + "type": "string", + "description": "Email artifact ID", + } + }, + "required": ["email_id"], + } + }, + } + }, + "get_pr": { + "toolSpec": { + "name": "get_pr", + "description": "Retrieve a pull request by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "pr_id": { + "type": "string", + "description": "Pull request ID", + } + }, + "required": ["pr_id"], + } + }, + } + }, + "get_zd_ticket": { + "toolSpec": { + "name": "get_zd_ticket", + "description": "Retrieve a Zendesk support ticket by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "ticket_id": { + "type": "string", + "description": "Zendesk ticket ID", + } + }, + "required": ["ticket_id"], + } + }, + } + }, + "get_sf_opportunity": { + "toolSpec": { + "name": "get_sf_opportunity", + "description": "Retrieve a Salesforce opportunity by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "opp_id": { + "type": "string", + "description": "Salesforce opportunity ID", + } + }, + "required": ["opp_id"], + } + }, + } + }, + "get_sf_account": { + "toolSpec": { + "name": "get_sf_account", + "description": "Retrieve a Salesforce account by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "account_id": { + "type": "string", + "description": "Salesforce account ID", + } + }, + "required": ["account_id"], + } + }, + } + }, + "get_zoom_transcript": { + "toolSpec": { + "name": "get_zoom_transcript", + "description": "Retrieve a Zoom meeting transcript by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "transcript_id": { + "type": "string", + "description": "Zoom transcript ID", + } + }, + "required": ["transcript_id"], + } + }, + } + }, + "get_datadog_alert": { + "toolSpec": { + "name": "get_datadog_alert", + "description": "Retrieve a Datadog alert by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "alert_id": { + "type": "string", + "description": "Datadog alert ID", + } + }, + "required": ["alert_id"], + } + }, + } + }, + "get_invoice": { + "toolSpec": { + "name": "get_invoice", + "description": "Retrieve an invoice by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "invoice_id": { + "type": "string", + "description": "Invoice ID", + } + }, + "required": ["invoice_id"], + } + }, + } + }, + "get_nps_response": { + "toolSpec": { + "name": "get_nps_response", + "description": "Retrieve an NPS survey response by account name.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "account_name": { + "type": "string", + "description": "Account name", + } + }, + "required": ["account_name"], + } + }, + } + }, + "get_events_for_day": { + "toolSpec": { + "name": "get_events_for_day", + "description": "Retrieve simulation events for a given day, optionally filtered by type.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "day": { + "type": "integer", + "description": "Simulation day number", + }, + "event_type": { + "type": "string", + "description": "Optional event type filter", + }, + }, + "required": ["day"], + } + }, + } + }, + "search_artifacts": { + "toolSpec": { + "name": "search_artifacts", + "description": "Search for artifacts by keyword when you don't have a specific ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Keyword or artifact ID to search for", + }, + "doc_type": { + "type": "string", + "description": "Filter by type: jira, confluence, slack, email, pr, zd_ticket, zoom", + }, + "actor": { + "type": "string", + "description": "Optional actor name filter", + }, + "after_day": { + "type": "integer", + "description": "Only return artifacts from this day onward", + }, + }, + "required": ["query"], + } + }, + } + }, + "get_graph_snapshot": { + "toolSpec": { + "name": "get_graph_snapshot", + "description": "Returns the social graph for a given simulation day: nodes (actor names), edges (source, target, weight).", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "day": { + "type": "integer", + "description": "Simulation day number", + } + }, + "required": ["day"], + } + }, + } + }, + "get_stress_snapshot": { + "toolSpec": { + "name": "get_stress_snapshot", + "description": "Returns stress scores {name: int} for all actors on a given day. Stress >= 72 indicates burnout risk.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "day": { + "type": "integer", + "description": "Simulation day number", + } + }, + "required": ["day"], + } + }, + } + }, + } + + def _build_tool_dispatch(self, gated: GatedTools) -> Dict[str, Any]: + return { + "get_ticket": lambda **kw: gated.get_ticket(kw["ticket_id"]), + "get_confluence_page": lambda **kw: gated.get_confluence_page( + kw["page_id"] + ), + "get_slack_thread": lambda **kw: gated.get_slack_thread(kw["thread_id"]), + "get_email": lambda **kw: gated.get_email(kw["email_id"]), + "get_pr": lambda **kw: gated.get_pr(kw["pr_id"]), + "get_zd_ticket": lambda **kw: gated.get_zd_ticket(kw["ticket_id"]), + "get_sf_opportunity": lambda **kw: gated.get_sf_opportunity(kw["opp_id"]), + "get_sf_account": lambda **kw: gated.get_sf_account(kw["account_id"]), + "get_zoom_transcript": lambda **kw: gated.get_zoom_transcript( + kw["transcript_id"] + ), + "get_datadog_alert": lambda **kw: gated.get_datadog_alert(kw["alert_id"]), + "get_invoice": lambda **kw: gated.get_invoice(kw["invoice_id"]), + "get_nps_response": lambda **kw: gated.get_nps_response(kw["account_name"]), + "get_events_for_day": lambda **kw: gated.get_events_for_day( + kw["day"], kw.get("event_type") + ), + "search_artifacts": lambda **kw: gated.search_artifacts( + kw["query"], + kw.get("doc_type", ""), + actor=kw.get("actor", ""), + after_day=kw.get("after_day"), + ), + "get_graph_snapshot": lambda **kw: gated.get_graph_snapshot(kw["day"]), + "get_stress_snapshot": lambda **kw: gated.get_stress_snapshot(kw["day"]), + } + + _SUBSYSTEM_TOOLS: Dict[str, List[str]] = { + "jira": ["get_ticket"], + "confluence": ["get_confluence_page"], + "slack": ["get_slack_thread"], + "email": ["get_email"], + "invoice": ["get_invoice"], + "git": ["get_pr"], + "zendesk": ["get_zd_ticket"], + "salesforce": ["get_sf_opportunity", "get_sf_account", "get_nps_response"], + "zoom": ["get_zoom_transcript"], + "datadog": ["get_datadog_alert"], + } + + _SEARCH_SPACE_SUBSYSTEM: List[Tuple[str, str]] = [ + ("confluence/", "confluence"), + ("slack/", "slack"), + ("zendesk/", "zendesk"), + ("zoom/", "zoom"), + ("git/", "git"), + ("salesforce/", "salesforce"), + ("jira/", "jira"), + ("export/emails/", "email"), + ("CONF-", "confluence"), + ("ENG-", "jira"), + ("IT-", "jira"), + ("ZD-", "zendesk"), + ("PR-", "git"), + ("SF-OPP", "salesforce"), + ("SF-ACC", "salesforce"), + ("ZOOM-", "zoom"), + ("DD-", "datadog"), + ("INV-", "email"), + ("slack_", "slack"), + ("ext_email_", "email"), + ("EMAIL-", "email"), + ("INV-", "invoice"), + ] + + def _subsystems_for_question(self, question: dict) -> Optional[Set[str]]: + qtype = question.get("question_type", "") + + if qtype == "PERSPECTIVE": + visible = question.get("actor_visible_artifacts", []) + if visible: + needed: Set[str] = set() + for entry in visible: + for prefix, sub in self._SEARCH_SPACE_SUBSYSTEM: + if entry.startswith(prefix): + needed.add(sub) + break + if needed: + if not question.get("cross_subsystem") or len(needed) > 1: + return needed + + event_type = question.get("event_type", "") + for sub, types in _SUBSYSTEM_EVENT_TYPES.items(): + if event_type in types: + return {sub} + + subs = question.get("subsystem_access") + return set(subs) if subs else None + + if qtype == "COUNTERFACTUAL": + subs = question.get("subsystems_involved") + if subs: + return set(subs) + + gt = question.get("ground_truth", {}) + chain = gt.get("evidence_chain_artifacts", {}) + all_ids = chain.get("cause", []) + chain.get("effect", []) + needed: Set[str] = set() + for entry in all_ids: + for prefix, sub in self._SEARCH_SPACE_SUBSYSTEM: + if entry.startswith(prefix): + needed.add(sub) + break + if needed: + return needed + + cause_type = gt.get("cause_event_type", "") + effect_type = gt.get("effect_event_type", "") + for event_type in [cause_type, effect_type]: + for sub, types in _SUBSYSTEM_EVENT_TYPES.items(): + if event_type in types: + needed.add(sub) + if needed: + return needed + + return {"__events_only__"} + + if qtype == "GRAPH": + return None + + space = question.get("expected_search_space", []) + if not space: + return None + needed: Set[str] = set() + for entry in space: + for prefix, sub in self._SEARCH_SPACE_SUBSYSTEM: + if entry.startswith(prefix) or entry.lower().startswith(prefix.lower()): + needed.add(sub) + break + return needed if needed else None + + def _selected_tool_defs(self, question: dict) -> List[dict]: + if self._zero_shot: + return [] + + needed = self._subsystems_for_question(question) + + if needed is None: + return list(self._CONVERSE_TOOL_DEFS.values()) + + allowed: Set[str] = {"get_events_for_day", "search_artifacts"} + for sub in needed: + for tool_name in self._SUBSYSTEM_TOOLS.get(sub, []): + allowed.add(tool_name) + + selected = [v for k, v in self._CONVERSE_TOOL_DEFS.items() if k in allowed] + logger.debug( + f" _selected_tool_defs: {len(selected)}/{len(self._CONVERSE_TOOL_DEFS)} tools " + f"(subsystems={needed}): " + f"{[list(t['toolSpec'].keys())[0] if 'toolSpec' in t else '?' for t in selected]}" + ) + return selected + + def _build_system_prompt(self, question: dict) -> str: + qtype = question["question_type"] + + base = ( + "You are an Enterprise Knowledge Analyst evaluating enterprise AI systems. " + "You reason carefully over corporate documents to answer complex questions. " + "You cite evidence by artifact ID, stay within stated constraints, and never guess. " + "Respond ONLY with the requested JSON object — no preamble. " + "You have access to the full conversation history. Never call a tool to retrieve " + "an artifact you have already retrieved in a previous step. Re-read the earlier " + "result instead. IMPORTANT: Only call one tool at a time. Calling multiple tools " + "is a violation." + ) + + if self._zero_shot: + base += ( + "\nYou have no tools available. " + "Answer based on your knowledge alone and respond directly with the JSON object." + ) + if qtype == "COUNTERFACTUAL": + full_taxonomy = "\n".join( + f"- {k}: {v}" for k, v in self._CAUSAL_LINK_TAXONOMY.items() + ) + return ( + f"{base}\n\n" + f"IMPORTANT: This is a counterfactual question. You must identify the explicit " + f"causal link — do not speculate.\n\n" + f"You MUST categorize the link using one of the following labels:\n{full_taxonomy}\n\n" + f"Determine whether removing the cause would have changed the effect. " + f"You must return a boolean for outcome_changed. Use false if you cannot " + f"confirm the outcome changed. Never return null." + ) + if qtype == "PERSPECTIVE": + actor = question.get("actor", "the actor") + day = question.get("as_of_day", "?") + return ( + f"{base}\n\n" + f"IMPORTANT: You are answering from the perspective of {actor} as of Day {day}. " + f"Using information the actor would not have had is a scoring violation." + ) + return base + + if qtype == "PERSPECTIVE": + actor = question.get("actor", "the actor") + day = question.get("as_of_day", "?") + subs = ", ".join(question.get("subsystem_access", [])) + return ( + f"{base}\n\n" + f"IMPORTANT: You are answering from the perspective of {actor} as of Day {day}. " + f"This actor has credentials for: {subs}. " + f"Having credentials for a system does not mean the actor can see all content in it — " + f"access depends on the actor's role and direct involvement. " + f"Using information the actor would not have had is a scoring violation." + ) + + if qtype == "COUNTERFACTUAL": + full_taxonomy = "\n".join( + f"- {k}: {v}" for k, v in self._CAUSAL_LINK_TAXONOMY.items() + ) + return ( + f"{base}\n\n" + f"IMPORTANT: This is a counterfactual question. You must identify the explicit " + f"causal link in the data — do not speculate.\n\n" + f"You MUST categorize the link using one of the following labels:\n{full_taxonomy}\n\n" + f"Find the cause event and the effect event, then determine whether " + f"removing the cause would have changed the effect. " + f"Once you have retrieved both the cause artifact and the effect artifact, " + f"stop using tools immediately and provide your answer. " + "If you cannot find sufficient evidence to determine whether the outcome changed, " + "you must still return a boolean for outcome_changed. Use false if you cannot " + "confirm the outcome changed. Never return null." + ) + + if qtype == "GRAPH": + return ( + f"{base}\n\n" + "You have access to two additional tools for GRAPH questions:\n" + " get_graph_snapshot(day: int) — returns the collaboration graph for a given\n" + " simulation day: nodes (actor names), edges (source, target, weight).\n" + " Edge weights reflect interaction frequency; higher = stronger relationship.\n" + " get_stress_snapshot(day: int) — returns stress scores {name: int} for all\n" + " actors on a given day. Stress >= 72 indicates burnout risk.\n\n" + "To answer GRAPH questions you MUST call at least one of these tools.\n" + "Guessing from memory without calling a graph tool will score 0 on trajectory\n" + "even if the answer is correct." + ) + + return ( + f"{base}\n\n" + f"IMPORTANT: This is an absence question. Search the corpus before concluding absence. " + f"If you find clear evidence, stop immediately and answer. " + f"Only conclude absence after you have checked the expected locations." + ) + + def _build_user_prompt(self, question: dict) -> str: + qtype = question["question_type"] + allowed_links = self._allowed_links_str(question) + + schema = { + "PERSPECTIVE": ( + '{"could_actor_have_known": bool, "reasoning": "str", ' + '"evidence_artifacts": ["id", ...], "blocked_subsystems": ["str", ...]}' + ), + "COUNTERFACTUAL": ( + '{"outcome_changed": bool, ' + f'"causal_mechanism": "", ' + '"mechanism": "str", "actors": ["name", ...], "reasoning": "str"}' + ), + "SILENCE": ( + '{"exists": bool, "answer": "yes|no", ' + '"reasoning": "what you searched and found"}' + ), + "GRAPH": ( + '{"answer": "str", "reasoning": "str", ' + '"evidence_nodes": ["name", ...], "evidence_days": [int, ...]}' + ), + }[qtype] + + return f"{question.get('question_text') or question.get('question_prose', '')}\n\nRespond with JSON:\n{schema}" + + _CAUSAL_LINK_TAXONOMY: Dict[str, str] = { + "involves_gap": "incident ← knowledge gap", + "recurrence_of": "incident ← prior unresolved incident", + "spawned_doc": "confluence ← design discussion", + "email_dropped": "communication failure ← routing gap", + "sf_ownership_lapsed": "CRM gap ← employee departure", + "zd_escalation_source": "incident ← support ticket escalation", + "blocker_flagged": "blocker → delayed progress", + "incident_coordination": "incident → external contact", + "departure_reassignment": "departure → ticket/escalation shift", + "assignment_domain_mismatch": "planning mismatch → knowledge gap → incident", + "jira_from_vendor_email": "Jira ticket ← inbound vendor email", + "jira_from_customer_email": "Jira ticket ← inbound customer email", + "customer_escalation_relayed": "customer escalation ← support email routed", + "incident_handoff": "escalation chain ← forced handoff on departure", + "pr_gap_detected": "knowledge gap ← undocumented domain in PR review", + "async_gap_detected": "knowledge gap ← undocumented domain in async thread", + "centrality_vacuum": "knowledge gap ← key person departure", + "sf_stage_advanced_by_customer": "CRM stage advanced ← inbound customer email", + "feature_request_fyi": "product FYI ← inbound feature request", + "proactive_outreach_from_crm_signal": "outreach ← at-risk CRM signal", + "ticket_completion_notifies_lead": "lead notified ← dependent ticket done", + "org_collision_tension": "org tension ← overlapping responsibilities", + "postmortem_from_incident": "postmortem ← incident resolved", + "incident_triggers_risk_flag": "SF risk flag ← active incident", + "review_triggers_revision": "PR revision ← code review", + "hire_fills_knowledge_gap": "gap closed ← new hire expertise", + "escalation_from_zendesk": "incident ← Zendesk escalation", + } + + _LINK_DISTRACTORS: Dict[str, List[str]] = { + "involves_gap": ["recurrence_of", "assignment_domain_mismatch"], + "recurrence_of": ["involves_gap", "blocker_flagged"], + "spawned_doc": ["involves_gap", "incident_coordination"], + "email_dropped": ["sf_ownership_lapsed", "incident_coordination"], + "sf_ownership_lapsed": ["departure_reassignment", "email_dropped"], + "zd_escalation_source": ["involves_gap", "incident_coordination"], + "blocker_flagged": ["recurrence_of", "involves_gap"], + "incident_coordination": ["zd_escalation_source", "blocker_flagged"], + "departure_reassignment": ["sf_ownership_lapsed", "incident_handoff"], + "assignment_domain_mismatch": ["involves_gap", "recurrence_of"], + "jira_from_vendor_email": ["jira_from_customer_email", "incident_coordination"], + "jira_from_customer_email": [ + "jira_from_vendor_email", + "customer_escalation_relayed", + ], + "customer_escalation_relayed": [ + "zd_escalation_source", + "jira_from_customer_email", + ], + "incident_handoff": ["departure_reassignment", "centrality_vacuum"], + "pr_gap_detected": ["async_gap_detected", "involves_gap"], + "async_gap_detected": ["pr_gap_detected", "involves_gap"], + "centrality_vacuum": ["involves_gap", "departure_reassignment"], + "sf_stage_advanced_by_customer": [ + "proactive_outreach_from_crm_signal", + "feature_request_fyi", + ], + "feature_request_fyi": [ + "sf_stage_advanced_by_customer", + "jira_from_customer_email", + ], + "proactive_outreach_from_crm_signal": [ + "sf_stage_advanced_by_customer", + "incident_triggers_risk_flag", + ], + "ticket_completion_notifies_lead": [ + "blocker_flagged", + "departure_reassignment", + ], + "org_collision_tension": ["centrality_vacuum", "assignment_domain_mismatch"], + "postmortem_from_incident": ["spawned_doc", "involves_gap"], + "incident_triggers_risk_flag": [ + "proactive_outreach_from_crm_signal", + "escalation_from_zendesk", + ], + "review_triggers_revision": ["pr_gap_detected", "blocker_flagged"], + "hire_fills_knowledge_gap": ["centrality_vacuum", "involves_gap"], + "escalation_from_zendesk": ["zd_escalation_source", "incident_coordination"], + } + + def _trimmed_taxonomy(self, link_type: str) -> str: + if link_type and link_type in self._CAUSAL_LINK_TAXONOMY: + distractors = self._LINK_DISTRACTORS.get(link_type, []) + relevant = {link_type} | set(distractors) + return "\n".join( + f"- {k}: {v}" + for k, v in self._CAUSAL_LINK_TAXONOMY.items() + if k in relevant + ) + return "\n".join(f"- {k}: {v}" for k, v in self._CAUSAL_LINK_TAXONOMY.items()) + + def _allowed_links_str(self, question: dict) -> str: + # Always return the full set — narrowing leaks the answer. + return ", ".join(self._CAUSAL_LINK_TAXONOMY.keys()) + + _RETRYABLE_ERRORS = ( + "InternalServerException", + "ThrottlingException", + "ServiceUnavailableException", + "ModelTimeoutException", + ) + _MAX_RETRIES = 5 + _RETRY_BASE_DELAY = 2.0 # seconds; doubles each attempt + + # ── Ollama helpers ─────────────────────────────────────────────────────── + + @staticmethod + def _bedrock_tool_to_openai(tool: dict) -> dict: + """Convert a Bedrock toolSpec dict to OpenAI-compatible function tool format.""" + spec = tool["toolSpec"] + return { + "type": "function", + "function": { + "name": spec["name"], + "description": spec.get("description", ""), + "parameters": spec.get("inputSchema", {}).get("json", {}), + }, + } + + @staticmethod + def _bedrock_messages_to_openai(system_prompt: str, messages: list) -> list: + """ + Convert Bedrock Converse message format to OpenAI-compatible chat messages. + Bedrock uses content arrays with {text:}, {toolUse:}, {toolResult:} blocks. + OpenAI uses role/content strings plus tool_calls / tool role messages. + """ + result = [{"role": "system", "content": system_prompt}] + for msg in messages: + role = msg["role"] + content_blocks = msg.get("content", []) + + # Collect plain text and tool_use blocks + text_parts: list[str] = [] + tool_calls: list[dict] = [] + tool_results: list[dict] = [] + + for block in content_blocks: + if "text" in block: + cleaned = re.sub( + r"(?:<\|think\|>)?.*?", + "", + block["text"], + flags=re.DOTALL, + ).strip() + if cleaned: + text_parts.append(cleaned) + elif "toolUse" in block: + tu = block["toolUse"] + tool_calls.append( + { + "id": tu["toolUseId"], + "type": "function", + "function": { + "name": tu["name"], + "arguments": json.dumps(tu["input"]), + }, + } + ) + elif "toolResult" in block: + tr = block["toolResult"] + result_text = tr.get("content", [{}])[0].get("text", "") + tool_results.append( + { + "role": "tool", + "tool_call_id": tr["toolUseId"], + "content": result_text, + } + ) + + if tool_results: + result.extend(tool_results) + elif tool_calls: + out: dict = {"role": role} + if text_parts: + out["content"] = "\n".join(text_parts) + else: + out["content"] = None + out["tool_calls"] = tool_calls + result.append(out) + else: + result.append({"role": role, "content": "\n".join(text_parts)}) + + return result + + @staticmethod + def _openai_response_to_bedrock(response: dict) -> dict: + """ + Convert an OpenAI-compatible chat completion response to the Bedrock + Converse response shape that the rest of _run_agent expects. + + Thought blocks (<|channel>thought\n...) are stripped from + text content here so they never reach _parse_structured_answer or the + multi-turn message history (Gemma 4 best practice: thoughts must not + appear in history for subsequent turns). + """ + + choice = response["choices"][0] + msg = choice["message"] + finish_reason = choice.get("finish_reason", "stop") + + content_blocks: list[dict] = [] + + raw_text = (msg.get("reasoning") or msg.get("content") or "").strip() + clean_text = re.sub( + r"<\|think\|>.*?(?:<\|/think\|>|$)", "", raw_text, flags=re.DOTALL + ).strip() + if clean_text: + content_blocks.append({"text": clean_text}) + + tool_calls = msg.get("tool_calls") or [] + for tc in tool_calls: + content_blocks.append( + { + "toolUse": { + "toolUseId": tc["id"], + "name": tc["function"]["name"], + "input": json.loads(tc["function"]["arguments"] or "{}"), + } + } + ) + + stop_reason = "tool_use" if tool_calls else "end_turn" + if finish_reason == "length": + stop_reason = "max_tokens" + + usage = response.get("usage", {}) + return { + "output": {"message": {"role": "assistant", "content": content_blocks}}, + "stopReason": stop_reason, + "usage": { + "inputTokens": usage.get("prompt_tokens", 0), + "outputTokens": usage.get("completion_tokens", 0), + }, + } + + def _call_ollama(self, kwargs: Dict[str, Any]) -> dict: + """ + Translate a Bedrock Converse kwargs dict into an Ollama /v1/chat/completions + (OpenAI-compatible) request, call it, and return a Bedrock-shaped response. + + Gemma 4 best practices applied: + - Sampling: temperature=1.0, top_p=0.95, top_k=64 (via Ollama options) + - Thinking: <|think|> prepended to system prompt to enable thinking mode + - top_k is passed via the Ollama-specific `options` field (not a top-level + OpenAI param) because the /v1 endpoint forwards unknown options to the + model runtime. + - Thought blocks are stripped in _openai_response_to_bedrock so they + never appear in multi-turn history (Gemma 4 requirement). + """ + system_prompt = (kwargs.get("system") or [{}])[0].get("text", "") + + gemma_system_prompt = "<|think|>\n" + system_prompt + + bedrock_messages = kwargs.get("messages", []) + openai_messages = self._bedrock_messages_to_openai( + gemma_system_prompt, bedrock_messages + ) + + bedrock_tools = kwargs.get("toolConfig", {}).get("tools", []) + openai_tools = [self._bedrock_tool_to_openai(t) for t in bedrock_tools] + + inference = kwargs.get("inferenceConfig", {}) + payload: dict = { + "model": self._ollama_model, + "messages": openai_messages, + # Gemma 4 recommended sampling parameters + "temperature": inference.get("temperature", 0.0), + "max_tokens": inference.get("maxTokens", 4096), + "stream": False, + } + if openai_tools: + payload["tools"] = openai_tools + tool_choice = kwargs.get("toolConfig", {}).get("toolChoice") + if tool_choice: + if "tool" in tool_choice: + payload["tool_choice"] = { + "type": "function", + "function": {"name": tool_choice["tool"]["name"]}, + } + elif "any" in tool_choice: + payload["tool_choice"] = "required" + else: + payload["tool_choice"] = "auto" + else: + payload["tool_choice"] = "auto" + + logger.info(f" ollama payload: {json.dumps(payload, indent=2)}") + + url = f"{self._ollama_base_url}/v1/chat/completions" + resp = _requests.post(url, json=payload, timeout=600) + resp.raise_for_status() + + raw_response = resp.json() + logger.info(f" ollama raw response: {json.dumps(raw_response)!r}") + return self._openai_response_to_bedrock(raw_response) + + def _converse_with_retry(self, kwargs: Dict[str, Any]) -> dict: + """ + Dispatches to either Bedrock or Ollama depending on self._backend. + Applies exponential backoff for transient Bedrock errors. + """ + if self._backend == "ollama": + if self._call_delay > 0: + time.sleep(self._call_delay) + return self._call_ollama(kwargs) + + import random + + delay = self._RETRY_BASE_DELAY + last_exc: Exception = RuntimeError("No attempts made") + + for attempt in range(self._MAX_RETRIES): + try: + if self._call_delay > 0: + time.sleep(self._call_delay) + return self._bedrock.converse(**kwargs) + except Exception as exc: + last_exc = exc + exc_str = str(exc) + retryable = any(tag in exc_str for tag in self._RETRYABLE_ERRORS) + if not retryable: + raise + jitter = random.uniform(0, delay * 0.25) + wait = delay + jitter + logger.warning( + f" Bedrock transient error (attempt {attempt + 1}/{self._MAX_RETRIES}), " + f"retrying in {wait:.1f}s: {exc}" + ) + time.sleep(wait) + delay = min(delay * 2, 60.0) + + raise last_exc + + def _strip_confluence_content(self, messages: list, seen: set) -> list: + """Return messages with confluence page content stripped from all but the first occurrence. + + Bug fix: the original implementation immediately reassigned `seen` to a new + empty set, so deduplication never occurred and the context window grew + unboundedly on repeated confluence retrievals. + """ + stripped = [] + for msg in messages: + if msg["role"] != "user": + stripped.append(msg) + continue + new_content = [] + for block in msg.get("content", []): + if "toolResult" not in block: + new_content.append(block) + continue + tool_result = block["toolResult"] + result_text = tool_result.get("content", [{}])[0].get("text", "") + try: + doc = json.loads(result_text) + page_id = doc.get("id", "") + if page_id and page_id.startswith("CONF-") and "content" in doc: + if page_id in seen: + doc.pop("content") + doc["_note"] = "content already provided in earlier step" + block = { + "toolResult": { + **tool_result, + "content": [{"text": json.dumps(doc)}], + } + } + else: + seen.add(page_id) + except (json.JSONDecodeError, AttributeError): + pass + new_content.append(block) + stripped.append({**msg, "content": new_content}) + return stripped + + def _run_agent(self, question: dict, tools: GatedTools) -> AgentTrajectory: + """ + Runs the agent against the question using the Bedrock Converse API. + Returns a populated AgentTrajectory. + """ + + qtype = question["question_type"] + trajectory = AgentTrajectory( + question_id=question["question_id"], + question_type=qtype, + ) + + system_prompt = self._build_system_prompt(question) + user_prompt = self._build_user_prompt(question) + tool_defs = self._selected_tool_defs(question) + dispatch = self._build_tool_dispatch(tools) + + messages = [ + {"role": "user", "content": [{"text": user_prompt}]}, + ] + + converse_kwargs: Dict[str, Any] = { + "modelId": self._model, + "system": [{"text": system_prompt}], + "messages": messages, + "inferenceConfig": {"temperature": 0.0, "maxTokens": 4096}, + } + + if self._backend == "bedrock": + converse_kwargs["serviceTier"] = {"type": self._service_tier} + + if tool_defs: + converse_kwargs["toolConfig"] = { + "tools": tool_defs, + } + if self._backend == "bedrock": + if not self._model.startswith("us.anthropic."): + converse_kwargs["toolConfig"]["toolChoice"] = {"auto": {}} + elif self._model.startswith("us.anthropic."): + converse_kwargs["additionalModelRequestFields"] = { + "tool_choice": { + "type": "auto", + "disable_parallel_tool_use": True, + } + } + if self._model.startswith(("qwen.qwen3", "mistral.mistral")): + converse_kwargs["additionalModelRequestFields"] = { + "parallel_tool_calls": False + } + + _FINAL_ANSWER_TOOL_NAME = "final_answer" + _FINAL_ANSWER_SCHEMAS: Dict[str, dict] = { + "PERSPECTIVE": { + "type": "object", + "properties": { + "could_actor_have_known": {"type": "boolean"}, + "reasoning": {"type": "string"}, + "evidence_artifacts": { + "type": "array", + "items": {"type": "string"}, + }, + "blocked_subsystems": { + "type": "array", + "items": {"type": "string"}, + }, + }, + "required": [ + "could_actor_have_known", + "reasoning", + "evidence_artifacts", + "blocked_subsystems", + ], + }, + "COUNTERFACTUAL": { + "type": "object", + "properties": { + "outcome_changed": {"type": "boolean"}, + "causal_mechanism": {"type": "string"}, + "mechanism": {"type": "string"}, + "actors": {"type": "array", "items": {"type": "string"}}, + "reasoning": {"type": "string"}, + }, + "required": [ + "outcome_changed", + "causal_mechanism", + "mechanism", + "actors", + "reasoning", + ], + }, + "SILENCE": { + "type": "object", + "properties": { + "exists": {"type": "boolean"}, + "answer": {"type": "string"}, + "reasoning": {"type": "string"}, + }, + "required": ["exists", "answer", "reasoning"], + }, + "GRAPH": { + "type": "object", + "properties": { + "answer": {"type": "string"}, + "reasoning": {"type": "string"}, + "evidence_nodes": {"type": "array", "items": {"type": "string"}}, + "evidence_days": {"type": "array", "items": {"type": "integer"}}, + }, + "required": ["answer", "reasoning", "evidence_nodes", "evidence_days"], + }, + } + + t_start = time.time() + budget_exceeded = False + total_input = 0 + total_output = 0 + confluence_seen: set = set() + stop_reason = "end_turn" + output_message: dict = {"content": []} + + max_steps = self._max_steps + if qtype == "SILENCE": + search_space = question.get("expected_search_space", []) + max_steps = max(self._max_steps, len(search_space) + 5) + elif qtype == "GRAPH": + max_steps = max(self._max_steps, 4) + + for step in range(max_steps): + converse_kwargs["messages"] = self._strip_confluence_content( + messages, confluence_seen + ) + + if step == max_steps - 1: + logger.info(f" [step {step}] forcing final_answer tool for {qtype}") + final_answer_schema = _FINAL_ANSWER_SCHEMAS.get( + qtype, + { + "type": "object", + "properties": { + "answer": {"type": "string"}, + "reasoning": {"type": "string"}, + }, + "required": ["answer", "reasoning"], + }, + ) + final_answer_tool = { + "toolSpec": { + "name": "final_answer", + "description": ( + "You have reached your maximum number of steps. " + "Call this tool to provide your final answer based on what you have found so far." + ), + "inputSchema": {"json": final_answer_schema}, + } + } + + if self._backend == "ollama" or self._model.startswith( + ("mistral.", "moonshot.", "moonshotai.", "qwen.") + ): + converse_kwargs["toolConfig"] = { + "tools": [final_answer_tool], + "toolChoice": {"any": {}}, + } + converse_kwargs.pop("additionalModelRequestFields", None) + else: + converse_kwargs["toolConfig"] = { + "tools": [final_answer_tool], + "toolChoice": {"tool": {"name": _FINAL_ANSWER_TOOL_NAME}}, + } + converse_kwargs.pop("additionalModelRequestFields", None) + + try: + response = self._converse_with_retry(converse_kwargs) + except Exception as exc: + logger.error(f" LLM call error (gave up after retries): {exc}") + break + + usage = response.get("usage", {}) + total_input += usage.get("inputTokens", 0) + total_output += usage.get("outputTokens", 0) + + output_message = response["output"]["message"] + stop_reason = response.get("stopReason", "end_turn") + + messages.append(output_message) + logger.info(f" [step {step}] stop_reason: {stop_reason}") + for block in output_message["content"]: + if "text" in block: + logger.info(f" [step {step}] agent: {block['text'][:500]}") + elif "toolUse" in block: + logger.info( + f" [step {step}] tool_use: {block['toolUse']['name']}({block['toolUse']['input']})" + ) + + converse_kwargs["messages"] = messages + + if stop_reason in ("end_turn", "max_tokens"): + text_parts = [ + block["text"] + for block in output_message["content"] + if "text" in block + ] + raw = "\n".join(text_parts) + trajectory.final_answer = self._parse_structured_answer(raw) + break + + if stop_reason == "tool_use": + for block in output_message["content"]: + if ( + "toolUse" in block + and block["toolUse"]["name"] == _FINAL_ANSWER_TOOL_NAME + ): + trajectory.final_answer = block["toolUse"].get("input", {}) + logger.info( + f" [step {step}] final_answer extracted from forced tool call" + ) + break + + if trajectory.final_answer: + break + + tool_results = [] + for block in output_message["content"]: + if "toolUse" not in block: + continue + + tc = block["toolUse"] + fn_name = tc["name"] + fn_input = tc["input"] + tool_use_id = tc["toolUseId"] + + handler = dispatch.get(fn_name) + if handler: + t0 = time.time() + try: + result = handler(**fn_input) + except Exception as exc: + result = {"error": str(exc)} + tools._call_log.append( + ToolCall( + tool_name=fn_name, + arguments=fn_input, + result_ids=[], + result_types=[], + timestamp_requested=None, + timestamp_applied=None, + temporal_drift_days=None, + temporal_drift_violation=False, + horizon_violation=False, + actor_gate_violation=False, + subsystem_violation=False, + returned_empty=True, + latency_ms=(time.time() - t0) * 1000, + ) + ) + else: + result = {"error": f"Unknown tool: {fn_name}"} + + result_str = ( + result + if isinstance(result, str) + else json.dumps(result, default=str) + ) + + tool_results.append( + { + "toolResult": { + "toolUseId": tool_use_id, + "content": [{"text": result_str}], + } + } + ) + + messages.append({"role": "user", "content": tool_results}) + converse_kwargs["messages"] = messages + else: + break + else: + budget_exceeded = True + for msg in reversed(messages): + if msg.get("role") != "assistant": + continue + for block in msg.get("content", []): + if "text" in block: + trajectory.final_answer = self._parse_structured_answer( + block["text"] + ) + break + if trajectory.final_answer: + break + if not trajectory.final_answer: + logger.warning( + f" [budget_exceeded] No assistant text found in message history for {question['question_id']}" + ) + + trajectory.total_latency_ms = (time.time() - t_start) * 1000 + trajectory.budget_exceeded = budget_exceeded + trajectory.tool_calls = list(tools.call_log) + trajectory.final_answer = trajectory.final_answer or {} + trajectory.prompt_tokens = total_input + trajectory.completion_tokens = total_output + trajectory.total_tokens = total_input + total_output + trajectory.horizon_violations = sum( + 1 for c in trajectory.tool_calls if c.horizon_violation + ) + trajectory.actor_gate_violations = sum( + 1 for c in trajectory.tool_calls if c.actor_gate_violation + ) + trajectory.subsystem_violations = sum( + 1 for c in trajectory.tool_calls if c.subsystem_violation + ) + trajectory.dead_ends_hit = sum( + 1 for c in trajectory.tool_calls if c.returned_empty + ) + for i, call in enumerate(trajectory.tool_calls): + if call.returned_empty and i + 1 < len(trajectory.tool_calls): + if not trajectory.tool_calls[i + 1].returned_empty: + trajectory.dead_ends_recovered += 1 + + return trajectory + + def _parse_structured_answer(self, raw: str) -> Dict: + """Extract JSON from agent response. + + Strips, in order: + 1. Gemma 4 thinking blocks (<|channel>thought\n...) + 2. Markdown code fences (```json ... ```) + Then attempts JSON parsing with a regex fallback. + """ + + text = raw.strip() + text = re.sub(r"^```(?:json)?\s*", "", text) + text = re.sub(r"\s*```$", "", text) + try: + return json.loads(text) + except json.JSONDecodeError: + # Try to find JSON object in response + match = re.search(r"\{.*\}", text, re.DOTALL) + if match: + try: + return json.loads(match.group()) + except json.JSONDecodeError: + pass + return {"raw_response": raw} + + def _infer_as_of_time(self, question: dict) -> str: + qtype = question.get("question_type", "") + if qtype == "SILENCE": + events = self._mem.get_event_log(from_db=True) + max_day = max((e.day for e in events), default=1) + return _business_day_to_date(_SIM_START, max_day).isoformat() + if qtype == "PERSPECTIVE": + return question.get("as_of_time", datetime.now().isoformat()) + if qtype == "COUNTERFACTUAL": + effect_id = question.get("ground_truth", {}).get("effect_event_id") + if effect_id: + try: + ev = self._mem._db["events"].find_one({"_id": effect_id}) + if ev and ev.get("timestamp"): + return str(ev["timestamp"]) + except Exception: + pass + if qtype == "GRAPH": + day = question.get("as_of_day", 1) + return _business_day_to_date(_SIM_START, day).isoformat() + day = question.get("day", question.get("event_day", 1)) + return _business_day_to_date(_SIM_START, day).isoformat() + + def _aggregate(self, results: List[EvalResult]) -> dict: + def mean(vals): + return round(sum(vals) / len(vals), 4) if vals else 0.0 + + # ── Violation-adjusted scoring ──────────────────────────────────────── + # violation_rate = total_actor_gate_violations / total_tool_calls + # compliance_factor = max(0, 1 − violation_rate) ** _VIOLATION_EXPONENT + # adjusted_score = combined_score × compliance_factor + # + # Quadratic exponent (2) means violations compound non-linearly: + # 0% violations → 1.00× multiplier (no penalty) + # 25% violations → 0.56× multiplier + # 50% violations → 0.25× multiplier (score quartered) + # 75% violations → 0.06× multiplier (effectively disqualified) + # + # This decouples compliance from trajectory scoring and makes it a + # multiplicative gate at the aggregate level — a cheating agent cannot + # overcome the penalty through high answer accuracy alone. + _VIOLATION_EXPONENT = 2 + + def _compliance_tier(rate: float) -> str: + if rate < 0.05: + return "compliant" + if rate < 0.20: + return "borderline" + return "non_compliant" + + def _violation_adjusted(combined: float, violation_rate: float) -> float: + factor = max(0.0, 1.0 - violation_rate) ** _VIOLATION_EXPONENT + return round(combined * factor, 4) + + by_type: Dict[str, List[EvalResult]] = {} + by_difficulty: Dict[str, List[EvalResult]] = {} + for r in results: + by_type.setdefault(r.question_type, []).append(r) + by_difficulty.setdefault(r.difficulty, []).append(r) + + by_type_summary = {} + for qtype, rs in by_type.items(): + total_calls = sum(r.tool_call_count for r in rs) + total_violations = sum(r.meta.get("actor_gate_violations", 0) for r in rs) + violation_rate = ( + round(total_violations / total_calls, 4) if total_calls else 0.0 + ) + base_combined = mean([r.combined_score for r in rs]) + + summary: Dict[str, Any] = { + "n": len(rs), + "answer_score": mean([r.answer_score for r in rs]), + "trajectory_score": mean([r.trajectory_score for r in rs]), + "combined_score": base_combined, + "accuracy": round(sum(r.answer_correct for r in rs) / len(rs), 4), + "avg_tool_calls": mean([r.tool_call_count for r in rs]), + "budget_exceeded_count": sum( + 1 for r in rs if r.meta.get("budget_exceeded") + ), + } + + if qtype == "PERSPECTIVE": + compliance_factor = round( + max(0.0, 1.0 - violation_rate) ** _VIOLATION_EXPONENT, 4 + ) + summary.update( + { + "violation_rate": violation_rate, + "compliance_factor": compliance_factor, + "compliance_tier": _compliance_tier(violation_rate), + # Primary leaderboard axis — combined_score alone allows a + # cheating agent to rank above a disciplined one. This number + # prevents that by applying the compliance penalty independently + # of answer quality. + "violation_adjusted_combined_score": _violation_adjusted( + base_combined, violation_rate + ), + "avg_actor_gate_violations": mean( + [r.meta.get("actor_gate_violations", 0) for r in rs] + ), + "avg_subsystem_violations": mean( + [r.meta.get("subsystem_violations", 0) for r in rs] + ), + } + ) + elif qtype == "SILENCE": + summary["search_space_coverage"] = mean( + [ + r.meta.get("trajectory_detail", {}).get( + "search_space_coverage", 0 + ) + for r in rs + ] + ) + + by_type_summary[qtype] = summary + + # ── Global violation_adjusted_combined_score ────────────────────────── + # A single number for cross-model ranking. Agents without PERSPECTIVE + # questions are not penalised (violation_rate = 0, factor = 1.0). + all_calls = sum(r.tool_call_count for r in results) + all_violations = sum(r.meta.get("actor_gate_violations", 0) for r in results) + global_violation_rate = ( + round(all_violations / all_calls, 4) if all_calls else 0.0 + ) + overall_combined = mean([r.combined_score for r in results]) + + return { + "overall": { + "n": len(results), + "answer_score": mean([r.answer_score for r in results]), + "trajectory_score": mean([r.trajectory_score for r in results]), + "combined_score": overall_combined, + "accuracy": round( + sum(r.answer_correct for r in results) / len(results), 4 + ), + "avg_tool_calls": mean([r.tool_call_count for r in results]), + "budget_exceeded_count": sum( + 1 for r in results if r.meta.get("budget_exceeded") + ), + "global_violation_rate": global_violation_rate, + "global_compliance_factor": round( + max(0.0, 1.0 - global_violation_rate) ** _VIOLATION_EXPONENT, 4 + ), + "global_compliance_tier": _compliance_tier(global_violation_rate), + # Primary cross-track ranking number + "violation_adjusted_combined_score": _violation_adjusted( + overall_combined, global_violation_rate + ), + }, + "by_type": by_type_summary, + "by_difficulty": { + diff: { + "n": len(rs), + "answer_score": mean([r.answer_score for r in rs]), + "trajectory_score": mean([r.trajectory_score for r in rs]), + "combined_score": mean([r.combined_score for r in rs]), + } + for diff, rs in by_difficulty.items() + }, + } + + +# ───────────────────────────────────────────────────────────────────────────── +# ENTRYPOINT +# ───────────────────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + parser = argparse.ArgumentParser( + description="OrgForge Agentic Eval Harness - PERSPECTIVE, COUNTERFACTUAL, SILENCE" + ) + parser.add_argument( + "--questions", + type=Path, + default=EVAL_DIR / "eval_questions.json", + ) + parser.add_argument( + "--out", + type=Path, + default=EVAL_DIR / "agentic_results.json", + ) + parser.add_argument( + "--model", + type=str, + default="us.anthropic.claude-sonnet-4-6", + ) + parser.add_argument( + "--max-steps", + type=int, + default=5, + help="Max tool-use steps per question (SILENCE questions may need more)", + ) + parser.add_argument( + "--types", + nargs="+", + choices=["PERSPECTIVE", "COUNTERFACTUAL", "SILENCE", "GRAPH"], + help="Run only specific tracks", + ) + parser.add_argument( + "--max-questions", + type=int, + default=None, + ) + parser.add_argument( + "--ungated", + action="store_true", + default=False, + help=( + "Disable all actor/subsystem gates — god-mode corpus access. " + "Establishes the Epistemic Tax ceiling. " + "Default output: export/eval/ungated_results.json" + ), + ) + parser.add_argument( + "--zero-shot", + action="store_true", + default=False, + help=( + "Provide no tools to the agent (no corpus access). " + "Establishes the hallucination / prior-knowledge floor. " + "Default output: export/eval/zero_shot_results.json" + ), + ) + parser.add_argument( + "--call-delay", + type=float, + default=1.0, + metavar="SECONDS", + help="Sleep between LLM calls to avoid throttling (default: 1.0s). " + "Increase to 2-3 for Opus or if you keep hitting ThrottlingException.", + ) + parser.add_argument( + "--service-tier", + type=str, + default="default", + choices=["default", "flex"], + ) + parser.add_argument( + "--backend", + type=str, + default="bedrock", + choices=["bedrock", "ollama"], + help="LLM backend to use. 'bedrock' uses AWS Bedrock Converse API; " + "'ollama' uses a local Ollama instance via its OpenAI-compatible endpoint.", + ) + parser.add_argument( + "--ollama-url", + type=str, + default="http://localhost:11434", + metavar="URL", + help="Base URL of the Ollama server (default: http://localhost:11434). " + "Only used when --backend=ollama.", + ) + parser.add_argument( + "--ollama-model", + type=str, + default="gemma4", + metavar="MODEL", + help="Ollama model name to use (default: gemma4). " + "Only used when --backend=ollama. " + "Example values: gemma4, gemma4:27b, llama3.1, qwen2.5:14b", + ) + + args = parser.parse_args() + + stem = f"{args.model.replace('/', '_').replace(':', '_')}" + if args.zero_shot: + args.out = EVAL_DIR / f"zero_shot_{stem}.json" + elif args.ungated: + args.out = EVAL_DIR / f"ungated_{stem}.json" + else: + args.out = EVAL_DIR / f"gated_{stem}.json" + + runner = AgenticEvalRunner( + model=args.model, + max_steps=args.max_steps, + ungated=args.ungated, + zero_shot=args.zero_shot, + call_delay=args.call_delay, + service_tier=args.service_tier, + backend=args.backend, + ollama_url=args.ollama_url, + ollama_model=args.ollama_model, + ) + runner.run( + questions_path=args.questions, + out_path=args.out, + question_types=args.types, + max_questions=args.max_questions, + ) diff --git a/eval/agentic_eval_harness.py b/eval/agentic_eval_harness.py new file mode 100644 index 0000000..66b439e --- /dev/null +++ b/eval/agentic_eval_harness.py @@ -0,0 +1,4007 @@ +""" +agentic_eval_harness.py +======================= +OrgForge Agentic Evaluation Harness — v2 + +Evaluates AI agents on three novel tracks that require the deterministic +state machine to exist. No retrieval scoring. Each track has its own +trajectory model and scorer because the reasoning structure is fundamentally +different for each. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TRACK 1 — PERSPECTIVE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Temporal gate: as_of_time from question (actor's knowledge horizon) +Actor gate: tool calls filtered to actor_visible_artifacts +Trajectory: did the agent stay within the actor's visibility cone? + did it correctly identify what was and wasn't accessible? +Score penalty: using artifacts outside the actor's cone, even to reach + the correct answer. The point is epistemic discipline. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TRACK 2 — COUNTERFACTUAL +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Temporal gate: as_of_time of the effect event (agent can see everything up to + and including the effect to understand what happened) +No actor gate: agent has read access to all subsystems +Trajectory: did the agent identify the correct causal mechanism? + did it trace cause → effect correctly? +Answer scoring: structured extraction of (outcome_changed, mechanism, actors) + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TRACK 3 — SILENCE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Temporal gate: end of simulation (agent can see the full corpus) +No actor gate: agent has read access to all subsystems +Trajectory: CRITICAL — did the agent search expected_search_space before + concluding absence? A correct "no" without checking the right + places is scored as a trajectory failure even if the boolean is right. +Answer scoring: boolean only — did the agent correctly conclude absence? + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Score weights (per track) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Track Answer Trajectory Notes +─────────────── ─────── ────────── ───────────────────────── +PERSPECTIVE 0.40 0.60 Trajectory is primary — epistemic discipline matters +COUNTERFACTUAL 0.50 0.50 Both matter — wrong mechanism, wrong answer +SILENCE 0.30 0.70 Can't score a "no" without proof of search +""" + +from __future__ import annotations + +from collections import Counter +import json +import logging +import re +from statistics import mean +import time +from dataclasses import dataclass, field, asdict +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any, Dict, List, Optional, Set, Tuple +import argparse +from config_loader import CONFIG +import yaml + +from eval_harness import _ARTIFACT_SUBSYSTEM +from eval_divergence import NLIScorer + +from graph_eval_track import ( + register_graph_tool, + score_graph_trajectory, + score_graph_answer, +) + +logger = logging.getLogger("orgforge.agentic_eval") + +with open(Path(__file__).resolve().parent.parent / "config" / "config.yaml") as f: + _CFG = yaml.safe_load(f) + +_SIM_CFG = _CFG.get("simulation", {}) +BASE = Path(_SIM_CFG.get("output_dir", "./export")) +EVAL_DIR = BASE / "eval" +_SIM_START = datetime.strptime(CONFIG["simulation"]["start_date"], "%Y-%m-%d") + +_TRACK_WEIGHTS = { + "PERSPECTIVE": {"answer": 0.40, "trajectory": 0.60}, + "COUNTERFACTUAL": {"answer": 0.50, "trajectory": 0.50}, + "SILENCE": {"answer": 0.30, "trajectory": 0.70}, + "GRAPH": {"answer": 0.50, "trajectory": 0.50}, +} + +_DOCTYPE_TO_TOOL = { + "jira": "get_ticket", + "confluence": "get_confluence_page", + "slack": "get_slack_thread", + "email": "get_email", + "pr": "get_pr", + "zd_ticket": "get_zd_ticket", + "sf_opp": "get_sf_opportunity", + "sf_account": "get_sf_account", + "zoom": "get_zoom_transcript", + "datadog": "get_datadog_alert", + "invoice": "get_invoice", + "nps": "get_nps_response", +} + + +_TOOL_SUBSYSTEM = { + "get_ticket": "jira", + "get_confluence_page": "confluence", + "get_slack_thread": "slack", + "get_email": "email", + "get_pr": "git", + "get_zd_ticket": "zendesk", + "get_sf_opportunity": "salesforce", + "get_sf_account": "salesforce", + "get_zoom_transcript": "zoom", + "get_datadog_alert": "datadog", + "get_invoice": "invoice", + "get_nps_response": "salesforce", + "get_events_for_day": None, + "search_artifacts": None, +} + +_SUBSYSTEM_EVENT_TYPES: Dict[str, Set[str]] = { + "jira": { + "incident_opened", + "incident_resolved", + "ticket_progress", + "pr_review", + "sprint_planned", + "sprint_goal_updated", + "jira_ticket_created", + "postmortem_created", + }, + "slack": { + "standup", + "normal_day_slack", + "watercooler_chat", + "farewell_message", + "onboarding_session", + "warmup_1on1", + "morale_intervention", + "1on1_scheduled", + }, + "confluence": { + "confluence_created", + "design_discussion", + "retrospective", + "leadership_sync", + }, + "git": { + "pr_review", + "code_review_comment", + }, + "email": { + "inbound_external_email", + "customer_email_routed", + "vendor_email_routed", + "hr_outbound_email", + "sales_outbound_email", + "email_dropped", + "hr_checkin", + }, + "zoom": { + "zoom_meeting", + "design_discussion", + "vendor_meeting", + "async_question", + "deep_work_session", + }, + "salesforce": { + "crm_touchpoint", + "crm_account_at_risk", + "customer_health_briefing", + "feature_request_from_sales", + "stability_update_to_sales", + "proactive_outreach_initiated", + "sf_deals_risk_flagged", + }, + "zendesk": { + "zd_ticket_opened", + "zd_tickets_escalated", + "zd_tickets_resolved", + "customer_escalation", + }, + "datadog": { + "dlp_alert", + "secret_detected", + }, +} + +# Sim-internal types never exposed to any actor +_INTERNAL_EVENT_TYPES = { + "knowledge_gap_detected", + "escalation_chain", + "assignment_domain_mismatch", + "sf_ownership_lapsed", + "fix_in_progress", + "day_summary", + "employee_departed", + "employee_hired", + "external_contact_summarized", + "vendor_email_routed", + "secret_detected", + "deep_work_session", + "watercooler_chat", + "ticket_progress", + "standup", + "dept_plan_created", + "mentoring", + "inbound_external_email", + "customer_email_routed", +} + +KNOWN_EVENT_TYPES = { + "incident_opened", + "incident_resolved", + "escalation_chain", + "fix_in_progress", + "postmortem_created", + "knowledge_gap_detected", + "standup", + "pr_review", + "ticket_progress", + "design_discussion", + "async_question", + "code_review_comment", + "deep_work_session", + "sprint_planned", + "retrospective", + "sprint_goal_updated", + "leadership_sync", + "feature_request_from_sales", + "stability_update_to_sales", + "hr_checkin", + "morale_intervention", + "1on1_scheduled", + "external_contact_summarized", + "vendor_meeting", + "customer_escalation", + "normal_day_slack", + "confluence_created", + "day_summary", + "employee_departed", + "employee_hired", + "onboarding_session", + "farewell_message", + "warmup_1on1", + "watercooler_chat", + "inbound_external_email", + "customer_email_routed", + "customer_escalation", + "vendor_email_routed", + "hr_outbound_email", + "email_dropped", + "dlp_alert", + "secret_detected", + "zoom_meeting", + "sales_outbound_email", + "proactive_outreach_initiated", + "zd_ticket_opened", + "zd_tickets_escalated", + "zd_tickets_resolved", + "sf_deals_risk_flagged", + "sf_ownership_lapsed", + "crm_touchpoint", + "crm_account_at_risk", + "customer_health_briefing", + "assignment_domain_mismatch", +} + +_JIRA_PROJECT_ACCESS: Dict[str, Set[str]] = { + "ENG": {"engineering_backend", "engineering_mobile", "ceo"}, + "HR": {"hr_ops", "ceo"}, + "SALES": {"sales_marketing", "ceo"}, + "PROD": {"product", "ceo"}, + "DES": {"design", "product", "ceo"}, + "QA": {"qa_support", "ceo"}, + "ORG": { + "engineering_backend", + "engineering_mobile", + "product", + "ceo", + "hr_ops", + "design", + "sales_marketing", + "qa_support", + }, +} + +_TEMPORAL_DRIFT_THRESHOLD_DAYS = 5 + + +def _jira_project_visible(ticket_id: str, role: str) -> bool: + prefix = ticket_id.split("-")[0] if "-" in ticket_id else "" + allowed_roles = _JIRA_PROJECT_ACCESS.get(prefix) + if allowed_roles is None: + return True # unknown prefix — don't restrict + return role in allowed_roles + + +def _business_day_to_date(start: datetime, n: int) -> datetime: + """Convert a 1-based business day counter to a calendar date.""" + current = start + days_counted = 0 + while days_counted < n: + current += timedelta(days=1) + if current.weekday() < 5: + days_counted += 1 + return current + + +def _date_to_business_day(start: datetime, target: datetime) -> int: + count = 0 + current = start + while current < target: + current += timedelta(days=1) + if current.weekday() < 5: + count += 1 + return count + + +# ───────────────────────────────────────────────────────────────────────────── +# DATA CLASSES +# ───────────────────────────────────────────────────────────────────────────── + + +@dataclass +class ToolCall: + tool_name: str + arguments: Dict[str, Any] + result_ids: List[str] + result_types: List[str] + timestamp_requested: Optional[str] + timestamp_applied: Optional[str] + temporal_drift_days: Optional[float] + temporal_drift_violation: bool + horizon_violation: bool + actor_gate_violation: bool + subsystem_violation: bool + returned_empty: bool + latency_ms: float + + +@dataclass +class AgentTrajectory: + question_id: str + question_type: str + tool_calls: List[ToolCall] = field(default_factory=list) + final_answer: Dict[str, Any] = field(default_factory=dict) + total_latency_ms: float = 0.0 + horizon_violations: int = 0 + actor_gate_violations: int = 0 # PERSPECTIVE track + subsystem_violations: int = 0 # PERSPECTIVE track + search_space_coverage: float = 0.0 # SILENCE track + causal_mechanism_found: bool = False # COUNTERFACTUAL track + graph_tool_called: bool = False # GRAPH track + graph_correct_day_queried: bool = False + dead_ends_hit: int = 0 + dead_ends_recovered: int = 0 + budget_exceeded: bool = False + prompt_tokens: int = 0 + completion_tokens: int = 0 + total_tokens: int = 0 + + +@dataclass +class PerspectiveTrajectoryScore: + epistemic_discipline: float # 1.0 - (cone violations / total calls) + subsystem_discipline: float # 1.0 - (subsystem violations / total calls) + horizon_discipline: float # 1.0 - (horizon violations / total calls) + temporal_precision: float + temporal_drift_discipline: float + conclusion_grounding: float # did final answer cite in-cone artifacts? + dead_end_recovery: float + composite: float + + +@dataclass +class CounterfactualTrajectoryScore: + cause_identified: float # did agent retrieve the cause event? + effect_identified: float # did agent retrieve the effect event? + mechanism_correct: float # did agent name the correct link_type? + causal_chain_complete: float # did agent traverse cause → effect in order? + horizon_discipline: float + composite: float + + +@dataclass +class SilenceTrajectoryScore: + search_space_coverage: float # fraction of expected_search_space the agent checked + correct_absence_conclusion: float # did agent explicitly conclude "does not exist"? + premature_conclusion: float # did agent conclude before searching? (penalty) + horizon_discipline: float + composite: float + + +@dataclass +class EvalResult: + question_id: str + question_type: str + difficulty: str + answer_score: float + answer_correct: bool + trajectory_score: float + combined_score: float + failure_reason: Optional[str] + tool_call_count: int + meta: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict: + return asdict(self) + + +class GatedTools: + """ + Wraps the document corpus and enforces gates per question type. + + PERSPECTIVE: temporal gate (as_of_time) + actor gate (visibility cone) + COUNTERFACTUAL: temporal gate only (as_of_time = effect event timestamp) + SILENCE: no gate (agent sees full corpus — the absence must be real) + + Violations are logged but results are still returned (the agent should + observe them and self-correct). Violations penalize trajectory score. + """ + + def __init__( + self, + mem, + question: dict, + as_of_time: str, + actor_visible_artifacts: Optional[Set[str]] = None, + actor_visible_by_subsystem: Optional[Dict[str, Set[str]]] = None, + actor_subsystem_access: Optional[Set[str]] = None, + ): + self._mem = mem + self._question = question + self._as_of_time = as_of_time + self._actor_visible = actor_visible_artifacts or set() + self._actor_visible_by_subsystem = actor_visible_by_subsystem or {} + self._actor_subsystems = actor_subsystem_access + self._question_type = question.get("question_type", "") + self._call_log: List[ToolCall] = [] + self._confluence_seen: set = set() + + def _gate_ts(self) -> str: + if self._question_type == "SILENCE": + if not hasattr(self, "_silence_gate_ts"): + events = self._mem.get_event_log(from_db=True) + max_day = max((e.day for e in events), default=1) if events else 1 + self._silence_gate_ts = _business_day_to_date( + _SIM_START, max_day + ).isoformat() + return self._silence_gate_ts + return self._as_of_time + + @property + def call_log(self) -> List[ToolCall]: + return self._call_log + + def _temporal_gate(self, doc: dict) -> bool: + ts = doc.get("timestamp") or doc.get("created") or doc.get("date") + if not ts: + return True + try: + return datetime.fromisoformat(str(ts)) <= datetime.fromisoformat( + self._gate_ts() + ) + except (ValueError, TypeError): + return True + + def _check_actor_gate(self, doc_id: str, doc_type: str) -> Tuple[bool, bool]: + """ + Returns (actor_gate_violation, subsystem_violation). + Only meaningful for PERSPECTIVE questions. + """ + if self._question_type != "PERSPECTIVE": + return False, False + + if doc_type in ("jira", "jira_tickets"): + if not _jira_project_visible(doc_id, self._question.get("actor_role", "")): + return True, False + + subsystem = _ARTIFACT_SUBSYSTEM.get(doc_type, "default") + + subsystem_violation = ( + bool(self._actor_subsystems) + and subsystem not in self._actor_subsystems + and subsystem != "default" + ) + + actor_gate_violation = ( + bool(self._actor_visible) and doc_id not in self._actor_visible + ) + + return actor_gate_violation, subsystem_violation + + def _record( + self, + tool_name: str, + arguments: Dict, + results: List[dict], + t0: float, + horizon_violation: bool = False, + timestamp_applied: Optional[str] = None, + ) -> List[dict]: + latency = (time.time() - t0) * 1000 + filtered = [r for r in results if self._temporal_gate(r)] + horizon_violation = horizon_violation or len(filtered) < len(results) + + result_ids = [str(r.get("id", r.get("_id", ""))) for r in filtered] + result_types = [str(r.get("doc_type", r.get("type", ""))) for r in filtered] + + actor_gate_violation = False + subsystem_violation = False + for rid, rtype in zip(result_ids, result_types): + agv, sv = self._check_actor_gate(rid, rtype) + if agv: + actor_gate_violation = True + if sv: + subsystem_violation = True + + # Check subsystem from tool name too + tool_subsystem = _TOOL_SUBSYSTEM.get(tool_name) + if ( + self._question_type == "PERSPECTIVE" + and tool_subsystem + and self._actor_subsystems + and tool_subsystem not in self._actor_subsystems + ): + subsystem_violation = True + + requested = arguments.get("as_of_time") + drift = None + if requested and timestamp_applied: + try: + drift = ( + datetime.fromisoformat(timestamp_applied) + - datetime.fromisoformat(requested) + ).days + except (ValueError, TypeError): + pass + + temporal_drift_violation = ( + drift is not None and drift < -_TEMPORAL_DRIFT_THRESHOLD_DAYS + ) + + self._call_log.append( + ToolCall( + tool_name=tool_name, + arguments=arguments, + result_ids=result_ids, + result_types=result_types, + timestamp_requested=requested, + timestamp_applied=timestamp_applied, + temporal_drift_days=drift, + temporal_drift_violation=temporal_drift_violation, + horizon_violation=horizon_violation, + actor_gate_violation=actor_gate_violation, + subsystem_violation=subsystem_violation, + returned_empty=len(filtered) == 0, + latency_ms=latency, + ) + ) + return filtered + + _COLLECTION_TS_FIELD = { + "jira": "created_at", + "jira_tickets": "created_at", + "confluence": "timestamp", + "slack": "timestamp", + "email": "timestamp", + "pr": "created_at", + "zd_ticket": "timestamp", + "sf_opp": "timestamp", + "sf_account": "timestamp", + "zoom": "timestamp", + "datadog": "timestamp", + "invoice": "timestamp", + "nps": "timestamp", + } + + def _build_query( + self, + base: dict, + doc_type: str = "", + id_field: str = "id", + agent_as_of_time: Optional[str] = None, + ) -> Tuple[Optional[dict], str]: + """ + Constructs a MongoDB filter with temporal and actor gates applied. + base: the caller's own filter fields e.g. {"id": ticket_id} + doc_type: the artifact type for subsystem gate checking + """ + ceiling = self._gate_ts() + if agent_as_of_time: + effective_ts = min(agent_as_of_time, ceiling) + else: + effective_ts = ceiling + + query = {**base} + + ts_field = self._COLLECTION_TS_FIELD.get(doc_type, "timestamp") + query[ts_field] = {"$lte": effective_ts} + + if self._question_type == "PERSPECTIVE" and doc_type: + subsystem = _ARTIFACT_SUBSYSTEM.get(doc_type, "default") + + if ( + self._actor_subsystems + and subsystem not in self._actor_subsystems + and subsystem != "default" + ): + return None, effective_ts + + if self._actor_visible: + query[id_field] = {"$in": list(self._actor_visible)} + if id_field in base: + requested_id = base[id_field] + query[id_field] = ( + requested_id + if requested_id in self._actor_visible + else "__blocked__" + ) + + return query, effective_ts + + def get_ticket(self, ticket_id: str) -> dict: + t0 = time.time() + gate = self._gate_ts() + query, effective_ts = self._build_query({"id": ticket_id}, doc_type="jira") + if query is None: + self._record( + "get_ticket", + {"ticket_id": ticket_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + doc = ( + self._mem._db["jira_tickets"].find_one( + query, + { + "_id": 0, + "causal_chain": 0, + "dept_type": 0, + "story_points": 0, + "bot_threads": 0, + "escalation_narrative": 0, + "gap_areas": 0, + "recurrence_chain_depth": 0, + "recurrence_chain_root": 0, + "recurrence_gap_days": 0, + "recurrence_of": 0, + "prior_postmortem": 0, + "sprint": 0, + }, + ) + or {} + ) + + if doc: + comments = doc.get("comments", []) + doc["comments"] = [c for c in comments if c.get("created", "9999") <= gate] + + created = doc.get("created_at", "9999") + in_progress_day = doc.get("in_progress_since") + in_review_day = doc.get("in_review_since") + + def day_to_iso(day): + return ( + _business_day_to_date(_SIM_START, day).isoformat() + if day + else "9999" + ) + + in_progress_dt = day_to_iso(in_progress_day) + in_review_dt = day_to_iso(in_review_day) + completed = ( + doc.get("updated_at", "9999") if doc.get("status") == "Done" else "9999" + ) + + if completed <= gate: + derived_status = "Done" + elif in_review_dt <= gate: + derived_status = "In Review" + elif in_progress_dt <= gate: + derived_status = "In Progress" + else: + derived_status = "To Do" + + doc["status"] = derived_status + if derived_status != "Done": + doc.pop("completion_artifact", None) + + if doc.get("linked_prs"): + visible_prs = [] + for pr_id in doc["linked_prs"]: + pr = self._mem._db["prs"].find_one( + {"id": pr_id, "created_at": {"$lte": gate}}, {"id": 1} + ) + if pr: + visible_prs.append(pr_id) + doc["linked_prs"] = visible_prs + + if in_progress_day: + if in_progress_dt > gate: + doc.pop("in_progress_since", None) + if in_review_day: + if in_review_dt > gate: + doc.pop("in_review_since", None) + doc.pop("last_review_requested_day", None) + + doc["comments"] = [ + {k: v for k, v in c.items() if k not in ("day", "updated")} + for c in comments + if c.get("created", "9999") <= gate + ] + + results = self._record( + "get_ticket", + {"ticket_id": ticket_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + + return results[0] if results else {} + + def get_confluence_page(self, page_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query({"_id": page_id}, doc_type="confluence") + if query is None: + self._record("get_confluence_page", {"page_id": page_id}, [], t0) + return {} + query["type"] = "confluence" + + doc = ( + self._mem._db["artifacts"].find_one( + query, + { + "_id": 0, + "embedding": 0, + "type": 0, + "metadata": 0, + "date": 0, + "timestamp": 0, + }, + ) + or {} + ) + + results = self._record( + "get_confluence_page", + {"page_id": page_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + + return results[0] if results else {} + + def get_slack_thread(self, thread_id: str) -> List[dict]: + t0 = time.time() + query, effective_ts = self._build_query( + {"_id": thread_id}, + doc_type="slack", + id_field="_id", + ) + if query is None: + return self._record("get_slack_thread", {"thread_id": thread_id}, [], t0) + query["type"] = "slack_thread" + doc = ( + self._mem._db["artifacts"].find_one(query, {"_id": 0, "embedding": 0}) or {} + ) + docs = [doc] if doc else [] + return self._record( + "get_slack_thread", + {"thread_id": thread_id}, + docs, + t0, + timestamp_applied=effective_ts, + ) + + def get_email(self, email_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query({"_id": email_id}, doc_type="email") + if query is None: + self._record("get_email", {"email_id": email_id}, [], t0) + return {} + query["type"] = "email" + doc = ( + self._mem._db["artifacts"].find_one(query, {"_id": 0, "embedding": 0}) or {} + ) + results = self._record( + "get_email", + {"email_id": email_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_pr(self, pr_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query({"pr_id": pr_id}, doc_type="pr") + if query is None: + self._record("get_pr", {"pr_id": pr_id}, [], t0) + return {} + doc = ( + self._mem._db["pull_requests"].find_one( + query, {"_id": 0, "author_email": 0} + ) + or {} + ) + + results = self._record( + "get_pr", + {"pr_id": pr_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + + return results[0] if results else {} + + def get_zd_ticket(self, ticket_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query({"id": ticket_id}, doc_type="zd_ticket") + if query is None: + self._record("get_zd_ticket", {"ticket_id": ticket_id}, [], t0) + return {} + doc = self._mem._db["zd_tickets"].find_one(query, {"_id": 0}) or {} + results = self._record( + "get_zd_ticket", + {"ticket_id": ticket_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_sf_opportunity(self, opp_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query( + {"id": opp_id}, doc_type="sf_opportunity" + ) + if query is None: + self._record("get_sf_opportunity", {"opp_id": opp_id}, [], t0) + return {} + doc = self._mem._db["salesforce_opps"].find_one(query, {"_id": 0}) or {} + results = self._record( + "get_sf_opportunity", + {"opp_id": opp_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_sf_account(self, account_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query( + {"id": account_id}, doc_type="sf_account" + ) + if query is None: + self._record("get_sf_account", {"account_id": account_id}, [], t0) + return {} + doc = self._mem._db["salesforce_accounts"].find_one(query, {"_id": 0}) or {} + results = self._record( + "get_sf_account", + {"account_id": account_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_zoom_transcript(self, transcript_id: str) -> dict: + t0 = time.time() + query, effective_ts = self._build_query({"id": transcript_id}, doc_type="zoom") + if query is None: + self._record( + "get_zoom_transcript", {"transcript_id": transcript_id}, [], t0 + ) + return {} + + doc = ( + self._mem._db["artifacts"].find_one(query, {"_id": 0, "embedding": 0}) or {} + ) + if not doc: + self._record( + "get_zoom_transcript", + {"transcript_id": transcript_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + date_str = doc.get("date", "") + md_path = BASE / "zoom" / date_str / f"{transcript_id}.md" + try: + doc["transcript"] = md_path.read_text(encoding="utf-8") + except FileNotFoundError: + logger.warning( + f"[get_zoom_transcript] Transcript file not found: {md_path}" + ) + doc["transcript"] = doc.get("content", "") + + results = self._record( + "get_zoom_transcript", + {"transcript_id": transcript_id}, + [doc], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_datadog_alert(self, alert_id: str) -> dict: + t0 = time.time() + effective_ts = self._gate_ts() + + path = BASE / "datadog" / "alerts.jsonl" + if not path.exists(): + self._record( + "get_datadog_alert", + {"alert_id": alert_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + doc = None + with open(path) as f: + for line in f: + try: + alert = json.loads(line) + if alert.get("id") == alert_id: + doc = alert + break + except json.JSONDecodeError: + continue + + if not doc: + self._record( + "get_datadog_alert", + {"alert_id": alert_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + if self._question_type != "SILENCE": + date_happened = doc.get("date_happened", 0) + if date_happened: + doc_ts = datetime.fromtimestamp(date_happened).isoformat() + if doc_ts > self._gate_ts(): + self._record( + "get_datadog_alert", + {"alert_id": alert_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + results = self._record("get_datadog_alert", {"alert_id": alert_id}, [doc], t0) + return results[0] if results else {} + + def get_invoice(self, invoice_id: str) -> dict: + t0 = time.time() + effective_ts = self._gate_ts() + + path = BASE / "invoices" / f"{invoice_id}.json" + if not path.exists(): + self._record( + "get_invoice", + {"invoice_id": invoice_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + doc = json.loads(path.read_text()) + + ts = doc.get("timestamp") or doc.get("date") or doc.get("created_at", "") + if self._question_type != "SILENCE" and ts and ts > effective_ts: + self._record( + "get_invoice", + {"invoice_id": invoice_id}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + results = self._record( + "get_invoice", + {"invoice_id": invoice_id}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + def get_nps_response(self, account_name: str) -> dict: + t0 = time.time() + effective_ts = self._gate_ts() + + fname = ( + account_name.lower().replace(" ", "_").replace(".", "").replace(",", "") + + ".json" + ) + path = BASE / "nps" / "responses" / fname + if not path.exists(): + self._record( + "get_nps_response", + {"account_name": account_name}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + doc = json.loads(path.read_text()) + + ts = doc.get("timestamp") or doc.get("date") or doc.get("created_at", "") + if self._question_type != "SILENCE" and ts and ts > effective_ts: + self._record( + "get_nps_response", + {"account_name": account_name}, + [], + t0, + timestamp_applied=effective_ts, + ) + return {} + + results = self._record( + "get_nps_response", + {"account_name": account_name}, + [doc] if doc else [], + t0, + timestamp_applied=effective_ts, + ) + return results[0] if results else {} + + _ARTIFACT_ID_BLOCKLIST = { + "eml_path", + "artifact_path", + "slack_path", + "jira", + "slack", + } + + def _project_event(self, event: dict, actor_visible: Set[str]) -> dict: + drop_keys = {"tags", "date", "artifact_path", "eml_path", "slack_path"} + doc = {k: v for k, v in event.items() if k not in drop_keys} + + if "artifact_ids" in doc: + doc["artifact_ids"] = { + k: v + for k, v in doc["artifact_ids"].items() + if v + and v != "[]" + and k not in self._ARTIFACT_ID_BLOCKLIST + and ( + (isinstance(v, list) and any(item in actor_visible for item in v)) + or (isinstance(v, str) and v in actor_visible) + ) + } + + return doc + + def get_events_for_day( + self, day: int, event_type: Optional[str] = None + ) -> List[dict]: + t0 = time.time() + + if not event_type: + logger.warning("[get_events_for_day] Called without event_type — blocked") + return self._record( + "get_events_for_day", {"day": day, "event_type": None}, [], t0 + ) + + if self._question_type == "SILENCE": + trigger_day = self._question.get("trigger_day", 1) + gate_ts = self._gate_ts() + gate_day = _date_to_business_day( + _SIM_START, datetime.fromisoformat(gate_ts) + ) + if day < trigger_day or day > gate_day: + logger.warning( + f"[get_events_for_day] Day {day} requested but window is Day {trigger_day}–{gate_day} — blocked" + ) + return self._record( + "get_events_for_day", {"day": day, "event_type": event_type}, [], t0 + ) + else: + gate_day = _date_to_business_day( + _SIM_START, datetime.fromisoformat(self._as_of_time) + ) + gate_ts = self._as_of_time + if day > gate_day: + logger.warning( + f"[get_events_for_day] Day {day} requested but gate is Day {gate_day} — blocked" + ) + return self._record( + "get_events_for_day", {"day": day, "event_type": event_type}, [], t0 + ) + + query: Dict = { + "day": day, + } + + allowed_types: Set[str] = set() + if self._actor_subsystems is None: + allowed_types = set(KNOWN_EVENT_TYPES) + else: + for subsystem in self._actor_subsystems: + allowed_types.update(_SUBSYSTEM_EVENT_TYPES.get(subsystem, set())) + + if self._question_type != "COUNTERFACTUAL": + allowed_types -= _INTERNAL_EVENT_TYPES + + if event_type: + if ( + event_type in _INTERNAL_EVENT_TYPES + and self._question_type != "COUNTERFACTUAL" + ): + logger.warning( + f"[get_events_for_day] Internal event type requested: {event_type}" + ) + return self._record( + "get_events_for_day", {"day": day, "event_type": event_type}, [], t0 + ) + query["type"] = event_type + else: + query["type"] = {"$in": list(allowed_types)} + + if self._question_type == "PERSPECTIVE": + actor = self._question.get("actor", "") + visible = self._actor_visible_by_subsystem + query["$or"] = [ + {"actors": actor}, + {"artifact_ids.email": {"$in": list(visible.get("email", set()))}}, + {"artifact_ids.jira": {"$in": list(visible.get("jira", set()))}}, + { + "artifact_ids.confluence": { + "$in": list(visible.get("confluence", set())) + } + }, + { + "artifact_ids.slack_thread": { + "$in": list(visible.get("slack", set())) + } + }, + {"artifact_ids.pr": {"$in": list(visible.get("git", set()))}}, + { + "artifact_ids.zoom_transcript": { + "$in": list(visible.get("zoom", set())) + } + }, + ] + + # logger.info(f"[get_events_for_day] - query: {query}") + + docs = list( + self._mem._db["events"].find( + query, + { + "_id": 0, + "event_id": 1, + "type": 1, + "day": 1, + "actors": 1, + "summary": 1, + "artifact_ids": 1, + }, + ) + ) + + # logger.info(f"[get_events_for_day] - results before project: {docs}") + if self._actor_visible is not None: + docs = [self._project_event(d, self._actor_visible) for d in docs] + + # logger.info(f"[get_events_for_day] - results: {docs}") + + return self._record( + "get_events_for_day", {"day": day, "event_type": event_type}, docs, t0 + ) + + def search_artifacts( + self, + query: str, + doc_type: str, + actor: Optional[str] = None, + after_day: Optional[int] = None, + limit: int = 6, + ) -> List[dict]: + t0 = time.time() + + if after_day is not None: + try: + after_day = int(after_day) + except (ValueError, TypeError): + after_day = None + + effective_ts = self._gate_ts() + MAX_SEARCH_LIMIT = 15 + limit = min(limit, MAX_SEARCH_LIMIT) + + actor_id_filter = None + if self._question_type == "PERSPECTIVE" and self._actor_visible: + actor_id_filter = list(self._actor_visible) + + exact_doc = self._mem._db["artifacts"].find_one( + {"_id": query}, + {"embedding": 0, "timestamp": 0, "created_at": 0, "date": 0}, + ) + if exact_doc: + # logger.info(f"[search_artifacts] - exact_doc: {exact_doc}") + + ts_filter = {"timestamp": {"$lte": effective_ts}} + if after_day is not None: + floor_ts = _business_day_to_date(_SIM_START, after_day).isoformat() + ts_filter["timestamp"]["$gte"] = floor_ts + if actor_id_filter is not None and query not in self._actor_visible: + return self._record( + "search_artifacts", + { + "query": query, + "doc_type": doc_type, + "actor": actor, + "after_day": after_day, + }, + [], + t0, + timestamp_applied=effective_ts, + ) + exact_doc_ts = exact_doc.get("timestamp", "") + if exact_doc_ts <= effective_ts and ( + after_day is None or exact_doc_ts >= floor_ts + ): + return self._record( + "search_artifacts", + { + "query": query, + "doc_type": doc_type, + "actor": actor, + "after_day": after_day, + }, + [exact_doc], + t0, + timestamp_applied=effective_ts, + ) + return self._record( + "search_artifacts", + { + "query": query, + "doc_type": doc_type, + "actor": actor, + "after_day": after_day, + }, + [], + t0, + timestamp_applied=effective_ts, + ) + + text_filter: dict = { + "$text": {"$search": query}, + "timestamp": {"$lte": effective_ts}, + } + if actor_id_filter is not None: + text_filter["_id"] = {"$in": actor_id_filter} + if after_day is not None: + floor_ts = _business_day_to_date(_SIM_START, after_day).isoformat() + text_filter["timestamp"] = { + "$gte": floor_ts, + "$lte": effective_ts, + } + if doc_type: + text_filter["type"] = doc_type + if actor: + text_filter["metadata.author"] = actor + + # logger.info(f"[search_artifacts] - query: {text_filter}") + # logger.info(f"[search_artifacts] - limit: {limit}") + + results = list( + self._mem._db["artifacts"] + .find( + text_filter, + { + "content": 0, + "embedding": 0, + "timestamp": 0, + "created_at": 0, + "date": 0, + "metadata.tags": 0, + "metadata.parent_id": 0, + "metadata.is_chunk": 0, + "score": {"$meta": "textScore"}, + }, + ) + .sort([("score", {"$meta": "textScore"})]) + .limit(limit) + ) + + # logger.info(f"[search_artifacts] - results length: {len(results)}") + + # logger.info(f"[search_artifacts] - results: {results}") + + return self._record( + "search_artifacts", + { + "query": query, + "doc_type": doc_type, + "actor": actor, + "after_day": after_day, + }, + results, + t0, + timestamp_applied=effective_ts, + ) + + def get_graph_snapshot(self, day: int) -> dict: + """ + Returns the social graph snapshot for the given day: + nodes (actor names), edges (source, target, weight), and org state. + Registered dynamically for GRAPH-track questions via register_graph_tool(). + Calling this on a non-GRAPH question logs a tool call but returns {}. + """ + + t0 = time.time() + if self._question_type != "GRAPH": + self._record("get_graph_snapshot", {"day": day}, [], t0) + return {} + + logger.warning( + "[get_graph_snapshot] stub reached for GRAPH question — register_graph_tool() may not have run" + ) + self._record("get_graph_snapshot", {"day": day}, [], t0) + return {} + + def get_stress_snapshot(self, day: int) -> dict: + """ + Returns {day, stress: {name: int}} for the given day. + Registered dynamically for GRAPH-track questions. + """ + + t0 = time.time() + if self._question_type != "GRAPH": + self._record("get_stress_snapshot", {"day": day}, [], t0) + return {} + self._record("get_stress_snapshot", {"day": day}, [], t0) + return {} + + +# ───────────────────────────────────────────────────────────────────────────── +# SCORERS +# ───────────────────────────────────────────────────────────────────────────── + + +class PerspectiveScorer: + """ + Scores a PERSPECTIVE trajectory. + + Answer scoring: + - Exact match on ground_truth.could_actor_have_known (boolean) + - Partial credit for correctly identifying blocked_subsystems + - Partial credit for citing in-cone evidence + + Trajectory scoring: + - Epistemic discipline: fraction of tool calls that stayed within cone + - Subsystem discipline: fraction of tool calls to accessible subsystems + - Conclusion grounding: did the final answer cite in-cone artifacts? + """ + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + gt_bool = ground_truth.get("could_actor_have_known", False) + + # Extract boolean from agent answer — schema violation scores 0, not 0.1 + agent_bool = self._extract_boolean(final_answer) + if agent_bool is None: + return 0.0, False + + correct = agent_bool == gt_bool + if not correct: + return 0.0, False + + # Partial credit for explaining the mechanism correctly + score = 0.6 # base for correct boolean + + gt_blocked = set(ground_truth.get("blocked_subsystems", [])) + agent_blocked = set(final_answer.get("blocked_subsystems", [])) + if gt_blocked and agent_blocked: + overlap = len(gt_blocked & agent_blocked) / len(gt_blocked) + score += 0.2 * overlap + + gt_evidence = set(ground_truth.get("evidence_artifacts", [])) + agent_evidence = set(final_answer.get("evidence_artifacts", [])) + if gt_evidence and agent_evidence: + overlap = len(gt_evidence & agent_evidence) / len(gt_evidence) + score += 0.2 * overlap + elif not gt_evidence: + score += 0.2 + + return min(score, 1.0), True + + def score_trajectory( + self, + trajectory: AgentTrajectory, + question: dict, + ) -> PerspectiveTrajectoryScore: + calls = trajectory.tool_calls + if not calls: + return PerspectiveTrajectoryScore( + epistemic_discipline=0.0, + subsystem_discipline=0.0, + horizon_discipline=0.0, + temporal_precision=0.0, + temporal_drift_discipline=1.0, + conclusion_grounding=0.0, + dead_end_recovery=0.0, + composite=0.0, + ) + + n = len(calls) + actor_cone_violations = sum(1 for c in calls if c.actor_gate_violation) + subsystem_violations = sum(1 for c in calls if c.subsystem_violation) + horizon_violations = sum(1 for c in calls if c.horizon_violation) + dead_ends = sum(1 for c in calls if c.returned_empty) + dead_ends_recovered = trajectory.dead_ends_recovered + + epistemic_discipline = 1.0 - (actor_cone_violations / n) + subsystem_discipline = 1.0 - (subsystem_violations / n) + horizon_discipline = 1.0 - (horizon_violations / n) + + # Conclusion grounding: continuous coverage fraction of required evidence + # cited within the actor's cone. Binary hit/miss loses resolution at the + # top of the leaderboard where frontier models will be differentiating. + actor_visible = set(question.get("actor_visible_artifacts", [])) + gt_evidence = set( + question.get("ground_truth", {}).get("evidence_artifacts", []) + ) + cited = set(trajectory.final_answer.get("evidence_artifacts", [])) + + if gt_evidence: + in_cone_required = gt_evidence & actor_visible + if in_cone_required: + conclusion_grounding = len(cited & in_cone_required) / len( + in_cone_required + ) + else: + conclusion_grounding = 1.0 # nothing in-cone required + elif cited: + conclusion_grounding = 1.0 if (cited & actor_visible) else 0.3 + else: + conclusion_grounding = 0.0 + + dead_end_recovery = dead_ends_recovered / dead_ends if dead_ends > 0 else 1.0 + + sim_days = CONFIG["simulation"].get("num_days", 60) + drifts = [ + c.temporal_drift_days for c in calls if c.temporal_drift_days is not None + ] + temporal_precision = ( + max(0.0, 1.0 - mean(abs(d) / sim_days for d in drifts)) if drifts else 1.0 + ) + + drift_violations = sum(1 for c in calls if c.temporal_drift_violation) + temporal_drift_discipline = 1.0 - (drift_violations / n) + + composite = ( + 0.30 * epistemic_discipline + + 0.25 * subsystem_discipline + + 0.20 * conclusion_grounding + + 0.10 * horizon_discipline + + 0.05 * temporal_precision + + 0.05 * temporal_drift_discipline + + 0.05 * dead_end_recovery + ) + + return PerspectiveTrajectoryScore( + epistemic_discipline=round(epistemic_discipline, 4), + subsystem_discipline=round(subsystem_discipline, 4), + horizon_discipline=round(horizon_discipline, 4), + temporal_precision=round(temporal_precision, 4), + temporal_drift_discipline=round(temporal_drift_discipline, 4), + conclusion_grounding=round(conclusion_grounding, 4), + dead_end_recovery=round(dead_end_recovery, 4), + composite=round(composite, 4), + ) + + def _extract_boolean(self, answer: Dict) -> Optional[bool]: + val = answer.get("could_actor_have_known") + if isinstance(val, bool): + return val + # Schema violation — model did not return the required boolean field. + # Log and return None so the caller scores this as 0. + logger.warning( + " PERSPECTIVE schema violation: 'could_actor_have_known' missing or " + f"non-boolean (got {val!r})" + ) + return None + + +class CounterfactualScorer: + """ + Scores a COUNTERFACTUAL trajectory. + + Answer scoring: + - outcome_changed: boolean match (0.4) + - mechanism: correct link_type identified (0.35) + - actors: at least one correct actor identified (0.25) + + Trajectory scoring: + - cause_identified: agent retrieved the cause event + - effect_identified: agent retrieved the effect event + - mechanism_correct: agent named the right link_type + - causal_chain_complete: agent traversed cause → effect in order + """ + + @staticmethod + def _mechanism_matches(aliases: set, text: str) -> bool: + return any(re.search(rf"\b{re.escape(alias)}\b", text) for alias in aliases) + + _MECHANISM_ALIASES = { + "involves_gap": { + "knowledge gap", + "gap", + "undocumented", + "missing documentation", + "knowledge_gap", + }, + "recurrence_of": { + "recurrence", + "repeat incident", + "recurred", + "same issue", + "recurring", + }, + "spawned_doc": { + "spawned", + "documentation", + "confluence", + "design discussion", + "produced doc", + }, + "email_dropped": { + "dropped", + "unactioned", + "routing failure", + "missed email", + "no response", + }, + "sf_ownership_lapsed": { + "ownership lapsed", + "crm gap", + "salesforce", + "account owner", + "orphaned", + }, + "blocker_flagged": { + "blocker", + "blocked", + "delay", + "progress", + "technical blocker", + "blocker_flagged", + }, + "incident_coordination": { + "coordination", + "external contact", + "external party", + "incident_coordination", + "coordinated with", + }, + "departure_reassignment": { + "reassignment", + "departed", + "departure", + "reassigned", + "departure_reassignment", + "not reassigned", + }, + "zd_escalation_source": { + "zendesk escalation", + "support ticket escalation", + "zd escalation", + "escalated from zendesk", + "zd_escalation_source", + }, + "assignment_domain_mismatch": { + "domain mismatch", + "planning mismatch", + "wrong domain", + "misassigned", + "assignment_domain_mismatch", + }, + "jira_from_vendor_email": { + "vendor email", + "jira from vendor", + "vendor ticket", + "inbound vendor", + "jira_from_vendor_email", + }, + "jira_from_customer_email": { + "customer email", + "jira from customer", + "customer ticket", + "inbound customer", + "jira_from_customer_email", + }, + "customer_escalation_relayed": { + "escalation relayed", + "customer escalation", + "support email routed", + "customer_escalation_relayed", + }, + "incident_handoff": { + "handoff", + "forced handoff", + "escalation chain", + "incident_handoff", + "handoff on departure", + }, + "pr_gap_detected": { + "pr review gap", + "undocumented domain in pr", + "pr gap", + "code review gap", + "pr_gap_detected", + }, + "async_gap_detected": { + "async gap", + "undocumented domain in async", + "async thread gap", + "async_gap_detected", + }, + "doc_gap_detected": { + "doc gap", + "documentation gap", + "undocumented domain in doc", + "documentation review gap", + "doc_gap_detected", + }, + "centrality_vacuum": { + "centrality vacuum", + "key person departure", + "single point of failure", + "bus factor", + "centrality_vacuum", + }, + "sf_stage_advanced_by_customer": { + "stage advanced", + "crm stage", + "customer advanced stage", + "deal stage", + "sf_stage_advanced_by_customer", + }, + "feature_request_fyi": { + "feature request", + "product fyi", + "inbound feature", + "feature_request_fyi", + }, + "proactive_outreach_from_crm_signal": { + "proactive outreach", + "at-risk", + "crm signal", + "outreach from crm", + "proactive_outreach_from_crm_signal", + }, + "ticket_completion_notifies_lead": { + "ticket completion", + "lead notified", + "dependent ticket done", + "ticket_completion_notifies_lead", + }, + "org_collision_tension": { + "org tension", + "collision", + "overlapping responsibilities", + "org conflict", + "org_collision_tension", + }, + "postmortem_from_incident": { + "postmortem", + "post-mortem", + "incident postmortem", + "postmortem created", + "postmortem_from_incident", + }, + "incident_triggers_risk_flag": { + "risk flag", + "sf risk", + "incident risk", + "salesforce risk flag", + "incident_triggers_risk_flag", + }, + "review_triggers_revision": { + "pr revision", + "code review revision", + "review revision", + "revision triggered", + "review_triggers_revision", + }, + "hire_fills_knowledge_gap": { + "hire fills gap", + "new hire", + "gap closed", + "expertise hire", + "hire_fills_knowledge_gap", + }, + "escalation_from_zendesk": { + "zendesk escalation", + "escalation from zendesk", + "zd escalation", + "escalation_from_zendesk", + }, + } + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + score = 0.0 + gt_outcome = ground_truth.get("outcome_changed", True) + agent_outcome = self._extract_boolean(final_answer, "outcome_changed") + + if agent_outcome is None: + return 0.0, False + if agent_outcome == gt_outcome: + score += 0.4 + + gt_mechanism = ground_truth.get("causal_mechanism", "") + agent_mechanism_prose = str(final_answer.get("mechanism", "")).lower() + agent_mechanism_structured = str( + final_answer.get("causal_mechanism", "") + ).lower() + agent_mechanism = agent_mechanism_prose + " " + agent_mechanism_structured + aliases = self._MECHANISM_ALIASES.get(gt_mechanism, {gt_mechanism}) + if self._mechanism_matches(aliases, agent_mechanism): + score += 0.35 + + gt_actors = set(ground_truth.get("actors", [])) + agent_actors_raw = final_answer.get( + "actors", final_answer.get("involved_actors", []) + ) + agent_actors = ( + set(agent_actors_raw) if isinstance(agent_actors_raw, list) else set() + ) + if gt_actors and agent_actors and (gt_actors & agent_actors): + score += 0.25 + elif not gt_actors: + score += 0.25 + + is_correct = score >= 0.75 + return round(min(score, 1.0), 4), is_correct + + def score_trajectory( + self, + trajectory: AgentTrajectory, + question: dict, + ground_truth: Dict, + ) -> CounterfactualTrajectoryScore: + calls = trajectory.tool_calls + if not calls: + return CounterfactualTrajectoryScore(0, 0, 0, 0, 1.0, 0.0) + + n = len(calls) + retrieved_ids = set() + for call in calls: + retrieved_ids.update(call.result_ids) + + evidence_artifacts = ground_truth.get("evidence_chain_artifacts", {}) + cause_artifacts = set(evidence_artifacts.get("cause", [])) + effect_artifacts = set(evidence_artifacts.get("effect", [])) + + cause_identified = ( + 1.0 if (cause_artifacts and cause_artifacts & retrieved_ids) else 0.0 + ) + effect_identified = ( + 1.0 if (effect_artifacts and effect_artifacts & retrieved_ids) else 0.0 + ) + + gt_mechanism = ground_truth.get("causal_mechanism", "") + aliases = self._MECHANISM_ALIASES.get(gt_mechanism, {gt_mechanism}) + agent_text = " ".join( + [str(c.arguments) for c in calls] + [str(trajectory.final_answer)] + ).lower() + mechanism_correct = 1.0 if self._mechanism_matches(aliases, agent_text) else 0.0 + + cause_only_artifacts = cause_artifacts - effect_artifacts + effect_only_artifacts = effect_artifacts - cause_artifacts + shared_artifacts = cause_artifacts & effect_artifacts + + cause_call_idx = next( + ( + i + for i, c in enumerate(calls) + if cause_only_artifacts & set(c.result_ids) + ), + next( + ( + i + for i, c in enumerate(calls) + if shared_artifacts & set(c.result_ids) + ), + None, + ), + ) + effect_call_idx = next( + ( + i + for i, c in enumerate(calls) + if effect_only_artifacts & set(c.result_ids) + ), + next( + ( + i + for i, c in enumerate(calls) + if shared_artifacts & set(c.result_ids) + ), + None, + ), + ) + if cause_call_idx is not None and effect_call_idx is not None: + if cause_call_idx == effect_call_idx and not ( + cause_only_artifacts or effect_only_artifacts + ): + causal_chain_complete = ( + 0.5 # single shared artifact, no ordered traversal + ) + elif cause_call_idx <= effect_call_idx: + causal_chain_complete = 1.0 + else: + causal_chain_complete = 0.5 + elif cause_call_idx is not None or effect_call_idx is not None: + causal_chain_complete = 0.5 + else: + causal_chain_complete = 0.0 + + horizon_violations = sum(1 for c in calls if c.horizon_violation) + horizon_discipline = 1.0 - (horizon_violations / n) + + composite = ( + 0.25 * cause_identified + + 0.25 * effect_identified + + 0.25 * mechanism_correct + + 0.15 * causal_chain_complete + + 0.10 * horizon_discipline + ) + + return CounterfactualTrajectoryScore( + cause_identified=cause_identified, + effect_identified=effect_identified, + mechanism_correct=mechanism_correct, + causal_chain_complete=causal_chain_complete, + horizon_discipline=round(horizon_discipline, 4), + composite=round(composite, 4), + ) + + def _extract_boolean(self, answer: Dict, key: str) -> Optional[bool]: + val = answer.get(key) + if isinstance(val, bool): + return val + # Schema violation — model did not return the required boolean field. + # Log and return None so the caller scores this as 0. + logger.warning( + f" COUNTERFACTUAL schema violation: '{key}' missing or " + f"non-boolean (got {val!r})" + ) + return None + + +class SilenceScorer: + """ + Scores a SILENCE trajectory. + + The key insight: absence is only meaningful if the agent searched somewhere + relevant. A correct "no" is disqualified as a guess if the agent made zero + tool calls or every call was outside the expected search space. + + Combined score is binary (see _run_question): + - Wrong answer → 0.0 + - Correct answer + no relevant search → 0.0 (ruled a guess) + - Correct answer + at least one relevant call → 1.0 + + Trajectory scoring (diagnostic only — does not contribute to combined_score): + - made_relevant_search: did at least one tool call overlap expected search space? + - correct_absence_conclusion: did agent say "does not exist" explicitly? + - search_space_coverage: fraction of expected_search_space checked (for analysis) + """ + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + gt_answer = ground_truth.get("answer", False) # Always False for SILENCE + agent_answer = self._extract_absence_conclusion(final_answer) + + if agent_answer is None: + return 0.0, False + + correct = agent_answer == gt_answer + return (1.0, True) if correct else (0.0, False) + + _PATH_SUBSYSTEM_MAP: List[Tuple[str, str]] = [ + ("CONF-", "confluence"), + ("ENG-", "jira"), + ("IT-", "jira"), + ("ORG-", "jira"), + ("QA-", "jira"), + ("HR-", "jira"), + ("PROD-", "jira"), + ("DES-", "jira"), + ("SALES-", "jira"), + ("PR-", "git"), + ("ZD-", "zendesk"), + ("SF-ACC", "salesforce"), + ("SF-OPP", "salesforce"), + ("ZOOM-", "zoom"), + ("DD-", "datadog"), + ("INV-", "invoice"), + ("NPS-", "salesforce"), + ("slack_", "slack"), + ("ext_email_", "email"), + ("customer_reply_", "email"), + ("reply_customer_", "email"), + ("ack_vendor_", "email"), + ("hr_outbound_", "email"), + ] + + _TOOL_SUBSYSTEM_MAP: Dict[str, str] = { + "get_ticket": "jira", + "get_confluence_page": "confluence", + "get_slack_thread": "slack", + "get_email": "email", + "get_pr": "git", + "get_zd_ticket": "zendesk", + "get_sf_opportunity": "salesforce", + "get_sf_account": "salesforce", + "get_zoom_transcript": "zoom", + "get_datadog_alert": "datadog", + } + + _SEARCH_DOCTYPE_MAP: Dict[str, str] = { + "jira": "jira", + "confluence": "confluence", + "slack": "slack", + "email": "email", + "pr": "git", + "zd_ticket": "zendesk", + "zoom": "zoom", + } + + _QUERY_STOPWORDS: frozenset = frozenset( + { + "the", + "a", + "an", + "of", + "in", + "for", + "on", + "to", + "at", + "by", + "is", + "was", + "and", + "or", + "not", + "be", + "it", + "its", + } + ) + + def _infer_artifact_subsystem(self, entry: str) -> Optional[str]: + for prefix, sub in self._PATH_SUBSYSTEM_MAP: + if entry.startswith(prefix): + return sub + return None + + def _semantic_coverage_check( + self, + expected_entry: str, + calls: List[ToolCall], + question: dict, + normalized_tool_args: List[str], + ) -> bool: + """ + Returns True when the agent's search behavior would plausibly surface + the expected artifact without exactly naming its path segment. + + Requires BOTH to pass: + 1. Subsystem coverage — at least one call reached the artifact's subsystem. + search_artifacts with no doc_type counts as covering all subsystems. + 2. Query relevance — at least one query contains trigger actor names, + the link_value domain, or event-type keywords from the question. + """ + target_sub = self._infer_artifact_subsystem(expected_entry) + + subsystem_covered = False + for call in calls: + direct_sub = self._TOOL_SUBSYSTEM_MAP.get(call.tool_name) + if direct_sub is not None: + if target_sub is None or direct_sub == target_sub: + subsystem_covered = True + break + elif call.tool_name == "search_artifacts": + doc_type = call.arguments.get("doc_type", "") + mapped_sub = self._SEARCH_DOCTYPE_MAP.get(doc_type) + if not doc_type or mapped_sub == target_sub or target_sub is None: + subsystem_covered = True + break + + if not subsystem_covered: + return False + + gt = question.get("ground_truth", {}) + semantic_keywords: Set[str] = set() + + for actor in question.get("trigger_actors", gt.get("trigger_actors", [])): + semantic_keywords.update( + w.lower() for w in str(actor).split() if len(w) > 2 + ) + for term in str(gt.get("link_value", "")).split(): + if len(term) > 2: + semantic_keywords.add(term.lower()) + for term in question.get("trigger_event_type", "").replace("_", " ").split(): + if len(term) > 2: + semantic_keywords.add(term.lower()) + + semantic_keywords -= self._QUERY_STOPWORDS + + if not semantic_keywords: + return subsystem_covered + + return any( + kw in arg for kw in semantic_keywords for arg in normalized_tool_args + ) + + def score_trajectory( + self, + trajectory: AgentTrajectory, + question: dict, + ) -> SilenceTrajectoryScore: + calls = trajectory.tool_calls + expected_space = set(question.get("expected_search_space", [])) + if not calls: + return SilenceTrajectoryScore(0.0, 0.0, 0.0, 1.0, 0.0) + + n = len(calls) + + # What did the agent search? + searched_ids: Set[str] = set() + searched_tool_args: List[str] = [] + for call in calls: + searched_ids.update(call.result_ids) + searched_tool_args.append(str(call.arguments).lower()) + + def _normalize_search_term(s: str) -> str: + """ + Extract the terminal component of a path-style search space entry. + e.g. "confluence/postmortems/IT-108" → "it-108" + "slack/channels/incidents" → "incidents" + "IT-108" → "it-108" + """ + return s.strip("/").split("/")[-1].lower() + + normalized_expected: Dict[str, str] = { + _normalize_search_term(e): e for e in expected_space + } + + # Also normalize all tool arg strings and result IDs for matching. + normalized_tool_args: List[str] = [arg.lower() for arg in searched_tool_args] + normalized_result_ids: Set[str] = { + _normalize_search_term(rid) for rid in searched_ids + } + + covered = set() + for norm_term, original in normalized_expected.items(): + if any(norm_term in arg for arg in normalized_tool_args): + covered.add(original) + elif norm_term in normalized_result_ids: + covered.add(original) + elif any(original.lower() in arg for arg in normalized_tool_args): + covered.add(original) + elif self._semantic_coverage_check( + original, calls, question, normalized_tool_args + ): + covered.add(original) + + search_space_coverage = ( + len(covered) / len(expected_space) if expected_space else 1.0 + ) + + # Binary soundness gate: did the agent make at least one tool call that + # overlaps with the expected search space? A single relevant call is + # sufficient — we are ruling out guesses, not rewarding exhaustiveness. + # A call to the wrong subsystem does not count, even if the answer is correct. + made_relevant_search = len(covered) > 0 if expected_space else len(calls) > 0 + + conclusion_text = str(trajectory.final_answer.get("reasoning", "")).lower() + conclusion_text += str(trajectory.final_answer.get("answer", "")).lower() + explicit_negative = any( + phrase in conclusion_text + for phrase in ( + "does not exist", + "was not created", + "no postmortem", + "not found", + "could not find", + "no record", + "never created", + "not in the corpus", + "no evidence", + "not present", + "absent", + ) + ) + correct_absence_conclusion = 1.0 if explicit_negative else 0.3 + + # premature_conclusion kept for diagnostics but no longer penalises composite + premature_conclusion = 1.0 - max(0.0, min(1.0, (n - 1) / 3)) + + horizon_discipline = 1.0 # SILENCE has no temporal gate + + # Composite is now the binary relevant-search gate. + # search_space_coverage and correct_absence_conclusion are preserved + # in the dataclass for diagnostic use in meta — they do not contribute + # to combined_score. + composite = 1.0 if made_relevant_search else 0.0 + + return SilenceTrajectoryScore( + search_space_coverage=round(search_space_coverage, 4), + correct_absence_conclusion=round(correct_absence_conclusion, 4), + premature_conclusion=round(premature_conclusion, 4), + horizon_discipline=round(horizon_discipline, 4), + composite=round(composite, 4), + ) + + def _extract_absence_conclusion(self, answer: Dict) -> Optional[bool]: + """True = artifact exists, False = artifact does not exist.""" + val = answer.get("exists", answer.get("found", answer.get("answer"))) + if isinstance(val, bool): + return val + # Schema violation — model did not return the required boolean field. + # Log and return None so the caller scores this as 0. + logger.warning( + f" SILENCE schema violation: 'exists' missing or non-boolean (got {val!r})" + ) + return None + + +# ───────────────────────────────────────────────────────────────────────────── +# AGENT RUNNER +# ───────────────────────────────────────────────────────────────────────────── + + +class AgenticEvalRunner: + """ + Runs the agent on each question and scores the result. + + For each question: + 1. Sets up the gated tool layer appropriate for the track + 2. Runs the agent with the typed tool surface + 3. Collects the trajectory + 4. Scores answer + trajectory with the track-specific scorer + 5. Combines scores with track-specific weights + """ + + def __init__( + self, + model: str = "claude-sonnet-4-6", + max_steps: int = 6, + ungated: bool = False, + zero_shot: bool = False, + call_delay: float = 1.0, + service_tier: str = "default", + skip_nli: bool = False, + ): + self._model = model + self._max_steps = max_steps + self._call_delay = call_delay + self._service_tier = service_tier + + from memory import Memory + import boto3 + + self._mem = Memory() + self._bedrock = boto3.client("bedrock-runtime") + + self._perspective_scorer = PerspectiveScorer() + self._counterfactual_scorer = CounterfactualScorer() + self._silence_scorer = SilenceScorer() + + # --ungated: all actor/subsystem gates disabled regardless of question type. + # Establishes the "god-mode" information ceiling for the Epistemic Tax. + self._ungated = ungated + + # --zero-shot: agent receives no tools at all (no corpus access). + # Establishes the hallucination / prior-knowledge floor. + # Mutually exclusive with --ungated; zero_shot takes precedence if both set. + self._zero_shot = zero_shot + + # NLI scorer for zero-shot reasoning validation. + # Loaded only when running zero-shot — no overhead for gated/ungated runs. + # If skip_nli=True, zero-shot falls back to answer_score with a warning. + self._nli: Optional[NLIScorer] = None + if zero_shot and not skip_nli: + try: + logger.info("Loading NLI scorer for zero-shot reasoning validation...") + self._nli = NLIScorer() + logger.info("NLI scorer loaded.") + except Exception as exc: + logger.warning( + f"NLI scorer failed to load — zero-shot will use answer_score only: {exc}" + ) + + vis_path = EVAL_DIR / "actor_visibility.json" + if vis_path.exists(): + with open(vis_path) as f: + self._actor_visibility = json.load(f) + else: + logger.warning( + "actor_visibility.json not found — PERSPECTIVE gates will not be enforced" + ) + self._actor_visibility = {} + self._artifact_timestamps = { + doc["_id"]: doc.get("timestamp", "") + for doc in self._mem._db["artifacts"].find({}, {"_id": 1, "timestamp": 1}) + } + + def run( + self, + questions_path: Path, + out_path: Path, + question_types: Optional[List[str]] = None, + max_questions: Optional[int] = None, + ) -> None: + with open(questions_path) as f: + data = json.load(f) + + questions = data["questions"] + if question_types: + questions = [q for q in questions if q["question_type"] in question_types] + if max_questions: + questions = questions[:max_questions] + + logger.info( + f"Running agentic eval on {len(questions)} questions " + f"(model={self._model}, max_steps={self._max_steps})" + ) + + results: List[EvalResult] = [] + per_question: List[dict] = [] + token_counter = Counter() + + for i, question in enumerate(questions): + qtype = question["question_type"] + logger.info( + f"[{i + 1}/{len(questions)}] {qtype} — {question['question_id']}" + ) + + try: + result = self._run_question(question) + except Exception as exc: + logger.error(f" Failed: {exc}") + result = EvalResult( + question_id=question["question_id"], + question_type=qtype, + difficulty=question.get("difficulty", "unknown"), + answer_score=0.0, + answer_correct=False, + trajectory_score=0.0, + combined_score=0.0, + failure_reason=str(exc), + tool_call_count=0, + meta={"error": str(exc)}, + ) + + results.append(result) + per_question.append(result.to_dict()) + + token_counter["prompt"] += result.meta.get("prompt_tokens", 0) + token_counter["completion"] += result.meta.get("completion_tokens", 0) + token_counter["total"] += result.meta.get("total_tokens", 0) + + logger.info( + f" answer={result.answer_score:.3f} " + f"trajectory={result.trajectory_score:.3f} " + f"combined={result.combined_score:.3f} " + f"tools={result.tool_call_count} " + f"total tokens={result.meta.get('total_tokens', 0):,} " + f"input tokens={result.meta.get('prompt_tokens', 0):,} " + f"[total input tokens: {token_counter['prompt']:,}]" + f"[running: {token_counter['total']:,}]" + ) + + summary = self._aggregate(results) + out_path.parent.mkdir(parents=True, exist_ok=True) + output = { + "meta": { + "model": self._model, + "max_steps": self._max_steps, + "n_questions": len(results), + "track_weights": _TRACK_WEIGHTS, + }, + "summary": summary, + "per_question": per_question, + } + with open(out_path, "w") as f: + json.dump(output, f, indent=2, default=str) + + logger.info(f"Results written to {out_path}") + logger.info( + f"Overall — answer: {summary['overall']['answer_score']:.3f} " + f"trajectory: {summary['overall']['trajectory_score']:.3f} " + f"combined: {summary['overall']['combined_score']:.3f}" + + ( + f" | violation_adjusted: " + f"{summary['overall'].get('violation_adjusted_combined_score', 'n/a')}" + ) + ) + + def _build_events_tool_def(self, question: dict) -> dict: + qtype = question.get("question_type", "") + actor_subsystems = ( + set(question.get("subsystem_access", [])) + if qtype == "PERSPECTIVE" + else None + ) + + allowed_types: Set[str] = set() + if actor_subsystems is None: + allowed_types = set(KNOWN_EVENT_TYPES) + else: + for subsystem in actor_subsystems: + allowed_types.update(_SUBSYSTEM_EVENT_TYPES.get(subsystem, set())) + + if qtype != "COUNTERFACTUAL": + allowed_types -= _INTERNAL_EVENT_TYPES + + logger.info(f"[allowed_types] - {allowed_types}") + + return { + "toolSpec": { + "name": "get_events_for_day", + "description": "Retrieve simulation events for a given day, optionally filtered by type.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "day": { + "type": "integer", + "description": "Simulation day number", + }, + "event_type": { + "type": "string", + "description": "Required. You must always filter by event type. Never call this tool without specifying an event_type.", + "enum": sorted(allowed_types), + }, + }, + "required": ["day", "event_type"], + } + }, + } + } + + def _run_question(self, question: dict) -> EvalResult: + qtype = question["question_type"] + ground_truth = question["ground_truth"] + + # Set up gated tools. + # --ungated: strip all actor/subsystem gates by passing None for both, + # regardless of question type. Temporal gate still applies. + # --zero-shot: GatedTools is still constructed (for consistent call + # logging infrastructure) but _tool_list() returns [] so the agent + # never actually invokes any tool. + as_of_time = self._infer_as_of_time(question) + actor_visible_by_subsystem = {} + + if self._ungated: + actor_visible = None + actor_subsystems = None + else: + if qtype == "PERSPECTIVE" and not self._ungated: + actor = question.get("actor", "") + as_of_time = question.get("as_of_time", "") + cone_data = self._actor_visibility.get(actor, {}) + if cone_data: + actor_visible = set( + aid + for ids in cone_data.get("visible_artifacts", {}).values() + for aid in ids + if self._artifact_timestamps.get(aid, "9999") <= as_of_time + ) + for subsystem, ids in cone_data.get( + "visible_artifacts", {} + ).items(): + filtered = { + aid + for aid in ids + if self._artifact_timestamps.get(aid, "9999") <= as_of_time + } + actor_visible_by_subsystem[subsystem] = filtered + actor_visible.update(filtered) + else: + logger.warning(f"No visibility cone found for {actor}") + actor_visible = set() + actor_subsystems = set(question.get("subsystem_access", [])) + else: + actor_visible = None + actor_subsystems = None + + tools = GatedTools( + mem=self._mem, + question=question, + as_of_time=as_of_time, + actor_visible_artifacts=actor_visible, + actor_visible_by_subsystem=actor_visible_by_subsystem, + actor_subsystem_access=actor_subsystems, + ) + + if question.get("question_type") == "GRAPH": + register_graph_tool(tools, self._mem, question) + + # Run agent + trajectory = self._run_agent(question, tools) + + # Score + if qtype == "PERSPECTIVE": + answer_score, answer_correct = self._perspective_scorer.score_answer( + trajectory.final_answer, ground_truth + ) + question_with_cone = { + **question, + "actor_visible_artifacts": list(actor_visible or []), + } + traj = self._perspective_scorer.score_trajectory( + trajectory, question_with_cone + ) + traj_score = traj.composite + traj_detail = asdict(traj) + + elif qtype == "COUNTERFACTUAL": + answer_score, answer_correct = self._counterfactual_scorer.score_answer( + trajectory.final_answer, ground_truth + ) + traj = self._counterfactual_scorer.score_trajectory( + trajectory, question, ground_truth + ) + traj_score = traj.composite + traj_detail = asdict(traj) + + elif qtype == "SILENCE": + answer_score, answer_correct = self._silence_scorer.score_answer( + trajectory.final_answer, ground_truth + ) + traj = self._silence_scorer.score_trajectory(trajectory, question) + traj_score = traj.composite + traj_detail = asdict(traj) + + elif qtype == "GRAPH": + graph_subtype = question.get("graph_subtype", "") + traj_score = score_graph_trajectory( + trajectory=trajectory, + question=question, + ground_truth=question.get("ground_truth", {}), + ) + answer_score, answer_correct = score_graph_answer( + agent_answer=trajectory.final_answer, + ground_truth=ground_truth, + graph_subtype=graph_subtype, + ) + traj_detail = {"traj_score": traj_score} # ← add this line + + else: + raise ValueError(f"Unknown question type: {qtype}") + + weights = _TRACK_WEIGHTS[qtype] + + # ── Combined score ──────────────────────────────────────────────────── + # Zero-shot: no tools, so trajectory is meaningless. combined = answer_score + # directly. This establishes the hallucination/prior-knowledge floor without + # penalising absence of tool use or inflating correct answers to 1.0. + # + # Gated (and ungated): binary — "did the agent get it right the right way?" + # A wrong answer is always 0 — trajectory cannot rescue it. + # A correct answer is disqualified if trajectory was unsound — the agent + # got lucky, not good. Otherwise combined = 1.0. + # + # Track-specific soundness conditions: + # PERSPECTIVE — answer correct + # COUNTERFACTUAL — answer correct + # SILENCE — answer correct + made at least one relevant tool call + # (traj_score == 0.0 flags no relevant search was made) + # GRAPH — answer correct + # + # answer_score and trajectory_score are preserved in meta for diagnostic + # use and model-improvement analysis. _TRACK_WEIGHTS are retained for the + # same purpose but no longer drive combined_score in gated mode. + + if self._zero_shot: + # Zero-shot: no tools, so trajectory is meaningless. + # Combined is binary on correctness — wrong answers score 0.0 regardless + # of partial credit, making zero-shot directly comparable to gated mode. + # NLI contradiction gate still applies on top: if the agent's reasoning + # contradicts the question premise it hallucinated a justification — + # combined=0.0 regardless of the boolean answer. + # If NLI is unavailable, fall back to answer_correct gate with a warning. + if not answer_correct: + combined = 0.0 + failure_reason_gate = "wrong_answer_gated" + else: + reasoning = str(trajectory.final_answer.get("reasoning", "")) + question_text = question.get("question_text") or question.get( + "question_prose", "" + ) + if self._nli and reasoning and question_text: + nli_result = self._nli.score( + premise=question_text, + hypothesis=reasoning, + ) + contradiction = nli_result.get("contradiction", 0.0) + if contradiction >= 0.70: + combined = 0.0 + failure_reason_gate = "reasoning_contradicts_question" + else: + combined = answer_score + failure_reason_gate = None + else: + if self._nli and not reasoning: + logger.warning( + " [zero-shot] no reasoning field — skipping NLI, using answer_score" + ) + combined = answer_score + failure_reason_gate = None + elif not answer_correct: + combined = 0.0 + failure_reason_gate = "wrong_answer_gated" + elif qtype == "SILENCE" and traj_score == 0.0: + combined = 0.0 + failure_reason_gate = "no_relevant_search_gated" + else: + combined = 1.0 + failure_reason_gate = None + + failure_reason = None + if trajectory.budget_exceeded: + failure_reason = "step_budget_exceeded" + if failure_reason_gate: + failure_reason = failure_reason or failure_reason_gate + + return EvalResult( + question_id=question["question_id"], + question_type=qtype, + difficulty=question.get("difficulty", "unknown"), + answer_score=round(answer_score, 4), + answer_correct=answer_correct, + trajectory_score=round(traj_score, 4), + combined_score=round(combined, 4), + failure_reason=failure_reason, + tool_call_count=len(trajectory.tool_calls), + meta={ + "model": self._model, + "eval_mode": ( + "zero_shot" + if self._zero_shot + else "ungated" + if self._ungated + else "gated" + ), + "as_of_time": as_of_time, + "budget_exceeded": trajectory.budget_exceeded, + "trajectory_detail": traj_detail, + "horizon_violations": trajectory.horizon_violations, + "actor_gate_violations": trajectory.actor_gate_violations, + "subsystem_violations": trajectory.subsystem_violations, + "dead_ends_hit": trajectory.dead_ends_hit, + "dead_ends_recovered": trajectory.dead_ends_recovered, + "total_latency_ms": round(trajectory.total_latency_ms, 1), + "tool_calls": [asdict(tc) for tc in trajectory.tool_calls], + "final_answer": trajectory.final_answer, + "prompt_tokens": trajectory.prompt_tokens, + "completion_tokens": trajectory.completion_tokens, + "total_tokens": trajectory.total_tokens, + }, + ) + + # ── Converse API tool definitions ──────────────────────────────────────── + + _CONVERSE_TOOL_DEFS: Dict[str, dict] = { + "get_ticket": { + "toolSpec": { + "name": "get_ticket", + "description": "Retrieve a Jira ticket by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "ticket_id": { + "type": "string", + "description": "Jira ticket ID, e.g. 'ENG-42'", + } + }, + "required": ["ticket_id"], + } + }, + } + }, + "get_confluence_page": { + "toolSpec": { + "name": "get_confluence_page", + "description": "Retrieve a Confluence page by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "page_id": { + "type": "string", + "description": "Confluence page ID", + } + }, + "required": ["page_id"], + } + }, + } + }, + "get_slack_thread": { + "toolSpec": { + "name": "get_slack_thread", + "description": "Retrieve a Slack thread by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "thread_id": { + "type": "string", + "description": "Slack thread ID", + } + }, + "required": ["thread_id"], + } + }, + } + }, + "get_email": { + "toolSpec": { + "name": "get_email", + "description": "Retrieve an email by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "email_id": { + "type": "string", + "description": "Email artifact ID", + } + }, + "required": ["email_id"], + } + }, + } + }, + "get_pr": { + "toolSpec": { + "name": "get_pr", + "description": "Retrieve a pull request by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "pr_id": { + "type": "string", + "description": "Pull request ID", + } + }, + "required": ["pr_id"], + } + }, + } + }, + "get_zd_ticket": { + "toolSpec": { + "name": "get_zd_ticket", + "description": "Retrieve a Zendesk support ticket by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "ticket_id": { + "type": "string", + "description": "Zendesk ticket ID", + } + }, + "required": ["ticket_id"], + } + }, + } + }, + "get_sf_opportunity": { + "toolSpec": { + "name": "get_sf_opportunity", + "description": "Retrieve a Salesforce opportunity by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "opp_id": { + "type": "string", + "description": "Salesforce opportunity ID", + } + }, + "required": ["opp_id"], + } + }, + } + }, + "get_sf_account": { + "toolSpec": { + "name": "get_sf_account", + "description": "Retrieve a Salesforce account by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "account_id": { + "type": "string", + "description": "Salesforce account ID", + } + }, + "required": ["account_id"], + } + }, + } + }, + "get_zoom_transcript": { + "toolSpec": { + "name": "get_zoom_transcript", + "description": "Retrieve a Zoom meeting transcript by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "transcript_id": { + "type": "string", + "description": "Zoom transcript ID", + } + }, + "required": ["transcript_id"], + } + }, + } + }, + "get_datadog_alert": { + "toolSpec": { + "name": "get_datadog_alert", + "description": "Retrieve a Datadog alert by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "alert_id": { + "type": "string", + "description": "Datadog alert ID", + } + }, + "required": ["alert_id"], + } + }, + } + }, + "get_invoice": { + "toolSpec": { + "name": "get_invoice", + "description": "Retrieve an invoice by ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "invoice_id": { + "type": "string", + "description": "Invoice ID", + } + }, + "required": ["invoice_id"], + } + }, + } + }, + "get_nps_response": { + "toolSpec": { + "name": "get_nps_response", + "description": "Retrieve an NPS survey response by account name.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "account_name": { + "type": "string", + "description": "Account name", + } + }, + "required": ["account_name"], + } + }, + } + }, + "search_artifacts": { + "toolSpec": { + "name": "search_artifacts", + "description": "Search for artifacts by keyword when you don't have a specific ID.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Keyword or artifact ID to search for", + }, + "doc_type": { + "type": "string", + "description": "Filter by type: jira, confluence, slack, email, pr, zd_ticket, zoom", + }, + "actor": { + "type": "string", + "description": "Optional actor name filter", + }, + "after_day": { + "type": "integer", + "description": "Only return artifacts from this day onward", + }, + }, + "required": ["query"], + } + }, + } + }, + "get_graph_snapshot": { + "toolSpec": { + "name": "get_graph_snapshot", + "description": "Returns the social graph for a given simulation day: nodes (actor names), edges (source, target, weight).", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "day": { + "type": "integer", + "description": "Simulation day number", + } + }, + "required": ["day"], + } + }, + } + }, + "get_stress_snapshot": { + "toolSpec": { + "name": "get_stress_snapshot", + "description": "Returns stress scores {name: int} for all actors on a given day. Stress >= 72 indicates burnout risk.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "day": { + "type": "integer", + "description": "Simulation day number", + } + }, + "required": ["day"], + } + }, + } + }, + } + + def _build_tool_dispatch(self, gated: GatedTools) -> Dict[str, Any]: + return { + "get_ticket": lambda **kw: gated.get_ticket(kw["ticket_id"]), + "get_confluence_page": lambda **kw: gated.get_confluence_page( + kw["page_id"] + ), + "get_slack_thread": lambda **kw: gated.get_slack_thread(kw["thread_id"]), + "get_email": lambda **kw: gated.get_email(kw["email_id"]), + "get_pr": lambda **kw: gated.get_pr(kw["pr_id"]), + "get_zd_ticket": lambda **kw: gated.get_zd_ticket(kw["ticket_id"]), + "get_sf_opportunity": lambda **kw: gated.get_sf_opportunity(kw["opp_id"]), + "get_sf_account": lambda **kw: gated.get_sf_account(kw["account_id"]), + "get_zoom_transcript": lambda **kw: gated.get_zoom_transcript( + kw["transcript_id"] + ), + "get_datadog_alert": lambda **kw: gated.get_datadog_alert(kw["alert_id"]), + "get_invoice": lambda **kw: gated.get_invoice(kw["invoice_id"]), + "get_nps_response": lambda **kw: gated.get_nps_response(kw["account_name"]), + "get_events_for_day": lambda **kw: gated.get_events_for_day( + kw["day"], kw.get("event_type") + ), + "search_artifacts": lambda **kw: gated.search_artifacts( + kw["query"], + kw.get("doc_type", ""), + actor=kw.get("actor", ""), + after_day=kw.get("after_day"), + ), + "get_graph_snapshot": lambda **kw: gated.get_graph_snapshot(kw["day"]), + "get_stress_snapshot": lambda **kw: gated.get_stress_snapshot(kw["day"]), + } + + _SUBSYSTEM_TOOLS: Dict[str, List[str]] = { + "jira": ["get_ticket"], + "confluence": ["get_confluence_page"], + "slack": ["get_slack_thread"], + "email": ["get_email"], + "invoice": ["get_invoice"], + "git": ["get_pr"], + "zendesk": ["get_zd_ticket"], + "salesforce": ["get_sf_opportunity", "get_sf_account", "get_nps_response"], + "zoom": ["get_zoom_transcript"], + "datadog": ["get_datadog_alert"], + } + + _SEARCH_SPACE_SUBSYSTEM: List[Tuple[str, str]] = [ + ("CONF-", "confluence"), + ("ENG-", "jira"), + ("IT-", "jira"), + ("ORG-", "jira"), + ("QA-", "jira"), + ("HR-", "jira"), + ("PROD-", "jira"), + ("DES-", "jira"), + ("SALES-", "jira"), + ("PR-", "git"), + ("ZD-", "zendesk"), + ("SF-ACC", "salesforce"), + ("SF-OPP", "salesforce"), + ("ZOOM-", "zoom"), + ("DD-", "datadog"), + ("INV-", "invoice"), + ("NPS-", "salesforce"), + ("slack_", "slack"), + ("ext_email_", "email"), + ("customer_reply_", "email"), + ("reply_customer_", "email"), + ("ack_vendor_", "email"), + ("hr_outbound_", "email"), + ] + + def _subsystems_for_question(self, question: dict) -> Optional[Set[str]]: + qtype = question.get("question_type", "") + + if qtype == "PERSPECTIVE": + visible = question.get("actor_visible_artifacts", []) + if visible: + needed: Set[str] = set() + for entry in visible: + for prefix, sub in self._SEARCH_SPACE_SUBSYSTEM: + if entry.startswith(prefix): + needed.add(sub) + break + if needed: + if not question.get("cross_subsystem") or len(needed) > 1: + return needed + + event_type = question.get("event_type", "") + for sub, types in _SUBSYSTEM_EVENT_TYPES.items(): + if event_type in types: + return {sub} + + subs = question.get("subsystem_access") + return set(subs) if subs else None + + if qtype == "COUNTERFACTUAL": + subs = question.get("subsystems_involved") + if subs: + return set(subs) + + gt = question.get("ground_truth", {}) + chain = gt.get("evidence_chain_artifacts", {}) + all_ids = chain.get("cause", []) + chain.get("effect", []) + needed: Set[str] = set() + for entry in all_ids: + for prefix, sub in self._SEARCH_SPACE_SUBSYSTEM: + if entry.startswith(prefix): + needed.add(sub) + break + if needed: + return needed + + cause_type = gt.get("cause_event_type", "") + effect_type = gt.get("effect_event_type", "") + for event_type in [cause_type, effect_type]: + for sub, types in _SUBSYSTEM_EVENT_TYPES.items(): + if event_type in types: + needed.add(sub) + if needed: + return needed + + return {"__events_only__"} + + if qtype == "GRAPH": + return None + + space = question.get("expected_search_space", []) + if not space: + return None + needed: Set[str] = set() + for entry in space: + for prefix, sub in self._SEARCH_SPACE_SUBSYSTEM: + if entry.startswith(prefix) or entry.lower().startswith(prefix.lower()): + needed.add(sub) + break + return needed if needed else None + + def _selected_tool_defs(self, question: dict) -> List[dict]: + if self._zero_shot: + return [] + + needed = self._subsystems_for_question(question) + + if needed is None: + selected = [ + v + for k, v in self._CONVERSE_TOOL_DEFS.items() + if k != "get_events_for_day" + ] + else: + allowed: Set[str] = {"search_artifacts"} + for sub in needed: + for tool_name in self._SUBSYSTEM_TOOLS.get(sub, []): + allowed.add(tool_name) + + selected = [ + v + for k, v in self._CONVERSE_TOOL_DEFS.items() + if k in allowed and k != "get_events_for_day" + ] + + selected.append(self._build_events_tool_def(question)) + + return selected + + def _build_system_prompt(self, question: dict) -> str: + qtype = question["question_type"] + + base = ( + "You are an Enterprise Knowledge Analyst evaluating enterprise AI systems. " + "You reason carefully over corporate documents to answer complex questions. " + "You cite evidence by artifact ID, stay within stated constraints, and never guess. " + "Respond ONLY with the requested JSON object — no preamble. " + "You have access to the full conversation history. Never call a tool to retrieve " + "an artifact you have already retrieved in a previous step. Re-read the earlier " + "result instead. IMPORTANT: Only call one tool at a time. Calling multiple tools " + "is a violation." + ) + + if self._zero_shot: + base += ( + "\nYou have no tools available. " + "Answer based on your knowledge alone and respond directly with the JSON object." + ) + if qtype == "COUNTERFACTUAL": + full_taxonomy = "\n".join( + f"- {k}: {v}" for k, v in self._CAUSAL_LINK_TAXONOMY.items() + ) + return ( + f"{base}\n\n" + f"IMPORTANT: This is a counterfactual question. You must identify the explicit " + f"causal link — do not speculate.\n\n" + f"You MUST categorize the link using one of the following labels:\n{full_taxonomy}\n\n" + f"Determine whether removing the cause would have changed the effect. " + f"You must return a boolean for outcome_changed. Use false if you cannot " + f"confirm the outcome changed. Never return null." + ) + if qtype == "PERSPECTIVE": + actor = question.get("actor", "the actor") + day = question.get("as_of_day", "?") + return ( + f"{base}\n\n" + f"IMPORTANT: You are answering from the perspective of {actor} as of Day {day}. " + f"Using information the actor would not have had is a scoring violation." + ) + return base + + if qtype == "PERSPECTIVE": + actor = question.get("actor", "the actor") + day = question.get("as_of_day", "?") + subs = ", ".join(question.get("subsystem_access", [])) + return ( + f"{base}\n\n" + f"IMPORTANT: You are answering from the perspective of {actor} as of Day {day}. " + f"This actor has credentials for: {subs}. " + f"Having credentials for a system does not mean the actor can see all content in it — " + f"access depends on the actor's role and direct involvement. " + f"Using information the actor would not have had is a scoring violation." + ) + + if qtype == "COUNTERFACTUAL": + full_taxonomy = "\n".join( + f"- {k}: {v}" for k, v in self._CAUSAL_LINK_TAXONOMY.items() + ) + return ( + f"{base}\n\n" + f"IMPORTANT: This is a counterfactual question. You must identify the explicit " + f"causal link in the data — do not speculate.\n\n" + f"You MUST categorize the link using one of the following labels:\n{full_taxonomy}\n\n" + f"Find the cause event and the effect event, then determine whether " + f"removing the cause would have changed the effect. " + f"Once you have retrieved both the cause artifact and the effect artifact, " + f"stop using tools immediately and provide your answer. " + "If you cannot find sufficient evidence to determine whether the outcome changed, " + "you must still return a boolean for outcome_changed. Use false if you cannot " + "confirm the outcome changed. Never return null." + ) + + if qtype == "GRAPH": + return ( + f"{base}\n\n" + "You have access to two additional tools for GRAPH questions:\n" + " get_graph_snapshot(day: int) — returns the collaboration graph for a given\n" + " simulation day: nodes (actor names), edges (source, target, weight).\n" + " Edge weights reflect interaction frequency; higher = stronger relationship.\n" + " get_stress_snapshot(day: int) — returns stress scores {name: int} for all\n" + " actors on a given day. Stress >= 72 indicates burnout risk.\n\n" + "To answer GRAPH questions you MUST call at least one of these tools.\n" + "Guessing from memory without calling a graph tool will score 0 on trajectory\n" + "even if the answer is correct." + ) + + return ( + f"{base}\n\n" + f"IMPORTANT: This is an absence question. Search the corpus before concluding absence. " + f"If you find clear evidence, stop immediately and answer. " + f"Only conclude absence after you have checked the expected locations." + ) + + def _build_user_prompt(self, question: dict) -> str: + qtype = question["question_type"] + allowed_links = self._allowed_links_str(question) + + schema = { + "PERSPECTIVE": ( + '{"could_actor_have_known": bool, "reasoning": "str", ' + '"evidence_artifacts": ["id", ...], "blocked_subsystems": ["str", ...]}' + ), + "COUNTERFACTUAL": ( + '{"outcome_changed": bool, ' + f'"causal_mechanism": "", ' + '"mechanism": "str", "actors": ["name", ...], "reasoning": "str"}' + ), + "SILENCE": ( + '{"exists": bool, "answer": "yes|no", ' + '"reasoning": "what you searched and found"}' + ), + "GRAPH": ( + '{"answer": "str", "reasoning": "str", ' + '"evidence_nodes": ["name", ...], "evidence_days": [int, ...]}' + ), + }[qtype] + + return f"{question.get('question_text') or question.get('question_prose', '')}\n\nRespond with JSON:\n{schema}" + + _CAUSAL_LINK_TAXONOMY: Dict[str, str] = { + "involves_gap": "incident ← knowledge gap", + "recurrence_of": "incident ← prior unresolved incident", + "spawned_doc": "confluence ← design discussion", + "email_dropped": "communication failure ← routing gap", + "sf_ownership_lapsed": "CRM gap ← employee departure", + "zd_escalation_source": "incident ← support ticket escalation", + "blocker_flagged": "blocker → delayed progress", + "incident_coordination": "incident → external contact", + "departure_reassignment": "departure → ticket/escalation shift", + "assignment_domain_mismatch": "planning mismatch → knowledge gap → incident", + "jira_from_vendor_email": "Jira ticket ← inbound vendor email", + "jira_from_customer_email": "Jira ticket ← inbound customer email", + "customer_escalation_relayed": "customer escalation ← support email routed", + "incident_handoff": "escalation chain ← forced handoff on departure", + "pr_gap_detected": "knowledge gap ← undocumented domain in PR review", + "async_gap_detected": "knowledge gap ← undocumented domain in async thread", + "centrality_vacuum": "knowledge gap ← key person departure", + "sf_stage_advanced_by_customer": "CRM stage advanced ← inbound customer email", + "feature_request_fyi": "product FYI ← inbound feature request", + "proactive_outreach_from_crm_signal": "outreach ← at-risk CRM signal", + "ticket_completion_notifies_lead": "lead notified ← dependent ticket done", + "org_collision_tension": "org tension ← overlapping responsibilities", + "postmortem_from_incident": "postmortem ← incident resolved", + "incident_triggers_risk_flag": "SF risk flag ← active incident", + "review_triggers_revision": "PR revision ← code review", + "hire_fills_knowledge_gap": "gap closed ← new hire expertise", + "escalation_from_zendesk": "incident ← Zendesk escalation", + "doc_gap_detected": "knowledge gap ← undocumented domain in documentation review", + } + + _LINK_DISTRACTORS: Dict[str, List[str]] = { + "involves_gap": ["recurrence_of", "assignment_domain_mismatch"], + "recurrence_of": ["involves_gap", "blocker_flagged"], + "spawned_doc": ["involves_gap", "incident_coordination"], + "email_dropped": ["sf_ownership_lapsed", "incident_coordination"], + "sf_ownership_lapsed": ["departure_reassignment", "email_dropped"], + "zd_escalation_source": ["involves_gap", "incident_coordination"], + "doc_gap_detected": ["pr_gap_detected", "async_gap_detected"], + "blocker_flagged": ["recurrence_of", "involves_gap"], + "incident_coordination": ["zd_escalation_source", "blocker_flagged"], + "departure_reassignment": ["sf_ownership_lapsed", "incident_handoff"], + "assignment_domain_mismatch": ["involves_gap", "recurrence_of"], + "jira_from_vendor_email": ["jira_from_customer_email", "incident_coordination"], + "jira_from_customer_email": [ + "jira_from_vendor_email", + "customer_escalation_relayed", + ], + "customer_escalation_relayed": [ + "zd_escalation_source", + "jira_from_customer_email", + ], + "incident_handoff": ["departure_reassignment", "centrality_vacuum"], + "pr_gap_detected": ["async_gap_detected", "involves_gap"], + "async_gap_detected": ["pr_gap_detected", "involves_gap"], + "centrality_vacuum": ["involves_gap", "departure_reassignment"], + "sf_stage_advanced_by_customer": [ + "proactive_outreach_from_crm_signal", + "feature_request_fyi", + ], + "feature_request_fyi": [ + "sf_stage_advanced_by_customer", + "jira_from_customer_email", + ], + "proactive_outreach_from_crm_signal": [ + "sf_stage_advanced_by_customer", + "incident_triggers_risk_flag", + ], + "ticket_completion_notifies_lead": [ + "blocker_flagged", + "departure_reassignment", + ], + "org_collision_tension": ["centrality_vacuum", "assignment_domain_mismatch"], + "postmortem_from_incident": ["spawned_doc", "involves_gap"], + "incident_triggers_risk_flag": [ + "proactive_outreach_from_crm_signal", + "escalation_from_zendesk", + ], + "review_triggers_revision": ["pr_gap_detected", "blocker_flagged"], + "hire_fills_knowledge_gap": ["centrality_vacuum", "involves_gap"], + "escalation_from_zendesk": ["zd_escalation_source", "incident_coordination"], + } + + def _trimmed_taxonomy(self, link_type: str) -> str: + if link_type and link_type in self._CAUSAL_LINK_TAXONOMY: + distractors = self._LINK_DISTRACTORS.get(link_type, []) + relevant = {link_type} | set(distractors) + return "\n".join( + f"- {k}: {v}" + for k, v in self._CAUSAL_LINK_TAXONOMY.items() + if k in relevant + ) + return "\n".join(f"- {k}: {v}" for k, v in self._CAUSAL_LINK_TAXONOMY.items()) + + def _allowed_links_str(self, question: dict) -> str: + # Always return the full set — narrowing leaks the answer. + return ", ".join(self._CAUSAL_LINK_TAXONOMY.keys()) + + _RETRYABLE_ERRORS = ( + "InternalServerException", + "ThrottlingException", + "ServiceUnavailableException", + "ModelTimeoutException", + ) + _MAX_RETRIES = 5 + _RETRY_BASE_DELAY = 2.0 # seconds; doubles each attempt + + def _converse_with_retry(self, kwargs: Dict[str, Any]) -> dict: + """ + Calls bedrock.converse with exponential backoff for transient errors. + Raises the final exception if all retries are exhausted. + """ + import random + + delay = self._RETRY_BASE_DELAY + last_exc: Exception = RuntimeError("No attempts made") + + for attempt in range(self._MAX_RETRIES): + try: + if self._call_delay > 0: + time.sleep(self._call_delay) + return self._bedrock.converse(**kwargs) + except Exception as exc: + last_exc = exc + exc_str = str(exc) + retryable = any(tag in exc_str for tag in self._RETRYABLE_ERRORS) + if not retryable: + raise + jitter = random.uniform(0, delay * 0.25) + wait = delay + jitter + logger.warning( + f" Bedrock transient error (attempt {attempt + 1}/{self._MAX_RETRIES}), " + f"retrying in {wait:.1f}s: {exc}" + ) + time.sleep(wait) + delay = min(delay * 2, 60.0) + + raise last_exc + + def _strip_confluence_content(self, messages: list, seen: set) -> list: + """Return messages with confluence page content stripped from all but the first occurrence. + + Bug fix: the original implementation immediately reassigned `seen` to a new + empty set, so deduplication never occurred and the context window grew + unboundedly on repeated confluence retrievals. + """ + # Do NOT reassign `seen` — it is passed in by reference so the caller's + # set accumulates across multiple calls within the same agent loop. + stripped = [] + for msg in messages: + if msg["role"] != "user": + stripped.append(msg) + continue + new_content = [] + for block in msg.get("content", []): + if "toolResult" not in block: + new_content.append(block) + continue + tool_result = block["toolResult"] + result_text = tool_result.get("content", [{}])[0].get("text", "") + try: + doc = json.loads(result_text) + page_id = doc.get("id", "") + if page_id and page_id.startswith("CONF-") and "content" in doc: + if page_id in seen: + doc.pop("content") + doc["_note"] = "content already provided in earlier step" + block = { + "toolResult": { + **tool_result, + "content": [{"text": json.dumps(doc)}], + } + } + else: + seen.add(page_id) + except (json.JSONDecodeError, AttributeError): + pass + new_content.append(block) + stripped.append({**msg, "content": new_content}) + return stripped + + def _run_agent(self, question: dict, tools: GatedTools) -> AgentTrajectory: + """ + Runs the agent against the question using the Bedrock Converse API. + Returns a populated AgentTrajectory. + """ + + qtype = question["question_type"] + trajectory = AgentTrajectory( + question_id=question["question_id"], + question_type=qtype, + ) + + system_prompt = self._build_system_prompt(question) + user_prompt = self._build_user_prompt(question) + tool_defs = self._selected_tool_defs(question) + dispatch = self._build_tool_dispatch(tools) + + messages = [ + {"role": "user", "content": [{"text": user_prompt}]}, + ] + + converse_kwargs: Dict[str, Any] = { + "modelId": self._model, + "system": [{"text": system_prompt}], + "messages": messages, + "inferenceConfig": {"temperature": 0.0, "maxTokens": 4096}, + "serviceTier": {"type": self._service_tier}, + } + + if tool_defs: + converse_kwargs["toolConfig"] = { + "tools": tool_defs, + } + if not self._model.startswith("us.anthropic."): + converse_kwargs["toolConfig"]["toolChoice"] = {"auto": {}} + elif self._model.startswith("us.anthropic."): + converse_kwargs["additionalModelRequestFields"] = { + "tool_choice": {"type": "auto", "disable_parallel_tool_use": True} + } + if self._model.startswith(("qwen.qwen3", "mistral.mistral")): + converse_kwargs["additionalModelRequestFields"] = { + "parallel_tool_calls": False + } + + _FINAL_ANSWER_TOOL_NAME = "final_answer" + _FINAL_ANSWER_SCHEMAS: Dict[str, dict] = { + "PERSPECTIVE": { + "type": "object", + "properties": { + "could_actor_have_known": {"type": "boolean"}, + "reasoning": {"type": "string"}, + "evidence_artifacts": { + "type": "array", + "items": {"type": "string"}, + }, + "blocked_subsystems": { + "type": "array", + "items": {"type": "string"}, + }, + }, + "required": [ + "could_actor_have_known", + "reasoning", + "evidence_artifacts", + "blocked_subsystems", + ], + }, + "COUNTERFACTUAL": { + "type": "object", + "properties": { + "outcome_changed": {"type": "boolean"}, + "causal_mechanism": {"type": "string"}, + "mechanism": {"type": "string"}, + "actors": {"type": "array", "items": {"type": "string"}}, + "reasoning": {"type": "string"}, + }, + "required": [ + "outcome_changed", + "causal_mechanism", + "mechanism", + "actors", + "reasoning", + ], + }, + "SILENCE": { + "type": "object", + "properties": { + "exists": {"type": "boolean"}, + "answer": {"type": "string"}, + "reasoning": {"type": "string"}, + }, + "required": ["exists", "answer", "reasoning"], + }, + "GRAPH": { + "type": "object", + "properties": { + "answer": {"type": "string"}, + "reasoning": {"type": "string"}, + "evidence_nodes": {"type": "array", "items": {"type": "string"}}, + "evidence_days": {"type": "array", "items": {"type": "integer"}}, + }, + "required": ["answer", "reasoning", "evidence_nodes", "evidence_days"], + }, + } + + t_start = time.time() + budget_exceeded = False + total_input = 0 + total_output = 0 + confluence_seen: set = set() + stop_reason = "end_turn" + output_message: dict = {"content": []} + + max_steps = self._max_steps + if qtype == "SILENCE": + search_space = question.get("expected_search_space", []) + max_steps = max(self._max_steps, len(search_space) + 5) + elif qtype == "GRAPH": + max_steps = max(self._max_steps, 4) + + for step in range(max_steps): + converse_kwargs["messages"] = self._strip_confluence_content( + messages, confluence_seen + ) + + if step == max_steps - 1: + logger.info(f" [step {step}] forcing final_answer tool for {qtype}") + final_answer_schema = _FINAL_ANSWER_SCHEMAS.get( + qtype, + { + "type": "object", + "properties": { + "answer": {"type": "string"}, + "reasoning": {"type": "string"}, + }, + "required": ["answer", "reasoning"], + }, + ) + final_answer_tool = { + "toolSpec": { + "name": "final_answer", + "description": ( + "You have reached your maximum number of steps. " + "Call this tool to provide your final answer based on what you have found so far." + ), + "inputSchema": {"json": final_answer_schema}, + } + } + + if self._model.startswith( + ( + "mistral.", + "moonshot.", + "moonshotai.", + "qwen.", + "deepseek.", + "zai.", + "minimax.", + ) + ): + converse_kwargs["toolConfig"] = { + "tools": [final_answer_tool], + "toolChoice": {"any": {}}, + } + converse_kwargs.pop("additionalModelRequestFields", None) + else: + converse_kwargs["toolConfig"] = { + "tools": [final_answer_tool], + "toolChoice": {"tool": {"name": _FINAL_ANSWER_TOOL_NAME}}, + } + converse_kwargs.pop("additionalModelRequestFields", None) + + try: + response = self._converse_with_retry(converse_kwargs) + except Exception as exc: + logger.error(f" Bedrock Converse error (gave up after retries): {exc}") + break + + usage = response.get("usage", {}) + total_input += usage.get("inputTokens", 0) + total_output += usage.get("outputTokens", 0) + + output_message = response["output"]["message"] + stop_reason = response.get("stopReason", "end_turn") + + messages.append(output_message) + logger.info(f" [step {step}] stop_reason: {stop_reason}") + for block in output_message["content"]: + if "text" in block: + logger.info(f" [step {step}] agent: {block['text'][:500]}") + elif "toolUse" in block: + logger.info( + f" [step {step}] tool_use: {block['toolUse']['name']}({block['toolUse']['input']})" + ) + + converse_kwargs["messages"] = messages + + if stop_reason in ("end_turn", "max_tokens"): + text_parts = [ + block["text"] + for block in output_message["content"] + if "text" in block + ] + raw = "\n".join(text_parts) + trajectory.final_answer = self._parse_structured_answer(raw) + break + + if stop_reason == "tool_use": + for block in output_message["content"]: + if ( + "toolUse" in block + and block["toolUse"]["name"] == _FINAL_ANSWER_TOOL_NAME + ): + trajectory.final_answer = block["toolUse"].get("input", {}) + logger.info( + f" [step {step}] final_answer extracted from forced tool call" + ) + break + + if trajectory.final_answer: + break + + tool_results = [] + for block in output_message["content"]: + if "toolUse" not in block: + continue + + tc = block["toolUse"] + fn_name = tc["name"] + fn_input = tc["input"] + tool_use_id = tc["toolUseId"] + + handler = dispatch.get(fn_name) + if handler: + t0 = time.time() + try: + result = handler(**fn_input) + except Exception as exc: + result = {"error": str(exc)} + tools._call_log.append( + ToolCall( + tool_name=fn_name, + arguments=fn_input, + result_ids=[], + result_types=[], + timestamp_requested=None, + timestamp_applied=None, + temporal_drift_days=None, + temporal_drift_violation=False, + horizon_violation=False, + actor_gate_violation=False, + subsystem_violation=False, + returned_empty=True, + latency_ms=(time.time() - t0) * 1000, + ) + ) + else: + result = {"error": f"Unknown tool: {fn_name}"} + + result_str = ( + result + if isinstance(result, str) + else json.dumps(result, default=str) + ) + + tool_results.append( + { + "toolResult": { + "toolUseId": tool_use_id, + "content": [{"text": result_str}], + } + } + ) + + messages.append({"role": "user", "content": tool_results}) + converse_kwargs["messages"] = messages + else: + break + else: + budget_exceeded = True + for msg in reversed(messages): + if msg.get("role") != "assistant": + continue + for block in msg.get("content", []): + if "text" in block: + trajectory.final_answer = self._parse_structured_answer( + block["text"] + ) + break + if trajectory.final_answer: + break + if not trajectory.final_answer: + logger.warning( + f" [budget_exceeded] No assistant text found in message history for {question['question_id']}" + ) + + trajectory.total_latency_ms = (time.time() - t_start) * 1000 + trajectory.budget_exceeded = budget_exceeded + trajectory.tool_calls = list(tools.call_log) + trajectory.final_answer = trajectory.final_answer or {} + trajectory.prompt_tokens = total_input + trajectory.completion_tokens = total_output + trajectory.total_tokens = total_input + total_output + trajectory.horizon_violations = sum( + 1 for c in trajectory.tool_calls if c.horizon_violation + ) + trajectory.actor_gate_violations = sum( + 1 for c in trajectory.tool_calls if c.actor_gate_violation + ) + trajectory.subsystem_violations = sum( + 1 for c in trajectory.tool_calls if c.subsystem_violation + ) + trajectory.dead_ends_hit = sum( + 1 for c in trajectory.tool_calls if c.returned_empty + ) + for i, call in enumerate(trajectory.tool_calls): + if call.returned_empty and i + 1 < len(trajectory.tool_calls): + if not trajectory.tool_calls[i + 1].returned_empty: + trajectory.dead_ends_recovered += 1 + + return trajectory + + def _parse_structured_answer(self, raw: str) -> Dict: + """Extract JSON from agent response. Strips markdown fences.""" + text = raw.strip() + text = re.sub(r"^```(?:json)?\s*", "", text) + text = re.sub(r"\s*```$", "", text) + try: + return json.loads(text) + except json.JSONDecodeError: + # Try to find JSON object in response + match = re.search(r"\{.*\}", text, re.DOTALL) + if match: + try: + return json.loads(match.group()) + except json.JSONDecodeError: + pass + return {"raw_response": raw} + + def _infer_as_of_time(self, question: dict) -> str: + qtype = question.get("question_type", "") + if qtype == "SILENCE": + events = self._mem.get_event_log(from_db=True) + max_day = max((e.day for e in events), default=1) + return _business_day_to_date(_SIM_START, max_day).isoformat() + if qtype == "PERSPECTIVE": + return question.get("as_of_time", datetime.now().isoformat()) + if qtype == "COUNTERFACTUAL": + effect_id = question.get("ground_truth", {}).get("effect_event_id") + if effect_id: + try: + ev = self._mem._db["events"].find_one({"_id": effect_id}) + if ev and ev.get("timestamp"): + return str(ev["timestamp"]) + except Exception: + pass + if qtype == "GRAPH": + day = question.get("as_of_day", 1) + return _business_day_to_date(_SIM_START, day).isoformat() + day = question.get("day", question.get("event_day", 1)) + return _business_day_to_date(_SIM_START, day).isoformat() + + def _aggregate(self, results: List[EvalResult]) -> dict: + def mean(vals): + return round(sum(vals) / len(vals), 4) if vals else 0.0 + + # ── Violation-adjusted scoring ──────────────────────────────────────── + # violation_rate = total_actor_gate_violations / total_tool_calls + # compliance_factor = max(0, 1 − violation_rate) ** _VIOLATION_EXPONENT + # adjusted_score = combined_score × compliance_factor + # + # Quadratic exponent (2) means violations compound non-linearly: + # 0% violations → 1.00× multiplier (no penalty) + # 25% violations → 0.56× multiplier + # 50% violations → 0.25× multiplier (score quartered) + # 75% violations → 0.06× multiplier (effectively disqualified) + # + # This decouples compliance from trajectory scoring and makes it a + # multiplicative gate at the aggregate level — a cheating agent cannot + # overcome the penalty through high answer accuracy alone. + _VIOLATION_EXPONENT = 2 + + def _compliance_tier(rate: float) -> str: + if rate < 0.05: + return "compliant" + if rate < 0.20: + return "borderline" + return "non_compliant" + + def _violation_adjusted(combined: float, violation_rate: float) -> float: + factor = max(0.0, 1.0 - violation_rate) ** _VIOLATION_EXPONENT + return round(combined * factor, 4) + + by_type: Dict[str, List[EvalResult]] = {} + by_difficulty: Dict[str, List[EvalResult]] = {} + for r in results: + by_type.setdefault(r.question_type, []).append(r) + by_difficulty.setdefault(r.difficulty, []).append(r) + + by_type_summary = {} + for qtype, rs in by_type.items(): + total_calls = sum(r.tool_call_count for r in rs) + total_violations = sum(r.meta.get("actor_gate_violations", 0) for r in rs) + violation_rate = ( + round(total_violations / total_calls, 4) if total_calls else 0.0 + ) + base_combined = mean([r.combined_score for r in rs]) + + summary: Dict[str, Any] = { + "n": len(rs), + "answer_score": mean([r.answer_score for r in rs]), + "trajectory_score": mean([r.trajectory_score for r in rs]), + "combined_score": base_combined, + "accuracy": round(sum(r.answer_correct for r in rs) / len(rs), 4), + "avg_tool_calls": mean([r.tool_call_count for r in rs]), + "budget_exceeded_count": sum( + 1 for r in rs if r.meta.get("budget_exceeded") + ), + } + + if qtype == "PERSPECTIVE": + compliance_factor = round( + max(0.0, 1.0 - violation_rate) ** _VIOLATION_EXPONENT, 4 + ) + summary.update( + { + "violation_rate": violation_rate, + "compliance_factor": compliance_factor, + "compliance_tier": _compliance_tier(violation_rate), + # Primary leaderboard axis — combined_score alone allows a + # cheating agent to rank above a disciplined one. This number + # prevents that by applying the compliance penalty independently + # of answer quality. + "violation_adjusted_combined_score": _violation_adjusted( + base_combined, violation_rate + ), + "avg_actor_gate_violations": mean( + [r.meta.get("actor_gate_violations", 0) for r in rs] + ), + "avg_subsystem_violations": mean( + [r.meta.get("subsystem_violations", 0) for r in rs] + ), + } + ) + elif qtype == "SILENCE": + summary["relevant_search_rate"] = round( + sum( + 1 + for r in rs + if r.meta.get("trajectory_detail", {}).get("composite", 0) > 0 + ) + / len(rs), + 4, + ) + summary["avg_search_space_coverage"] = mean( + [ + r.meta.get("trajectory_detail", {}).get( + "search_space_coverage", 0 + ) + for r in rs + ] + ) + elif qtype == "COUNTERFACTUAL": + total_calls = sum(r.tool_call_count for r in rs) + total_horizon_violations = sum( + r.meta.get("horizon_violations", 0) for r in rs + ) + cf_violation_rate = ( + round(total_horizon_violations / total_calls, 4) + if total_calls + else 0.0 + ) + summary.update( + { + "horizon_violation_rate": cf_violation_rate, + "avg_horizon_violations": mean( + [r.meta.get("horizon_violations", 0) for r in rs] + ), + } + ) + + by_type_summary[qtype] = summary + + # ── Global violation_adjusted_combined_score ────────────────────────── + # A single number for cross-model ranking. Agents without PERSPECTIVE + # questions are not penalised (violation_rate = 0, factor = 1.0). + all_calls = sum(r.tool_call_count for r in results) + all_violations = sum(r.meta.get("actor_gate_violations", 0) for r in results) + global_violation_rate = ( + round(all_violations / all_calls, 4) if all_calls else 0.0 + ) + overall_combined = mean([r.combined_score for r in results]) + + return { + "overall": { + "n": len(results), + "answer_score": mean([r.answer_score for r in results]), + "trajectory_score": mean([r.trajectory_score for r in results]), + "combined_score": overall_combined, + "accuracy": round( + sum(r.answer_correct for r in results) / len(results), 4 + ), + "avg_tool_calls": mean([r.tool_call_count for r in results]), + "budget_exceeded_count": sum( + 1 for r in results if r.meta.get("budget_exceeded") + ), + "global_violation_rate": global_violation_rate, + "global_compliance_factor": round( + max(0.0, 1.0 - global_violation_rate) ** _VIOLATION_EXPONENT, 4 + ), + "global_compliance_tier": _compliance_tier(global_violation_rate), + # Primary cross-track ranking number + "violation_adjusted_combined_score": _violation_adjusted( + overall_combined, global_violation_rate + ), + }, + "by_type": by_type_summary, + "by_difficulty": { + diff: { + "n": len(rs), + "answer_score": mean([r.answer_score for r in rs]), + "trajectory_score": mean([r.trajectory_score for r in rs]), + "combined_score": mean([r.combined_score for r in rs]), + } + for diff, rs in by_difficulty.items() + }, + } + + +# ───────────────────────────────────────────────────────────────────────────── +# ENTRYPOINT +# ───────────────────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + parser = argparse.ArgumentParser( + description="OrgForge Agentic Eval Harness - PERSPECTIVE, COUNTERFACTUAL, SILENCE" + ) + parser.add_argument( + "--questions", + type=Path, + default=EVAL_DIR / "eval_questions.json", + ) + parser.add_argument( + "--out", + type=Path, + default=EVAL_DIR / "agentic_results.json", + ) + parser.add_argument( + "--model", + type=str, + default="us.anthropic.claude-sonnet-4-6", + ) + parser.add_argument( + "--max-steps", + type=int, + default=5, + help="Max tool-use steps per question (SILENCE questions may need more)", + ) + parser.add_argument( + "--types", + nargs="+", + choices=["PERSPECTIVE", "COUNTERFACTUAL", "SILENCE", "GRAPH"], + help="Run only specific tracks", + ) + parser.add_argument( + "--max-questions", + type=int, + default=None, + ) + parser.add_argument( + "--ungated", + action="store_true", + default=False, + help=( + "Disable all actor/subsystem gates — god-mode corpus access. " + "Establishes the Epistemic Tax ceiling. " + "Default output: export/eval/ungated_results.json" + ), + ) + parser.add_argument( + "--zero-shot", + action="store_true", + default=False, + help=( + "Provide no tools to the agent (no corpus access). " + "Establishes the hallucination / prior-knowledge floor. " + "Default output: export/eval/zero_shot_results.json" + ), + ) + parser.add_argument( + "--call-delay", + type=float, + default=1.0, + metavar="SECONDS", + help="Sleep between LLM calls to avoid throttling (default: 1.0s). " + "Increase to 2-3 for Opus or if you keep hitting ThrottlingException.", + ) + parser.add_argument( + "--service-tier", + type=str, + default="default", + choices=["default", "flex"], + ) + parser.add_argument( + "--skip-nli", + action="store_true", + default=False, + help=( + "Disable NLI-based reasoning validation for zero-shot runs. " + "Falls back to answer_score only. Useful if transformers/torch unavailable." + ), + ) + + args = parser.parse_args() + + stem = f"{args.model.replace('/', '_').replace(':', '_')}" + if args.zero_shot: + args.out = EVAL_DIR / f"zero_shot_{stem}.json" + elif args.ungated: + args.out = EVAL_DIR / f"ungated_{stem}.json" + else: + args.out = EVAL_DIR / f"gated_{stem}.json" + + runner = AgenticEvalRunner( + model=args.model, + max_steps=args.max_steps, + ungated=args.ungated, + zero_shot=args.zero_shot, + call_delay=args.call_delay, + service_tier=args.service_tier, + skip_nli=args.skip_nli, + ) + runner.run( + questions_path=args.questions, + out_path=args.out, + question_types=args.types, + max_questions=args.max_questions, + ) diff --git a/eval/build_leaderboard.py b/eval/build_leaderboard.py new file mode 100644 index 0000000..a32a0f2 --- /dev/null +++ b/eval/build_leaderboard.py @@ -0,0 +1,542 @@ +#!/usr/bin/env python3 +""" +build_leaderboard.py +==================== +Reads OrgForge agentic eval results from export/eval/ and produces: + + leaderboard/ + README.md ← HuggingFace dataset card (paste into your HF repo) + leaderboard.csv ← Raw numbers for programmatic use / Space rendering + per_model/ + .json ← Full merged stats per model (gated + zero-shot + ungated) + +Usage +----- + # From your project root (same level as export/): + python build_leaderboard.py + + # Point at a different eval dir or output dir: + python build_leaderboard.py --eval-dir path/to/eval --out-dir path/to/leaderboard + + # Only include specific models: + python build_leaderboard.py --models claude-3-5-sonnet gpt-4o + + # Regenerate after adding new model runs without rewriting existing per_model jsons: + python build_leaderboard.py --incremental +""" + +from __future__ import annotations + +import argparse +import csv +import json +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Optional + + +TRACKS = ["PERSPECTIVE", "COUNTERFACTUAL", "SILENCE"] + + +COLUMNS: List[tuple] = [ + ("rank", "Rank", "{}"), + ("display_name", "Model", "{}"), + ("model", "Model ID", "{}"), + ("gated.overall.violation_adjusted_combined_score", "⭐ Score (gated)", "{:.3f}"), + ("gated.overall.combined_score", "Combined", "{:.3f}"), + ("gated.overall.answer_score", "Answer", "{:.3f}"), + ("gated.overall.trajectory_score", "Trajectory", "{:.3f}"), + ("gated.overall.accuracy", "Accuracy", "{:.1%}"), + ("gated.overall.global_violation_rate", "Viol. Rate", "{:.1%}"), + ("gated.overall.global_compliance_tier", "Compliance", "{}"), + ("epistemic_tax", "Epistemic Tax", "{:.3f}"), + ("zero_shot.overall.combined_score", "Zero-Shot", "{:.3f}"), + ("gated.by_type.PERSPECTIVE.violation_adjusted_combined_score", "PERSP.", "{:.3f}"), + ("gated.by_type.COUNTERFACTUAL.combined_score", "COUNTF.", "{:.3f}"), + ("gated.by_type.SILENCE.combined_score", "SILENCE", "{:.3f}"), + ("gated.overall.avg_tool_calls", "Avg Tools", "{:.1f}"), + ("gated.overall.n", "N", "{}"), +] + + +MODEL_DISPLAY_NAMES: Dict[str, str] = { + "us.anthropic.claude-opus-4-6-v1": "Claude Opus 4.6", + "us.anthropic.claude-sonnet-4-6": "Claude Sonnet 4.6", + "claude-haiku-4-5": "Claude Haiku 4.5", + "claude-opus-4": "Claude Opus 4", + "claude-sonnet-4": "Claude Sonnet 4", + "claude-3-7-sonnet": "Claude 3.7 Sonnet", + "claude-3-5-sonnet-20241022": "Claude 3.5 Sonnet (Oct 24)", + "claude-3-5-sonnet-20240620": "Claude 3.5 Sonnet (Jun 24)", + "claude-3-5-sonnet": "Claude 3.5 Sonnet", + "claude-3-5-haiku": "Claude 3.5 Haiku", + "claude-3-opus": "Claude 3 Opus", + "claude-3-sonnet": "Claude 3 Sonnet", + "claude-3-haiku": "Claude 3 Haiku", + "gpt-4o-mini": "GPT-4o Mini", + "gpt-4o": "GPT-4o", + "gpt-4-turbo": "GPT-4 Turbo", + "gpt-4": "GPT-4", + "o3-mini": "o3-mini", + "o3": "o3", + "o1-mini": "o1-mini", + "o1": "o1", + "deepseek.v3.2": "DeepSeek v3.2", + "gemini-2.5-pro": "Gemini 2.5 Pro", + "gemini-2.5-flash": "Gemini 2.5 Flash", + "gemini-2.0-flash": "Gemini 2.0 Flash", + "gemini-1.5-pro": "Gemini 1.5 Pro", + "gemini-1.5-flash": "Gemini 1.5 Flash", + "llama-3.3-70b": "Llama 3.3 70B", + "llama-3.1-405b": "Llama 3.1 405B", + "llama-3.1-70b": "Llama 3.1 70B", + "llama-3.1-8b": "Llama 3.1 8B", + "amazon.nova-pro": "Amazon Nova Pro", + "amazon.nova-lite": "Amazon Nova Lite", + "amazon.nova-micro": "Amazon Nova Micro", + "mistral.mistral-large-3-675b-instruct": "Mistral Large 3", + "mistral-small": "Mistral Small", + "mixtral-8x7b": "Mixtral 8×7B", + "command-r-plus": "Cohere Command R+", + "moonshotai.kimi-k2.5": "Kimi 2.5", + "qwen.qwen3-235b-a22b-2507-v1_0": "Qwen3 235B", + "qwen.qwen3-235b-a22b-2507-v1/0": "Qwen3 235B", +} + +_DISPLAY_NAME_KEYS = sorted(MODEL_DISPLAY_NAMES, key=len, reverse=True) + + +def _pretty_model_name(model_id: str) -> str: + """Return a human-readable name for a model ID, or the ID itself if unknown.""" + lower = model_id.lower() + for key in _DISPLAY_NAME_KEYS: + if key.lower() in lower: + return MODEL_DISPLAY_NAMES[key] + return model_id + + +def _deep_get(d: Dict, dotpath: str, default: Any = None) -> Any: + """Safely traverse a nested dict with a dot-separated path.""" + parts = dotpath.split(".") + cur = d + for p in parts: + if not isinstance(cur, dict): + return default + cur = cur.get(p, default) + if cur is default: + return default + return cur + + +def _fmt(val: Any, fmt: str) -> str: + if val is None: + return "—" + try: + return fmt.format(val) + except (ValueError, TypeError): + return str(val) + + +def _stem_to_model(stem: str) -> str: + """Reverse the filename sanitisation applied by the harness.""" + return stem.replace("_", "/").replace(":", ":") + + +def _discover_models(eval_dir: Path, only: Optional[List[str]] = None) -> List[str]: + """Return sorted list of model stems that have at least a gated result file.""" + stems = set() + for f in eval_dir.glob("gated_*.json"): + stem = f.stem[len("gated_") :] + stems.add(stem) + if only: + keep = set() + for s in stems: + model = _stem_to_model(s) + if any(o in model or o in s for o in only): + keep.add(s) + stems = keep + return sorted(stems) + + +def _load_run(path: Path) -> Optional[Dict]: + if not path.exists(): + return None + with open(path) as f: + data = json.load(f) + return data.get("summary", data) + + +def _compute_epistemic_tax( + gated: Optional[Dict], ungated: Optional[Dict] +) -> Optional[float]: + """ + Epistemic Tax = ungated_combined - gated_combined. + Positive value means the model pays a cost for being gated (normal). + A negative value means gating somehow helped (unusual, flag it). + """ + if not gated or not ungated: + return None + g = _deep_get(gated, "overall.combined_score") + u = _deep_get(ungated, "overall.combined_score") + if g is None or u is None: + return None + return round(u - g, 4) + + +def _merge_model(stem: str, eval_dir: Path) -> Dict: + gated = _load_run(eval_dir / f"gated_{stem}.json") + zero = _load_run(eval_dir / f"zero_shot_{stem}.json") + ungated = _load_run(eval_dir / f"ungated_{stem}.json") + + raw_model_id = _stem_to_model(stem) + merged: Dict[str, Any] = { + "model": raw_model_id, + "display_name": _pretty_model_name(raw_model_id), + "stem": stem, + "generated": datetime.now(timezone.utc).isoformat(), + "gated": gated, + "zero_shot": zero, + "ungated": ungated, + "epistemic_tax": _compute_epistemic_tax(gated, ungated), + } + + merged["primary_score"] = ( + _deep_get(gated, "overall.violation_adjusted_combined_score") if gated else None + ) + return merged + + +def _build_csv_rows(ranked: List[Dict]) -> List[Dict]: + rows = [] + for i, m in enumerate(ranked, 1): + row: Dict[str, Any] = { + "rank": i, + "model": m["model"], + "display_name": m["display_name"], + } + for key, _label, _fmt in COLUMNS: + if key in ("rank", "model", "display_name"): + continue + if key == "epistemic_tax": + row[key] = m.get("epistemic_tax") + else: + row[key] = _deep_get(m, key) + rows.append(row) + return rows + + +def write_csv(ranked: List[Dict], out_path: Path) -> None: + rows = _build_csv_rows(ranked) + headers = [col[0] for col in COLUMNS] + with open(out_path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=headers, extrasaction="ignore") + writer.writeheader() + writer.writerows(rows) + print(f" ✓ CSV → {out_path}") + + +_COMPLIANCE_EMOJI = { + "compliant": "🟢", + "borderline": "🟡", + "non_compliant": "🔴", +} + + +def _compliance_badge(tier: Optional[str]) -> str: + if tier is None: + return "—" + return f"{_COMPLIANCE_EMOJI.get(tier, '')} {tier}" + + +def _md_table(ranked: List[Dict]) -> str: + """Render the main leaderboard markdown table.""" + # Header row + header_labels = [col[1] for col in COLUMNS] + sep = ["---"] * len(header_labels) + lines = [ + "| " + " | ".join(header_labels) + " |", + "| " + " | ".join(sep) + " |", + ] + + for i, m in enumerate(ranked, 1): + cells = [] + for key, _label, fmt in COLUMNS: + if key == "rank": + cells.append(str(i)) + elif key == "display_name": + cells.append(m.get("display_name") or m["model"]) + elif key == "model": + cells.append(f"`{m['model']}`") + elif key == "epistemic_tax": + val = m.get("epistemic_tax") + cells.append(_fmt(val, fmt)) + elif key == "gated.overall.global_compliance_tier": + val = _deep_get(m, key) + cells.append(_compliance_badge(val)) + else: + val = _deep_get(m, key) + cells.append(_fmt(val, fmt)) + lines.append("| " + " | ".join(cells) + " |") + + return "\n".join(lines) + + +def _per_track_table(ranked: List[Dict]) -> str: + """Secondary table: per-track breakdown for gated run.""" + track_cols = [ + ("PERSPECTIVE", "violation_adjusted_combined_score", "Score"), + ("PERSPECTIVE", "accuracy", "Acc"), + ("PERSPECTIVE", "violation_rate", "Viol%"), + ("COUNTERFACTUAL", "combined_score", "Score"), + ("COUNTERFACTUAL", "accuracy", "Acc"), + ("SILENCE", "combined_score", "Score"), + ("SILENCE", "search_space_coverage", "Search%"), + ] + headers = ["Model"] + [f"{t[:5]}/{k}" for t, k, _ in track_cols] + sep = ["---"] * len(headers) + lines = [ + "| " + " | ".join(headers) + " |", + "| " + " | ".join(sep) + " |", + ] + for m in ranked: + row = [f"`{m['model']}`"] + for track, field, _ in track_cols: + val = _deep_get(m, f"gated.by_type.{track}.{field}") + if val is None: + row.append("—") + elif isinstance(val, float): + row.append(f"{val:.3f}") + else: + row.append(str(val)) + lines.append("| " + " | ".join(row) + " |") + return "\n".join(lines) + + +def write_readme(ranked: List[Dict], out_path: Path, dataset_name: str) -> None: + now = datetime.now(timezone.utc).strftime("%Y-%m-%d") + n_models = len(ranked) + + readme = f"""--- +tags: + - leaderboard + - agentic-eval + - orgforge +license: apache-2.0 +pretty_name: {dataset_name} +size_categories: + - n<1K +--- + +# {dataset_name} + +> Last updated: **{now}** · {n_models} model(s) evaluated + +## What is this? + +This leaderboard benchmarks language models on the **OrgForge Agentic Evaluation +Harness** — a simulator-grounded benchmark for three distinct reasoning tracks: + +| Track | What it tests | Answer weight | Trajectory weight | +|---|---|---|---| +| **PERSPECTIVE** | Epistemic discipline — stay within an actor's visibility cone | 40% | 60% | +| **COUNTERFACTUAL** | Causal tracing — identify mechanism and actors | 50% | 50% | +| **SILENCE** | Absence detection — prove something doesn't exist by searching | 30% | 70% | + +Each model is evaluated under three conditions: + +| Condition | File prefix | Description | +|---|---|---| +| **Gated** | `gated_` | Actor/subsystem gates enforced — normal operating conditions | +| **Ungated** | `ungated_` | God-mode corpus access — sets the *Epistemic Tax* ceiling | +| **Zero-Shot** | `zero_shot_` | No tools provided — establishes the hallucination floor | + +**Epistemic Tax** = `ungated_combined − gated_combined`. The higher the tax, the +more a model is penalised by operating within realistic permission boundaries. + +**Primary ranking metric**: `violation_adjusted_combined_score` on the gated run. +This is `combined_score × (1 − violation_rate)²`, so a model cannot buy ranking +through high answer accuracy while ignoring epistemic discipline. + +--- + +## Leaderboard + +{_md_table(ranked)} + +### Per-track breakdown (gated run) + +{_per_track_table(ranked)} + +--- + +## Score definitions + +| Column | Definition | +|---|---| +| ⭐ Score | `combined × (1 − violation_rate)²` — primary ranking axis | +| Combined | Weighted average of answer + trajectory per track weights | +| Answer | Correctness of the final answer | +| Trajectory | Quality of the tool-call path taken | +| Accuracy | Fraction of questions answered correctly (binary) | +| Viol. Rate | Actor-gate violations / total tool calls (PERSPECTIVE track) | +| Compliance | `compliant` < 5% · `borderline` < 20% · `non_compliant` ≥ 20% | +| Epistemic Tax | ungated_combined − gated_combined | +| Zero-Shot | combined_score with no tools — hallucination floor | +| Avg Tools | Mean tool calls per question | + +--- + +## Files + +| File | Description | +|---|---| +| `leaderboard.csv` | Machine-readable leaderboard with all metrics | +| `per_model/.json` | Full merged stats per model | + +--- + +## How to reproduce + +```bash +# Gated (standard) run +python agentic_eval_harness.py --model + +# Zero-shot floor +python agentic_eval_harness.py --model --zero-shot + +# Ungated ceiling +python agentic_eval_harness.py --model --ungated + +# Build leaderboard from eval/ directory +python build_leaderboard.py +``` + +--- + +## Citation + +```bibtex +@misc{{orgforge-agentic-eval, + title = {{{dataset_name}}}, + year = {{{now[:4]}}}, + note = {{OrgForge Agentic Evaluation Harness v2}}, + url = {{https://huggingface.co/datasets/YOUR_ORG/{dataset_name.lower().replace(" ", "-")}}} +}} +``` +""" + out_path.write_text(readme, encoding="utf-8") + print(f" ✓ README → {out_path}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Build OrgForge leaderboard for HuggingFace" + ) + parser.add_argument( + "--eval-dir", + type=Path, + default=Path("export/eval"), + help="Directory containing gated_*.json / zero_shot_*.json / ungated_*.json", + ) + parser.add_argument( + "--out-dir", + type=Path, + default=Path("leaderboard"), + help="Output directory (created if missing)", + ) + parser.add_argument( + "--models", + nargs="+", + default=None, + help="Filter to specific model name substrings", + ) + parser.add_argument( + "--dataset-name", + default="OrgForge Agentic Eval Leaderboard", + help="Pretty name shown in the HuggingFace dataset card", + ) + parser.add_argument( + "--incremental", + action="store_true", + help="Skip per_model/ JSON generation for models that already have one", + ) + args = parser.parse_args() + + eval_dir: Path = args.eval_dir + out_dir: Path = args.out_dir + + if not eval_dir.exists(): + raise SystemExit(f"eval-dir not found: {eval_dir}") + + out_dir.mkdir(parents=True, exist_ok=True) + per_model_dir = out_dir / "per_model" + per_model_dir.mkdir(exist_ok=True) + + print(f"\n🔍 Scanning {eval_dir} …") + stems = _discover_models(eval_dir, only=args.models) + + if not stems: + raise SystemExit("No gated_*.json files found. Have you run the harness yet?") + + print( + f" Found {len(stems)} model(s): {', '.join(_stem_to_model(s) for s in stems)}\n" + ) + + merged_models: List[Dict] = [] + for stem in stems: + per_model_path = per_model_dir / f"{stem}.json" + if args.incremental and per_model_path.exists(): + print(f" ↩ {stem} — loading cached per_model JSON") + with open(per_model_path) as f: + m = json.load(f) + else: + print(f" ⚙ {stem} — merging gated / zero_shot / ungated …") + m = _merge_model(stem, eval_dir) + with open(per_model_path, "w") as f: + json.dump(m, f, indent=2) + print(f" saved → {per_model_path}") + merged_models.append(m) + + def _sort_key(m: Dict): + s = m.get("primary_score") + return (-s if s is not None else float("inf"), m["model"]) + + ranked = sorted(merged_models, key=_sort_key) + + print(f"\n📊 Writing leaderboard to {out_dir} …") + write_csv(ranked, out_dir / "leaderboard.csv") + write_readme(ranked, out_dir / "README.md", args.dataset_name) + + print(""" +✅ Done! + +Next steps to publish on HuggingFace +───────────────────────────────────── +1. Create a new Dataset repo on huggingface.co + (Datasets → New Dataset → set it to Public or Private) + +2. Push the leaderboard/ folder: + + pip install huggingface_hub + huggingface-cli upload YOUR_ORG/YOUR_REPO leaderboard/ . + + Or with the Python SDK: + + from huggingface_hub import HfApi + api = HfApi() + api.upload_folder( + folder_path="leaderboard", + repo_id="YOUR_ORG/YOUR_REPO", + repo_type="dataset", + ) + +3. (Optional) Create a Gradio Space that reads leaderboard.csv for + an interactive table — the standard HF leaderboard template works + out of the box with the CSV columns this script generates. + + Template: https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard +""") + + +if __name__ == "__main__": + main() diff --git a/eval/comparison-v2.py b/eval/comparison-v2.py new file mode 100644 index 0000000..ce1b712 --- /dev/null +++ b/eval/comparison-v2.py @@ -0,0 +1,1572 @@ +""" +eval_consistency.py +=================== +Cross-document consistency comparison: OrgForge vs. LLM-only baselines. + +Usage: + python eval_consistency.py --model anthropic.claude-3-5-sonnet-20241022-v2:0 + python eval_consistency.py --incidents 3 --region us-west-2 + python eval_consistency.py --db orgforge --skip-baselines + python eval_consistency.py --skip-nli # skip prose-SimEvent divergence +""" + +from __future__ import annotations + +import argparse +import json +import logging +import random +import re +import sys +from dataclasses import dataclass, field +from datetime import datetime +from itertools import combinations +from statistics import mean +import time +from typing import Any + +import boto3 +from config_loader import COMPANY_NAME +from pymongo import MongoClient + +from eval_divergence import NLIScorer, measure_artifact_divergence + +logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") +logger = logging.getLogger("eval_consistency") + + +class Bedrock: + _RETRYABLE_ERRORS = ( + "ThrottlingException", + "InternalServerException", + "ServiceUnavailableException", + "ModelTimeoutException", + "Too Many Requests", + ) + _MAX_RETRIES = 5 + _RETRY_BASE_DELAY = 2.0 + + def __init__(self, model_id: str, region: str = "us-east-1"): + self.model_id = model_id + self._client = boto3.client("bedrock-runtime", region_name=region) + + def generate( + self, + prompt: str, + system: str = "", + temperature: float = 0.7, + max_tokens: int = 4096, + ) -> str: + messages = [{"role": "user", "content": [{"text": prompt}]}] + system_list = [{"text": system}] if system else [] + kwargs: dict[str, Any] = { + "modelId": self.model_id, + "messages": messages, + "inferenceConfig": { + "maxTokens": max_tokens, + "temperature": temperature, + }, + "serviceTier": {"type": "flex"}, + } + if system_list: + kwargs["system"] = system_list + + resp = self._converse_with_retry(kwargs) + content_blocks = resp["output"]["message"]["content"] + for block in content_blocks: + if block.get("type") == "text" or "text" in block: + return block["text"] + raise ValueError(f"No text block in Bedrock response: {content_blocks}") + + def _converse_with_retry(self, kwargs: dict[str, Any]) -> dict: + delay = self._RETRY_BASE_DELAY + last_exc: Exception = RuntimeError("No attempts made") + for attempt in range(self._MAX_RETRIES): + try: + return self._client.converse(**kwargs) + except Exception as exc: + last_exc = exc + retryable = any(tag in str(exc) for tag in self._RETRYABLE_ERRORS) + if not retryable: + raise + jitter = random.uniform(0, delay * 0.25) + wait = delay + jitter + logger.warning( + f"Bedrock transient error (attempt {attempt + 1}/{self._MAX_RETRIES}), " + f"retrying in {wait:.1f}s: {exc}" + ) + time.sleep(wait) + delay = min(delay * 2, 60.0) + raise last_exc + + +@dataclass +class ArtifactDoc: + artifact_id: str + artifact_type: str + title: str + content: str + timestamp: str + author: str = "" + metadata: dict = field(default_factory=dict) + sim_event_type: str = "" + sim_event_facts: dict = field(default_factory=dict) + + +@dataclass +class IncidentBundle: + incident_id: str + root_cause: str + system_fault: str + on_call: str + day: int + duration_days: int + health_at_open: int + actors: list[str] = field(default_factory=list) + artifacts: list[ArtifactDoc] = field(default_factory=list) + sim_event_facts: dict = field(default_factory=dict) + + +@dataclass +class EntitySet: + artifact_id: str + tech_components: set[str] = field(default_factory=set) + person_names: set[str] = field(default_factory=set) + ticket_ids: set[str] = field(default_factory=set) + root_cause_tokens: set[str] = field(default_factory=set) + + +@dataclass +class EvalResult: + arm: str + entity_agreement: float + temporal_violations: float + contradictions: float + prose_divergence: float + n_incidents: int + per_incident: list[dict] = field(default_factory=list) + + +class MongoExtractor: + def __init__( + self, + db_name: str = "orgforge", + uri: str = "mongodb://localhost:27017?directConnection=true", + ): + self._client = MongoClient(uri) + self._db = self._client[db_name] + + def _resolve_sim_event_facts(self, artifact_id: str) -> tuple[str, dict]: + ev = None + if re.match(r"^[A-Z]+-\d+$", artifact_id): + ev = self._db["events"].find_one( + {"type": "incident_opened", "artifact_ids.jira": artifact_id} + ) + + if not ev: + ev = self._db["events"].find_one( + { + "$or": [ + {"artifact_ids.jira": artifact_id}, + {"artifact_ids.confluence": artifact_id}, + {"artifact_ids.slack_thread": artifact_id}, + {"artifact_ids.pr": artifact_id}, + {"artifact_ids.zendesk": artifact_id}, + ] + }, + sort=[("day", 1)], + ) + if not ev: + ev = ( + self._db["events"].find_one( + { + "$or": [ + {"artifact_ids.jira": {"$elemMatch": {"$eq": artifact_id}}}, + { + "artifact_ids.confluence": { + "$elemMatch": {"$eq": artifact_id} + } + }, + { + "artifact_ids.slack_thread": { + "$elemMatch": {"$eq": artifact_id} + } + }, + ] + }, + sort=[("day", 1)], + ) + or {} + ) + + facts = dict(ev.get("facts", {})) + + if ( + ev.get("type", "") != "knowledge_gap_detected" + and "actors" in ev + and "actors" not in facts + ): + facts["actors"] = ev["actors"] + + if re.match(r"^[A-Z]+-\d+$", artifact_id): + jira_doc = self._db["jira_tickets"].find_one( + {"id": artifact_id}, {"assignee": 1, "escalation_actors": 1} + ) + if jira_doc: + doc_actors = list( + dict.fromkeys( + filter( + None, + [ + jira_doc.get("assignee", ""), + *jira_doc.get("escalation_actors", []), + ], + ) + ) + ) + if doc_actors: + facts["actors"] = doc_actors + + if "vendor_org" not in facts: + vendor = ( + facts.get("external_party") + or facts.get("org") + or facts.get("vendor") + or facts.get("vendor_name") + ) + if vendor: + facts["vendor_org"] = vendor + + artifact_ids = ev.get("artifact_ids", {}) + for src_key, dst_key in [ + ("jira", "ticket_id"), + ("pr", "pr_id"), + ("confluence", "confluence_id"), + ("zendesk", "zd_ticket_id"), + ("salesforce", "sf_opportunity_id"), + ]: + if src_key in artifact_ids and dst_key not in facts: + facts[dst_key] = artifact_ids[src_key] + + if "assigned_to" not in facts: + jira_doc = self._db["jira_tickets"].find_one( + {"id": artifact_ids.get("jira", "")}, {"assignee": 1} + ) + if jira_doc and jira_doc.get("assignee"): + facts["assigned_to"] = jira_doc["assignee"] + + return ev.get("type", ""), facts + + _CAMEL_RE = re.compile(r"[A-Z][a-z]+|[A-Z]+(?=[A-Z][a-z]|\b)|[a-z]+") + + def _camel_split(self, word: str) -> list[str]: + parts = self._CAMEL_RE.findall(word) + return parts if len(parts) > 1 else [] + + def vendor_aliases(self) -> dict[str, set[str]]: + doc = self._db["sim_config"].find_one({"_id": "inbound_email_sources"}) + if not doc: + return {} + + tech = self.tech_stack_components() + aliases: dict[str, set[str]] = {} + + for source in doc.get("sources", []): + if source.get("category") != "vendor": + continue + + name = source["name"] + alias_set: set[str] = {name} + + org = source.get("org", "") + if org: + alias_set.add(org) + + words = name.split() + if len(words) >= 2: + acronym = "".join(w[0] for w in words if w[0].isupper()) + if len(acronym) >= 3: + alias_set.add(acronym) + + for word in words: + parts = self._camel_split(word) + if parts: + alias_set.add(" ".join(p.lower() for p in parts)) + + for exp in source.get("persona", {}).get("expertise", []): + if exp in tech: + alias_set.add(exp) + + alias_set.discard("") + aliases[name] = alias_set + + return aliases + + def load_cached_baseline( + self, arm: str, incident_id: str, model_id: str + ) -> list[ArtifactDoc] | None: + doc = self._db["baseline_cache"].find_one( + {"_id": f"{arm}_{incident_id}_{model_id}"} + ) + if not doc: + return None + return [ArtifactDoc(**a) for a in doc["artifacts"]] + + def save_cached_baseline( + self, arm: str, incident_id: str, model_id: str, artifacts: list[ArtifactDoc] + ) -> None: + self._db["baseline_cache"].replace_one( + {"_id": f"{arm}_{incident_id}_{model_id}"}, + { + "_id": f"{arm}_{incident_id}_{model_id}", + "arm": arm, + "incident_id": incident_id, + "model_id": model_id, + "artifacts": [vars(a) for a in artifacts], + "created_at": datetime.utcnow().isoformat(), + }, + upsert=True, + ) + + def tech_stack_components(self) -> set[str]: + doc = self._db["sim_config"].find_one({"_id": "tech_stack"}) + if not doc: + return set() + explicit = { + "PostgreSQL", + "TitanDB", + "Java", + "Spring", + "Boot", + "Python", + "FastAPI", + "React", + "TypeScript", + "Vite", + "Kafka", + "RabbitMQ", + "AWS", + "EC2", + "EKS", + "RDS", + "S3", + "CloudFront", + "Terraform", + "GitHub", + "Jenkins", + "Docker", + "ECR", + "Datadog", + "Prometheus", + "Grafana", + "PagerDuty", + "Swift", + "Kotlin", + } + return explicit + + def org_names(self) -> set[str]: + names: set[str] = set() + for doc in self._db["events"].find( + {"type": "day_summary"}, {"facts.active_actors": 1} + ): + for name in doc.get("facts", {}).get("active_actors", []): + names.add(name) + if not names: + for doc in self._db["artifacts"].find( + {"type": "persona_skill"}, {"metadata": 1} + ): + name = doc.get("metadata", {}).get("name") + if name: + names.add(name) + return names + + def extract_incidents(self, max_n: int = 5) -> list[IncidentBundle]: + events = list( + self._db["events"] + .find({"type": "incident_opened"}) + .sort("day", 1) + .limit(max_n) + ) + bundles: list[IncidentBundle] = [] + for ev in events: + facts = ev.get("facts", {}) + ticket_id = ev.get("artifact_ids", {}).get("jira", "") + if not ticket_id: + continue + + resolved_ev = self._db["events"].find_one( + {"type": "incident_resolved", "artifact_ids.jira": ticket_id} + ) + duration = ( + resolved_ev.get("facts", {}).get("duration_days", 0) + if resolved_ev + else 0 + ) + + bundle = IncidentBundle( + incident_id=ticket_id, + root_cause=facts.get("root_cause", ""), + system_fault=facts.get("system_fault", ""), + on_call=ev.get("actors", [""])[0], + day=ev.get("day", 0), + duration_days=duration, + health_at_open=facts.get("system_health", 100), + actors=ev.get("actors", []), + sim_event_facts=facts, + ) + + jira_doc = self._db["jira_tickets"].find_one({"id": ticket_id}, {"_id": 0}) + if jira_doc: + bundle.actors = list( + set( + bundle.actors + + jira_doc.get("escalation_actors", []) + + [c.get("author", "") for c in jira_doc.get("comments", [])] + ) + - {""} + ) + + causal_chain = ( + jira_doc.get("causal_chain", facts.get("causal_chain", [ticket_id])) + if jira_doc + else facts.get("causal_chain", [ticket_id]) + ) + bot_threads = set( + jira_doc.get("bot_threads", facts.get("bot_threads", [])) + if jira_doc + else facts.get("bot_threads", []) + ) + bundle.artifacts = self._collect_artifacts( + ticket_id, causal_chain, bot_threads + ) + if bundle.artifacts: + bundles.append(bundle) + + logger.info(f"Extracted {len(bundles)} incidents from MongoDB") + return bundles + + def _collect_artifacts( + self, ticket_id: str, causal_chain: list[str], bot_threads: set[str] = set() + ) -> list[ArtifactDoc]: + docs: list[ArtifactDoc] = [] + seen: set[str] = set() + + def _is_noise(art_doc: ArtifactDoc) -> bool: + return art_doc.content.startswith( + "GitHub: 💬" + ) or art_doc.content.startswith("AWS Cost Explorer:") + + def _try_append(art_doc: ArtifactDoc | None) -> None: + if art_doc and not _is_noise(art_doc): + docs.append(art_doc) + + def _stamp_facts(doc: ArtifactDoc) -> ArtifactDoc: + if not doc.sim_event_facts: + ev_type, facts = self._resolve_sim_event_facts(doc.artifact_id) + doc.sim_event_type = ev_type + doc.sim_event_facts = facts + return doc + + jira = self._db["jira_tickets"].find_one({"id": ticket_id}) + if jira: + assignee = jira.get("assignee", "") + escalation = " ".join(jira.get("escalation_actors", [])) + body_parts = [ + jira.get("title", ""), + f"Assignee: {assignee}" if assignee else "", + f"Escalation: {escalation}" if escalation else "", + jira.get("description", ""), + jira.get("root_cause", ""), + ] + + ev_type, facts = self._resolve_sim_event_facts(ticket_id) + + docs.append( + ArtifactDoc( + artifact_id=ticket_id, + artifact_type="jira", + title=jira.get("title", ticket_id), + content="\n".join(filter(None, body_parts)), + timestamp=jira.get("created_at", ""), + author=jira.get("assignee", ""), + sim_event_type=ev_type, + sim_event_facts=facts, + ) + ) + seen.add(ticket_id) + + for comment in jira.get("comments", []): + comment_author = comment.get("author", "") + comment_text = comment.get("text", "") + comment_ts = comment.get("created", "") + if not comment_text: + continue + comment_id = f"{ticket_id}_comment_{comment.get('day', comment_ts)}" + if comment_id in seen: + continue + seen.add(comment_id) + docs.append( + ArtifactDoc( + artifact_id=comment_id, + artifact_type="jira_comment", + title=f"Comment on {ticket_id} by {comment_author}", + content=f"{comment_author}: {comment_text}", + timestamp=comment_ts, + author=comment_author, + sim_event_type=ev_type, + sim_event_facts=facts, + ) + ) + + for chain_id in causal_chain: + if chain_id in seen or chain_id in bot_threads: + continue + if re.match(r"^[A-Z]+-\d+_comment_\d+$", chain_id): + seen.add(chain_id) + continue + seen.add(chain_id) + resolved = self._resolve_artifact_by_id(chain_id) + if resolved: + _try_append(_stamp_facts(resolved)) + + related_events = list( + self._db["events"].find( + { + "$or": [ + {"artifact_ids.jira": ticket_id}, + {"facts.causal_chain": ticket_id}, + ] + } + ) + ) + for ev in related_events: + ev_facts = ev.get("facts", {}) + ev_type = ev.get("type", "") + for key, raw_aid in ev.get("artifact_ids", {}).items(): + aids = raw_aid if isinstance(raw_aid, list) else [raw_aid] + for aid in aids: + if not aid or aid in seen or key in ("jira",): + continue + seen.add(aid) + art_doc = self._resolve_artifact(aid, key, ev) + if art_doc and not _is_noise(art_doc): + art_doc.sim_event_type = ev_type + art_doc.sim_event_facts = ev_facts + docs.append(art_doc) + + postmortem_ev = self._db["events"].find_one( + {"type": "postmortem_created", "artifact_ids.jira": ticket_id} + ) + if postmortem_ev: + pm_facts = postmortem_ev.get("facts", {}) + raw_conf = postmortem_ev.get("artifact_ids", {}).get("confluence", "") + conf_ids = ( + raw_conf + if isinstance(raw_conf, list) + else [raw_conf] + if raw_conf + else [] + ) + for conf_id in conf_ids: + if conf_id and conf_id not in seen: + seen.add(conf_id) + resolved = self._resolve_artifact_by_id(conf_id) + if resolved: + resolved.sim_event_type = "postmortem_created" + resolved.sim_event_facts = pm_facts + _try_append(resolved) + + seen_content: set[str] = set() + deduped: list[ArtifactDoc] = [] + for doc in docs: + fingerprint = doc.content[:100].strip() + if fingerprint not in seen_content: + seen_content.add(fingerprint) + deduped.append(doc) + return deduped + + def _resolve_artifact( + self, aid: str, key_hint: str, event: dict + ) -> ArtifactDoc | None: + if "slack" in key_hint: + doc = self._db["artifacts"].find_one({"_id": aid, "type": "slack_thread"}) + if doc: + return ArtifactDoc( + artifact_id=aid, + artifact_type="slack", + title=doc.get("title", f"Slack thread {aid}"), + content=doc.get("content", ""), + timestamp=doc.get("timestamp", ""), + author=doc.get("metadata", {}).get("participants", [""])[0], + ) + if "confluence" in key_hint: + doc = self._db["artifacts"].find_one({"_id": aid, "type": "confluence"}) + if doc: + return ArtifactDoc( + artifact_id=aid, + artifact_type="confluence", + title=doc.get("title", aid), + content=doc.get("content", ""), + timestamp=doc.get("timestamp", ""), + author=doc.get("metadata", {}).get("author", ""), + ) + if "pr" in key_hint: + doc = self._db["pull_requests"].find_one({"pr_id": aid}) + if doc: + author = doc.get("author", "") + reviewers = " ".join(doc.get("reviewers", [])) + comment_parts = [] + for c in doc.get("comments", []): + c_author = c.get("author", "") + c_text = c.get("text", "") + c_verdict = c.get("verdict", "") + line = f"{c_author}: {c_text}" + if c_verdict: + line += f"\nReview verdict: {c_verdict.replace('_', ' ')}" + comment_parts.append(line) + content = "\n".join( + filter( + None, + [ + doc.get("title", ""), + f"Author: {author}" if author else "", + f"Reviewers: {reviewers}" if reviewers else "", + f"Status: {doc.get('status', '')}" + if doc.get("status") + else "", + doc.get("description", ""), + *comment_parts, + ], + ) + ) + return ArtifactDoc( + artifact_id=aid, + artifact_type="pr", + title=doc.get("title", aid), + content=content, + timestamp=doc.get("created_at", ""), + author=author, + ) + return None + + def _resolve_artifact_by_id(self, aid: str) -> ArtifactDoc | None: + for key_hint in ("confluence", "slack", "pr"): + result = self._resolve_artifact(aid, key_hint, {}) + if result: + return result + + messages = list( + self._db["slack_messages"].find({"thread_id": aid}).sort("ts", 1) + ) + if messages: + content = "\n".join( + f"{m.get('user', '?')}: {m.get('text', '')}" + for m in messages + if not m.get("is_bot", False) + ) + participants = list({m.get("user", "") for m in messages if m.get("user")}) + if content: + return ArtifactDoc( + artifact_id=aid, + artifact_type="slack", + title=f"Slack thread {messages[0].get('channel', '')} {aid}", + content=content, + timestamp=messages[0].get("ts", ""), + author=messages[0].get("user", ""), + metadata={"participants": participants}, + ) + + doc = self._db["artifacts"].find_one({"_id": aid}) + if doc: + return ArtifactDoc( + artifact_id=aid, + artifact_type=doc.get("type", "unknown"), + title=doc.get("title", aid), + content=doc.get("content", ""), + timestamp=doc.get("timestamp", ""), + ) + return None + + +class EntityExtractor: + def __init__(self, tech_components: set[str], org_names: set[str]): + self.canonical_tech = tech_components + self.canonical_names = org_names + self._tech = { + re.compile(rf"\b{re.escape(c)}\b", re.IGNORECASE): c + for c in tech_components + } + self._names = { + re.compile(rf"\b{re.escape(n)}\b", re.IGNORECASE): n for n in org_names + } + self._ticket_re = re.compile( + r"(?:^|(?<=\s))(?:ENG|HR|SALES|PROD|DES|QA|ORG)-\d+\b" + ) + + def extract(self, artifact: ArtifactDoc) -> EntitySet: + text = artifact.content + text_lower = text.lower() + tech_hits = { + canonical + for pattern, canonical in self._tech.items() + if pattern.search(text) + } + name_hits = { + canonical + for pattern, canonical in self._names.items() + if pattern.search(text) + } + ticket_hits = set(self._ticket_re.findall(text)) + rc_tokens: set[str] = set() + for word in re.split(r"\s+", text_lower): + cleaned = word.strip(".,;:!?'\"()[]{}").lower() + if len(cleaned) >= 4 and cleaned.isalpha(): + rc_tokens.add(cleaned) + return EntitySet( + artifact_id=artifact.artifact_id, + tech_components=tech_hits, + person_names=name_hits, + ticket_ids=ticket_hits, + root_cause_tokens=rc_tokens, + ) + + +def _jaccard(a: set, b: set) -> float: + if not a and not b: + return 1.0 + union = a | b + if not union: + return 1.0 + return len(a & b) / len(union) + + +def entity_agreement_grounded( + entity_sets: list[EntitySet], + incident: IncidentBundle, + canonical_tech: set[str], + canonical_names: set[str], +) -> dict[str, float]: + rc_text = " ".join( + filter( + None, + [ + incident.root_cause, + incident.system_fault, + incident.sim_event_facts.get("title", ""), + ], + ) + ).lower() + + incident_tech = canonical_tech + incident_names = set(incident.actors) + incident_tickets = {incident.incident_id} + for cid in incident.sim_event_facts.get("causal_chain", []): + if re.match(r"^[A-Z]+-\d+$", cid): + incident_tickets.add(cid) + + precision_scores: list[float] = [] + hallucination_scores: list[float] = [] + + for es in entity_sets: + checks: list[float] = [] + + if es.tech_components: + known = es.tech_components & canonical_tech + if known: + correct = known & incident_tech + checks.append(len(correct) / len(known)) + + if es.person_names: + correct = es.person_names & (incident_names | canonical_names) + checks.append(len(correct) / len(es.person_names)) + + if es.ticket_ids: + correct = es.ticket_ids & incident_tickets + checks.append(len(correct) / len(es.ticket_ids)) + + precision_scores.append(mean(checks) if checks else 1.0) + hallucinated_names = es.person_names - canonical_names + if es.person_names: + hallucination_scores.append( + 1.0 - len(hallucinated_names) / len(es.person_names) + ) + else: + hallucination_scores.append(1.0) + + avg_precision = mean(precision_scores) if precision_scores else 1.0 + avg_no_hallucination = mean(hallucination_scores) if hallucination_scores else 1.0 + + return { + "tech": round(avg_precision, 4), + "names": round(avg_no_hallucination, 4), + "tickets": 1.0, + "overall": round(mean([avg_precision, avg_no_hallucination]), 4), + } + + +def entity_agreement(entity_sets: list[EntitySet]) -> dict[str, float]: + if len(entity_sets) < 2: + return {"tech": 1.0, "names": 1.0, "tickets": 1.0, "overall": 1.0} + tech_scores = [] + name_scores = [] + ticket_scores = [] + for s1, s2 in combinations(entity_sets, 2): + tech_scores.append(_jaccard(s1.tech_components, s2.tech_components)) + name_scores.append(_jaccard(s1.person_names, s2.person_names)) + if s1.ticket_ids or s2.ticket_ids: + ticket_scores.append(_jaccard(s1.ticket_ids, s2.ticket_ids)) + avg_tickets = mean(ticket_scores) if ticket_scores else 1.0 + avg_tech = mean(tech_scores) + avg_names = mean(name_scores) + return { + "tech": avg_tech, + "names": avg_names, + "tickets": avg_tickets, + "overall": mean([avg_tech, avg_names, avg_tickets]), + } + + +_AFTER_OPEN_SIM_TYPES: frozenset[str] = frozenset( + {"postmortem_created", "incident_resolved"} +) + + +def _stratified_pairs( + artifacts: list[ArtifactDoc], + max_pairs: int = 10, + seed: int = 42, +) -> list[tuple[ArtifactDoc, ArtifactDoc]]: + by_type: dict[str, list[ArtifactDoc]] = {} + for a in artifacts: + by_type.setdefault(a.artifact_type, []).append(a) + + cross_type: list[tuple[ArtifactDoc, ArtifactDoc]] = [] + types = list(by_type.keys()) + for i, t1 in enumerate(types): + for t2 in types[i + 1 :]: + for a in by_type[t1]: + for b in by_type[t2]: + cross_type.append((a, b)) + + same_type: list[tuple[ArtifactDoc, ArtifactDoc]] = [] + for docs in by_type.values(): + same_type.extend(combinations(docs, 2)) + + rng = random.Random(seed) + rng.shuffle(cross_type) + rng.shuffle(same_type) + + pairs = cross_type[:max_pairs] + if len(pairs) < max_pairs: + pairs += same_type[: max_pairs - len(pairs)] + return pairs + + +def temporal_violations(artifacts: list[ArtifactDoc]) -> int: + parsed: list[tuple[datetime, ArtifactDoc]] = [] + + for a in artifacts: + if not a.timestamp: + continue + try: + ts = datetime.fromisoformat(a.timestamp.replace("Z", "+00:00")) + parsed.append((ts, a)) + except ValueError: + continue + + if not parsed: + return 0 + + anchor_ts = next( + (ts for ts, a in parsed if a.artifact_type == "jira"), + min(ts for ts, _ in parsed), + ) + + violations = 0 + for ts, art in parsed: + title_lower = art.title.lower() + is_causal_after_open = ( + art.sim_event_type in _AFTER_OPEN_SIM_TYPES + or "postmortem" in title_lower + or "resolv" in title_lower + ) + if is_causal_after_open and ts < anchor_ts: + violations += 1 + + return violations + + +JUDGE_PROMPT = """You are a factual consistency auditor comparing two documents about the same incident. + +Your task: identify FACTUAL CONTRADICTIONS where the documents make mutually exclusive claims +about the same concrete fact. + +STRICT RULES: +- "doc_a_quote" and "doc_b_quote" must be VERBATIM text copied from the document. + Copy the exact words. Do not paraphrase, summarize, or introduce any term not present + in the source text. If you cannot find an exact quote that supports the contradiction, + do not report it. +- Only flag claims that DIRECTLY CONTRADICT each other (both cannot be true simultaneously). +- Do NOT flag: omissions, different levels of detail, different perspectives, different + writing styles, or additional context in one document that the other lacks. +- Do NOT flag different specific values (e.g., permission names like s3:PutObject vs + s3:PutObjectAcl) when both are plausible specifics of the same underlying concept and + neither document claims the other is wrong. +- Specific technical identifiers (IAM actions, error codes, API names) vary legitimately + across document types. Only flag if one document explicitly contradicts the other's + specific claim. + +WHAT TO FLAG: +- Person A credited with an action in Doc A, Person B credited with the same action in Doc B +- "resolved in 2 days" vs "resolved in 5 days" +- "service was down" vs "service was degraded but available" +- Explicit root cause statements that are mutually exclusive + +Document A ({id_a}): +{content_a} + +Document B ({id_b}): +{content_b} + +Respond with a JSON object. The "contradictions" array must contain only entries where +you can provide a verbatim quote from each document: + +{{ + "contradictions": [ + {{ + "field": "brief label for what fact is contradicted", + "doc_a_quote": "exact verbatim text from Document A", + "doc_b_quote": "exact verbatim text from Document B" + }} + ] +}} + +If no qualifying contradictions exist, return {{"contradictions": []}}. +JSON only. No preamble.""" + + +def judge_contradictions( + bedrock: Bedrock, + bedrock_judge: Bedrock, + artifacts: list[ArtifactDoc], + max_pairs: int = 10, + sleep_between_calls: float = 2.0, +) -> int: + if len(artifacts) < 2: + return 0 + pairs = _stratified_pairs(artifacts, max_pairs=max_pairs) + total = 0 + for a, b in pairs: + content_a = a.content[:3000] + content_b = b.content[:3000] + prompt = JUDGE_PROMPT.format( + id_a=a.artifact_id, + content_a=content_a, + id_b=b.artifact_id, + content_b=content_b, + ) + try: + raw = bedrock_judge.generate(prompt, temperature=0.0, max_tokens=2048) + clean = raw.strip() + if clean.startswith("```"): + clean = re.sub(r"^```\w*\n?", "", clean).rstrip("`\n ") + parsed = json.loads(clean) + count = parsed.get("count", 0) + total += count + if count > 0: + for c in parsed.get("contradictions", []): + logger.info( + f" Contradiction: {c.get('field', '?')} — " + f"A says '{c.get('doc_a_says', '?')}', " + f"B says '{c.get('doc_b_says', '?')}'" + ) + except (json.JSONDecodeError, KeyError) as exc: + logger.warning( + f" Judge parse failed for {a.artifact_id}↔{b.artifact_id}: {exc}" + ) + except Exception as exc: + logger.warning(f" Judge call failed: {exc}") + time.sleep(sleep_between_calls) + return total + + +ARTIFACT_SEQUENCE = [ + ("jira", "JIRA Ticket", "Write a JIRA ticket for this incident."), + ("slack", "Slack Thread", "Write a Slack #incidents thread about this incident."), + ( + "pr", + "Pull Request", + "Write a GitHub PR description for the fix to this incident.", + ), + ( + "postmortem", + "Postmortem", + "Write a Confluence postmortem document for this incident.", + ), + ( + "email", + "Customer Email", + "Write an inbound customer complaint email triggered by this incident.", + ), +] + + +def _baseline_system_prompt(tech_stack: str, org_chart_str: str, company: str) -> str: + return ( + f"You are generating realistic organizational documents for {company}.\n" + f"Tech stack:\n{tech_stack}\n\n" + f"Team members:\n{org_chart_str}\n\n" + f"Use ONLY the system names and person names provided above. " + f"Do not invent any names not in this list." + ) + + +_BASELINE_SIM_EVENT_TYPES: dict[str, str] = { + "jira": "incident_opened", + "slack": "async_question", + "pr": "pr_review", + "postmortem": "postmortem_created", + "email": "inbound_external_email", +} + +_SCORING_TYPE_MAP: dict[str, str] = { + "jira": "jira", + "slack": "slack", + "pr": "pr", + "postmortem": "confluence", + "email": "email", +} + + +def generate_baseline_artifacts( + bedrock: Bedrock, + incident: IncidentBundle, + tech_stack_str: str, + org_chart_str: str, + company: str, + chained: bool, + rng: random.Random | None = None, +) -> list[ArtifactDoc]: + _rng = rng or random.Random() + system = _baseline_system_prompt(tech_stack_str, org_chart_str, company) + context_prefix = ( + f"Incident: {incident.root_cause}\n" + f"System fault: {incident.system_fault}\n" + f"On-call engineer: {incident.on_call}\n" + f"Duration: {incident.duration_days} days\n" + f"System health at open: {incident.health_at_open}/100\n\n" + ) + + baseline_facts: dict[str, Any] = { + "root_cause": incident.root_cause, + "affected_system": incident.system_fault, + "assigned_to": incident.on_call, + "actors": incident.actors, + "incident_id": incident.incident_id, + } + if incident.duration_days: + baseline_facts["incident_duration_hours"] = incident.duration_days * 24 + baseline_facts.update( + {k: v for k, v in incident.sim_event_facts.items() if k not in baseline_facts} + ) + + JIRA_ANCHOR_TS = "2026-03-10T09:00:00" # fixed incident open anchor + + prior_artifacts: list[str] = [] + results: list[ArtifactDoc] = [] + + for idx, (art_type, art_label, instruction) in enumerate(ARTIFACT_SEQUENCE): + prompt = f"{context_prefix}{instruction}\n\n" + if chained and prior_artifacts: + prompt += "Previously generated documents for this incident:\n\n" + for prior in prior_artifacts: + prompt += f"{prior}\n\n---\n\n" + prompt += ( + "Your document must be consistent with all of the above. " + "Use the same names, systems, and timeline.\n\n" + ) + prompt += ( + f"Write the {art_label} now. Output only the document content, " + f"no preamble or meta-commentary." + ) + try: + content = bedrock.generate(prompt, system=system, max_tokens=2048) + except Exception as exc: + logger.warning(f" Baseline generation failed for {art_type}: {exc}") + content = f"[Generation failed: {exc}]" + + if art_type == "jira": + fake_ts = JIRA_ANCHOR_TS + elif chained: + fake_ts = f"2026-03-{10 + idx:02d}T{9 + idx:02d}:00:00" + else: + day_delta = _rng.randint(-2, 5) + hour = _rng.randint(8, 20) + minute = _rng.randint(0, 59) + fake_ts = f"2026-03-{10 + day_delta:02d}T{hour:02d}:{minute:02d}:00" + + doc = ArtifactDoc( + artifact_id=f"baseline_{art_type}_{incident.incident_id}", + artifact_type=_SCORING_TYPE_MAP[art_type], + title=f"{art_label}: {incident.root_cause[:60]}", + content=content, + timestamp=fake_ts, + author=incident.on_call, + sim_event_type=_BASELINE_SIM_EVENT_TYPES[art_type], + sim_event_facts=baseline_facts, + ) + results.append(doc) + if chained: + prior_artifacts.append(f"[{art_label}]\n{content}") + + return results + + +SCORING_ARTIFACT_TYPES = {"jira", "slack", "pr", "confluence"} + + +def evaluate_arm( + arm_name: str, + incidents_with_artifacts: list[tuple[IncidentBundle, list[ArtifactDoc]]], + extractor: EntityExtractor, + bedrock: Bedrock | None, + bedrock_judge: Bedrock | None, + nli: NLIScorer | None = None, + vendor_aliases: dict[str, set[str]] | None = None, + skip_judge: bool = False, + skip_nli: bool = False, +) -> EvalResult: + all_agreement: list[float] = [] + all_violations: list[int] = [] + all_contradictions: list[int] = [] + all_divergence: list[float] = [] + per_incident: list[dict] = [] + + for incident, artifacts in incidents_with_artifacts: + if not artifacts: + continue + + scoring_artifacts = [ + a for a in artifacts if a.artifact_type in SCORING_ARTIFACT_TYPES + ] + entity_sets = [extractor.extract(a) for a in scoring_artifacts] + + if arm_name == "OrgForge": + agreement = entity_agreement_grounded( + entity_sets, + incident, + extractor.canonical_tech, + extractor.canonical_names, + ) + else: + agreement = entity_agreement(entity_sets) + violations = temporal_violations(scoring_artifacts) + + contradictions = 0 + if not skip_judge and bedrock_judge and bedrock and len(scoring_artifacts) >= 2: + logger.info( + f" Judging {arm_name}/{incident.incident_id} " + f"({len(artifacts)} artifacts)..." + ) + contradictions = judge_contradictions( + bedrock, + bedrock_judge, + scoring_artifacts, + ) + + divergence_score = 1.0 + divergence_detail: list[dict] = [] + per_type_divergence: dict = {} + + DIVERGENCE_ARTIFACT_TYPES = {"jira", "slack", "pr", "confluence"} + + if not skip_nli: + facts_artifacts = [ + a + for a in scoring_artifacts + if a.sim_event_facts and a.artifact_type in DIVERGENCE_ARTIFACT_TYPES + ] + if facts_artifacts: + logger.info( + f" NLI divergence {arm_name}/{incident.incident_id} " + f"({len(facts_artifacts)} artifacts with facts)..." + ) + reports = [ + measure_artifact_divergence( + a.artifact_id, + a.artifact_type, + a.content, + a.sim_event_facts, + nli, + vendor_aliases=vendor_aliases, + sim_event_type=a.sim_event_type, + ) + for a in facts_artifacts + ] + divergence_score = mean(r.composite_score for r in reports) + for r in reports: + status = "⚠" if r.composite_score < 0.7 else "✓" + logger.info( + f" {status} {r.artifact_id}: " + f"composite={r.composite_score:.3f} " + f"entity={r.entity_score:.3f} " + f"nli={r.nli_score:.3f} " + f"numeric={r.numeric_score:.3f} " + f"({len(r.divergences)} divergences)" + ) + for d in r.divergences: + logger.info(f" → {d}") + type_scores: dict[str, list[dict]] = {} + for r in reports: + atype = next( + ( + a.artifact_type + for a in facts_artifacts + if a.artifact_id == r.artifact_id + ), + "unknown", + ) + type_scores.setdefault(atype, []).append( + { + "s_ent": r.entity_score, + "s_nli": r.nli_score, + "s_num": r.numeric_score, + "composite": r.composite_score, + } + ) + + per_type_divergence = { + atype: { + "s_ent": round(mean(v["s_ent"] for v in scores), 4), + "s_nli": round(mean(v["s_nli"] for v in scores), 4), + "s_num": round(mean(v["s_num"] for v in scores), 4), + "composite": round(mean(v["composite"] for v in scores), 4), + "n": len(scores), + } + for atype, scores in type_scores.items() + } + + flagged = [r for r in reports if r.composite_score < 0.7] + divergence_detail = [ + { + "artifact_id": r.artifact_id, + "composite": round(r.composite_score, 3), + "entity": round(r.entity_score, 3), + "nli": round(r.nli_score, 3), + "numeric": round(r.numeric_score, 3), + "n_divergences": len(r.divergences), + } + for r in flagged + ] + logger.info( + f" Divergence {arm_name}/{incident.incident_id}: " + f"{divergence_score:.3f} ({len(facts_artifacts)} scored, " + f"{len(flagged)} flagged)" + ) + else: + logger.warning( + f" No sim_event_facts found for {arm_name}/{incident.incident_id} " + f"— skipping divergence check" + ) + + all_agreement.append(agreement["overall"]) + all_violations.append(violations) + all_contradictions.append(contradictions) + all_divergence.append(divergence_score) + + per_incident.append( + { + "incident_id": incident.incident_id, + "n_artifacts": len(artifacts), + "agreement": agreement, + "temporal_violations": violations, + "contradictions": contradictions, + "prose_divergence": round(divergence_score, 4), + "divergence_flagged": divergence_detail, + "divergence_by_type": per_type_divergence, + } + ) + + return EvalResult( + arm=arm_name, + entity_agreement=round(mean(all_agreement), 4) if all_agreement else 0.0, + temporal_violations=round(mean(all_violations), 2) if all_violations else 0.0, + contradictions=round(mean(all_contradictions), 2) + if all_contradictions + else 0.0, + prose_divergence=round(mean(all_divergence), 4) if all_divergence else 1.0, + n_incidents=len(per_incident), + per_incident=per_incident, + ) + + +def print_results(results: list[EvalResult]) -> None: + col_w = 22 + header = f"{'Metric':<35}" + for r in results: + header += f"{r.arm:>{col_w}}" + print("\n" + "=" * len(header)) + print(header) + print("-" * len(header)) + + for label, attr, fmt in [ + ("Entity agreement rate", "entity_agreement", ".4f"), + ("Temporal violations / incident", "temporal_violations", ".2f"), + ("Contradictions / incident (judge)", "contradictions", ".2f"), + ("Prose-SimEvent divergence", "prose_divergence", ".4f"), + ]: + row = f"{label:<35}" + for r in results: + row += f"{getattr(r, attr):>{col_w}{fmt}}" + print(row) + + row_n = f"{'Incidents evaluated':<35}" + for r in results: + row_n += f"{r.n_incidents:>{col_w}d}" + print(row_n) + + print("=" * len(header)) + + for r in results: + print(f"\n── {r.arm} per-incident detail ──") + for p in r.per_incident: + print( + f" {p['incident_id']}: " + f"agreement={p['agreement']['overall']:.3f} " + f"violations={p['temporal_violations']} " + f"contradictions={p['contradictions']} " + f"divergence={p['prose_divergence']:.3f} " + f"artifacts={p['n_artifacts']}" + ) + for d in p.get("divergence_flagged", []): + print( + f" ⚠ {d['artifact_id']}: " + f"composite={d['composite']:.3f} " + f"entity={d['entity']:.3f} " + f"nli={d['nli']:.3f} " + f"numeric={d['numeric']:.3f} " + f"({d['n_divergences']} divergences)" + ) + + +def save_results(results: list[EvalResult], path: str = "eval_results.json") -> None: + out = [] + for r in results: + out.append( + { + "arm": r.arm, + "entity_agreement": r.entity_agreement, + "temporal_violations": r.temporal_violations, + "contradictions": r.contradictions, + "prose_divergence": r.prose_divergence, + "n_incidents": r.n_incidents, + "per_incident": r.per_incident, + } + ) + with open(path, "w") as f: + json.dump(out, f, indent=2) + logger.info(f"Results saved to {path}") + + +def debug_entity_extraction( + incidents: list[IncidentBundle], + extractor: EntityExtractor, +) -> None: + for inc in incidents: + print(f"\n{'=' * 80}") + print(f"INCIDENT: {inc.incident_id} ({len(inc.artifacts)} artifacts)") + print(f"{'=' * 80}") + print( + f"\nTECH COMPONENTS ({len(extractor._tech)}): {sorted(extractor._tech.values())[:20]}" + ) + print( + f"ORG NAMES ({len(extractor._names)}): {sorted(extractor._names.values())}" + ) + for art in inc.artifacts: + entity_set = extractor.extract(art) + print(f"\n [{art.artifact_type.upper()}] {art.artifact_id}") + print(f" sim_event_type : {art.sim_event_type}") + print(f" sim_event_facts: {len(art.sim_event_facts)} keys") + print(f" title : {art.title[:80]}") + print(f" timestamp: {art.timestamp}") + print(f" content : {art.content[:200].replace(chr(10), ' ')!r}") + print(f" tech : {sorted(entity_set.tech_components)}") + print(f" names : {sorted(entity_set.person_names)}") + print(f" tickets : {sorted(entity_set.ticket_ids)}") + + +def main(): + parser = argparse.ArgumentParser( + description="OrgForge cross-document consistency evaluation", + ) + parser.add_argument("--model", default="openai.gpt-oss-120b-1:0") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--db", default="orgforge") + parser.add_argument( + "--mongo-uri", default="mongodb://localhost:27017?directConnection=true" + ) + parser.add_argument("--incidents", type=int, default=5) + parser.add_argument("--skip-baselines", action="store_true") + parser.add_argument("--skip-judge", action="store_true") + parser.add_argument( + "--skip-nli", + action="store_true", + help="Skip NLI-based prose-SimEvent divergence checks", + ) + parser.add_argument( + "--nli-model", + default="cross-encoder/nli-deberta-v3-base", + help="HuggingFace NLI model for divergence scoring", + ) + parser.add_argument("--output", default="eval_results.json") + parser.add_argument("--judge-model", default="us.anthropic.claude-opus-4-6-v1") + parser.add_argument("--debug-entities", action="store_true") + args = parser.parse_args() + + # ── NLI model ──────────────────────────────────────────────────────── + nli: NLIScorer | None = None + if not args.skip_nli: + logger.info(f"Loading NLI model: {args.nli_model}") + nli = NLIScorer(model_name=args.nli_model) + logger.info("NLI model loaded") + else: + logger.info("NLI divergence checks disabled (--skip-nli)") + + mongo = MongoExtractor(db_name=args.db, uri=args.mongo_uri) + bedrock = Bedrock(model_id=args.model, region=args.region) + bedrock_judge = Bedrock(model_id=args.judge_model or args.model, region=args.region) + + logger.info("Loading canonical entities from MongoDB...") + tech_components = mongo.tech_stack_components() + org_names = mongo.org_names() + logger.info( + f" Tech components: {len(tech_components)} | Org names: {len(org_names)}" + ) + + extractor = EntityExtractor(tech_components, org_names) + + logger.info(f"Extracting up to {args.incidents} incidents from OrgForge...") + incidents = mongo.extract_incidents(max_n=args.incidents) + + if args.debug_entities: + debug_entity_extraction(incidents, extractor) + sys.exit(0) + + if not incidents: + logger.error("No incidents found in MongoDB. Run OrgForge first.") + sys.exit(1) + + for inc in incidents: + facts_count = sum(1 for a in inc.artifacts if a.sim_event_facts) + logger.info( + f" {inc.incident_id}: {inc.root_cause[:80]} " + f"({len(inc.artifacts)} artifacts, {facts_count} with facts, " + f"{inc.duration_days}d)" + ) + + # ── Arm 1: OrgForge ────────────────────────────────────────────────── + logger.info("\n━━━ Evaluating OrgForge arm ━━━") + orgforge_data = [(inc, inc.artifacts) for inc in incidents] + vendor_aliases_map = mongo.vendor_aliases() + + orgforge_result = evaluate_arm( + "OrgForge", + orgforge_data, + extractor, + bedrock, + bedrock_judge, + nli=nli, + vendor_aliases=vendor_aliases_map, + skip_judge=args.skip_judge, + skip_nli=args.skip_nli, + ) + results = [orgforge_result] + + if not args.skip_baselines: + tech_stack_str = "" + ts_doc = mongo._db["artifacts"].find_one({"type": "tech_stack"}) + if ts_doc: + content = ts_doc.get("content", "") + tech_stack_str = ( + json.dumps(content, indent=2) + if isinstance(content, dict) + else str(content) + ) + org_chart_str = "\n".join(sorted(org_names)) + company = COMPANY_NAME + + # ── Arm 2: Chained baseline ────────────────────────────────────── + logger.info("\n━━━ Generating chained baseline ━━━") + chained_data: list[tuple[IncidentBundle, list[ArtifactDoc]]] = [] + for inc in incidents: + logger.info(f" Generating chained artifacts for {inc.incident_id}...") + arts = mongo.load_cached_baseline("chained", inc.incident_id, args.model) + if arts is None: + arts = generate_baseline_artifacts( + bedrock, + inc, + tech_stack_str, + org_chart_str, + company, + chained=True, + ) + mongo.save_cached_baseline("chained", inc.incident_id, args.model, arts) + else: + logger.info(f" Using cached chained baseline for {inc.incident_id}") + chained_data.append((inc, arts)) + + logger.info("\n━━━ Evaluating chained baseline ━━━") + chained_result = evaluate_arm( + "Chained", + chained_data, + extractor, + bedrock, + bedrock_judge, + nli=nli, + skip_judge=args.skip_judge, + skip_nli=True, + ) + results.append(chained_result) + + # ── Arm 3: Parallel baseline ───────────────────────────────────── + logger.info("\n━━━ Generating parallel baseline ━━━") + parallel_data: list[tuple[IncidentBundle, list[ArtifactDoc]]] = [] + for inc in incidents: + logger.info(f" Generating parallel artifacts for {inc.incident_id}...") + arts = mongo.load_cached_baseline("parallel", inc.incident_id, args.model) + if arts is None: + arts = generate_baseline_artifacts( + bedrock, + inc, + tech_stack_str, + org_chart_str, + company, + chained=False, + ) + mongo.save_cached_baseline( + "parallel", inc.incident_id, args.model, arts + ) + else: + logger.info(f" Using cached parallel baseline for {inc.incident_id}") + parallel_data.append((inc, arts)) + + logger.info("\n━━━ Evaluating parallel baseline ━━━") + parallel_result = evaluate_arm( + "Parallel", + parallel_data, + extractor, + bedrock, + bedrock_judge, + nli=nli, + skip_judge=args.skip_judge, + skip_nli=True, + ) + results.append(parallel_result) + + print_results(results) + save_results(results, args.output) + + +if __name__ == "__main__": + main() diff --git a/eval/eval_consistency.py b/eval/eval_consistency.py new file mode 100644 index 0000000..6988f35 --- /dev/null +++ b/eval/eval_consistency.py @@ -0,0 +1,1220 @@ +""" +eval_consistency.py +=================== +Cross-document consistency comparison: OrgForge vs. LLM-only baselines. + +Metric: Ground Truth Fidelity +------------------------------ +Instead of comparing artifacts against each other (Jaccard overlap), each +artifact is scored against the SimEvent ground truth that produced it. + +For each ground truth field (on_call, root_cause, system_fault, ticket_id) +we compute TOKEN RECALL: what fraction of the field's significant tokens +appear in the artifact prose. This catches paraphrase while still failing +on genuine drift or hallucination. + +Results are broken down per artifact type (jira, slack, pr, confluence, email) +so document-type coverage differences don't pollute the signal. + +Usage: + python eval_consistency.py --model anthropic.claude-3-5-sonnet-20241022-v2:0 + python eval_consistency.py --incidents 3 --region us-west-2 + python eval_consistency.py --db orgforge --skip-baselines + python eval_consistency.py --skip-judge # fidelity only, no contradiction judge +""" + +from __future__ import annotations + +import argparse +import json +import logging +import random +import re +import sys +from collections import defaultdict +from dataclasses import dataclass, field +from datetime import datetime +from itertools import combinations +from statistics import mean +import time +from typing import Any + +import boto3 +from config_loader import COMPANY_NAME +from pymongo import MongoClient + +logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") +logger = logging.getLogger("eval_consistency") + + +# ── Stopwords (excluded from token recall) ─────────────────────────────────── + +_STOPWORDS = { + "a", + "an", + "the", + "and", + "or", + "but", + "in", + "on", + "at", + "to", + "for", + "of", + "with", + "by", + "from", + "is", + "was", + "are", + "were", + "be", + "been", + "being", + "have", + "has", + "had", + "do", + "does", + "did", + "will", + "would", + "could", + "should", + "may", + "might", + "that", + "this", + "these", + "those", + "it", + "its", + "which", + "who", + "what", + "when", + "where", + "how", + "not", + "no", + "nor", + "so", + "yet", + "both", + "either", + "neither", + "each", + "than", + "such", + "as", + "if", + "then", + "than", + "too", + "very", + "just", + "also", +} + + +# ── Bedrock wrapper ────────────────────────────────────────────────────────── + + +class Bedrock: + _RETRYABLE_ERRORS = ( + "ThrottlingException", + "InternalServerException", + "ServiceUnavailableException", + "ModelTimeoutException", + "Too Many Requests", + ) + _MAX_RETRIES = 5 + _RETRY_BASE_DELAY = 2.0 + + def __init__(self, model_id: str, region: str = "us-east-1"): + self.model_id = model_id + self._client = boto3.client("bedrock-runtime", region_name=region) + + def generate( + self, + prompt: str, + system: str = "", + temperature: float = 0.7, + max_tokens: int = 4096, + ) -> str: + messages = [{"role": "user", "content": [{"text": prompt}]}] + system_list = [{"text": system}] if system else [] + kwargs: dict[str, Any] = { + "modelId": self.model_id, + "messages": messages, + "inferenceConfig": { + "maxTokens": max_tokens, + "temperature": temperature, + }, + } + if system_list: + kwargs["system"] = system_list + + resp = self._converse_with_retry(kwargs) + content_blocks = resp["output"]["message"]["content"] + for block in content_blocks: + if block.get("type") == "text" or "text" in block: + return block["text"] + raise ValueError(f"No text block in Bedrock response: {content_blocks}") + + def _converse_with_retry(self, kwargs: dict[str, Any]) -> dict: + delay = self._RETRY_BASE_DELAY + last_exc: Exception = RuntimeError("No attempts made") + for attempt in range(self._MAX_RETRIES): + try: + return self._client.converse(**kwargs) + except Exception as exc: + last_exc = exc + retryable = any(tag in str(exc) for tag in self._RETRYABLE_ERRORS) + if not retryable: + raise + jitter = random.uniform(0, delay * 0.25) + wait = delay + jitter + logger.warning( + f"Bedrock transient error (attempt {attempt + 1}/{self._MAX_RETRIES}), " + f"retrying in {wait:.1f}s: {exc}" + ) + time.sleep(wait) + delay = min(delay * 2, 60.0) + raise last_exc + + +# ── Data model ─────────────────────────────────────────────────────────────── + + +@dataclass +class ArtifactDoc: + artifact_id: str + artifact_type: str + title: str + content: str + timestamp: str + author: str = "" + metadata: dict = field(default_factory=dict) + + +@dataclass +class IncidentBundle: + incident_id: str + root_cause: str + system_fault: str + on_call: str + day: int + duration_days: int + health_at_open: int + artifacts: list[ArtifactDoc] = field(default_factory=list) + sim_event_facts: dict = field(default_factory=dict) + + +@dataclass +class FieldFidelity: + """Token recall score for one ground truth field in one artifact.""" + + field_name: str + ground_truth: str + recall: float # fraction of GT tokens found in artifact prose + tokens_expected: int + tokens_found: int + + +@dataclass +class ArtifactFidelity: + """Fidelity scores for a single artifact against its incident ground truth.""" + + artifact_id: str + artifact_type: str + fields: list[FieldFidelity] = field(default_factory=list) + + @property + def overall(self) -> float: + if not self.fields: + return 1.0 + return mean(f.recall for f in self.fields) + + def by_field(self) -> dict[str, float]: + return {f.field_name: f.recall for f in self.fields} + + +@dataclass +class IncidentFidelityResult: + incident_id: str + artifacts: list[ArtifactFidelity] = field(default_factory=list) + + @property + def overall(self) -> float: + if not self.artifacts: + return 1.0 + return mean(a.overall for a in self.artifacts) + + def by_artifact_type(self) -> dict[str, float]: + """Mean fidelity grouped by artifact type.""" + buckets: dict[str, list[float]] = defaultdict(list) + for a in self.artifacts: + buckets[a.artifact_type].append(a.overall) + return {t: mean(scores) for t, scores in buckets.items()} + + def by_field(self) -> dict[str, float]: + """Mean recall per ground truth field across all artifacts.""" + buckets: dict[str, list[float]] = defaultdict(list) + for a in self.artifacts: + for f in a.fields: + buckets[f.field_name].append(f.recall) + return {fname: mean(scores) for fname, scores in buckets.items()} + + +@dataclass +class EvalResult: + arm: str + n_incidents: int + overall_fidelity: float + fidelity_by_field: dict[str, float] + fidelity_by_artifact_type: dict[str, float] + contradictions: float # mean per incident, 0.0 if judge skipped + per_incident: list[dict] = field(default_factory=list) + + +# ── Ground truth fidelity ──────────────────────────────────────────────────── + + +def _tokenize(text: str) -> set[str]: + """Lower-case alpha tokens, length >= 3, excluding stopwords.""" + tokens = re.findall(r"[a-zA-Z]+", text.lower()) + return {t for t in tokens if len(t) >= 3 and t not in _STOPWORDS} + + +def _token_recall(ground_truth: str, artifact_text: str) -> tuple[float, int, int]: + """ + What fraction of significant GT tokens appear in the artifact? + Returns (recall, n_expected, n_found). + Empty GT is treated as perfect (field not applicable). + """ + gt_tokens = _tokenize(ground_truth) + if not gt_tokens: + return 1.0, 0, 0 + art_tokens = _tokenize(artifact_text) + found = gt_tokens & art_tokens + return len(found) / len(gt_tokens), len(gt_tokens), len(found) + + +# Ground truth fields to check, and which artifact types are expected to +# mention them. Artifact types NOT in the set are skipped for that field +# (e.g. a customer email is not expected to name the on-call engineer). +# +# Rationale for per-type scoping: +# on_call — operational docs (jira, slack, pr) name the responder; +# postmortems sometimes do, emails almost never should. +# root_cause — all internal docs should reflect it; customer email gets +# a sanitised version so we allow it but weight it lower. +# system_fault — same as root_cause. +# ticket_id — internal cross-references; email unlikely to contain it. + +FIELD_SCOPE: dict[str, set[str]] = { + "on_call": {"jira", "jira_comment", "slack", "pr", "confluence"}, + "root_cause": { + "jira", + "jira_comment", + "slack", + "pr", + "confluence", + "postmortem", + "email", + }, + "system_fault": { + "jira", + "jira_comment", + "slack", + "pr", + "confluence", + "postmortem", + "email", + }, + "ticket_id": {"jira", "jira_comment", "slack", "pr", "confluence", "postmortem"}, +} + + +def score_artifact_fidelity( + artifact: ArtifactDoc, + incident: IncidentBundle, +) -> ArtifactFidelity: + """ + Score a single artifact against the incident's SimEvent ground truth. + Only fields scoped to this artifact type are evaluated. + """ + gt_fields = { + "on_call": incident.on_call, + "root_cause": incident.root_cause, + "system_fault": incident.system_fault, + "ticket_id": incident.incident_id, + } + + result = ArtifactFidelity( + artifact_id=artifact.artifact_id, + artifact_type=artifact.artifact_type, + ) + + for field_name, gt_value in gt_fields.items(): + scoped_types = FIELD_SCOPE.get(field_name, set()) + if artifact.artifact_type not in scoped_types: + continue + if not gt_value: + continue + + recall, n_expected, n_found = _token_recall(gt_value, artifact.content) + result.fields.append( + FieldFidelity( + field_name=field_name, + ground_truth=gt_value, + recall=recall, + tokens_expected=n_expected, + tokens_found=n_found, + ) + ) + + return result + + +def score_incident_fidelity( + incident: IncidentBundle, + artifacts: list[ArtifactDoc], +) -> IncidentFidelityResult: + result = IncidentFidelityResult(incident_id=incident.incident_id) + for artifact in artifacts: + af = score_artifact_fidelity(artifact, incident) + if af.fields: # skip artifacts with no applicable fields + result.artifacts.append(af) + return result + + +# ── MongoDB extraction ─────────────────────────────────────────────────────── + + +class MongoExtractor: + def __init__( + self, + db_name: str = "orgforge", + uri: str = "mongodb://localhost:27017?directConnection=true", + ): + self._client = MongoClient(uri) + self._db = self._client[db_name] + + def load_cached_baseline( + self, arm: str, incident_id: str, model_id: str + ) -> list[ArtifactDoc] | None: + doc = self._db["baseline_cache"].find_one( + {"_id": f"{arm}_{incident_id}_{model_id}"} + ) + if not doc: + return None + return [ArtifactDoc(**a) for a in doc["artifacts"]] + + def save_cached_baseline( + self, arm: str, incident_id: str, model_id: str, artifacts: list[ArtifactDoc] + ) -> None: + self._db["baseline_cache"].replace_one( + {"_id": f"{arm}_{incident_id}_{model_id}"}, + { + "_id": f"{arm}_{incident_id}_{model_id}", + "arm": arm, + "incident_id": incident_id, + "model_id": model_id, + "artifacts": [vars(a) for a in artifacts], + "created_at": datetime.utcnow().isoformat(), + }, + upsert=True, + ) + + def org_names(self) -> set[str]: + names: set[str] = set() + for doc in self._db["events"].find( + {"type": "day_summary"}, {"facts.active_actors": 1} + ): + for name in doc.get("facts", {}).get("active_actors", []): + names.add(name) + if not names: + for doc in self._db["artifacts"].find( + {"type": "persona_skill"}, {"metadata": 1} + ): + name = doc.get("metadata", {}).get("name") + if name: + names.add(name) + return names + + def extract_incidents(self, max_n: int = 5) -> list[IncidentBundle]: + events = list( + self._db["events"] + .find({"type": "incident_opened"}) + .sort("day", 1) + .limit(max_n) + ) + bundles: list[IncidentBundle] = [] + for ev in events: + facts = ev.get("facts", {}) + ticket_id = ev.get("artifact_ids", {}).get("jira", "") + if not ticket_id: + continue + + resolved_ev = self._db["events"].find_one( + {"type": "incident_resolved", "artifact_ids.jira": ticket_id} + ) + duration = ( + resolved_ev.get("facts", {}).get("duration_days", 0) + if resolved_ev + else 0 + ) + + bundle = IncidentBundle( + incident_id=ticket_id, + root_cause=facts.get("root_cause", ""), + system_fault=facts.get("system_fault", ""), + on_call=ev.get("actors", [""])[0], + day=ev.get("day", 0), + duration_days=duration, + health_at_open=facts.get("system_health", 100), + sim_event_facts=facts, + ) + + jira_doc = self._db["jira_tickets"].find_one( + {"id": ticket_id}, {"causal_chain": 1, "bot_threads": 1} + ) + causal_chain = ( + jira_doc.get("causal_chain", facts.get("causal_chain", [ticket_id])) + if jira_doc + else facts.get("causal_chain", [ticket_id]) + ) + bot_threads = set( + jira_doc.get("bot_threads", facts.get("bot_threads", [])) + if jira_doc + else facts.get("bot_threads", []) + ) + bundle.artifacts = self._collect_artifacts( + ticket_id, causal_chain, bot_threads + ) + if bundle.artifacts: + bundles.append(bundle) + + logger.info(f"Extracted {len(bundles)} incidents from MongoDB") + return bundles + + def _collect_artifacts( + self, ticket_id: str, causal_chain: list[str], bot_threads: set[str] = set() + ) -> list[ArtifactDoc]: + docs: list[ArtifactDoc] = [] + seen: set[str] = set() + + def _is_noise(art_doc: ArtifactDoc) -> bool: + return art_doc.content.startswith( + "GitHub: 💬" + ) or art_doc.content.startswith("AWS Cost Explorer:") + + def _try_append(art_doc: ArtifactDoc | None) -> None: + if art_doc and not _is_noise(art_doc): + docs.append(art_doc) + + # ── Jira ticket + comments ──────────────────────────────────────────── + jira = self._db["jira_tickets"].find_one({"id": ticket_id}) + if jira: + assignee = jira.get("assignee", "") + escalation = " ".join(jira.get("escalation_actors", [])) + body_parts = [ + jira.get("title", ""), + f"Assignee: {assignee}" if assignee else "", + f"Escalation: {escalation}" if escalation else "", + jira.get("description", ""), + jira.get("root_cause", ""), + ] + docs.append( + ArtifactDoc( + artifact_id=ticket_id, + artifact_type="jira", + title=jira.get("title", ticket_id), + content="\n".join(filter(None, body_parts)), + timestamp=jira.get("created_at", ""), + author=jira.get("assignee", ""), + ) + ) + seen.add(ticket_id) + + for comment in jira.get("comments", []): + comment_author = comment.get("author", "") + comment_text = comment.get("text", "") + comment_ts = comment.get("created", "") + if not comment_text: + continue + comment_id = f"{ticket_id}_comment_{comment.get('day', comment_ts)}" + if comment_id in seen: + continue + seen.add(comment_id) + docs.append( + ArtifactDoc( + artifact_id=comment_id, + artifact_type="jira_comment", + title=f"Comment on {ticket_id} by {comment_author}", + content=f"{comment_author}: {comment_text}", + timestamp=comment_ts, + author=comment_author, + ) + ) + + # ── Causal chain ────────────────────────────────────────────────────── + for chain_id in causal_chain: + if chain_id in seen or chain_id in bot_threads: + continue + if re.match(r"^[A-Z]+-\d+_comment_\d+$", chain_id): + seen.add(chain_id) + continue + seen.add(chain_id) + _try_append(self._resolve_artifact_by_id(chain_id)) + + # ── Related events ──────────────────────────────────────────────────── + related_events = list( + self._db["events"].find( + { + "$or": [ + {"artifact_ids.jira": ticket_id}, + {"facts.causal_chain": ticket_id}, + ] + } + ) + ) + for ev in related_events: + for key, raw_aid in ev.get("artifact_ids", {}).items(): + aids = raw_aid if isinstance(raw_aid, list) else [raw_aid] + for aid in aids: + if not aid or aid in seen or key in ("jira",): + continue + seen.add(aid) + art_doc = self._resolve_artifact(aid, key, ev) + if art_doc and not _is_noise(art_doc): + docs.append(art_doc) + + # ── Postmortem ──────────────────────────────────────────────────────── + postmortem_ev = self._db["events"].find_one( + {"type": "postmortem_created", "artifact_ids.jira": ticket_id} + ) + if postmortem_ev: + raw_conf = postmortem_ev.get("artifact_ids", {}).get("confluence", "") + conf_ids = ( + raw_conf + if isinstance(raw_conf, list) + else [raw_conf] + if raw_conf + else [] + ) + for conf_id in conf_ids: + if conf_id and conf_id not in seen: + seen.add(conf_id) + _try_append(self._resolve_artifact_by_id(conf_id)) + + seen_content: set[str] = set() + deduped: list[ArtifactDoc] = [] + for doc in docs: + fingerprint = doc.content[:100].strip() + if fingerprint not in seen_content: + seen_content.add(fingerprint) + deduped.append(doc) + return deduped + + def _resolve_artifact( + self, aid: str, key_hint: str, event: dict + ) -> ArtifactDoc | None: + if "slack" in key_hint: + doc = self._db["artifacts"].find_one({"_id": aid, "type": "slack_thread"}) + if doc: + return ArtifactDoc( + artifact_id=aid, + artifact_type="slack", + title=doc.get("title", f"Slack thread {aid}"), + content=doc.get("content", ""), + timestamp=doc.get("timestamp", ""), + author=doc.get("metadata", {}).get("participants", [""])[0], + ) + if "confluence" in key_hint: + doc = self._db["artifacts"].find_one({"_id": aid, "type": "confluence"}) + if doc: + return ArtifactDoc( + artifact_id=aid, + artifact_type="confluence", + title=doc.get("title", aid), + content=doc.get("content", ""), + timestamp=doc.get("timestamp", ""), + author=doc.get("metadata", {}).get("author", ""), + ) + if "pr" in key_hint: + doc = self._db["pull_requests"].find_one({"pr_id": aid}) + if doc: + author = doc.get("author", "") + reviewers = " ".join(doc.get("reviewers", [])) + comment_parts = [ + f"{c.get('author', '')}: {c.get('text', '')}" + for c in doc.get("comments", []) + ] + content = "\n".join( + filter( + None, + [ + doc.get("title", ""), + f"Author: {author}" if author else "", + f"Reviewers: {reviewers}" if reviewers else "", + doc.get("description", ""), + *comment_parts, + ], + ) + ) + return ArtifactDoc( + artifact_id=aid, + artifact_type="pr", + title=doc.get("title", aid), + content=content, + timestamp=doc.get("created_at", ""), + author=author, + ) + return None + + def _resolve_artifact_by_id(self, aid: str) -> ArtifactDoc | None: + for key_hint in ("confluence", "slack", "pr"): + result = self._resolve_artifact(aid, key_hint, {}) + if result: + return result + + messages = list( + self._db["slack_messages"].find({"thread_id": aid}).sort("ts", 1) + ) + if messages: + content = "\n".join( + f"{m.get('user', '?')}: {m.get('text', '')}" + for m in messages + if not m.get("is_bot", False) + ) + participants = list({m.get("user", "") for m in messages if m.get("user")}) + if content: + return ArtifactDoc( + artifact_id=aid, + artifact_type="slack", + title=f"Slack thread {messages[0].get('channel', '')} {aid}", + content=content, + timestamp=messages[0].get("ts", ""), + author=messages[0].get("user", ""), + metadata={"participants": participants}, + ) + + doc = self._db["artifacts"].find_one({"_id": aid}) + if doc: + return ArtifactDoc( + artifact_id=aid, + artifact_type=doc.get("type", "unknown"), + title=doc.get("title", aid), + content=doc.get("content", ""), + timestamp=doc.get("timestamp", ""), + ) + return None + + +# ── LLM-as-judge (contradiction detection) ─────────────────────────────────── +# Now used as a *second layer* on top of fidelity: only run if fidelity flags +# a potential issue, or on demand. Still available for full pairwise sweeps. + +JUDGE_PROMPT = """You are a factual consistency auditor. You will be given two documents +that describe the same organizational incident. Your job is to identify +FACTUAL CONTRADICTIONS — places where the two documents disagree on concrete +facts (names, systems, timelines, root causes, outcomes). + +Do NOT flag: +- Different levels of detail (one doc says more than the other) +- Different writing styles or perspectives +- Omissions (one doc doesn't mention something the other does) + +DO flag: +- Different system/component names for the same failure +- Different people credited with the same action +- Contradictory timelines (e.g., "resolved in 2 days" vs. "resolved in 5 days") +- Different root causes for the same incident + +Document A ({id_a}): +{content_a} + +Document B ({id_b}): +{content_b} + +Respond with a JSON object: +{{ + "contradictions": [ + {{"field": "string", "doc_a_says": "string", "doc_b_says": "string"}} + ], + "count": int +}} + +If no contradictions, return {{"contradictions": [], "count": 0}}. +JSON only. No preamble.""" + + +def judge_contradictions( + bedrock_judge: Bedrock, + artifacts: list[ArtifactDoc], + max_pairs: int = 10, + sleep_between_calls: float = 2.0, +) -> int: + if len(artifacts) < 2: + return 0 + + pairs = list(combinations(artifacts, 2))[:max_pairs] + total = 0 + + for a, b in pairs: + prompt = JUDGE_PROMPT.format( + id_a=a.artifact_id, + content_a=a.content[:3000], + id_b=b.artifact_id, + content_b=b.content[:3000], + ) + try: + raw = bedrock_judge.generate(prompt, temperature=0.0, max_tokens=2048) + clean = raw.strip() + if clean.startswith("```"): + clean = re.sub(r"^```\w*\n?", "", clean).rstrip("`\n ") + parsed = json.loads(clean) + count = parsed.get("count", 0) + total += count + if count > 0: + for c in parsed.get("contradictions", []): + logger.info( + f" Contradiction: {c.get('field', '?')} — " + f"A says '{c.get('doc_a_says', '?')}', " + f"B says '{c.get('doc_b_says', '?')}'" + ) + except (json.JSONDecodeError, KeyError) as exc: + logger.warning( + f" Judge parse failed for {a.artifact_id}↔{b.artifact_id}: {exc}" + ) + except Exception as exc: + logger.warning(f" Judge call failed: {exc}") + + time.sleep(sleep_between_calls) + + return total + + +# ── Baseline generation ────────────────────────────────────────────────────── + +ARTIFACT_SEQUENCE = [ + ("jira", "JIRA Ticket", "Write a JIRA ticket for this incident."), + ("slack", "Slack Thread", "Write a Slack #incidents thread about this incident."), + ( + "pr", + "Pull Request", + "Write a GitHub PR description for the fix to this incident.", + ), + ( + "postmortem", + "Postmortem", + "Write a Confluence postmortem document for this incident.", + ), + ( + "email", + "Customer Email", + "Write an inbound customer complaint email triggered by this incident.", + ), +] + + +def _baseline_system_prompt(tech_stack: str, org_chart_str: str, company: str) -> str: + return ( + f"You are generating realistic organizational documents for {company}.\n" + f"Tech stack:\n{tech_stack}\n\n" + f"Team members:\n{org_chart_str}\n\n" + f"Use ONLY the system names and person names provided above. " + f"Do not invent any names not in this list." + ) + + +def generate_baseline_artifacts( + bedrock: Bedrock, + incident: IncidentBundle, + tech_stack_str: str, + org_chart_str: str, + company: str, + chained: bool, +) -> list[ArtifactDoc]: + system = _baseline_system_prompt(tech_stack_str, org_chart_str, company) + context_prefix = ( + f"Incident: {incident.root_cause}\n" + f"System fault: {incident.system_fault}\n" + f"On-call engineer: {incident.on_call}\n" + f"Duration: {incident.duration_days} days\n" + f"System health at open: {incident.health_at_open}/100\n\n" + ) + + prior_artifacts: list[str] = [] + results: list[ArtifactDoc] = [] + + for idx, (art_type, art_label, instruction) in enumerate(ARTIFACT_SEQUENCE): + prompt = f"{context_prefix}{instruction}\n\n" + + if chained and prior_artifacts: + prompt += "Previously generated documents for this incident:\n\n" + for prior in prior_artifacts: + prompt += f"{prior}\n\n---\n\n" + prompt += ( + "Your document must be consistent with all of the above. " + "Use the same names, systems, and timeline.\n\n" + ) + + prompt += ( + f"Write the {art_label} now. Output only the document content, " + f"no preamble or meta-commentary." + ) + + try: + content = bedrock.generate(prompt, system=system, max_tokens=2048) + except Exception as exc: + logger.warning(f" Baseline generation failed for {art_type}: {exc}") + content = f"[Generation failed: {exc}]" + + fake_ts = f"2026-03-{10 + idx:02d}T{9 + idx:02d}:00:00" + doc = ArtifactDoc( + artifact_id=f"baseline_{art_type}_{incident.incident_id}", + artifact_type=art_type, + title=f"{art_label}: {incident.root_cause[:60]}", + content=content, + timestamp=fake_ts, + author=incident.on_call, + ) + results.append(doc) + + if chained: + prior_artifacts.append(f"[{art_label}]\n{content}") + + return results + + +# ── Evaluation orchestrator ────────────────────────────────────────────────── + +SCORING_ARTIFACT_TYPES = { + "jira", + "jira_comment", + "slack", + "pr", + "confluence", + "postmortem", + "email", +} + + +def evaluate_arm( + arm_name: str, + incidents_with_artifacts: list[tuple[IncidentBundle, list[ArtifactDoc]]], + bedrock_judge: Bedrock | None, + skip_judge: bool = False, +) -> EvalResult: + all_fidelity: list[float] = [] + all_contradictions: list[int] = [] + field_buckets: dict[str, list[float]] = defaultdict(list) + type_buckets: dict[str, list[float]] = defaultdict(list) + per_incident: list[dict] = [] + + for incident, artifacts in incidents_with_artifacts: + if not artifacts: + continue + + scoring_artifacts = [ + a for a in artifacts if a.artifact_type in SCORING_ARTIFACT_TYPES + ] + if not scoring_artifacts: + continue + + # ── Ground truth fidelity ───────────────────────────────────────── + inc_fidelity = score_incident_fidelity(incident, scoring_artifacts) + all_fidelity.append(inc_fidelity.overall) + + for fname, score in inc_fidelity.by_field().items(): + field_buckets[fname].append(score) + for atype, score in inc_fidelity.by_artifact_type().items(): + type_buckets[atype].append(score) + + # ── Optional contradiction judge ─────────────────────────────────── + contradictions = 0 + if not skip_judge and bedrock_judge and len(scoring_artifacts) >= 2: + logger.info( + f" Judging {arm_name}/{incident.incident_id} " + f"({len(scoring_artifacts)} artifacts)..." + ) + contradictions = judge_contradictions(bedrock_judge, scoring_artifacts) + all_contradictions.append(contradictions) + + # ── Per-artifact detail for output ──────────────────────────────── + artifact_detail = [] + for af in inc_fidelity.artifacts: + artifact_detail.append( + { + "artifact_id": af.artifact_id, + "artifact_type": af.artifact_type, + "overall": round(af.overall, 4), + "fields": {f.field_name: round(f.recall, 4) for f in af.fields}, + } + ) + + per_incident.append( + { + "incident_id": incident.incident_id, + "n_artifacts": len(scoring_artifacts), + "fidelity_overall": round(inc_fidelity.overall, 4), + "fidelity_by_field": { + k: round(v, 4) for k, v in inc_fidelity.by_field().items() + }, + "fidelity_by_artifact_type": { + k: round(v, 4) for k, v in inc_fidelity.by_artifact_type().items() + }, + "contradictions": contradictions, + "artifacts": artifact_detail, + } + ) + + logger.info( + f" {incident.incident_id}: fidelity={inc_fidelity.overall:.3f} " + f"contradictions={contradictions} artifacts={len(scoring_artifacts)}" + ) + + return EvalResult( + arm=arm_name, + n_incidents=len(per_incident), + overall_fidelity=round(mean(all_fidelity), 4) if all_fidelity else 0.0, + fidelity_by_field={k: round(mean(v), 4) for k, v in field_buckets.items()}, + fidelity_by_artifact_type={ + k: round(mean(v), 4) for k, v in type_buckets.items() + }, + contradictions=round(mean(all_contradictions), 2) + if all_contradictions + else 0.0, + per_incident=per_incident, + ) + + +# ── Output formatting ──────────────────────────────────────────────────────── + + +def print_results(results: list[EvalResult]) -> None: + col_w = 18 + arms = [r.arm for r in results] + header = f"{'Metric':<40}" + "".join(f"{a:>{col_w}}" for a in arms) + + print("\n" + "=" * len(header)) + print(header) + print("-" * len(header)) + + # Overall fidelity + row = f"{'GT fidelity (overall)':<40}" + for r in results: + row += f"{r.overall_fidelity:>{col_w}.4f}" + print(row) + + # Per-field fidelity + all_fields = sorted({f for r in results for f in r.fidelity_by_field}) + for fname in all_fields: + row = f" fidelity:{fname:<30}" + for r in results: + val = r.fidelity_by_field.get(fname, float("nan")) + row += f"{val:>{col_w}.4f}" + print(row) + + print("-" * len(header)) + + # Per-artifact-type fidelity + all_types = sorted({t for r in results for t in r.fidelity_by_artifact_type}) + for atype in all_types: + row = f" fidelity:{atype:<30}" + for r in results: + val = r.fidelity_by_artifact_type.get(atype, float("nan")) + row += f"{val:>{col_w}.4f}" + print(row) + + print("-" * len(header)) + + row = f"{'Contradictions / incident (judge)':<40}" + for r in results: + row += f"{r.contradictions:>{col_w}.2f}" + print(row) + + row = f"{'Incidents evaluated':<40}" + for r in results: + row += f"{r.n_incidents:>{col_w}d}" + print(row) + + print("=" * len(header)) + + # Per-incident detail + for r in results: + print(f"\n── {r.arm} per-incident detail ──") + for p in r.per_incident: + field_str = " ".join( + f"{k}={v:.3f}" for k, v in p["fidelity_by_field"].items() + ) + print( + f" {p['incident_id']}: " + f"fidelity={p['fidelity_overall']:.3f} " + f"contradictions={p['contradictions']} " + f"artifacts={p['n_artifacts']}" + ) + if field_str: + print(f" fields: {field_str}") + for a in p.get("artifacts", []): + field_detail = " ".join(f"{k}={v:.3f}" for k, v in a["fields"].items()) + print( + f" [{a['artifact_type']:12s}] {a['artifact_id']}: {field_detail}" + ) + + +def save_results(results: list[EvalResult], path: str = "eval_results.json") -> None: + out = [] + for r in results: + out.append( + { + "arm": r.arm, + "overall_fidelity": r.overall_fidelity, + "fidelity_by_field": r.fidelity_by_field, + "fidelity_by_artifact_type": r.fidelity_by_artifact_type, + "contradictions": r.contradictions, + "n_incidents": r.n_incidents, + "per_incident": r.per_incident, + } + ) + with open(path, "w") as f: + json.dump(out, f, indent=2) + logger.info(f"Results saved to {path}") + + +# ── Main ───────────────────────────────────────────────────────────────────── + + +def main(): + parser = argparse.ArgumentParser( + description="OrgForge cross-document consistency evaluation" + ) + parser.add_argument("--model", default="openai.gpt-oss-120b-1:0") + parser.add_argument("--region", default="us-east-1") + parser.add_argument("--db", default="orgforge") + parser.add_argument( + "--mongo-uri", default="mongodb://localhost:27017?directConnection=true" + ) + parser.add_argument("--incidents", type=int, default=5) + parser.add_argument("--skip-baselines", action="store_true") + parser.add_argument("--skip-judge", action="store_true") + parser.add_argument("--output", default="eval_results.json") + parser.add_argument( + "--judge-model", + default="", + help="Model for contradiction judge (defaults to --model)", + ) + args = parser.parse_args() + + mongo = MongoExtractor(db_name=args.db, uri=args.mongo_uri) + bedrock = Bedrock(model_id=args.model, region=args.region) + bedrock_judge = ( + Bedrock( + model_id=args.judge_model or args.model, + region=args.region, + ) + if not args.skip_judge + else None + ) + + logger.info(f"Extracting up to {args.incidents} incidents from OrgForge...") + incidents = mongo.extract_incidents(max_n=args.incidents) + + if not incidents: + logger.error("No incidents found in MongoDB. Run OrgForge first.") + sys.exit(1) + + for inc in incidents: + logger.info( + f" {inc.incident_id}: on_call={inc.on_call!r} " + f"root_cause={inc.root_cause[:60]!r} " + f"({len(inc.artifacts)} artifacts)" + ) + + # ── Arm 1: OrgForge ────────────────────────────────────────────────── + logger.info("\n━━━ Evaluating OrgForge arm ━━━") + orgforge_result = evaluate_arm( + "OrgForge", + [(inc, inc.artifacts) for inc in incidents], + bedrock_judge, + skip_judge=args.skip_judge, + ) + results = [orgforge_result] + + if not args.skip_baselines: + org_names = mongo.org_names() + org_chart_str = "\n".join(sorted(org_names)) + + ts_doc = mongo._db["artifacts"].find_one({"type": "tech_stack"}) + tech_stack_str = "" + if ts_doc: + content = ts_doc.get("content", "") + tech_stack_str = ( + json.dumps(content, indent=2) + if isinstance(content, dict) + else str(content) + ) + + company = COMPANY_NAME + + # ── Arm 2: Chained baseline ────────────────────────────────────── + logger.info("\n━━━ Generating chained baseline ━━━") + chained_data = [] + for inc in incidents: + arts = mongo.load_cached_baseline("chained", inc.incident_id, args.model) + if arts is None: + logger.info(f" Generating chained artifacts for {inc.incident_id}...") + arts = generate_baseline_artifacts( + bedrock, inc, tech_stack_str, org_chart_str, company, chained=True + ) + mongo.save_cached_baseline("chained", inc.incident_id, args.model, arts) + else: + logger.info(f" Using cached chained baseline for {inc.incident_id}") + chained_data.append((inc, arts)) + + logger.info("\n━━━ Evaluating chained baseline ━━━") + chained_result = evaluate_arm( + "Chained", + chained_data, + bedrock_judge, + skip_judge=args.skip_judge, + ) + results.append(chained_result) + + # ── Arm 3: Parallel baseline ───────────────────────────────────── + logger.info("\n━━━ Generating parallel baseline ━━━") + parallel_data = [] + for inc in incidents: + arts = mongo.load_cached_baseline("parallel", inc.incident_id, args.model) + if arts is None: + logger.info(f" Generating parallel artifacts for {inc.incident_id}...") + arts = generate_baseline_artifacts( + bedrock, inc, tech_stack_str, org_chart_str, company, chained=False + ) + mongo.save_cached_baseline( + "parallel", inc.incident_id, args.model, arts + ) + else: + logger.info(f" Using cached parallel baseline for {inc.incident_id}") + parallel_data.append((inc, arts)) + + logger.info("\n━━━ Evaluating parallel baseline ━━━") + parallel_result = evaluate_arm( + "Parallel", + parallel_data, + bedrock_judge, + skip_judge=args.skip_judge, + ) + results.append(parallel_result) + + print_results(results) + save_results(results, args.output) + + +if __name__ == "__main__": + main() diff --git a/eval/eval_divergence.py b/eval/eval_divergence.py new file mode 100644 index 0000000..049d0d8 --- /dev/null +++ b/eval/eval_divergence.py @@ -0,0 +1,572 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from enum import Enum +from statistics import mean +from typing import Any + +import torch +from rapidfuzz import fuzz +from transformers import AutoModelForSequenceClassification, AutoTokenizer + + +class DivergenceType(Enum): + ENTITY_MISSING = "entity_missing" + ENTITY_CONTRADICTED = "entity_contradicted" + FACT_CONTRADICTED = "fact_contradicted" + NUMERIC_MISMATCH = "numeric_mismatch" + + +@dataclass +class Divergence: + divergence_type: DivergenceType + sim_event_field: str + sim_event_value: str + prose_value: str | None + artifact_id: str + confidence: float + detail: str = "" + + +@dataclass +class ProseSimEventReport: + artifact_id: str + entity_score: float + nli_score: float + numeric_score: float + composite_score: float + divergences: list[Divergence] = field(default_factory=list) + + +ENTITY_FIELDS: dict[str, list[str]] = { + "actors": ["actors", "assigned_to", "author", "reviewer", "responders"], + "identifiers": [ + "ticket_id", + "pr_id", + "incident_id", + "confluence_id", + "zd_ticket_id", + "sf_opportunity_id", + "invoice_id", + ], + "components": [ + "affected_system", + "tech_stack", + "depends_on_components", + "system_tags", + ], + "organizations": ["customer_org", "vendor_org", "org_name"], + "statuses": ["status", "verdict", "stage", "priority", "severity"], +} + +FACT_TEMPLATES: dict[str, str] = { + "root_cause": "The root cause of the incident is described as follows: {value}", + "affected_system": "{value} was involved in the incident.", + "status": "The status is {value}.", + "verdict": "The verdict was {value}.", + "assigned_to": "{value} worked on this.", + "author": "{value} authored this.", + "reviewer": "{value} reviewed this.", + "customer_org": "This involves {value}.", + "priority": "The priority is {value}.", + "severity": "The severity is {value}.", + "stage": "The stage is {value}.", + "incident_duration_hours": "The duration was {value} hours.", + "resolution_summary": "The resolution is described as follows: {value}", + "title": "This document is about {value}.", + "vendor_org": "This involves {value} as an external party.", +} + +_SHORT_ALIAS_RE_CACHE: dict[str, re.Pattern] = {} + + +def _alias_in_prose(alias: str, prose_lower: str) -> bool: + lowered = alias.lower() + if len(lowered) <= 4: + pattern = _SHORT_ALIAS_RE_CACHE.get(lowered) + if pattern is None: + pattern = re.compile(rf"\b{re.escape(lowered)}\b") + _SHORT_ALIAS_RE_CACHE[lowered] = pattern + return bool(pattern.search(prose_lower)) + return lowered in prose_lower + + +class NLIScorer: + def __init__(self, model_name: str = "cross-encoder/nli-deberta-v3-base"): + self.tokenizer = AutoTokenizer.from_pretrained(model_name) + self.model = AutoModelForSequenceClassification.from_pretrained(model_name) + self.model.eval() + self._label_map = { + i: label.lower() for i, label in self.model.config.id2label.items() + } + + def score(self, premise: str, hypothesis: str) -> dict[str, float]: + inputs = self.tokenizer( + premise, + hypothesis, + return_tensors="pt", + truncation=True, + max_length=512, + ) + with torch.no_grad(): + logits = self.model(**inputs).logits + probs = torch.softmax(logits, dim=-1)[0] + return {self._label_map[i]: probs[i].item() for i in range(3)} + + +def _flatten_fact_value(value: Any) -> list[str]: + if isinstance(value, list): + return [str(v) for v in value] + if isinstance(value, dict): + return [str(v) for v in value.values()] + return [str(value)] + + +def _clean_prose_for_nli(content: str, max_chars: int = 3500) -> str: + lines = content.splitlines() + filtered = [] + for line in lines: + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("|"): + continue + if stripped.startswith("#"): + filtered.append(stripped.lstrip("#").strip()) + continue + if re.match(r"^\*\*\w+[::]\*\*\s*\w+-\w+$", stripped): + continue + filtered.append(stripped) + return " ".join(filtered)[:max_chars] + + +# Fields whose numeric value is a boolean flag (0/1) with no prose counterpart. +_NUMERIC_BOOL_FIELDS: frozenset[str] = frozenset( + { + "has_question", + "incident_review", + "involves_gap", + "recurrence_chain_depth", + "is_escalated", + "is_resolved", + "has_postmortem", + "has_followup", + "escalation_harder", + } +) + +# Fields that are dimensionless ratios / percentages (0.0–1.0 scale). +# Prose never writes "0.25" for 25 % coverage — it writes "25 %" or "25 percent", +# so raw-number matching against these will always be off by 4×. +# They are skipped here and can be validated by NLI or a dedicated % check instead. +_NUMERIC_RATIO_FIELDS: frozenset[str] = frozenset( + { + "documented_pct", + "live_documentation_coverage", + "coverage", + "score_pct", + "completion_rate", + "pass_rate", + } +) + +_NUMERIC_INTERNAL_FIELDS: frozenset[str] = frozenset( + { + "semantic_score", + "days_since_departure", + "recurrence_gap_days", + "health_at_open", + "health_at_close", + "sim_score", + "quality_score", + } +) + + +def _extract_numeric_claims(facts: dict[str, Any]) -> dict[str, float]: + numeric_fields = {} + for key, value in facts.items(): + if ( + key in _NUMERIC_BOOL_FIELDS + or key in _NUMERIC_RATIO_FIELDS + or key in _NUMERIC_INTERNAL_FIELDS + ): + continue + if isinstance(value, int | float): + numeric_fields[key] = float(value) + elif isinstance(value, str): + try: + numeric_fields[key] = float(value) + except ValueError: + pass + return numeric_fields + + +def _find_numbers_in_prose(prose: str) -> list[tuple[float, str]]: + patterns = [ + (r"(\d+\.?\d*)\s*hours?", "hours"), + (r"(\d+\.?\d*)\s*minutes?", "minutes"), + (r"\$\s*([\d,]+\.?\d*)", "dollars"), + (r"(\d+\.?\d*)\s*%", "percent"), + (r"(\d+\.?\d*)\s*days?", "days"), + (r"(\d+)", "raw_number"), + ] + found: list[tuple[float, str]] = [] + for pattern, unit in patterns: + for match in re.finditer(pattern, prose, re.IGNORECASE): + raw = match.group(1).replace(",", "") + found.append((float(raw), unit)) + return found + + +_ORG_INDICATORS = re.compile( + r"\b(?:" + r"inc|llc|corp|ltd|services|web services|cloud|platform|" + r"aws|gcp|azure|hashicorp|terraform|" + r"jenkins|cloudbees|github|gitlab|circleci|buildkite|" + r"datadog|pagerduty|opsgenie|sentry|grafana|prometheus|" + r"docker|kubernetes|kafka|rabbitmq|redis|" + r"bot|ci[/-]cd|pipeline|automation|cron" + r")\b", + re.IGNORECASE, +) + +FIELD_ARTIFACT_SCOPE: dict[str, set[str]] = { + "assigned_to": {"jira", "jira_comment"}, + "reviewer": {"pr"}, + "verdict": {"pr"}, + "author": {"pr", "confluence", "jira"}, + "responders": {"jira", "jira_comment", "slack"}, + "actors": {"jira", "jira_comment", "slack"}, + "ticket_id": {"jira", "jira_comment", "pr", "confluence"}, + "vendor_org": {"jira", "jira_comment", "slack", "confluence"}, + "priority": {"jira", "jira_comment"}, + "severity": {"jira", "jira_comment"}, + "stage": {"jira", "jira_comment"}, + "resolution_summary": {"jira", "confluence"}, +} + + +def _tokens_contained(expected_normalized: str, prose_lower: str) -> bool: + expected_tokens = {t for t in expected_normalized.split() if len(t) > 2} + if not expected_tokens: + return False + prose_tokens = set(prose_lower.split()) + return expected_tokens.issubset(prose_tokens) + + +def _all_tokens_present( + expected: str, prose_lower: str, min_token_len: int = 3 +) -> bool: + tokens = [t.lower() for t in expected.split() if len(t) >= min_token_len] + if len(tokens) < 2: + return False + return all(t in prose_lower for t in tokens) + + +def check_entity_divergence( + artifact_id: str, + artifact_type: str, + content: str, + sim_event_facts: dict[str, Any], + fuzzy_threshold: int = 80, + vendor_aliases: dict[str, set[str]] | None = None, +) -> tuple[float, list[Divergence]]: + prose_lower = content.lower() + divergences: list[Divergence] = [] + checks = 0 + matches = 0 + checked_org_values: set[str] = set() + + for category, field_names in ENTITY_FIELDS.items(): + for field_name in field_names: + if field_name not in sim_event_facts: + continue + + allowed_types = FIELD_ARTIFACT_SCOPE.get(field_name) + if allowed_types and artifact_type not in allowed_types: + continue + + expected_values = _flatten_fact_value(sim_event_facts[field_name]) + for expected in expected_values: + if not expected or expected.lower() in ("none", "null", ""): + continue + + if category == "identifiers" and expected == artifact_id: + continue + + is_org = category in ("actors", "organizations") and ( + _ORG_INDICATORS.search(expected) + or (vendor_aliases and expected in vendor_aliases) + ) + + if is_org and vendor_aliases and expected in vendor_aliases: + if expected in checked_org_values: + continue + checked_org_values.add(expected) + checks += 1 + aliases = vendor_aliases[expected] + if any(_alias_in_prose(a, prose_lower) for a in aliases): + matches += 1 + else: + divergences.append( + Divergence( + divergence_type=DivergenceType.ENTITY_MISSING, + sim_event_field=field_name, + sim_event_value=expected, + prose_value=None, + artifact_id=artifact_id, + confidence=0.6, + detail=( + f"'{expected}' (vendor) not found via aliases " + f"{sorted(aliases)} (category: {category})" + ), + ) + ) + continue + + if is_org: + continue + + normalized = expected.lower().replace("_", " ") + checks += 1 + + exact_hit = normalized in prose_lower + fuzzy_hit = ( + fuzz.partial_ratio(normalized, prose_lower) >= fuzzy_threshold + ) + + if category == "statuses": + token_hit = _tokens_contained(normalized, prose_lower) + else: + token_hit = _all_tokens_present(expected, prose_lower) + + if exact_hit or fuzzy_hit or token_hit: + matches += 1 + else: + divergences.append( + Divergence( + divergence_type=DivergenceType.ENTITY_MISSING, + sim_event_field=field_name, + sim_event_value=expected, + prose_value=None, + artifact_id=artifact_id, + confidence=0.7, + detail=f"'{expected}' not found in prose (category: {category})", + ) + ) + + score = matches / checks if checks > 0 else 1.0 + return score, divergences + + +FACT_TEMPLATE_SCOPE: dict[str, set[str]] = { + "root_cause": {"jira", "jira_comment", "confluence", "slack", "pr"}, + "affected_system": {"jira", "jira_comment", "confluence", "slack", "pr"}, + "status": {"jira", "jira_comment"}, + "verdict": {"pr"}, + "assigned_to": {"jira", "jira_comment"}, + "author": {"pr", "confluence"}, + "reviewer": {"pr"}, + "customer_org": {"jira", "confluence"}, + "priority": {"jira", "jira_comment", "confluence"}, + "severity": {"jira", "jira_comment", "confluence"}, + "stage": {"jira"}, + "incident_duration_hours": {"jira", "confluence"}, + "resolution_summary": {"jira", "confluence"}, + "title": {"jira", "confluence", "pr"}, +} + +_NLI_SKIP_FIELDS_BY_EVENT_TYPE: dict[str, frozenset[str]] = { + "external_contact_summarized": frozenset({"root_cause", "title"}), + "inbound_external_email": frozenset({"root_cause"}), +} + + +def check_nli_divergence( + artifact_id: str, + artifact_type: str, + content: str, + sim_event_facts: dict[str, Any], + nli: NLIScorer, + contradiction_threshold: float = 0.70, + sim_event_type: str = "", +) -> tuple[float, list[Divergence]]: + _MAX_HYPOTHESIS_VALUE_CHARS = 200 + + divergences: list[Divergence] = [] + scores: list[float] = [] + prose_truncated = _clean_prose_for_nli(content) + skip_fields = _NLI_SKIP_FIELDS_BY_EVENT_TYPE.get(sim_event_type, frozenset()) + + for field_name, template in FACT_TEMPLATES.items(): + if field_name in skip_fields: + continue + + if field_name not in sim_event_facts: + continue + + allowed = FACT_TEMPLATE_SCOPE.get(field_name) + if allowed and artifact_type not in allowed: + continue + + allowed_types = FIELD_ARTIFACT_SCOPE.get(field_name) + if allowed_types and artifact_type not in allowed_types: + continue + + value = sim_event_facts[field_name] + if value is None or str(value).lower() in ("none", "null", ""): + continue + + if field_name == "vendor_org" and isinstance(value, str): + words = value.split() + if len(words) >= 2 and _ORG_INDICATORS.search(words[-1]): + value = words[0] + + if isinstance(value, list): + hypothesis = template.format(value=", ".join(str(v) for v in value)) + else: + hypothesis = template.format(value=str(value)) + + result = nli.score(premise=prose_truncated, hypothesis=hypothesis) + + if result["contradiction"] >= contradiction_threshold: + divergences.append( + Divergence( + divergence_type=DivergenceType.FACT_CONTRADICTED, + sim_event_field=field_name, + sim_event_value=str(value), + prose_value=f"NLI contradiction={result['contradiction']:.2f}", + artifact_id=artifact_id, + confidence=result["contradiction"], + detail=f"Hypothesis: '{hypothesis}'", + ) + ) + scores.append(0.0) + elif result["contradiction"] < 0.3: + scores.append(1.0) + elif result["entailment"] >= 0.5: + scores.append(1.0) + else: + scores.append(1.0 - result["contradiction"]) + + return mean(scores) if scores else 1.0, divergences + + +def check_numeric_divergence( + artifact_id: str, + content: str, + sim_event_facts: dict[str, Any], + relative_tolerance: float = 0.15, +) -> tuple[float, list[Divergence]]: + numeric_facts = _extract_numeric_claims(sim_event_facts) + if not numeric_facts: + return 1.0, [] + prose_numbers = _find_numbers_in_prose(content) + if not prose_numbers: + return 1.0, [] + + divergences: list[Divergence] = [] + checks = 0 + matches = 0 + + unit_mapping: dict[str, list[str]] = { + "duration_hours": ["hours"], + "incident_duration_hours": ["hours"], + "duration_minutes": ["minutes"], + "sla_credit": ["dollars"], + "credit_amount": ["dollars"], + "coverage": ["percent"], + "score": ["raw_number"], + } + + for field_name, expected_value in numeric_facts.items(): + relevant_units = unit_mapping.get(field_name, ["raw_number"]) + candidate_numbers = [ + num for num, unit in prose_numbers if unit in relevant_units + ] + if not candidate_numbers: + candidate_numbers = [num for num, _ in prose_numbers] + if not candidate_numbers: + continue + + checks += 1 + closest = min(candidate_numbers, key=lambda x: abs(x - expected_value)) + + if expected_value == 0: + is_match = closest == 0 + else: + is_match = ( + abs(closest - expected_value) / abs(expected_value) + <= relative_tolerance + ) + + if is_match: + matches += 1 + else: + divergences.append( + Divergence( + divergence_type=DivergenceType.NUMERIC_MISMATCH, + sim_event_field=field_name, + sim_event_value=str(expected_value), + prose_value=str(closest), + artifact_id=artifact_id, + confidence=min( + 1.0, + abs(closest - expected_value) / max(abs(expected_value), 1e-6), + ), + detail=f"Expected ~{expected_value}, closest prose number: {closest}", + ) + ) + + return (matches / checks if checks > 0 else 1.0), divergences + + +def measure_artifact_divergence( + artifact_id: str, + artifact_type: str, + artifact_content: str, + sim_event_facts: dict[str, Any], + nli: NLIScorer | None = None, + vendor_aliases: dict[str, set[str]] | None = None, + weights: tuple[float, float, float] = (0.35, 0.45, 0.20), + sim_event_type: str = "", +) -> ProseSimEventReport: + entity_score, entity_divs = check_entity_divergence( + artifact_id, + artifact_type, + artifact_content, + sim_event_facts, + vendor_aliases=vendor_aliases, + ) + + if nli is not None: + nli_score, nli_divs = check_nli_divergence( + artifact_id, + artifact_type, + artifact_content, + sim_event_facts, + nli, + sim_event_type=sim_event_type, + ) + else: + nli_score, nli_divs = 1.0, [] + + numeric_score, numeric_divs = check_numeric_divergence( + artifact_id, + artifact_content, + sim_event_facts, + ) + + w_ent, w_nli, w_num = weights + composite = w_ent * entity_score + w_nli * nli_score + w_num * numeric_score + + return ProseSimEventReport( + artifact_id=artifact_id, + entity_score=entity_score, + nli_score=nli_score, + numeric_score=numeric_score, + composite_score=composite, + divergences=entity_divs + nli_divs + numeric_divs, + ) diff --git a/eval/eval_harness.py b/eval/eval_harness.py new file mode 100644 index 0000000..92b2c9e --- /dev/null +++ b/eval/eval_harness.py @@ -0,0 +1,3644 @@ +""" +eval_harness.py +=============== +OrgForge Eval Dataset Generator + +Produces three novel eval tracks that require the deterministic state machine +to exist. No retrieval questions. Those are covered by other benchmarks. + +Run after flow.py and post_sim_artifacts.py complete: + python eval_harness.py + +Produces in export/eval/: + actor_visibility.json — per-actor artifact visibility cones, time-indexed + causal_link_index.json — explicit causal links derived from sim flags + absence_catalog.json — expected-but-absent artifact pairs + eval_questions.json — PERSPECTIVE + COUNTERFACTUAL + SILENCE questions + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TRACK 1 — PERSPECTIVE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Questions scoped to what a specific actor could have known at a specific moment, +given their actual subsystem access and information horizon. + +Ground truth is derived from the actor's visibility cone: the set of artifact IDs +reachable by that actor at or before as_of_time, filtered by subsystem access. + +Cross-subsystem questions (e.g. engineer sees Slack + Zoom but not Salesforce) +are flagged difficulty="hard". Single-subsystem questions are "medium". + +Example: + "Based only on what Morgan had access to as of Day 9, should she have known + that Acme Corp was at churn risk?" + ground_truth: { "answer": False, "reason": "sf_deals_risk_flagged not in Morgan's visibility cone" } + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TRACK 2 — COUNTERFACTUAL +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Questions of the form "if X had been different, would Y have occurred?" +Only generated where the sim encodes an explicit causal link: + - involves_gap / knowledge_gap_detected → incident causation + - recurrence_of → repeat incident prevention + - spawned_doc → design discussion → documentation + - email_dropped → unactioned communication + - sf_ownership_lapsed → CRM ownership gap + - zd_escalation_source → support ticket → incident + +Ground truth is always derivable from the explicit link without inference. + +Example: + "If Jordan had documented auth-service before departing, would incident IT-108 + have been diagnosed faster?" + ground_truth: { "outcome_changed": True, "mechanism": "knowledge_gap_detected", + "gap_domain": "auth-service", "causal_event": "evt_..." } + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +TRACK 3 — SILENCE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Questions about things that did NOT happen. The state machine is the arbiter: +if no event fired, absence is ground truth regardless of whether absence was +intentional. + +Each SILENCE question includes an expected_search_space — the artifact IDs the +agent MUST check before concluding absence. A correct "no" reached without +searching the right places scores 0 on trajectory even if the boolean is right. + +Example: + "Was a postmortem written for the Zendesk escalation on Day 6?" + ground_truth: False + expected_search_space: ["confluence/postmortems/", "jira/IT-*", "slack/incidents"] + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Design principles +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +- Ground truth is always derived from SimEvent log. LLMs only write prose. +- Question prose generation includes a structured validation loop. +- Actor visibility cones are first-class data structures, not a scoring afterthought. +- Subsystem access is explicitly modeled per actor per day. +- The absence catalog is built by pattern-matching expected event pairs, not heuristics. +""" + +from __future__ import annotations + +import json +import logging +import random +import re +from config_loader import CONFIG, DEPARTED_EMPLOYEES +import yaml +from collections import defaultdict +from dataclasses import dataclass, asdict +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List, Optional, Set, Tuple + +from agent_factory import make_agent +from crewai import Crew, Task +from memory import Memory, SimEvent + + +logger = logging.getLogger("orgforge.eval") + +with open(Path(__file__).resolve().parent.parent / "config" / "config.yaml") as f: + _CFG = yaml.safe_load(f) + +SRC_DIR = Path(__file__).resolve().parent +PROJECT_ROOT = SRC_DIR.parent + +EXPORT_DIR = PROJECT_ROOT / "export" + +raw_output = CONFIG["simulation"].get("output_dir") +if raw_output: + BASE = (PROJECT_ROOT / raw_output).resolve() +else: + BASE = EXPORT_DIR.resolve() + +EXPORT_DIR.mkdir(parents=True, exist_ok=True) +EVAL_DIR = BASE / "eval" +EVAL_DIR.mkdir(parents=True, exist_ok=True) + +_SIM_START = datetime.strptime(_CFG["simulation"]["start_date"], "%Y-%m-%d") + + +def _business_day_to_date(start: datetime, n: int) -> datetime: + """Convert a 1-based business day counter to a calendar date.""" + current = start + days_counted = 0 + while days_counted < n: + current += timedelta(days=1) + if current.weekday() < 5: + days_counted += 1 + return current + + +_ROLE_SUBSYSTEM_ACCESS: Dict[str, Set[str]] = { + "ceo": { + "slack", + "jira", + "confluence", + "zoom", + "email", + "salesforce", + "zendesk", + "datadog", + }, + "product": {"slack", "jira", "confluence", "zoom", "email"}, + "engineering_backend": { + "slack", + "jira", + "confluence", + "git", + "zoom", + "email", + "datadog", + }, + "engineering_mobile": { + "slack", + "jira", + "confluence", + "git", + "zoom", + "email", + "datadog", + }, + "design": {"slack", "confluence", "zoom", "email"}, + "sales_marketing": {"slack", "salesforce", "email", "zoom", "confluence"}, + "hr_ops": {"slack", "email", "confluence", "zoom"}, + "qa_support": {"slack", "zendesk", "confluence", "email"}, + "external": set(), +} + +_SYSTEM_ACTORS = {"John"} + + +_ARTIFACT_SUBSYSTEM: Dict[str, str] = { + "jira": "jira", + "confluence": "confluence", + "slack": "slack", + "pr": "git", + "email": "email", + "zd_ticket": "zendesk", + "sf_opp": "salesforce", + "sf_account": "salesforce", + "datadog": "datadog", + "zoom": "zoom", + "invoice": "email", + "nps": "salesforce", + "hr_email": "email", + "jira_comment": "jira", + "slack_thread": "slack", +} + +_EXPLICIT_CAUSAL_LINKS = { + "involves_gap", # incident ← knowledge gap + "recurrence_of", # incident ← prior unresolved incident + "spawned_doc", # confluence ← design discussion + "email_dropped", # communication failure ← routing gap + "sf_ownership_lapsed", # CRM gap ← employee departure + "blocker_flagged", # blocker → delayed progress + "incident_coordination", # incident → external contact + "departure_reassignment", # departure → ticket/escalation shift + "assignment_domain_mismatch", # planning mismatch → knowledge gap → incident + "jira_from_customer_email", + "jira_from_vendor_email", + "customer_escalation_relayed", + "incident_handoff", + "pr_gap_detected", + "async_gap_detected", + "doc_gap_detected", + "centrality_vacuum", + "sf_stage_advanced_by_customer", + "feature_request_fyi", + "proactive_outreach_from_crm_signal", + "ticket_completion_notifies_lead", + "org_collision_tension", + "postmortem_from_incident", + "incident_triggers_risk_flag", + "review_triggers_revision", + "hire_fills_knowledge_gap", + "escalation_from_zendesk", +} + + +_SILENCE_PAIRS: List[Tuple[str, str, str]] = [ + ("incident_opened", "postmortem_created", "jira"), + ("incident_opened", "incident_resolved", "jira"), + ("customer_escalation", "zd_ticket_opened", "email"), + ("customer_email_routed", "zd_ticket_opened", "email"), + ("inbound_external_email", "customer_email_routed", "email"), + ("inbound_external_email", "jira_from_vendor_email", "email"), + ("design_discussion", "confluence_created", "zoom_transcript"), + ("knowledge_gap_detected", "confluence_created", "gap_domain"), + ("zd_tickets_escalated", "incident_opened", "jira"), + ("employee_departed", "sf_ownership_lapsed", "actor"), + ("employee_departed", "ticket_reassigned", "actor"), + ("pr_opened", "pr_merged", "pr"), + ("incident_opened", "zd_tickets_escalated", "jira"), + ("employee_hired", "onboarding_session", "name"), + ("employee_hired", "warmup_1on1", "name"), + ("incident_opened", "sf_deals_risk_flagged", "jira"), + ("assignment_domain_mismatch", "knowledge_gap_detected", "ticket_id"), + ("design_discussion", "confluence_created", "slack_thread"), + ("email_dropped", "zd_ticket_opened", "email"), +] + + +_RESPONSE_TYPE_SUBSYSTEM: Dict[str, str] = { + "postmortem_created": "confluence", + "incident_resolved": "jira", + "zd_ticket_opened": "zendesk", + "customer_email_routed": "email", + "jira_from_vendor_email": "jira", + "confluence_created": "confluence", + "sf_ownership_lapsed": "salesforce", + "ticket_reassigned": "jira", + "pr_merged": "git", + "zd_tickets_escalated": "zendesk", + "incident_opened": "jira", + "onboarding_session": "slack", + "warmup_1on1": "slack", + "sf_deals_risk_flagged": "salesforce", + "knowledge_gap_detected": "confluence", +} + +_BROADCAST_CONFIG = { + "incident_opened": ["slack", "datadog"], + "incident_resolved": ["slack"], + "postmortem_created": ["slack", "confluence"], + "standup": ["slack"], + "pr_opened": ["git"], + "pr_merged": ["git"], + "knowledge_gap_detected": ["slack", "confluence"], +} + +_VAGUE_GAP_TERMS = { + "undocumented expertise", + "undocumented knowledge", + "general knowledge", + "tribal knowledge", + "institutional knowledge", + "unknown", + "unspecified", + "undocumented domain", + "unknown issue", +} + +_JIRA_PROJECT_ACCESS: Dict[str, Set[str]] = { + "ENG": {"engineering_backend", "engineering_mobile", "ceo"}, + "HR": {"hr_ops", "ceo"}, + "SALES": {"sales_marketing", "ceo"}, + "PROD": {"product", "ceo"}, + "DES": {"design", "product", "ceo"}, + "QA": {"qa_support", "ceo"}, + "ORG": { + "engineering_backend", + "engineering_mobile", + "product", + "ceo", + "hr_ops", + "design", + "sales_marketing", + "qa_support", + }, +} + +_MAX_QUESTIONS_PER_ACTOR = 5 +_MAX_QUESTIONS_PER_EVENT_TYPE = 5 + +_NEGATIVE_COUNTERFACTUAL_STYLES: List[str] = [ + "Phrase it to suggest the cause was necessary: 'If X had not happened, would Y still have occurred?'", + "Phrase it as a dependency question: 'Was Y dependent on X, or would it have happened regardless?'", + "Phrase it from the effect perspective: 'Would Y have been prevented if only X had been different?'", + "Phrase it as a challenge: 'Can we be sure X was actually required for Y, or would Y have happened anyway?'", + "Phrase it as an alternate history: 'Had X not taken place, would the outcome still have been Y?'", +] + + +def _jira_project_visible(ticket_id: str, role: str) -> bool: + prefix = ticket_id.split("-")[0] if "-" in ticket_id else "" + allowed_roles = _JIRA_PROJECT_ACCESS.get(prefix) + if allowed_roles is None: + return True # unknown prefix — don't restrict + return role in allowed_roles + + +def _strip_root(v: str) -> str: + """Strip absolute export or project root prefix from stored artifact paths.""" + for prefix in (str(BASE), str(BASE.parent)): + if v.startswith(prefix + "/"): + return v[len(prefix) + 1 :] + return v + + +def _safe_artifact_values(artifact_ids: dict) -> Set[str]: + """Flatten artifact_ids values — some may be lists. Skips disk-only keys.""" + vals: Set[str] = set() + _DISK_ONLY_KEYS = {"eml_path", "zoom_path", "transcript_path"} + for k, v in (artifact_ids or {}).items(): + if k in _DISK_ONLY_KEYS: + continue + if isinstance(v, list): + vals.update(_strip_root(str(x)) for x in v) + elif v: + vals.add(_strip_root(str(v))) + return vals + + +@dataclass +class ActorVisibilityCone: + """ + The complete set of artifact IDs visible to a specific actor at a specific + moment, partitioned by subsystem. + + Built from SimEvents: an actor can see an artifact if they appear in + event.actors for that artifact's creation event, or if the artifact was + broadcast to a channel/tool they have access to (e.g. an incident Slack + message is visible to all engineers). + """ + + actor: str + role: str + as_of_time: str + as_of_day: int + subsystem_access: Set[str] # subsystems this actor can query + visible_artifacts: Dict[str, Set[str]] # subsystem → set of artifact IDs + directly_involved: Set[str] # artifacts where actor appears in event.actors + broadcast_visible: Set[str] # artifacts visible via channel broadcast + + def all_visible(self) -> Set[str]: + """Return artifact IDs visible to this actor, restricted to accessible subsystems. + + Only subsystems in self.subsystem_access are included so that + all_visible() and can_see() encode the same access model. Previously + all_visible() unioned across ALL subsystems regardless of role, which + caused missed_artifacts in _find_asymmetry_events to be empty for + artifacts the actor could not actually access. + """ + all_ids: Set[str] = set() + for subsystem, ids in self.visible_artifacts.items(): + if subsystem in self.subsystem_access: + all_ids.update(ids) + return all_ids + + def can_see(self, artifact_id: str, doc_type: str) -> bool: + subsystem = _ARTIFACT_SUBSYSTEM.get(doc_type, "default") + if subsystem not in self.subsystem_access: + return False + return artifact_id in self.all_visible() + + def to_dict(self) -> dict: + return { + "actor": self.actor, + "role": self.role, + "as_of_time": self.as_of_time, + "as_of_day": self.as_of_day, + "subsystem_access": sorted(self.subsystem_access), + "visible_artifacts": { + k: sorted(v) for k, v in self.visible_artifacts.items() + }, + "directly_involved": sorted(self.directly_involved), + "broadcast_visible": sorted(self.broadcast_visible), + } + + +@dataclass +class CausalLink: + """ + An explicit causal relationship encoded in the simulation. + These are the only valid sources for COUNTERFACTUAL questions. + """ + + link_type: str # one of _EXPLICIT_CAUSAL_LINKS + cause_event_id: str + cause_event_type: str + effect_event_id: str + effect_event_type: str + actors: List[str] + day: int + link_field: str # the fact key that carries the link + link_value: str # the value of that field + subsystems_involved: Set[str] + counterfactual_premise: str # natural language "if X had been different" + counterfactual_outcome: str # natural language "then Y would have..." + outcome_changed: bool # does removing the cause change the effect? + + def to_dict(self) -> dict: + d = asdict(self) + d["subsystems_involved"] = sorted(self.subsystems_involved) + return d + + +@dataclass +class AbsenceRecord: + """ + A case where a trigger event fired but its expected response event did not. + The state machine is the arbiter — no inference about intent. + """ + + trigger_event_id: str + trigger_event_type: str + expected_response_type: str + trigger_day: int + trigger_actors: List[str] + trigger_artifact_ids: Dict[str, str] + link_field: str + link_value: str + subsystem: str + expected_search_space: List[str] # artifact IDs the agent must check + + def to_dict(self) -> dict: + return asdict(self) + + +# ───────────────────────────────────────────────────────────────────────────── +# ACTOR VISIBILITY BUILDER +# ───────────────────────────────────────────────────────────────────────────── + + +class ActorVisibilityBuilder: + """ + Reconstructs the knowledge cone for every actor at every day boundary. + + Visibility rules: + 1. DIRECT: actor appears in event.actors for an artifact's creation event + 2. BROADCAST: artifact was created in a shared channel (Slack incidents, + standups, engineering-wide announcements) — visible to all actors with + that subsystem access + 3. ROLE-GATED: actor's role must include the artifact's subsystem + 4. TEMPORAL: artifact timestamp must be <= as_of_time + + Broadcast channels are inferred from event type: + - standup, incident_alert, dept_announcement → broadcast to subsystem members + - direct_message, email, zd_ticket → direct only + """ + + # Event types whose artifacts are broadcast to all actors with subsystem access + _BROADCAST_EVENTS = { + "standup", + "incident_opened", + "incident_resolved", + "postmortem_created", + "pr_opened", + "pr_merged", + "knowledge_gap_detected", + } + + def __init__(self, mem: Memory): + self._mem = mem + self._events: List[SimEvent] = mem.get_event_log(from_db=True) + self._actor_roles: Dict[str, str] = self._infer_actor_roles() + + def _infer_actor_roles(self) -> Dict[str, str]: + roles: Dict[str, str] = {} + + for dept, members in CONFIG["org_chart"].items(): + for name in members: + roles[name] = dept.lower().replace(" ", "_") + + for name, data in DEPARTED_EMPLOYEES.items(): + roles[name] = data["dept"].lower().replace(" ", "_") + + lifecycle = CONFIG.get("org_lifecycle", {}) + + for hire in lifecycle.get("scheduled_hires", []): + roles[hire["name"]] = hire["dept"].lower().replace(" ", "_") + + for dep in lifecycle.get("scheduled_departures", []): + roles[dep["name"]] = dep["dept"].lower().replace(" ", "_") + + for actor in self._all_actors(): + if actor not in roles: + roles[actor] = "external" + + return roles + + def _subsystem_access_for(self, actor: str) -> Set[str]: + role = self._actor_roles.get(actor, "external") + return set(_ROLE_SUBSYSTEM_ACCESS.get(role, _ROLE_SUBSYSTEM_ACCESS["external"])) + + def _all_actors(self) -> Set[str]: + actors: Set[str] = set() + for event in self._events: + actors.update(event.actors) + return actors + + def _artifact_subsystem(self, doc_type: str) -> str: + return _ARTIFACT_SUBSYSTEM.get(doc_type, "default") + + def build_all(self) -> Dict[str, List[ActorVisibilityCone]]: + all_actors = self._all_actors() + result: Dict[str, List[ActorVisibilityCone]] = {} + max_day = max((e.day for e in self._events), default=1) + + events_by_day = defaultdict(list) + for event in self._events: + events_by_day[event.day].append(event) + + for actor in all_actors: + role = self._actor_roles.get(actor, "default") + access = self._subsystem_access_for(actor) + cones: List[ActorVisibilityCone] = [] + + current_visible = defaultdict(set) + current_directly_involved = set() + current_broadcast_visible = set() + + for day in range(1, max_day + 1): + for event in events_by_day.get(day, []): + is_direct = actor in (event.actors or []) + + broadcast_channels = _BROADCAST_CONFIG.get(event.type) + is_broadcast = broadcast_channels is not None + + for doc_type, artifact_id in (event.artifact_ids or {}).items(): + if not artifact_id: + continue + subsystem = self._artifact_subsystem(doc_type) + if subsystem not in access: + continue + + if is_direct: + current_visible[subsystem].add(artifact_id) + current_directly_involved.add(artifact_id) + elif is_broadcast and any( + sub in access for sub in broadcast_channels + ): + current_visible[subsystem].add(artifact_id) + current_broadcast_visible.add(artifact_id) + + as_of_dt = _business_day_to_date(_SIM_START, day).replace( + hour=23, minute=59 + ) + cones.append( + ActorVisibilityCone( + actor=actor, + role=role, + as_of_time=as_of_dt.isoformat(), + as_of_day=day, + subsystem_access=access, + visible_artifacts={ + k: set(v) for k, v in current_visible.items() + }, + directly_involved=set(current_directly_involved), + broadcast_visible=set(current_broadcast_visible), + ) + ) + + result[actor] = cones + + return result + + +class CausalLinkIndexer: + """ + Scans the SimEvent log for all explicit causal links. + Only links in _EXPLICIT_CAUSAL_LINKS are indexed — no inference. + + Each link becomes a potential COUNTERFACTUAL question source. + The counterfactual premise and outcome are templated deterministically + from the link type and event facts; LLMs only rephrase them. + """ + + MAX_LINKS_PER_TYPE: int = 15 + + def __init__(self, mem: Memory): + self._mem = mem + self._events: List[SimEvent] = mem.get_event_log(from_db=True) + + def _subsystems_for_event(self, event: SimEvent) -> Set[str]: + subsystems: Set[str] = set() + for doc_type in event.artifact_ids or {}: + s = _ARTIFACT_SUBSYSTEM.get(doc_type, "default") + if s != "default": + subsystems.add(s) + return subsystems + + def _find_effect_event(self, link_type: str, cause: SimEvent) -> Optional[SimEvent]: + if link_type == "involves_gap": + gap_areas = cause.facts.get("gap_areas", []) + if not gap_areas: + return None + + cause_domains: set[str] = set() + for area in gap_areas: + for part in str(area).split(","): + part = part.strip().lower() + if part: + cause_domains.add(part) + + if not cause_domains: + return None + + for e in self._events: + if e.day < cause.day: + continue + + if e.type == "incident_opened" and e.facts.get("involves_gap"): + incident_domains: set[str] = set() + for area in e.facts.get("gap_areas", []): + for part in str(area).split(","): + part = part.strip().lower() + if part: + incident_domains.add(part) + + if cause_domains & incident_domains: + return e + + relevant_types = { + "async_question_asked", + "pr_review_comment", + "confluence_created", + "postmortem_created", + } + if e.type in relevant_types: + event_domains: set[str] = set() + for area in e.facts.get("gap_areas") or e.facts.get("domain") or []: + for part in str(area).split(","): + part = part.strip().lower() + if part: + event_domains.add(part) + + if cause_domains & event_domains: + return e + + elif link_type == "recurrence_of": + prior_jira_id = cause.facts.get("recurrence_of") + if not prior_jira_id: + return None + for e in self._events: + if ( + e.type == "incident_opened" + and (e.artifact_ids or {}).get("jira") == prior_jira_id + ): + return e + return None + + elif link_type == "spawned_doc": + cause_artifacts = _safe_artifact_values(cause.artifact_ids) + conf_id = (cause.artifact_ids or {}).get("confluence") + if conf_id: + return next( + ( + e + for e in self._events + if e.type == "confluence_created" + and (e.artifact_ids or {}).get("confluence") == conf_id + ), + None, + ) + for e in self._events: + if ( + e.type == "confluence_created" + and e.facts.get("source_discussion") in cause_artifacts + ): + return e + return None + + elif link_type == "postmortem_from_incident": + jira_id = (cause.artifact_ids or {}).get("jira", "") + if not jira_id: + return None + for e in self._events: + if ( + e.type == "postmortem_created" + and jira_id in str(e.artifact_ids) + and e.day >= cause.day + ): + return e + return None + + elif link_type == "email_dropped": + source = cause.facts.get("source") + email_id = (cause.artifact_ids or {}).get("email", "") + if not source: + return None + for e in self._events: + if e.day <= cause.day: + continue + if e.type == "customer_escalation" and source in str(e.facts): + return e + if ( + e.type == "inbound_external_email" + and e.facts.get("source") == source + and e.facts.get("tone") in ("frustrated", "urgent") + ): + return e + if e.type == "zd_ticket_opened" and email_id in str( + e.facts.get("causal_chain", []) + ): + return e + return None + + elif link_type == "sf_ownership_lapsed": + actor = (cause.actors or [None])[0] + if not actor: + return None + for e in self._events: + if e.type == "sf_ownership_lapsed" and actor in (e.actors or []): + return e + + elif link_type == "zd_escalation_source": + jira_id = (cause.artifact_ids or {}).get("jira") + if isinstance(jira_id, list): + jira_id = jira_id[0] if jira_id else None + if not jira_id: + return None + for e in self._events: + if e.type == "zd_tickets_escalated" and e.day >= cause.day: + escalated_jira = (e.artifact_ids or {}).get("jira") + if escalated_jira == jira_id: + return e + if jira_id in str(e.facts): + return e + return None + + elif link_type == "blocker_flagged": + jira_id = cause.artifact_ids.get("jira") + return next( + ( + e + for e in self._events + if e.type == "ticket_progress" + and e.artifact_ids.get("jira") == jira_id + and e.day >= cause.day + ), + None, + ) + + elif link_type == "incident_coordination": + jira_id = cause.artifact_ids.get("jira") + return next( + ( + e + for e in self._events + if e.type == "external_contact_summarized" + and e.artifact_ids.get("jira") == jira_id + ), + None, + ) + + elif link_type == "departure_reassignment": + departed_actor = (cause.actors or [None])[0] + return next( + ( + e + for e in self._events + if e.type == "escalation_chain" + and e.facts.get("trigger") == "post_departure_reroute" + and e.facts.get("departed") == departed_actor + ), + None, + ) + + elif link_type == "deal_risk_propagation": + return next( + ( + e + for e in self._events + if e.type == "sf_deals_risk_flagged" and e.day >= cause.day + ), + None, + ) + + elif link_type == "onboarding_path": + new_hire = cause.facts.get("name") + return next( + ( + e + for e in self._events + if e.type == "onboarding_session" and new_hire in e.actors + ), + None, + ) + + elif link_type == "assignment_domain_mismatch": + # Look for a knowledge_gap_detected on the same ticket on a later day, + # or an incident_opened whose gap_areas overlap with the mismatch domains. + ticket_id = cause.facts.get("ticket_id") + mismatch_actors = set(cause.actors or []) + for e in self._events: + if e.day < cause.day: + continue + if e.type == "knowledge_gap_detected": + if ticket_id and e.artifact_ids.get("jira") == ticket_id: + return e + # Also match on overlapping actors (the assigned engineer surfaces the gap) + if mismatch_actors & set(e.actors or []): + return e + if e.type == "incident_opened": + gap_areas = e.facts.get("gap_areas", []) + mismatch_domains = cause.facts.get("assignment_risk_domains", []) + if ( + gap_areas + and mismatch_domains + and set(gap_areas) & set(mismatch_domains) + ): + return e + + elif link_type == "jira_from_vendor_email": + email_id = (cause.artifact_ids or {}).get("email", "") + if not email_id: + return None + for e in self._events: + if ( + e.type == "jira_ticket_created" + and e.facts.get("source") == "vendor_email" + and email_id + in ( + (e.artifact_ids or {}).get("source_email", ""), + str(e.facts.get("causal_chain", [])), + ) + and e.day >= cause.day + ): + return e + return None + + elif link_type == "jira_from_customer_email": + email_id = (cause.artifact_ids or {}).get("email", "") + if not email_id: + return None + for e in self._events: + if ( + e.type == "jira_ticket_created" + and e.facts.get("source") == "customer_email" + and (e.artifact_ids or {}).get("source_email") == email_id + and e.day >= cause.day + ): + return e + return None + + elif link_type == "customer_escalation_relayed": + source_email = (cause.artifact_ids or {}).get("source_email", "") + for e in self._events: + if ( + e.type == "customer_escalation" + and (e.artifact_ids or {}).get("source_email") == source_email + ): + return e + return None + + elif link_type == "incident_handoff": + departed = (cause.actors or [None])[0] + for e in self._events: + if ( + e.type == "escalation_chain" + and e.facts.get("trigger") == "forced_handoff_on_departure" + and departed in (e.actors or []) + and e.day >= cause.day + ): + return e + return None + + elif link_type == "pr_gap_detected": + pr_id = (cause.artifact_ids or {}).get("pr") + if not pr_id: + return None + for e in self._events: + if ( + e.type == "knowledge_gap_detected" + and e.facts.get("detection_method") == "reviewer_audit" + and (e.artifact_ids or {}).get("pr") == pr_id + and e.day >= cause.day + ): + return e + return None + + elif link_type == "async_gap_detected": + for e in self._events: + if ( + e.type == "knowledge_gap_detected" + and e.facts.get("detection_method") == "async_thread_classification" + and e.day >= cause.day + ): + slack_id = (cause.artifact_ids or {}).get("slack_thread") + if ( + slack_id + and (e.artifact_ids or {}).get("slack_thread") == slack_id + ): + return e + return None + + elif link_type == "doc_gap_detected": + conf_id = (cause.artifact_ids or {}).get("confluence") + if not conf_id: + return None + gap_domains = set(cause.facts.get("topics_beyond_expertise", [])) + if not gap_domains: + return None + for e in self._events: + if e.type == "incident_opened" and e.day > cause.day: + incident_tokens = set() + for entry in e.facts.get("gap_areas", []): + for part in str(entry).split(","): + part = part.strip().lower() + if part: + incident_tokens.add(part) + + gap_tokens = set() + for phrase in gap_domains: + gap_tokens.add(phrase.strip().lower()) + for word in phrase.lower().replace("-", " ").split(): + if len(word) > 4: + gap_tokens.add(word) + + if gap_tokens & incident_tokens: + return e + return None + + elif link_type == "centrality_vacuum": + for e in self._events: + if ( + e.type == "knowledge_gap_detected" + and e.facts.get("trigger") == "centrality_vacuum" + and e.day >= cause.day + and e.mongo_id != cause.mongo_id + ): + return e + return None + + elif link_type == "sf_stage_advanced_by_customer": + email_id = (cause.artifact_ids or {}).get("email", "") + if not email_id: + return None + for e in self._events: + if ( + e.type == "crm_touchpoint" + and e.facts.get("triggered_by") == email_id + and e.day >= cause.day + ): + return e + return None + + elif link_type == "feature_request_fyi": + email_id = (cause.artifact_ids or {}).get("email", "") + if not email_id: + return None + for e in self._events: + if ( + e.type == "normal_day_slack" + and "feature_request" in (e.tags or []) + and "fyi" in (e.tags or []) + and email_id in str(e.artifact_ids) + and e.day >= cause.day + ): + return e + return None + + elif link_type == "proactive_outreach_from_crm_signal": + opp_id = (cause.artifact_ids or {}).get("sf_opp", "") + if not opp_id: + return None + for e in self._events: + if ( + e.type == "proactive_outreach_initiated" + and (e.artifact_ids or {}).get("sf_opp") == opp_id + and e.day >= cause.day + ): + return e + return None + + elif link_type == "ticket_completion_notifies_lead": + jira_id = (cause.artifact_ids or {}).get("jira", "") + if not jira_id: + return None + for e in self._events: + if ( + e.type == "ticket_progress" + and (e.artifact_ids or {}).get("jira") == jira_id + and e.facts.get("new_status") == "Done" + and e.day >= cause.day + ): + return e + return None + + elif link_type == "org_collision_tension": + for e in self._events: + if ( + e.type == "org_collision" + and e.facts.get("tension") in ("conflict", "alignment") + and e.day >= cause.day + and set(e.actors or []) & set(cause.actors or []) + ): + return e + return None + + elif link_type == "incident_triggers_risk_flag": + jira_id = (cause.artifact_ids or {}).get("jira", "") + if not jira_id: + return None + for e in self._events: + if ( + e.type == "sf_deals_risk_flagged" + and e.day >= cause.day + and (jira_id in str(e.artifact_ids) or jira_id in str(e.facts)) + ): + return e + return None + + elif link_type == "review_triggers_revision": + pr_id = (cause.artifact_ids or {}).get("pr", "") + if not pr_id: + return None + for e in self._events: + if ( + e.type == "pr_review" + and (e.artifact_ids or {}).get("pr") == pr_id + and e.facts.get("verdict") == "approved" + and e.day >= cause.day + and e is not cause + ): + return e + return None + + elif link_type == "hire_fills_knowledge_gap": + departed_domains = cause.facts.get("knowledge_domains", []) + if not departed_domains: + return None + for e in self._events: + if e.type == "employee_hired" and e.day > cause.day: + expertise = e.facts.get("expertise", []) + if set(d.lower() for d in departed_domains) & set( + d.lower() for d in expertise + ): + return e + return None + + elif link_type == "escalation_from_zendesk": + jira_id = (cause.artifact_ids or {}).get("jira", "") + ticket_ids = cause.facts.get("ticket_ids", []) + if not (jira_id or ticket_ids): + return None + for e in self._events: + if e.type == "incident_opened" and e.day <= cause.day: + incident_jira = (e.artifact_ids or {}).get("jira", "") + if incident_jira == jira_id: + return e + if any(tid in str(e.facts) for tid in ticket_ids): + return e + return None + + return None + + def _counterfactual_template( + self, link_type: str, cause: SimEvent, effect: SimEvent + ) -> Tuple[str, str, bool]: + """ + Returns (premise, outcome, outcome_changed) as deterministic strings. + These become the ground_truth fields — no LLM involvement here. + """ + if link_type == "involves_gap": + gap_areas = cause.facts.get("gap_areas", ["unknown domain"]) + gap_str = ", ".join(gap_areas) + actor = ( + cause.facts.get("departed_employee") + or (cause.actors or ["the departing engineer"])[0] + ) + return ( + f"{actor} had fully documented {gap_str} before departing", + "the subsequent incident would have been diagnosed faster or avoided entirely", + True, + ) + + elif link_type == "recurrence_of": + original_jira = (effect.artifact_ids or {}).get( + "jira", "the original incident" + ) + recurring_jira = (cause.artifact_ids or {}).get( + "jira", "the recurring incident" + ) + gap_days = cause.facts.get("recurrence_gap_days", "") + gap_str = f" {gap_days} days later" if gap_days else "" + return ( + f"the root cause of {original_jira} had been fully addressed after it was first resolved", + f"{recurring_jira} would not have occurred{gap_str}", + True, + ) + + elif link_type == "postmortem_from_incident": + jira_id = (cause.artifact_ids or {}).get("jira", "the incident") + return ( + f"incident {jira_id} had not occurred", + "the postmortem page would not have been written and the root cause analysis would not exist", + True, + ) + + elif link_type == "spawned_doc": + topic = cause.facts.get("topic", "the design discussion") + conf_id = (effect.artifact_ids or {}).get( + "confluence", "the Confluence doc" + ) + premise = f"the discussion about '{topic}' had not been documented" + outcome = f"{conf_id} would not exist and related decisions would remain undocumented" + return premise, outcome, True + + elif link_type == "email_dropped": + source = cause.facts.get("source", "the sender") + subject = cause.facts.get("subject", "the email")[:80] + reason = cause.facts.get("reason", "no_action_taken").replace("_", " ") + return ( + f"the email from {source} ('{subject}') had been actioned rather than dropped due to {reason}", + "a support ticket or escalation would have been opened and the issue tracked", + True, + ) + + elif link_type == "sf_ownership_lapsed": + actor = (cause.actors or ["the departed employee"])[0] + accounts = effect.facts.get("lapsed_accounts", []) + acc_str = ", ".join(accounts[:3]) if accounts else "affected accounts" + premise = ( + f"{actor}'s Salesforce accounts had been reassigned before departure" + ) + outcome = ( + f"{acc_str} would not have lost ownership and pipeline would be intact" + ) + return premise, outcome, True + + elif link_type == "zd_escalation_source": + ticket_ids = effect.facts.get("ticket_ids", ["the support ticket"]) + tickets_str = ", ".join(ticket_ids[:3]) + premise = f"{tickets_str} had been resolved at the support level" + outcome = "the incident escalation would not have occurred" + return premise, outcome, True + + elif link_type == "blocker_flagged": + reason = cause.facts.get("blocker_reason", "a technical blocker") + jira_id = effect.artifact_ids.get("jira", "the ticket") + return ( + f"the blocker regarding '{reason}' had been resolved immediately", + f"work on {jira_id} would have progressed without delay", + True, + ) + + elif link_type == "incident_coordination": + contact = effect.facts.get("external_party", "the external contact") + return ( + f"the incident on Day {cause.day} had not occurred", + f"no coordination with {contact} would have been needed at all", + True, + ) + + elif link_type == "departure_reassignment": + actor = (cause.actors or ["the employee"])[0] + return ( + f"{actor} had not departed the company", + "their active tickets and escalation responsibilities would not have been reassigned", + True, + ) + + elif link_type == "deal_risk_propagation": + jira_id = cause.artifact_ids.get("jira", "the incident") + return ( + f"the incident {jira_id} had not occurred", + "the associated Salesforce deals would not have been flagged as at-risk", + True, + ) + + elif link_type == "onboarding_path": + name = cause.facts.get("name", "the new hire") + return ( + f"{name} had not been hired on Day {cause.day}", + "the onboarding sessions and warmup meetings for them would not have taken place", + True, + ) + + elif link_type == "assignment_domain_mismatch": + actors = cause.actors or ["the engineer"] + ticket_id = cause.facts.get("ticket_id", "the ticket") + coverage = cause.facts.get("documentation_coverage") + coverage_str = ( + f" (documentation coverage: {int(coverage * 100)}%)" if coverage else "" + ) + return ( + f"{actors[0]} had been assigned to {ticket_id} with matching domain expertise", + f"the knowledge gap{coverage_str} would likely not have been surfaced and the associated incident risk reduced", + True, + ) + + elif link_type == "jira_from_vendor_email": + vendor = cause.facts.get("source", (cause.actors or ["the vendor"])[0]) + org = cause.facts.get("org", "") + org_str = f" ({org})" if org else "" + return ( + f"the email from {vendor}{org_str} had not arrived", + "a Jira ticket would not have been created to track the request", + True, + ) + + elif link_type == "jira_from_customer_email": + customer = cause.facts.get("source", (cause.actors or ["the customer"])[0]) + org = cause.facts.get("org", "") + org_str = f" ({org})" if org else "" + return ( + f"the email from {customer}{org_str} had not been received or had been dropped", + "the resulting Jira ticket would not have been opened", + True, + ) + + elif link_type == "customer_escalation_relayed": + customer = cause.facts.get("source", "the customer") + slack_id = (cause.artifact_ids or {}).get( + "slack_thread", "the Slack thread" + ) + return ( + f"the email from {customer} had been dropped rather than relayed", + f"{slack_id} would not have been created and Product would not have been notified", + True, + ) + + elif link_type == "incident_handoff": + departed = (cause.actors or ["the employee"])[0] + return ( + f"{departed} had not departed and a forced handoff had not been required", + "the escalation chain would not have been transferred mid-incident", + True, + ) + + elif link_type == "pr_gap_detected": + actor = (cause.actors or ["the reviewer"])[0] + pr_id = (cause.artifact_ids or {}).get("pr", "the PR") + return ( + f"the author of {pr_id} had domain expertise matching the review scope", + f"{actor} would not have flagged a knowledge gap during review", + True, + ) + + elif link_type == "async_gap_detected": + actor = (cause.actors or ["the engineer"])[0] + return ( + f"{actor} had prior knowledge of the domain being discussed", + "the async thread would not have surfaced a knowledge gap", + True, + ) + + elif link_type == "doc_gap_detected": + author = cause.facts.get("author", (cause.actors or ["the author"])[0]) + conf_id = (cause.artifact_ids or {}).get("confluence", "the design doc") + domains = cause.facts.get("topics_beyond_author_expertise", ["the domain"]) + domain_str = " and ".join(domains[:2]) + return ( + f"{author} had expertise in {domain_str} when writing {conf_id}", + "the knowledge gap would not have been embedded in the design doc, " + "and the downstream incident would not have occurred", + True, + ) + + elif link_type == "centrality_vacuum": + actor = (cause.actors or ["the central actor"])[0] + gap = cause.facts.get("gap_domain", "the domain") + return ( + f"{actor} had not been the sole knowledge holder for {gap}", + "the centrality vacuum would not have created a knowledge gap", + True, + ) + + elif link_type == "sf_stage_advanced_by_customer": + customer = cause.facts.get("source", (cause.actors or ["the customer"])[0]) + account = cause.facts.get("org", "the account") + opp_id = (effect.artifact_ids or {}).get("sf_opp", "the opportunity") + return ( + f"the email from {customer} ({account}) had not been received", + f"{opp_id} would not have advanced to the next stage", + True, + ) + + elif link_type == "feature_request_fyi": + customer = cause.facts.get("source", (cause.actors or ["the customer"])[0]) + return ( + f"the feature request email from {customer} had not been received", + "the FYI thread in #product would not have been created", + True, + ) + + elif link_type == "proactive_outreach_from_crm_signal": + account = cause.facts.get("account_name", "the account") + opp_id = (cause.artifact_ids or {}).get("sf_opp", "the opportunity") + return ( + f"the CRM signal for {account} ({opp_id}) had not been logged", + "the proactive outreach email would not have been sent", + True, + ) + + elif link_type == "ticket_completion_notifies_lead": + jira_id = (effect.artifact_ids or {}).get("jira", "the ticket") + recipient = effect.facts.get("to", "the lead") + return ( + f"{jira_id} had not reached Done status", + f"the completion notification to {recipient} would not have been sent", + True, + ) + + elif link_type == "org_collision_tension": + actors = cause.actors or ["the parties involved"] + tension = cause.facts.get("tension", "tension") + return ( + f"the {tension} between {' and '.join(actors[:2])} had not occurred", + "the interpersonal collision event would not have been logged", + True, + ) + + elif link_type == "incident_triggers_risk_flag": + jira_id = (cause.artifact_ids or {}).get("jira", "the incident") + accounts = effect.facts.get("affected_accounts", []) + acc_str = ", ".join(accounts[:3]) if accounts else "associated accounts" + return ( + f"incident {jira_id} had not occurred or had been resolved immediately", + f"{acc_str} would not have been flagged as at-risk in Salesforce", + True, + ) + + elif link_type == "review_triggers_revision": + reviewer = cause.facts.get( + "reviewer", (cause.actors or ["the reviewer"])[0] + ) + author = cause.facts.get("author", (effect.actors or ["the author"])[0]) + return ( + f"{reviewer} had approved the pull request on Day {cause.day} without requesting changes", + f"{author} would not have revised the implementation before merge", + True, + ) + + elif link_type == "hire_fills_knowledge_gap": + departed = (cause.actors or ["the former employee"])[0] + domains = cause.facts.get("knowledge_domains", []) + domain_str = ", ".join(domains[:3]) + name = effect.facts.get("name", (effect.actors or ["the new hire"])[0]) + return ( + f"{name} had not been hired to fill the gap in {domain_str}", + f"the knowledge vacuum left by {departed}'s departure would remain unaddressed", + True, + ) + + elif link_type == "escalation_from_zendesk": + ticket_ids = cause.facts.get("ticket_ids", ["the support tickets"]) + tickets_str = ", ".join(ticket_ids[:3]) + jira_id = (effect.artifact_ids or {}).get("jira", "the incident") + return ( + f"the Zendesk tickets ({tickets_str}) had been resolved at the support level", + f"incident {jira_id} would not have been opened", + True, + ) + + return ( + "the causal condition had been different", + "the outcome would have changed", + True, + ) + + def build(self) -> List[CausalLink]: + links: List[CausalLink] = [] + + for link_type in _EXPLICIT_CAUSAL_LINKS: + if link_type == "involves_gap": + cause_events = [ + e + for e in self._events + if e.type == "knowledge_gap_detected" + and e.facts.get("trigger") != "centrality_vacuum" + and e.facts.get("detection_method") + not in ( + "reviewer_audit", + "async_thread_classification", + ) + ] + elif link_type == "recurrence_of": + cause_events = [ + e + for e in self._events + if e.type == "incident_opened" and e.facts.get("recurrence_of") + ] + elif link_type == "spawned_doc": + cause_events = [ + e + for e in self._events + if e.type == "design_discussion" and e.facts.get("spawned_doc") + ] + elif link_type == "email_dropped": + cause_events = [e for e in self._events if e.type == "email_dropped"] + elif link_type == "sf_ownership_lapsed": + cause_events = [ + e for e in self._events if e.type == "employee_departed" + ] + elif link_type == "zd_escalation_source": + cause_events = [e for e in self._events if e.type == "incident_opened"] + elif link_type == "blocker_flagged": + cause_events = [e for e in self._events if e.type == "blocker_flagged"] + elif link_type == "incident_coordination": + cause_events = [e for e in self._events if e.type == "incident_opened"] + elif link_type == "departure_reassignment": + cause_events = [ + e for e in self._events if e.type == "employee_departed" + ] + elif link_type == "assignment_domain_mismatch": + cause_events = [ + e for e in self._events if e.type == "assignment_domain_mismatch" + ] + elif link_type == "jira_from_vendor_email": + cause_events = [ + e + for e in self._events + if e.type == "inbound_external_email" + and e.facts.get("category") == "vendor" + ] + + elif link_type == "jira_from_customer_email": + cause_events = [ + e + for e in self._events + if e.type == "inbound_external_email" + and e.facts.get("category") == "customer" + and e.facts.get("email_type") != "escalation" + ] + + elif link_type == "customer_escalation_relayed": + cause_events = [ + e + for e in self._events + if e.type == "inbound_external_email" + and e.facts.get("category") == "customer" + ] + + elif link_type == "incident_handoff": + cause_events = [ + e for e in self._events if e.type == "employee_departed" + ] + + elif link_type == "pr_gap_detected": + cause_events = [e for e in self._events if e.type == "pr_review"] + + elif link_type == "async_gap_detected": + cause_events = [e for e in self._events if e.type == "async_question"] + + elif link_type == "doc_gap_detected": + cause_events = [ + e + for e in self._events + if e.type == "knowledge_gap_detected" + and e.facts.get("detection_method") == "author_self_audit" + and e.facts.get( + "topics_beyond_expertise" + ) # must have domains to match on + ] + + elif link_type == "centrality_vacuum": + cause_events = [ + e + for e in self._events + if e.type == "knowledge_gap_detected" + and e.facts.get("trigger") == "centrality_vacuum" + ] + + elif link_type == "sf_stage_advanced_by_customer": + cause_events = [ + e + for e in self._events + if e.type == "inbound_external_email" + and e.facts.get("category") == "customer" + ] + + elif link_type == "feature_request_fyi": + cause_events = [ + e + for e in self._events + if e.type == "inbound_external_email" + and "feature" in " ".join(e.tags or []).lower() + ] + + elif link_type == "proactive_outreach_from_crm_signal": + cause_events = [ + e + for e in self._events + if e.type == "crm_touchpoint" + and e.facts.get("triggered_by") is None + ] + + elif link_type == "ticket_completion_notifies_lead": + cause_events = [ + e for e in self._events if e.type == "ticket_completion_email" + ] + + elif link_type == "postmortem_from_incident": + cause_events = [e for e in self._events if e.type == "incident_opened"] + + elif link_type == "org_collision_tension": + cause_events = [e for e in self._events if e.type == "org_collision"] + + elif link_type == "incident_triggers_risk_flag": + cause_events = [e for e in self._events if e.type == "incident_opened"] + + elif link_type == "review_triggers_revision": + cause_events = [ + e + for e in self._events + if e.type == "pr_review" + and e.facts.get("verdict") == "changes_requested" + ] + + elif link_type == "hire_fills_knowledge_gap": + cause_events = [ + e + for e in self._events + if e.type == "employee_departed" + and e.facts.get("knowledge_domains") + ] + + elif link_type == "escalation_from_zendesk": + cause_events = [ + e for e in self._events if e.type == "zd_tickets_escalated" + ] + + else: + logger.debug( + f"[causal_index] No cause-event selector for link_type={link_type!r} — skipping" + ) + continue + + cause_events = list(cause_events) + random.shuffle(cause_events) + links_this_type = 0 + + for cause in cause_events: + if links_this_type >= self.MAX_LINKS_PER_TYPE: + break + + effect = self._find_effect_event(link_type, cause) + if not effect: + continue + + if link_type == "involves_gap": + link_value = str(cause.facts.get("gap_areas", "")) + cleaned = link_value.strip("[]'\" ").lower() + if cleaned in _VAGUE_GAP_TERMS: + logger.debug( + f"[causal_index] Skipping vague involves_gap: {link_value}" + ) + continue + + premise, outcome, changed = self._counterfactual_template( + link_type, cause, effect + ) + + subsystems = self._subsystems_for_event( + cause + ) | self._subsystems_for_event(effect) + + link_field = { + "involves_gap": "gap_areas", + "recurrence_of": "prior_postmortem", + "spawned_doc": "spawned_doc", + "email_dropped": "email", + "sf_ownership_lapsed": "actor", + "zd_escalation_source": "jira", + "blocker_flagged": "jira", + "incident_coordination": "jira", + "departure_reassignment": "actor", + "assignment_domain_mismatch": "ticket_id", + "jira_from_customer_email": "email", + "jira_from_vendor_email": "email", + "customer_escalation_relayed": "email", + "incident_handoff": "actor", + "pr_gap_detected": "pr", + "async_gap_detected": "slack_thread", + "centrality_vacuum": "gap_domain", + "sf_stage_advanced_by_customer": "sf_opp", + "feature_request_fyi": "email", + "proactive_outreach_from_crm_signal": "sf_opp", + "ticket_completion_notifies_lead": "jira", + "org_collision_tension": "actor", + "postmortem_from_incident": "jira", + "incident_triggers_risk_flag": "jira", + "review_triggers_revision": "pr", + "hire_fills_knowledge_gap": "actor", + "escalation_from_zendesk": "jira", + "doc_gap_detected": "confluence", + }.get(link_type, "") + + if link_type == "recurrence_of": + recurrence_jira_id = (cause.artifact_ids or {}).get("jira", "") + if recurrence_jira_id: + recurrence_ticket = self._mem._db["jira_tickets"].find_one( + {"id": recurrence_jira_id}, + {"prior_postmortem": 1, "recurrence_of": 1}, + ) + if recurrence_ticket: + link_value = ( + recurrence_ticket.get("prior_postmortem") + or recurrence_ticket.get("recurrence_of") + or "" + ) + else: + link_value = "" + else: + link_value = "" + else: + link_value = str( + cause.facts.get(link_field, "") + or (cause.artifact_ids or {}).get(link_field, "") + or (cause.actors or [""])[0] + ) + + links_this_type += 1 + + if link_type == "recurrence_of": + link_cause_id, link_cause_type = effect.mongo_id, effect.type + link_effect_id, link_effect_type = cause.mongo_id, cause.type + link_day = effect.day + else: + link_cause_id, link_cause_type = cause.mongo_id, cause.type + link_effect_id, link_effect_type = effect.mongo_id, effect.type + link_day = cause.day + + links.append( + CausalLink( + link_type=link_type, + cause_event_id=link_cause_id, + cause_event_type=link_cause_type, + effect_event_id=link_effect_id, + effect_event_type=link_effect_type, + actors=list(set((cause.actors or []) + (effect.actors or []))), + day=link_day, + link_field=link_field, + link_value=link_value, + subsystems_involved=subsystems, + counterfactual_premise=premise, + counterfactual_outcome=outcome, + outcome_changed=changed, + ) + ) + logger.info( + f"[causal_index] {link_type}: {len(cause_events)} candidates, " + f"{links_this_type} linked, " + f"{len(cause_events) - links_this_type} with no effect found" + ) + + by_type_counts = defaultdict(int) + for lnk in links: + by_type_counts[lnk.link_type] += 1 + logger.info( + f"[causal_index] {len(links)} explicit causal links indexed " + f"(cap={self.MAX_LINKS_PER_TYPE}/type, {len(by_type_counts)} types covered)" + ) + return links + + +class AbsenceCatalogBuilder: + """ + Builds the catalog of expected-but-absent artifact pairs. + + For each pair in _SILENCE_PAIRS, scans the event log for trigger events + that have no matching response event. The state machine is the arbiter: + if no response event fired, the absence is ground truth. + + Also derives expected_search_space: the set of artifact IDs the agent + must check before concluding absence. This is what separates a well-reasoned + "no" from a lucky guess. + """ + + def __init__(self, mem: Memory): + self._mem = mem + self._events: List[SimEvent] = mem.get_event_log(from_db=True) + + def _dept_of_actor(self, actor: str) -> Optional[str]: + for dept, members in CONFIG["org_chart"].items(): + if actor in members: + return dept.lower().replace(" ", "_") + return None + + def _match_key(self, event: SimEvent, link_field: str) -> Optional[str]: + val = (event.artifact_ids or {}).get(link_field) + if val: + return val + val = event.facts.get(link_field) + if val: + return str(val) + + if link_field == "gap_domain" and event.type == "knowledge_gap_detected": + method = event.facts.get("detection_method", "") + + if method in ("embedding_similarity", "centrality_vacuum"): + areas = event.facts.get("gap_areas") + if areas: + first = areas[0] if isinstance(areas, list) else areas + if str(first).strip().lower() not in _VAGUE_GAP_TERMS: + return str(first) + + elif method == "reviewer_audit": + pr = (event.artifact_ids or {}).get("pr") + if pr: + return str(pr) + + elif method == "author_self_audit": + return None + + if link_field == "actor" and event.actors: + return event.actors[0] + + return None + + def _expected_search_space( + self, + trigger: SimEvent, + expected_response_type: str, + artifact_ids_override: Optional[dict] = None, + ) -> List[str]: + artifact_ids = artifact_ids_override or trigger.artifact_ids + search_space: List[str] = list(_safe_artifact_values(artifact_ids)) + + if expected_response_type == "postmortem_created": + pass + + elif expected_response_type == "incident_resolved": + jira_id = (artifact_ids or {}).get("jira", "") + if jira_id: + search_space.append(jira_id) + + elif expected_response_type == "zd_ticket_opened": + email_id = (artifact_ids or {}).get("email", "") + if email_id: + search_space.append(email_id) + + elif expected_response_type == "customer_email_routed": + email_id = (artifact_ids or {}).get("email", "") + if email_id: + search_space.append(email_id) + + elif expected_response_type == "confluence_created": + zoom_id = (artifact_ids or {}).get("zoom_transcript", "") + if zoom_id: + search_space.append(zoom_id) + slack_id = (artifact_ids or {}).get("slack", "") + if slack_id: + search_space.append(slack_id) + + elif expected_response_type == "sf_ownership_lapsed": + pass + + elif expected_response_type == "ticket_reassigned": + pass + + elif expected_response_type == "pr_merged": + pr_id = (artifact_ids or {}).get("pr", "") + if pr_id: + search_space.append(pr_id) + + elif expected_response_type == "zd_tickets_escalated": + jira_id = (artifact_ids or {}).get("jira", "") + if jira_id: + search_space.append(jira_id) + + elif expected_response_type == "onboarding_session": + slack_thread = (artifact_ids or {}).get("slack_thread", "") + if slack_thread: + search_space.append(slack_thread) + + elif expected_response_type == "warmup_1on1": + slack_thread = (artifact_ids or {}).get("slack_thread", "") + if slack_thread: + search_space.append(slack_thread) + + elif expected_response_type == "sf_deals_risk_flagged": + jira_id = (artifact_ids or {}).get("jira", "") + if jira_id: + search_space.append(jira_id) + + elif expected_response_type == "jira_from_vendor_email": + email_id = (artifact_ids or {}).get("email", "") + if email_id: + search_space.append(email_id) + + elif expected_response_type == "knowledge_gap_detected": + ticket_id = trigger.facts.get("ticket_id", "") + if ticket_id: + search_space.append(ticket_id) + pr_id = (artifact_ids or {}).get("pr", "") + confluence_id = (artifact_ids or {}).get("confluence", "") + slack_thread = (artifact_ids or {}).get("slack_thread", "") + if pr_id: + search_space.append(pr_id) + if confluence_id: + search_space.append(confluence_id) + if slack_thread: + search_space.append(slack_thread) + + return list(dict.fromkeys(search_space)) + + def build(self) -> Tuple[List[AbsenceRecord], List[AbsenceRecord]]: + records: List[AbsenceRecord] = [] + confirmed: List[AbsenceRecord] = [] + + for trigger_type, response_type, link_field in _SILENCE_PAIRS: + trigger_events = [ + e for e in self._events if e.type == trigger_type and e.day >= 1 + ] + + for trigger in trigger_events: + trigger_artifacts = _safe_artifact_values(trigger.artifact_ids) + link_key = self._match_key(trigger, link_field) + + if trigger_type == "inbound_external_email": + category = trigger.facts.get("category", "") + if ( + response_type == "customer_email_routed" + and category == "vendor" + ): + continue + if ( + response_type == "jira_from_vendor_email" + and category != "vendor" + ): + continue + + detection_method = trigger.facts.get("detection_method", "") + if ( + trigger_type == "knowledge_gap_detected" + and detection_method == "author_self_audit" + ): + audited_confluence = (trigger.artifact_ids or {}).get("confluence") + if audited_confluence: + trigger_artifacts = trigger_artifacts - { + str(audited_confluence) + } + + if response_type == "confluence_created" and ( + trigger.facts.get("spawned_doc") + or "confluence" in (trigger.artifact_ids or {}) + ): + continue + + response_found = False + + for e in self._events: + if e.type != response_type or e.day < trigger.day: + continue + + e_match_key = self._match_key(e, link_field) + + if ( + link_key + and e_match_key is not None + and ( + e_match_key == link_key + or link_key in str(e.artifact_ids) + or link_key in str(e.facts) + ) + ): + response_found = True + break + + response_artifacts = _safe_artifact_values(e.artifact_ids) + if trigger_artifacts & response_artifacts: + response_found = True + break + + subsystem = _RESPONSE_TYPE_SUBSYSTEM.get( + response_type, + _ARTIFACT_SUBSYSTEM.get( + next(iter(trigger.artifact_ids or {}), ""), "default" + ), + ) + search_space = self._expected_search_space(trigger, response_type) + + trigger_artifact_ids = dict(trigger.artifact_ids or {}) + + if ( + response_type == "sf_ownership_lapsed" + and trigger_type == "employee_departed" + ): + actor = (trigger.actors or [""])[0] + if actor: + owned_accounts = [ + doc["account_id"] + for doc in self._mem._db["sf_accounts"].find( + {"owner": actor}, {"account_id": 1, "_id": 0} + ) + if doc.get("account_id") + ] + owned_opps = [ + doc["opportunity_id"] + for doc in self._mem._db["sf_opps"].find( + { + "owner": actor, + "stage": {"$nin": ["Closed Won", "Closed Lost"]}, + }, + {"opportunity_id": 1, "_id": 0}, + ) + if doc.get("opportunity_id") + ] + if owned_accounts: + trigger_artifact_ids["sf_accounts"] = owned_accounts + if owned_opps: + trigger_artifact_ids["sf_opps"] = owned_opps + + if ( + response_type == "zd_ticket_opened" + and trigger_type == "customer_email_routed" + ): + email_id = (trigger.artifact_ids or {}).get("email", "") + if email_id: + linked_tickets = [ + doc["id"] + for doc in self._mem._db["zd_tickets"].find( + {"source_email_id": email_id}, {"id": 1, "_id": 0} + ) + if doc.get("id") + ] + if linked_tickets: + trigger_artifact_ids["zd_ticket"] = linked_tickets + + if response_found: + response_event_artifacts = sorted( + _safe_artifact_values(e.artifact_ids) + ) + enriched_search_space = list( + dict.fromkeys(search_space + response_event_artifacts) + ) + + confirmed_rec = AbsenceRecord( + trigger_event_id=trigger.mongo_id, + trigger_event_type=trigger_type, + expected_response_type=response_type, + trigger_day=trigger.day, + trigger_actors=list(dict.fromkeys(trigger.actors or [])), + trigger_artifact_ids=dict(trigger.artifact_ids or {}), + link_field=link_field, + link_value=link_key or "N/A", + subsystem=subsystem, + expected_search_space=enriched_search_space, + ) + confirmed.append(confirmed_rec) + else: + absence_rec = AbsenceRecord( + trigger_event_id=trigger.mongo_id, + trigger_event_type=trigger_type, + expected_response_type=response_type, + trigger_day=trigger.day, + trigger_actors=list(dict.fromkeys(trigger.actors or [])), + trigger_artifact_ids=dict(trigger.artifact_ids or {}), + link_field=link_field, + link_value=link_key or "N/A", + subsystem=subsystem, + expected_search_space=search_space, + ) + records.append(absence_rec) + + logger.info(f"[absence_catalog] {len(records)} absence records cataloged") + logger.info( + f"[absence_catalog] {len(confirmed)} confirmed response records cataloged" + ) + + MAX_PER_TRIGGER = 8 + capped: List[AbsenceRecord] = [] + trigger_counts: Dict[str, int] = defaultdict(int) + random.shuffle(records) + for r in records: + if trigger_counts[r.trigger_event_type] < MAX_PER_TRIGGER: + capped.append(r) + trigger_counts[r.trigger_event_type] += 1 + logger.info( + f"[absence_catalog] Capped to {len(capped)} records ({MAX_PER_TRIGGER}/trigger_type)" + ) + + confirmed_capped: List[AbsenceRecord] = [] + confirmed_counts: Dict[str, int] = defaultdict(int) + random.shuffle(confirmed) + for r in confirmed: + if confirmed_counts[r.trigger_event_type] < MAX_PER_TRIGGER: + confirmed_capped.append(r) + confirmed_counts[r.trigger_event_type] += 1 + + return capped, confirmed_capped + + +class EvalQuestionGenerator: + """ + Generates PERSPECTIVE, COUNTERFACTUAL, and SILENCE questions. + + Ground truth is always derived deterministically from the three indexes. + LLMs only write question prose, and every generated question is validated + against a structured rubric before inclusion. + + Question prose validation checks: + - Ends with a question mark + - Does not contain the ground truth answer verbatim + - Does not name an artifact ID directly (keeps questions natural-language) + - Is unambiguous — references the actor/day/subsystem constraint explicitly + """ + + MAX_PERSPECTIVE = 30 + MAX_COUNTERFACTUAL = 40 + MAX_SILENCE = 30 + + def __init__( + self, + mem: Memory, + worker_llm, + visibility_map: Dict[str, List[ActorVisibilityCone]], + causal_links: List[CausalLink], + absence_catalog: List[AbsenceRecord], + confirmed_catalog: List[AbsenceRecord], + ): + self._mem = mem + self._worker_llm = worker_llm + self._visibility_map = visibility_map + self._causal_links = causal_links + self._absence_catalog = absence_catalog + self._confirmed_catalog = confirmed_catalog + self._events: List[SimEvent] = mem.get_event_log(from_db=True) + + def generate(self) -> List[dict]: + questions: List[dict] = [] + + logger.info("[eval] Generating PERSPECTIVE questions...") + questions.extend(self._perspective_questions()) + + logger.info("[eval] Generating COUNTERFACTUAL questions...") + questions.extend(self._counterfactual_questions()) + + logger.info("[eval] Generating SILENCE questions...") + questions.extend(self._silence_questions()) + + random.shuffle(questions) + + logger.info(f"[eval] {len(questions)} total questions generated") + return questions + + def _build_negative_counterfactual( + self, + link: CausalLink, + effect_cause_map: Dict[str, List[CausalLink]], + ) -> Optional[dict]: + """ + Generate a counterfactual where outcome_changed is False. + + Strategy: The effect event has MULTIPLE independent causes in the + causal link index. Removing THIS cause would not have prevented the + effect because at least one alternative cause is sufficient. + + Example: If three knowledge gaps (EVT-12, EVT-27, EVT-32) all + contributed to incident EVT-42, removing any single gap would NOT + have prevented the incident — the other gaps still exist. + """ + sibling_causes = effect_cause_map.get(link.effect_event_id, []) + alternatives = [ + s for s in sibling_causes if s.cause_event_id != link.cause_event_id + ] + + if not alternatives: + return None + + alternative = alternatives[0] + + cause_event = next( + (e for e in self._events if e.mongo_id == link.cause_event_id), None + ) + effect_event = next( + (e for e in self._events if e.mongo_id == link.effect_event_id), None + ) + alt_cause_event = next( + (e for e in self._events if e.mongo_id == alternative.cause_event_id), None + ) + + if not (cause_event and effect_event and alt_cause_event): + return None + + premise_parts = { + "involves_gap": lambda: ( + f"{(cause_event.facts.get('departed_employee') or (cause_event.actors or ['the engineer'])[0])} " + f"had fully documented {', '.join(cause_event.facts.get('gap_areas', ['the domain']))}" + ), + "doc_gap_detected": lambda: ( + f"{cause_event.facts.get('author', (cause_event.actors or ['the author'])[0])} " + f"had expertise in {' and '.join(cause_event.facts.get('topics_beyond_expertise', ['the domain'])[:2])} " + f"when writing {(cause_event.artifact_ids or {}).get('confluence', 'the design doc')}" + ), + "recurrence_of": lambda: ( + f"the root cause of {(cause_event.artifact_ids or {}).get('jira', 'the original incident')} " + f"had been fully addressed" + ), + } + + premise_fn = premise_parts.get( + link.link_type, + lambda: ( + f"the {link.link_type.replace('_', ' ')} on Day {link.day} had not occurred" + ), + ) + premise = premise_fn() + + alt_mechanism = alternative.link_type.replace("_", " ") + alt_actor = (alt_cause_event.actors or ["another source"])[0] + alt_day = alternative.day + + outcome = ( + f"the effect ({link.effect_event_type.replace('_', ' ')}) would still have " + f"occurred due to an independent {alt_mechanism} " + f"involving {alt_actor} on Day {alt_day}" + ) + + cause_artifacts = sorted( + _safe_artifact_values(cause_event.artifact_ids if cause_event else {}) + ) + effect_artifacts = sorted( + _safe_artifact_values(effect_event.artifact_ids if effect_event else {}) + ) + alt_cause_artifacts = sorted( + _safe_artifact_values( + alt_cause_event.artifact_ids if alt_cause_event else {} + ) + ) + + ground_truth = { + "outcome_changed": False, + "causal_mechanism": link.link_type, + "causal_link_field": link.link_field, + "causal_link_value": link.link_value, + "cause_event_id": link.cause_event_id, + "cause_event_type": link.cause_event_type, + "effect_event_id": link.effect_event_id, + "effect_event_type": link.effect_event_type, + "premise": premise, + "outcome": outcome, + "actors": link.actors, + "as_of_time": (_SIM_START + timedelta(days=link.day)).isoformat(), + "evidence_chain_artifacts": { + "cause": cause_artifacts, + "effect": effect_artifacts, + }, + "alternative_cause_event_id": alternative.cause_event_id, + "alternative_cause_type": alternative.cause_event_type, + "alternative_mechanism": alternative.link_type, + "alternative_cause_artifacts": alt_cause_artifacts, + "alternative_cause_count": len(sibling_causes) - 1, + } + + difficulty = "hard" if len(link.subsystems_involved) > 1 else "medium" + actors_str = ( + ", ".join(link.actors[:3]) if link.actors else "the involved parties" + ) + + _NEGATIVE_CF_DOMAIN_HINTS: Dict[str, str] = { + "involves_gap": ( + "a knowledge gap that contributed to an incident, BUT the incident " + "had multiple contributing knowledge gaps — removing just this one " + "would not have prevented it" + ), + "doc_gap_detected": ( + "a documentation gap in a design doc that contributed to an incident, " + "BUT other documentation gaps also contributed — removing just this " + "author's gap would not have prevented the incident" + ), + "recurrence_of": ( + "a recurring incident linked to a prior unresolved issue, BUT the " + "same effect was also caused by an independent path" + ), + } + + domain_hint = _NEGATIVE_CF_DOMAIN_HINTS.get( + link.link_type, + f"a {link.link_type.replace('_', ' ')} event that contributed to " + f"an outcome, BUT the outcome had multiple independent causes — " + f"removing just this one would not have changed it", + ) + + NEGATIVE_QUESTION_STYLES = [ + "Phrase it to suggest the cause was necessary: 'If X had not happened, would Y still have occurred?'", + "Phrase it as a dependency question: 'Was Y dependent on X, or would it have happened regardless?'", + "Phrase it from the effect perspective: 'Would Y have been prevented if only X had been different?'", + ] + + style = random.choice(NEGATIVE_QUESTION_STYLES) + + template = ( + f"Write a counterfactual yes/no question about events on Day {link.day} " + f"in a simulated company. {style} " + f"The question must name the Day and the actors involved: {actors_str}. " + f"The question is about: {domain_hint}. " + f"Do not name specific artifact IDs or the causal mechanism label. " + f"Do not reveal the answer or make the causal link obvious. " + f"The question should require investigation to answer. " + f"Output only the question text." + ) + + question_text = self._generate_and_validate_prose( + template=template, + ground_truth_str=outcome, + question_type="COUNTERFACTUAL", + ) + if not question_text: + return None + + return { + "question_id": f"counterfactual_neg_{link.cause_event_id}_{link.link_type}", + "question_type": "COUNTERFACTUAL", + "difficulty": difficulty, + "link_type": link.link_type, + "day": link.day, + "actors": link.actors, + "subsystems_involved": sorted(link.subsystems_involved), + "question_text": question_text, + "ground_truth": ground_truth, + "evidence_chain": [link.cause_event_id, link.effect_event_id], + "requires_reasoning": True, + } + + def _cap_by_key( + self, + items: list, + key_fn, + max_per_key: int, + max_total: int, + ) -> list: + by_key: Dict[str, list] = defaultdict(list) + for item in items: + by_key[key_fn(item)].append(item) + + selected = [] + overflow = [] + for group in by_key.values(): + random.shuffle(group) + selected.extend(group[:max_per_key]) + overflow.extend(group[max_per_key:]) + + random.shuffle(overflow) + if len(selected) < max_total: + selected.extend(overflow[: max_total - len(selected)]) + + random.shuffle(selected) + return selected[:max_total] + + def _cap_by_keys( + self, + items: list, + key_fns: list, + max_per_keys: list, + max_total: int, + ) -> list: + """Multi-key capping: an item is included only if ALL its keys + are below their respective caps.""" + counters = [defaultdict(int) for _ in key_fns] + selected = [] + random.shuffle(items) + + for item in items: + if len(selected) >= max_total: + break + keys = [fn(item) for fn in key_fns] + if all(counters[i][keys[i]] < max_per_keys[i] for i in range(len(key_fns))): + selected.append(item) + for i, k in enumerate(keys): + counters[i][k] += 1 + + return selected + + def _perspective_questions(self) -> List[dict]: + MAX_PER_EVENT_TYPE = 5 + MAX_PER_ACTOR = 5 + + internal_actors = { + actor + for actor in self._visibility_map.keys() + if self._visibility_map[actor][0].role != "external" + and actor not in _SYSTEM_ACTORS + } + + all_asymmetry = [ + ev for ev in self._find_asymmetry_events() if ev[0] in internal_actors + ] + + true_candidates = [ev for ev in all_asymmetry if ev[5]] + false_candidates = [ev for ev in all_asymmetry if not ev[5]] + + target_true = int(self.MAX_PERSPECTIVE * 0.38) + + true_capped = self._cap_by_keys( + true_candidates, + key_fns=[ + lambda ev: ev[2].type, + lambda ev: ev[0], + ], + max_per_keys=[MAX_PER_EVENT_TYPE, MAX_PER_ACTOR], + max_total=target_true, + ) + false_capped = self._cap_by_keys( + false_candidates, + key_fns=[ + lambda ev: ev[2].type, + lambda ev: ev[0], + ], + max_per_keys=[MAX_PER_EVENT_TYPE, MAX_PER_ACTOR], + max_total=self.MAX_PERSPECTIVE - target_true, + ) + + pool = true_capped + false_capped + random.shuffle(pool) + + questions: List[dict] = [] + for actor, cone, event, info_available, cross_subsystem, _ in pool: + question = self._build_perspective_question( + actor, cone, event, info_available, cross_subsystem + ) + if question: + questions.append(question) + + by_type: Dict[str, List[dict]] = defaultdict(list) + for q in questions: + by_type[q["event_type"]].append(q) + + final = [] + overflow = [] + for event_type, group in by_type.items(): + random.shuffle(group) + final.extend(group[:MAX_PER_EVENT_TYPE]) + overflow.extend(group[MAX_PER_EVENT_TYPE:]) + + if len(final) < self.MAX_PERSPECTIVE: + random.shuffle(overflow) + final.extend(overflow[: self.MAX_PERSPECTIVE - len(final)]) + + true_qs = [q for q in final if q["ground_truth"]["could_actor_have_known"]] + false_qs = [q for q in final if not q["ground_truth"]["could_actor_have_known"]] + n_t = min(len(true_qs), target_true) + n_f = min(len(false_qs), self.MAX_PERSPECTIVE - n_t) + final = random.sample(true_qs, n_t) + random.sample(false_qs, n_f) + random.shuffle(final) + + logger.info( + f"[eval] {len(final)} PERSPECTIVE questions built ({n_t} true / {n_f} false)" + ) + return final + + def _generate_from_ranked_pool( + self, + primary: List[Tuple], + reserve: List[Tuple], + target: int, + ) -> List[dict]: + questions: List[dict] = [] + reserve_iter = iter(reserve) + + for candidate in primary: + if len(questions) >= target: + break + + actor, cone, event, info_available, cross_subsystem, _ = ( + candidate # ← unpack, discard 6th + ) + question = self._build_perspective_question( + actor, cone, event, info_available, cross_subsystem + ) + if question: + questions.append(question) + else: + backup = next(reserve_iter, None) + if backup: + actor, cone, event, info_available, cross_subsystem, _ = backup + question = self._build_perspective_question( + actor, cone, event, info_available, cross_subsystem + ) + if question: + questions.append(question) + + while len(questions) < target: + backup = next(reserve_iter, None) + if not backup: + break + actor, cone, event, info_available, cross_subsystem, _ = backup + question = self._build_perspective_question( + actor, cone, event, info_available, cross_subsystem + ) + if question: + questions.append(question) + + return questions + + def _find_asymmetry_events(self) -> List[Tuple]: + results = [] + significant_types = { + "incident_opened", + "customer_escalation", + "sf_deals_risk_flagged", + "knowledge_gap_detected", + "employee_departed", + "design_discussion", + "customer_email_routed", + "zd_tickets_escalated", + "sf_ownership_lapsed", + "postmortem_created", + "inbound_external_email", + "assignment_domain_mismatch", + "email_dropped", + "vendor_email_routed", + "sales_outbound_email", + "vendor_ack_sent", + "customer_reply_sent", + "sf_stage_advanced_by_customer", + "watercooler_chat", + "blocker_flagged", + "onboarding_session", + "warmup_1on1", + "jira_ticket_created", + "feature_request_fyi", + "zd_ticket_opened", + "zd_tickets_resolved", + "proactive_outreach_initiated", + "crm_touchpoint", + "incident_resolved", + "pr_review", + "confluence_created", + "retrospective", + "sprint_planned", + "escalation_chain", + "org_collision", + "ticket_completion_email", + "external_contact_summarized", + } + + MAX_TUPLES_PER_TYPE = 5 + MAX_PER_EVENT_ID = 1 + MAX_PER_ACTOR = 4 + + type_tuple_counts: Dict[str, int] = defaultdict(int) + event_id_counts: Dict[str, int] = defaultdict(int) + actor_tuple_counts: Dict[str, int] = defaultdict(int) + + artifact_timestamps: Dict[str, str] = { + doc["_id"]: doc.get("timestamp", "") + for doc in self._mem._db["artifacts"].find({}, {"_id": 1, "timestamp": 1}) + } + + email_to_ticket: Dict[str, str] = {} + for doc in self._mem._db["jira_tickets"].find( + {"source_email_id": {"$exists": True, "$ne": ""}}, + {"_id": 0, "id": 1, "source_email_id": 1}, + ): + email_to_ticket[doc["source_email_id"]] = doc["id"] + + shuffled_events = list(self._events) + random.shuffle(shuffled_events) + + for event in shuffled_events: + if not event.mongo_id: + continue + if event.type not in significant_types: + continue + + if type_tuple_counts[event.type] >= MAX_TUPLES_PER_TYPE: + continue + event_subsystems = set() + for doc_type, aid in (event.artifact_ids or {}).items(): + if not aid: + continue + s = _ARTIFACT_SUBSYSTEM.get(doc_type, "default") + if s != "default": + event_subsystems.add(s) + + if not event_subsystems: + continue + + event_artifacts = _safe_artifact_values(event.artifact_ids) + + for actor, cones in self._visibility_map.items(): + if type_tuple_counts[event.type] >= MAX_TUPLES_PER_TYPE: + break + if actor_tuple_counts[actor] >= MAX_PER_ACTOR: # <-- NEW + continue + + if actor in (event.actors or []): + continue + + cone = next((c for c in cones if c.as_of_day == event.day), None) + if not cone: + continue + + all_visible = cone.all_visible() + + all_visible = { + aid + for aid in all_visible + if not ( + any(aid.startswith(p + "-") for p in _JIRA_PROJECT_ACCESS) + and not _jira_project_visible(aid, cone.role) + ) + } + + missed_artifacts = event_artifacts - all_visible + + if missed_artifacts and event.type in ( + "inbound_external_email", + "customer_email_routed", + ): + for email_id in event_artifacts: + ticket_id = email_to_ticket.get(email_id) + if ticket_id and ticket_id in all_visible: + missed_artifacts = set() + break + + if missed_artifacts: + missed_artifacts = { + aid + for aid in missed_artifacts + if artifact_timestamps.get(aid, "") <= cone.as_of_time + } + + if not missed_artifacts: + if ( + event.type not in ActorVisibilityBuilder._BROADCAST_EVENTS + or not (event_subsystems & cone.subsystem_access) + ): + continue + if event_id_counts[event.mongo_id] >= MAX_PER_EVENT_ID: + continue + event_id_counts[event.mongo_id] += 1 + type_tuple_counts[event.type] += 1 + results.append( + ( + actor, + cone, + event, + { + "actor_visible_subsystems": sorted( + cone.subsystem_access + ), + "event_subsystems": sorted(event_subsystems), + "blocked_by_role": [], + "missed_artifacts": [], + "related_artifacts_actor_saw": sorted( + event_artifacts & all_visible + ), + }, + False, + True, + ) + ) + continue + + if event_id_counts[event.mongo_id] >= MAX_PER_EVENT_ID: + continue + event_id_counts[event.mongo_id] += 1 + + blocked_by_role = event_subsystems - cone.subsystem_access + cross_subsystem = len(blocked_by_role) > 0 + + related_visible = [] + for e in self._events: + if e.day > event.day: + continue + if actor not in (e.actors or []): + continue + shared_artifacts = ( + _safe_artifact_values(e.artifact_ids) & event_artifacts + ) + if not shared_artifacts: + continue + for aid in _safe_artifact_values(e.artifact_ids): + if aid in all_visible: + ts = artifact_timestamps.get(aid, "") + if ts and ts <= cone.as_of_time: + related_visible.append(aid) + + info_available = { + "actor_visible_subsystems": sorted(cone.subsystem_access), + "event_subsystems": sorted(event_subsystems), + "blocked_by_role": sorted(blocked_by_role), + "missed_artifacts": sorted(missed_artifacts), + "related_artifacts_actor_saw": sorted(set(related_visible)), + } + + _event_artifacts_for_approx = _safe_artifact_values(event.artifact_ids) + _secondary = set(related_visible) + if not missed_artifacts: + _approx_known = True + elif _secondary & _event_artifacts_for_approx: + _approx_known = True + else: + _approx_known = False + + type_tuple_counts[event.type] += 1 + actor_tuple_counts[actor] += 1 + results.append( + (actor, cone, event, info_available, cross_subsystem, _approx_known) + ) + + return results + + def _build_perspective_question( + self, + actor: str, + cone: ActorVisibilityCone, + event: SimEvent, + info_available: dict, + cross_subsystem: bool, + ) -> Optional[dict]: + + missed = info_available["missed_artifacts"] + blocked = info_available["blocked_by_role"] + secondary_visible = set(info_available["related_artifacts_actor_saw"]) + event_artifacts = _safe_artifact_values(event.artifact_ids) + + if len(missed) == 0: + could_have_known = True + elif secondary_visible: + propagated_event_artifacts = secondary_visible & event_artifacts + could_have_known = len(propagated_event_artifacts) > 0 + else: + could_have_known = False + + is_broadcast = event.type in ActorVisibilityBuilder._BROADCAST_EVENTS + + if event.type == "design_discussion" and not could_have_known: + confluence_id = (event.artifact_ids or {}).get("confluence") + spawned_doc = event.facts.get("spawned_doc", False) + doc_in_corpus = bool( + spawned_doc + and confluence_id + and self._mem._db["artifacts"].find_one({"_id": confluence_id}) + ) + if doc_in_corpus and "confluence" in cone.subsystem_access: + could_have_known = True + + if not could_have_known: + if blocked: + reason = ( + f"Actor had access to {sorted(cone.subsystem_access)} but event " + f"involved {sorted(info_available['event_subsystems'])}; " + f"blocked by role from: {sorted(blocked)}" + ) + elif event.type == "design_discussion" and not ( + event.artifact_ids or {} + ).get("confluence"): + reason = ( + "No Confluence page was generated for this design discussion; " + "awareness requires direct participation" + ) + else: + reason = ( + f"Actor was not a direct participant in this " + f"{event.type.replace('_', ' ')} event, which is not broadcast " + f"org-wide; platform access alone is insufficient for private events" + ) + else: + if event.type == "design_discussion": + reason = ( + "Design discussion produced a Confluence page discoverable " + "by any actor with Confluence access, regardless of direct participation" + ) + else: + reason = ( + f"All event artifacts were in actor's visibility cone via " + f"{'direct involvement' if info_available['related_artifacts_actor_saw'] else 'broadcast'}" + ) + + evidence = sorted(info_available["related_artifacts_actor_saw"]) + + if event.type == "design_discussion" and event.facts.get("spawned_doc"): + conf_id = (event.artifact_ids or {}).get("confluence", "") + if conf_id and conf_id not in evidence: + evidence.append(conf_id) + + ground_truth = { + "actor": actor, + "as_of_day": cone.as_of_day, + "as_of_time": cone.as_of_time, + "could_actor_have_known": could_have_known, + "reason": reason, + "evidence_artifacts": evidence, + "missed_artifacts": sorted(missed), + "blocked_subsystems": sorted(blocked), + } + + difficulty = "hard" if cross_subsystem else "medium" + event_desc = self._event_description(event) + + PERSPECTIVE_QUESTION_STYLES = { + "positive_biased": [ + "Phrase it as an awareness question: 'Would X have been aware of Y as of Day Z, based only on what was accessible to them?'", + "Phrase it as an information access question: 'Could X have encountered Y through the systems available to them by Day Z?'", + "Phrase it as an organizational question: 'Would Y have reached X through normal channels by Day Z?'", + "Phrase it as a knowledge question: 'Based on X's role and access, is it likely they knew about Y before Day Z ended?'", + ], + "null_biased": [ + "Phrase it as an exclusion question: 'Would Y have been outside X's visibility by Day Z?'", + "Phrase it as a reach question: 'Was there any path by which Y could have reached X before Day Z ended?'", + "Phrase it as a gap question: 'Would X have had a blind spot around Y as of Day Z?'", + ], + } + + if could_have_known: + style = random.choice(PERSPECTIVE_QUESTION_STYLES["positive_biased"]) + else: + style_bucket = random.choices( + list(PERSPECTIVE_QUESTION_STYLES.keys()), weights=[0.6, 0.4] + )[0] + style = random.choice(PERSPECTIVE_QUESTION_STYLES[style_bucket]) + + template = ( + f"Write a question asking whether {actor} would have known about " + f"'{event_desc}' as of Day {cone.as_of_day}. " + f"The question must name the actor and the Day {cone.as_of_day} time constraint. " + f"Do not reveal the answer. Do not include artifact IDs. " + f"Do not mention which systems the actor can or cannot access. " + f"{style} " + f"Output only the question text." + ) + + question_text = self._generate_and_validate_prose( + template=template, + ground_truth_str=str(could_have_known), + question_type="PERSPECTIVE", + ) + if not question_text: + return None + + return { + "question_id": f"perspective_{actor}_{event.mongo_id}", + "question_type": "PERSPECTIVE", + "difficulty": difficulty, + "cross_subsystem": cross_subsystem, + "actor": actor, + "actor_role": cone.role, + "as_of_day": cone.as_of_day, + "as_of_time": cone.as_of_time, + "subsystem_access": sorted(cone.subsystem_access), + "blocked_subsystems": sorted(info_available["blocked_by_role"]), + "event_id": event.mongo_id, + "event_type": event.type, + "event_day": event.day, + "question_text": question_text, + "ground_truth": ground_truth, + "requires_reasoning": True, + } + + MAX_PER_LINK_TYPE = 5 + + def _counterfactual_questions(self) -> List[dict]: + effect_cause_map: Dict[str, List[CausalLink]] = defaultdict(list) + for link in self._causal_links: + effect_cause_map[link.effect_event_id].append(link) + + target_negative = self.MAX_COUNTERFACTUAL // 3 + target_positive = self.MAX_COUNTERFACTUAL - target_negative + + by_type: Dict[str, List[CausalLink]] = defaultdict(list) + for link in self._causal_links: + by_type[link.link_type].append(link) + + n_types = len(by_type) + per_type = max(2, target_positive // max(n_types, 1)) + + stratified: List[CausalLink] = [] + overflow: List[CausalLink] = [] + for link_type, group in by_type.items(): + random.shuffle(group) + stratified.extend(group[:per_type]) + overflow.extend(group[per_type:]) + + if len(stratified) < target_positive: + random.shuffle(overflow) + stratified.extend(overflow[: target_positive - len(stratified)]) + + guaranteed: List[CausalLink] = [] + seen_types: set = set() + for link in stratified: + if link.link_type not in seen_types: + guaranteed.append(link) + seen_types.add(link.link_type) + + already_guaranteed_ids = {id(lnk) for lnk in guaranteed} + remaining = [lnk for lnk in stratified if id(lnk) not in already_guaranteed_ids] + random.shuffle(remaining) + remaining_budget = target_positive - len(guaranteed) + positive_sampled = guaranteed + remaining[:remaining_budget] + random.shuffle(positive_sampled) + + seen_effects: set = set() + positive_questions: List[dict] = [] + for link in positive_sampled: + if link.effect_event_id in seen_effects: + continue + question = self._build_counterfactual_question(link) + if question: + positive_questions.append(question) + seen_effects.add(link.effect_event_id) + + negative_candidates = [ + link + for link in self._causal_links + if len(effect_cause_map.get(link.effect_event_id, [])) >= 2 + ] + random.shuffle(negative_candidates) + + seen_negative_effects: set = set() + negative_questions: List[dict] = [] + for link in negative_candidates: + if len(negative_questions) >= target_negative: + break + if link.effect_event_id in seen_negative_effects: + continue + if link.effect_event_id in seen_effects: + continue + q = self._build_negative_counterfactual(link, effect_cause_map) + if q: + negative_questions.append(q) + seen_negative_effects.add(link.effect_event_id) + + questions = positive_questions + negative_questions + random.shuffle(questions) + + logger.info( + f"[eval] {len(questions)} COUNTERFACTUAL questions built " + f"({len(positive_questions)} positive / {len(negative_questions)} negative)" + ) + return questions + + def _build_counterfactual_question(self, link: CausalLink) -> Optional[dict]: + + if link.link_type == "involves_gap": + cleaned = link.link_value.strip("[]'\" ").lower() + if cleaned in _VAGUE_GAP_TERMS: + logger.debug( + f"[counterfactual] Skipping {link.cause_event_id} — " + f"vague link_value: {link.link_value}" + ) + return None + + cause_event = next( + (e for e in self._events if e.mongo_id == link.cause_event_id), + None, + ) + effect_event = next( + (e for e in self._events if e.mongo_id == link.effect_event_id), + None, + ) + + def _clean_artifact_ids(ids: List[str]) -> List[str]: + """Remove absolute paths that survive _strip_root (scoring would never match them).""" + cleaned = [] + for aid in ids: + if aid.startswith("/"): + match = re.search(r"export/(.+)$", aid) + cleaned.append(match.group(1) if match else aid) + else: + cleaned.append(aid) + return cleaned + + cause_artifacts = _clean_artifact_ids( + sorted( + _safe_artifact_values(cause_event.artifact_ids if cause_event else {}) + ) + ) + effect_artifacts = _clean_artifact_ids( + sorted( + _safe_artifact_values(effect_event.artifact_ids if effect_event else {}) + ) + ) + + if link.link_type == "recurrence_of": + effect_jira_id = ( + (effect_event.artifact_ids or {}).get("jira", "") + if effect_event + else "" + ) + if effect_jira_id: + effect_ticket = self._mem._db["jira_tickets"].find_one( + {"id": effect_jira_id}, {"prior_postmortem": 1} + ) + if effect_ticket and effect_ticket.get("prior_postmortem"): + prior_pm = effect_ticket["prior_postmortem"] + if prior_pm not in cause_artifacts: + cause_artifacts.append(prior_pm) + + if link.link_type == "departure_reassignment" and effect_event: + reassigned = effect_event.facts.get("reassigned_tickets", []) + if reassigned: + effect_artifacts = sorted(set(effect_artifacts) | set(reassigned[:5])) + handoff_jira = (effect_event.artifact_ids or {}).get("jira") + if handoff_jira: + effect_artifacts = sorted(set(effect_artifacts) | {handoff_jira}) + + if not cause_artifacts and not effect_artifacts: + logger.debug( + f"[counterfactual] Skipping {link.cause_event_id} — " + f"departure_reassignment has no resolvable evidence artifacts" + ) + return None + + ground_truth = { + "outcome_changed": link.outcome_changed, + "causal_mechanism": link.link_type, + "causal_link_field": link.link_field, + "causal_link_value": link.link_value, + "cause_event_id": link.cause_event_id, + "cause_event_type": link.cause_event_type, + "effect_event_id": link.effect_event_id, + "effect_event_type": link.effect_event_type, + "premise": link.counterfactual_premise, + "outcome": link.counterfactual_outcome, + "actors": link.actors, + "as_of_time": (_SIM_START + timedelta(days=link.day)).isoformat(), + "evidence_chain_artifacts": { + "cause": cause_artifacts, + "effect": effect_artifacts, + }, + } + + if link.link_type == "recurrence_of" and effect_event: + day_context = ( + f"The original incident was resolved on Day {link.day}. " + f"The recurrence occurred on Day {effect_event.day}. " + f"Refer to the follow-on as 'the subsequent incident' — " + f"do not assign Day {link.day} to it." + ) + else: + day_context = "" + + difficulty = "hard" if len(link.subsystems_involved) > 1 else "medium" + + COUNTERFACTUAL_QUESTION_STYLES = [ + "Phrase it as a direct counterfactual: 'If X had not happened, would Y have occurred?'", + "Phrase it as a hypothetical: 'Had X not taken place, would Y still have happened?'", + "Phrase it from the outcome perspective: 'Would Y have occurred without X happening first?'", + "Phrase it as an investigative question: 'Was Y a direct consequence of X, or would it have happened regardless?'", + "Phrase it as a dependency question: 'Did Y depend on X occurring, or was it independently triggered?'", + ] + + style = random.choice(COUNTERFACTUAL_QUESTION_STYLES) + actors_str = ( + ", ".join(link.actors[:3]) if link.actors else "the involved parties" + ) + + _COUNTERFACTUAL_DOMAIN_HINTS: Dict[str, str] = { + "postmortem_from_incident": "a system incident and its aftermath documentation", + "recurrence_of": "a recurring system incident linked to a prior unresolved issue", + "review_triggers_revision": "a code review that led to an implementation revision before merge", + "zd_escalation_source": "a support ticket escalation that triggered an incident", + "escalation_from_zendesk": "Zendesk support tickets escalating into an incident", + "incident_coordination": "a system incident and resulting coordination with an external party", + "jira_from_vendor_email": "a vendor email and the Jira ticket it triggered", + "jira_from_customer_email": "a customer email and the Jira ticket it triggered", + "email_dropped": "an unactioned email and its downstream follow-up", + "departure_reassignment": "an employee departure and the ticket reassignments that followed", + "involves_gap": "a knowledge gap that contributed to a subsequent incident", + "spawned_doc": "a design discussion and the Confluence page it produced", + "sf_ownership_lapsed": "an employee departure and the lapsed Salesforce account ownership", + "blocker_flagged": "a technical blocker and its effect on ticket progress", + "assignment_domain_mismatch": "a domain mismatch in ticket assignment and the resulting knowledge gap", + "hire_fills_knowledge_gap": "a new hire's expertise filling a gap left by a departed employee", + "incident_triggers_risk_flag": "an incident and the Salesforce risk flags it triggered", + "pr_gap_detected": "a PR review that surfaced a knowledge gap", + "customer_escalation_relayed": "a customer email relayed as an internal escalation", + "async_gap_detected": "an async Slack Q&A thread that surfaced a knowledge gap in an undocumented domain", + "doc_gap_detected": "a knowledge gap embedded in a Confluence design document that contributed to a downstream incident", + "centrality_vacuum": "a key person's departure creating a knowledge vacuum in their domain", + "sf_stage_advanced_by_customer": "an inbound customer email that advanced a Salesforce opportunity stage", + "feature_request_fyi": "a customer feature request email relayed as an FYI to the Product team", + "proactive_outreach_from_crm_signal": "a CRM at-risk signal that triggered proactive outbound sales outreach", + "ticket_completion_notifies_lead": "a ticket reaching Done status and triggering a completion notification", + "org_collision_tension": "an unplanned cross-department friction event between overlapping responsibilities", + "customer_escalation_relayed": "a customer escalation email relayed internally via Slack to the Product team", + } + + domain_hint = _COUNTERFACTUAL_DOMAIN_HINTS.get( + link.link_type, "an organizational event and its downstream effects" + ) + + template = ( + f"Write a counterfactual yes/no question about events on Day {link.day} " + f"in a simulated company. {style} " + f"The question must name the Day and the actors involved: {actors_str}. " + f"The question is about: {domain_hint}. " + f"Do not name specific artifact IDs or the causal mechanism label. " + f"Do not reveal the answer or make the causal link obvious from the question text. " + f"The question should require investigation to answer. " + + (f"{day_context} " if day_context else "") + + "Output only the question text." + ) + + question_text = self._generate_and_validate_prose( + template=template, + ground_truth_str=link.counterfactual_outcome, + question_type="COUNTERFACTUAL", + ) + if not question_text: + return None + + return { + "question_id": f"counterfactual_{link.cause_event_id}_{link.link_type}", + "question_type": "COUNTERFACTUAL", + "difficulty": difficulty, + "link_type": link.link_type, + "day": link.day, + "actors": link.actors, + "subsystems_involved": sorted(link.subsystems_involved), + "question_text": question_text, + "ground_truth": ground_truth, + "evidence_chain": [link.cause_event_id, link.effect_event_id], + "requires_reasoning": True, + } + + def _silence_questions(self) -> List[dict]: + questions: List[dict] = [] + + target_false = self.MAX_SILENCE // 2 + target_true = self.MAX_SILENCE - target_false + + by_trigger: Dict[str, List[AbsenceRecord]] = defaultdict(list) + for record in self._absence_catalog: + by_trigger[record.trigger_event_type].append(record) + + n_triggers = len(by_trigger) + per_trigger = max(1, target_false // max(n_triggers, 1)) + + stratified: List[AbsenceRecord] = [] + for records in by_trigger.values(): + random.shuffle(records) + by_response: Dict[str, List[AbsenceRecord]] = defaultdict(list) + for r in records: + by_response[r.expected_response_type].append(r) + chosen: List[AbsenceRecord] = [] + response_groups = list(by_response.values()) + random.shuffle(response_groups) + for group in response_groups: + if len(chosen) >= per_trigger: + break + chosen.append(random.choice(group)) + stratified.extend(chosen) + + selected_ids = {id(r) for r in stratified} + remainder = [r for r in self._absence_catalog if id(r) not in selected_ids] + if len(stratified) < target_false and remainder: + extra = random.sample( + remainder, min(target_false - len(stratified), len(remainder)) + ) + stratified.extend(extra) + + random.shuffle(stratified) + false_pool = stratified[: target_false + 5] + + confirmed_pool = list(self._confirmed_catalog) + random.shuffle(confirmed_pool) + confirmed_pool = confirmed_pool[: target_true + 5] + + false_questions: List[dict] = [] + for record in false_pool: + if len(false_questions) >= target_false: + break + q = self._build_silence_question(record, exists=False) + if q: + false_questions.append(q) + + true_questions: List[dict] = [] + for record in confirmed_pool: + if len(true_questions) >= target_true: + break + q = self._build_silence_question(record, exists=True) + if q: + true_questions.append(q) + + questions = false_questions + true_questions + random.shuffle(questions) + + logger.info( + f"[eval] {len(questions)} SILENCE questions built " + f"({len(true_questions)} exists=true / {len(false_questions)} exists=false)" + ) + return questions + + def _build_silence_question( + self, record: AbsenceRecord, exists: bool = False + ) -> Optional[dict]: + + ground_truth = { + "answer": exists, + "absence_type": "state_machine_confirmed" + if not exists + else "state_machine_present", + "trigger_event_id": record.trigger_event_id, + "trigger_event_type": record.trigger_event_type, + "expected_response_type": record.expected_response_type, + "trigger_day": record.trigger_day, + "trigger_actors": record.trigger_actors, + "expected_search_space": record.expected_search_space, + "link_field": record.link_field, + "link_value": record.link_value, + } + + expected_desc = { + "postmortem_created": "a postmortem document", + "incident_resolved": "an incident resolution", + "zd_ticket_opened": "a Zendesk support ticket", + "customer_email_routed": "an internal routing of the customer email", + "confluence_created": "a Confluence documentation page", + "sf_ownership_lapsed": "a Salesforce ownership transfer", + "ticket_reassigned": "a Jira ticket reassignment", + "pr_merged": "a merged pull request", + "zd_tickets_escalated": "a Zendesk escalation", + "incident_opened": "an incident ticket", + "onboarding_session": "an onboarding session", + "warmup_1on1": "a warmup 1-on-1 meeting", + "sf_deals_risk_flagged": "a Salesforce risk flag on related deals", + "knowledge_gap_detected": "a formal knowledge gap detection event", + "jira_from_vendor_email": "a Jira ticket created from the vendor email", + }.get(record.expected_response_type, f"a {record.expected_response_type} event") + + trigger_desc = { + "incident_opened": f"the incident on Day {record.trigger_day}", + "customer_escalation": f"the customer escalation on Day {record.trigger_day}", + "inbound_external_email": f"the inbound email on Day {record.trigger_day}", + "design_discussion": f"the design discussion on Day {record.trigger_day}", + "knowledge_gap_detected": f"the knowledge gap detected on Day {record.trigger_day}", + "employee_departed": f"the employee departure on Day {record.trigger_day}", + "zd_tickets_escalated": f"the Zendesk escalation on Day {record.trigger_day}", + "pr_opened": f"the pull request opened on Day {record.trigger_day}", + "customer_email_routed": f"the routing of the customer email on Day {record.trigger_day}", + "sf_deals_risk_flagged": f"the CRM risk flagging on Day {record.trigger_day}", + "employee_hired": f"the hiring of {record.link_value} on Day {record.trigger_day}", + "assignment_domain_mismatch": f"the domain mismatch assignment flagged on Day {record.trigger_day}", + }.get(record.trigger_event_type, f"the event on Day {record.trigger_day}") + + actors_str = ( + ", ".join(record.trigger_actors[:2]) + if record.trigger_actors + else "the involved parties" + ) + + trigger_ev = next( + (e for e in self._events if e.mongo_id == record.trigger_event_id), + None, + ) + + if not trigger_ev: + logger.warning( + f"[eval] Skipping SILENCE question for unknown trigger type: {record.trigger_event_type}" + ) + return None + + _TRIVIAL_ENTRIES = {"jira", "confluence", "slack", "email", "git", "zendesk"} + effective_space = [ + e for e in record.expected_search_space if e not in _TRIVIAL_ENTRIES + ] + if not effective_space: + logger.debug( + f"[silence] Skipping {record.trigger_event_id} — " + f"expected_search_space is empty or trivially satisfied" + ) + return None + + if record.trigger_event_type == "knowledge_gap_detected": + detection_method = trigger_ev.facts.get("detection_method", "") + + if detection_method == "author_self_audit": + return None + + if trigger_ev.day < 1: + return None + + if detection_method == "reviewer_audit": + gap_topics = trigger_ev.facts.get("topics_beyond_author_expertise", []) + pr_title = trigger_ev.facts.get("pr_title", "") + gap_topic_str = ( + " and ".join(gap_topics) if gap_topics else record.link_value + ) + trigger_desc = ( + f"the knowledge gap detected on Day {record.trigger_day} involving {actors_str}, " + f"specifically regarding {gap_topic_str}" + + (f" identified during review of '{pr_title}'" if pr_title else "") + ) + + elif detection_method == "async_thread_classification": + gap_domain = trigger_ev.facts.get("gap_domain", record.link_value) + topic = trigger_ev.facts.get("topic", "") + trigger_desc = ( + f"the knowledge gap detected on Day {record.trigger_day} involving {actors_str}, " + f"specifically regarding {gap_domain}" + + (f" (topic: '{topic}')" if topic else "") + ) + + elif detection_method in ("embedding_similarity", "centrality_vacuum"): + gap_areas = trigger_ev.facts.get("gap_areas", []) + gap_topic_str = ", ".join(gap_areas) if gap_areas else record.link_value + trigger_desc = ( + f"the knowledge gap detected on Day {record.trigger_day} involving {actors_str}, " + f"specifically regarding {gap_topic_str}" + ) + + else: + trigger_desc = ( + f"the knowledge gap detected on Day {record.trigger_day} involving {actors_str}, " + f"specifically regarding {record.link_value}" + ) + + SILENCE_QUESTION_STYLES = [ + "Phrase it as a process compliance question a manager would ask.", + "Phrase it as an audit question checking whether a response was documented.", + "Phrase it as an operational question about whether follow-up occurred.", + "Phrase it as a gap analysis question about whether proper procedure was followed.", + ] + + style = random.choice(SILENCE_QUESTION_STYLES) + + template = ( + f"Write a yes/no question asking whether {expected_desc} was created " + f"in response to {trigger_desc} involving {actors_str}. " + f"CRITICAL: Only refer to the event exactly as described ('{trigger_desc}'). " + f"Do not call it an 'incident' or 'outage' unless those words are explicitly used. " + f"The question should require investigation to confirm or deny — do not imply the answer either way. " + f"Do not state or imply the answer. Do not include system IDs. " + f"Do NOT use phrases like 'as required by', 'as expected by', 'should have been', " + f"'was supposed to', or 'procedure requires' — these imply the answer. " + f"{style} " + f"Output only the question text." + ) + + question_text = self._generate_and_validate_prose( + template=template, + ground_truth_str=str(exists), + question_type="SILENCE", + ) + if not question_text: + return None + + return { + "question_id": f"silence_{record.trigger_event_id}_{record.expected_response_type}", + "question_type": "SILENCE", + "difficulty": "hard", + "trigger_event_id": record.trigger_event_id, + "trigger_event_type": record.trigger_event_type, + "trigger_day": record.trigger_day, + "expected_response_type": record.expected_response_type, + "subsystem": record.subsystem, + "question_text": question_text, + "ground_truth": ground_truth, + "expected_search_space": record.expected_search_space, + "requires_reasoning": True, + } + + def _event_description(self, event: SimEvent) -> str: + """Natural language description of an event for use in question templates.""" + descs = { + "incident_opened": lambda e: ( + f"a P1 incident ({e.facts.get('title', 'system incident')})" + ), + "customer_escalation": lambda e: ( + f"a customer escalation from {e.facts.get('customer', 'a customer')}" + ), + "sf_deals_risk_flagged": lambda e: ( + "Salesforce accounts being flagged at-risk" + ), + "knowledge_gap_detected": lambda e: ( + ( + f"a knowledge gap in {', '.join(e.facts.get('topics_beyond_author_expertise', ['an undocumented domain']))} " + f"identified during review of '{e.facts.get('pr_title', 'a pull request')}'" + ) + if e.facts.get("detection_method") + in ("reviewer_audit", "author_self_audit") + else ( + f"a knowledge gap regarding {e.facts.get('gap_domain', 'an undocumented domain')} " + f"surfaced in an async discussion about '{e.facts.get('topic', 'an unspecified topic')}'" + ) + if e.facts.get("detection_method") == "async_thread_classification" + else ( + f"a knowledge gap in {', '.join(e.facts.get('gap_areas', ['an undocumented domain']))} " + f"(triggered by {e.facts.get('triggered_by', 'unknown')}, " + f"left by {e.facts.get('departed_employee', 'a departed employee')}, " + f"detected via {e.facts.get('detection_method', 'automated analysis')})" + ) + ), + "employee_departed": lambda e: ( + f"the departure of {(e.actors or ['a team member'])[0]}" + ), + "design_discussion": lambda e: ( + f"a design discussion about {e.facts.get('topic', 'a technical topic')}" + ), + "customer_email_routed": lambda e: ( + "a customer email being routed to support" + ), + "zd_tickets_escalated": lambda e: ( + "Zendesk tickets being escalated to an incident" + ), + "sf_ownership_lapsed": lambda e: "Salesforce accounts losing their owner", + "postmortem_created": lambda e: "a postmortem being written", + "inbound_external_email": lambda e: ( + f"an inbound email from {e.facts.get('source', e.facts.get('sender', 'an external contact'))} " + f"({e.facts.get('org', 'external')}) " + f"regarding '{e.facts.get('subject', e.facts.get('topic', 'an unspecified topic'))[:80]}'" + ), + "pr_review": lambda e: ( + f"a {'change request' if e.facts.get('verdict') == 'changes_requested' else 'review'} " + f"by {e.facts.get('reviewer', 'a reviewer')} on " + f"'{e.facts.get('pr_title', 'an unnamed PR')[:80]}' " + f"authored by {e.facts.get('author', 'unknown')}: " + f'"{e.facts.get("review_text", "")[:120]}"' + ), + "employee_hired": lambda e: ( + f"{e.facts.get('name', 'a new hire')} joining {e.facts.get('dept', 'a department')} " + f"as {e.facts.get('role', 'an engineer')} with expertise in " + f"{', '.join(e.facts.get('expertise', ['unspecified']))}" + ), + "confluence_created": lambda e: ( + f"a {e.facts.get('type', 'document')} '{e.facts.get('title', 'untitled')}' " + f"created by {(e.actors or ['unknown'])[0]}" + + ( + f", spawning tickets {', '.join(e.facts['spawned_tickets'][:3])}" + if e.facts.get("spawned_tickets") + else "" + ) + + ( + f", updating domains: {', '.join(e.facts['domains_updated'][:3])}" + if e.facts.get("domains_updated") + else "" + ) + ), + "jira_ticket_created": lambda e: ( + f"a Jira ticket '{e.facts.get('title', 'untitled')[:80]}' opened by " + f"{(e.actors or ['unknown'])[0]}" + + ( + f" from a {e.facts.get('source', '').replace('_', ' ')}" + if e.facts.get("source") + else "" + ) + + ( + f" (vendor: {e.facts.get('vendor', '')})" + if e.facts.get("vendor") + else "" + ) + ), + "ticket_progress": lambda e: ( + f"ticket {e.facts.get('ticket_id', (e.artifact_ids or {}).get('jira', 'unknown'))} " + f"moved to '{e.facts.get('status', 'unknown status')}' " + f"by {(e.actors or ['unknown'])[0]}" + ), + "email_dropped": lambda e: ( + f"an unactioned email from {e.facts.get('source', 'an external contact')} " + f"with subject '{e.facts.get('subject', 'unspecified')[:80]}' " + f"(reason: {e.facts.get('reason', 'unknown').replace('_', ' ')})" + ), + "sales_outbound_email": lambda e: ( + f"an outbound sales email to {e.facts.get('account', 'a prospect')} " + f"sent by {(e.actors or ['a sales rep'])[0]}" + ), + "watercooler_chat": lambda e: ( + f"an informal chat between {' and '.join((e.actors or ['colleagues'])[:2])}" + ), + "vendor_email_routed": lambda e: ( + f"a vendor email from {e.facts.get('source', 'an external vendor')} " + f"being routed internally" + ), + } + fn = descs.get(event.type) + if fn: + try: + return fn(event) + except Exception: + pass + return f"a {event.type.replace('_', ' ')} event" + + def _generate_and_validate_prose( + self, + template: str, + ground_truth_str: str, + question_type: str, + max_attempts: int = 3, + ) -> Optional[str]: + """ + LLM writes question prose. Validates against structured rubric. + Retries up to max_attempts if validation fails. + + Validation rules: + 1. Must end with '?' + 2. Must not contain ground_truth_str verbatim + 3. Must not contain raw artifact IDs (pattern: XX-\\d+ or [a-f0-9]{8,}) + 4. Must be between 15 and 120 words + 5. Must contain at least one of: actor name, day reference, subsystem word + """ + agent = make_agent( + role="Eval Dataset Author", + goal="Write natural-sounding evaluation questions for AI agent benchmarks.", + backstory=( + "You write clear, specific questions for evaluating AI agents on reasoning " + "tasks. Questions must be unambiguous, naturally phrased, and answerable " + "only through careful reasoning over a corporate document corpus." + ), + llm=self._worker_llm, + ) + + for attempt in range(max_attempts): + retry_note = ( + " Previous attempt failed validation. Make sure the question: " + "ends with '?', does not reveal the answer, avoids artifact IDs, " + "and is 15-120 words long." + if attempt > 0 + else "" + ) + task = Task( + description=template + retry_note, + expected_output="One question ending with a question mark. No preamble or explanation.", + agent=agent, + ) + try: + result = str( + Crew(agents=[agent], tasks=[task], verbose=False).kickoff() + ).strip() + + if self._validate_prose(result, ground_truth_str, question_type): + return result + else: + logger.debug( + f"[eval] Prose validation failed (attempt {attempt + 1}): {result[:80]}" + ) + except Exception as exc: + logger.warning( + f"[eval] Prose generation error (attempt {attempt + 1}): {exc}" + ) + + return None + + def _validate_prose( + self, text: str, ground_truth_str: str, question_type: str + ) -> bool: + normalized = ( + text.replace("\u2010", "-") + .replace("\u2011", "-") + .replace("\u2012", "-") + .replace("\u2013", "-") + ) + + if not normalized.endswith("?"): + return False + + words = normalized.split() + if len(words) < 10 or len(words) > 150: + return False + + gt_lower = ground_truth_str.lower() + if gt_lower in normalized.lower() and len(gt_lower) > 4: + return False + + if re.search(r"\b[A-Z]{1,4}-\d{2,6}\b", normalized): + return False + if re.search(r"\b[a-f0-9]{8,}\b", normalized): + return False + + if question_type == "PERSPECTIVE": + if not re.search( + r"day\s+\d+|as of|by\s+[A-Z][a-z]+", normalized, re.IGNORECASE + ): + return False + + if question_type == "COUNTERFACTUAL": + if not re.search( + r"\b(if|had|would|could|might|hypothetically)\b", + normalized, + re.IGNORECASE, + ): + return False + + if question_type == "SILENCE": + if not re.search( + r"\b(was|were|did|has|have|is|are)\b", normalized, re.IGNORECASE + ): + return False + + _SILENCE_LEAK_PHRASES = ( + "as required by", + "as expected by", + "should have been", + "was supposed to", + "procedure requires", + "protocol requires", + "per the", + "as mandated", + ) + if any(phrase in normalized.lower() for phrase in _SILENCE_LEAK_PHRASES): + return False + + return True + + +class EvalHarness: + """ + Orchestrates the full eval dataset generation pipeline: + 1. Build actor visibility cones (ActorVisibilityBuilder) + 2. Index explicit causal links (CausalLinkIndexer) + 3. Catalog expected-but-absent artifacts (AbsenceCatalogBuilder) + 4. Generate eval questions (EvalQuestionGenerator) + 5. Write all intermediate structures + final questions to export/eval/ + """ + + def __init__(self): + from flow import build_llm + + self._mem = Memory() + self._worker_llm = build_llm("worker") + self._leads = list(_CFG.get("leads", {}).values()) + + def run(self) -> None: + logger.info("[bold cyan]🔬 Building OrgForge eval dataset...[/bold cyan]") + + logger.info("[eval] Building actor visibility cones...") + vis_builder = ActorVisibilityBuilder(self._mem) + visibility_map = vis_builder.build_all() + vis_export = { + actor: cones[-1].to_dict() + for actor, cones in visibility_map.items() + if cones + } + vis_path = EVAL_DIR / "actor_visibility.json" + with open(vis_path, "w") as f: + json.dump(vis_export, f, indent=2, default=str) + logger.info(f" → {vis_path} ({len(visibility_map)} actors)") + + logger.info("[eval] Indexing explicit causal links...") + link_indexer = CausalLinkIndexer(self._mem) + causal_links = link_indexer.build() + links_path = EVAL_DIR / "causal_link_index.json" + with open(links_path, "w") as f: + json.dump([lnk.to_dict() for lnk in causal_links], f, indent=2, default=str) + logger.info(f" → {links_path} ({len(causal_links)} links)") + + logger.info("[eval] Building absence catalog...") + absence_builder = AbsenceCatalogBuilder(self._mem) + absence_catalog, confirmed_catalog = absence_builder.build() + absence_path = EVAL_DIR / "absence_catalog.json" + with open(absence_path, "w") as f: + json.dump([r.to_dict() for r in absence_catalog], f, indent=2, default=str) + logger.info(f" → {absence_path} ({len(absence_catalog)} absence records)") + + confirmed_path = EVAL_DIR / "confirmed_catalog.json" + with open(confirmed_path, "w") as f: + json.dump( + [r.to_dict() for r in confirmed_catalog], f, indent=2, default=str + ) + logger.info( + f" → {confirmed_path} ({len(confirmed_catalog)} confirmed records)" + ) + + """ logger.info("[eval] Building GRAPH track...") + graph_snapshots, graph_questions = build_graph_track( + mem=self._mem, + worker_llm=self._worker_llm, + leads=self._leads, + eval_dir=EVAL_DIR, + ) + logger.info( + f" → {len(graph_snapshots)} graph snapshots, {len(graph_questions)} GRAPH questions" + ) """ + + logger.info("[eval] Generating eval questions...") + generator = EvalQuestionGenerator( + mem=self._mem, + worker_llm=self._worker_llm, + visibility_map=visibility_map, + causal_links=causal_links, + absence_catalog=absence_catalog, + confirmed_catalog=confirmed_catalog, + ) + questions = generator.generate() + # questions.extend(graph_questions) + random.shuffle(questions) + + by_type: Dict[str, int] = defaultdict(int) + by_difficulty: Dict[str, int] = defaultdict(int) + cross_subsystem_count = 0 + for q in questions: + by_type[q["question_type"]] += 1 + by_difficulty[q["difficulty"]] += 1 + if q.get("cross_subsystem"): + cross_subsystem_count += 1 + + questions_path = EVAL_DIR / "eval_questions.json" + with open(questions_path, "w") as f: + json.dump( + { + "metadata": { + "generated_at": datetime.now().isoformat(), + "version": "2.0", + "tracks": ["PERSPECTIVE", "COUNTERFACTUAL", "SILENCE", "GRAPH"], + "total_questions": len(questions), + "by_type": dict(by_type), + "by_difficulty": dict(by_difficulty), + "cross_subsystem_questions": cross_subsystem_count, + "actors_with_visibility_cones": len(visibility_map), + "causal_links_indexed": len(causal_links), + "absence_records": len(absence_catalog), + # "graph_snapshots": len(graph_snapshots), + }, + "questions": questions, + }, + f, + indent=2, + default=str, + ) + + logger.info(f" → {questions_path}") + logger.info( + f"[green]✓ Eval dataset complete.[/green] " + f"Types: {dict(by_type)} | Difficulty: {dict(by_difficulty)} | " + f"Cross-subsystem: {cross_subsystem_count}" + ) + + +if __name__ == "__main__": + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + ) + EvalHarness().run() diff --git a/eval/export_to_hf.py b/eval/export_to_hf.py index 5110493..53ff6f2 100644 --- a/eval/export_to_hf.py +++ b/eval/export_to_hf.py @@ -11,8 +11,8 @@ ------------- export/hf_dataset/ corpus/ - corpus-00000.parquet — flat document corpus (one row per artifact) - README.md — HuggingFace dataset card + corpus-00000.parquet - flat document corpus (one row per artifact) + README.md - HuggingFace dataset card """ from __future__ import annotations @@ -23,7 +23,7 @@ from collections import defaultdict from datetime import datetime from pathlib import Path -from typing import Any, Dict, List +from typing import Dict, List import email as email_lib from email.header import decode_header import shutil @@ -85,7 +85,7 @@ except ImportError: _PARQUET_AVAILABLE = False logger.warning( - "pandas/pyarrow not installed — Parquet output disabled. " + "pandas/pyarrow not installed - Parquet output disabled. " "pip install pandas pyarrow" ) @@ -111,10 +111,6 @@ def _dept_from_artifact_id(artifact_id: str) -> str: }.get(code, "") -import email as email_lib -from email.header import decode_header - - def _parse_eml(eml_path: Path) -> dict: """Parse a .eml file and return headers + decoded body.""" raw = eml_path.read_text(encoding="utf-8", errors="replace") @@ -165,7 +161,6 @@ def _load_confluence_from_disk() -> List[dict]: text = p.read_text(encoding="utf-8", errors="replace") lines = text.splitlines() - # Parse header fields doc_id = p.stem title = "" author = "" @@ -201,7 +196,7 @@ def _load_confluence_from_disk() -> List[dict]: } ) except Exception as exc: - logger.warning(f" confluence disk read failed: {p} — {exc}") + logger.warning(f" confluence disk read failed: {p} - {exc}") logger.info(f" confluence disk fallback: {len(rows)} pages loaded") return rows @@ -235,7 +230,7 @@ def _load_slack_from_disk() -> List[dict]: if text: bucket["texts"].append(f"{user}: {text}" if user else text) except Exception as exc: - logger.warning(f" slack disk read failed: {p} — {exc}") + logger.warning(f" slack disk read failed: {p} - {exc}") rows = [] for tid, bucket in buckets.items(): @@ -343,7 +338,7 @@ def build(self) -> List[dict]: # Deduplication strategy: # - For artifact doc_ids (jira, confluence, slack, etc.): keep the row - # with the longest body — the MongoDB-enriched version wins over the + # with the longest body - the MongoDB-enriched version wins over the # thin SimEvent version. # - Internal event rows (EVT-* doc_ids) are unique by construction and # never conflict with artifact rows, so they pass through intact. @@ -626,7 +621,7 @@ def _sim_event_to_row( "category": "artifact", "title": str( facts.get("account_name", opp_id) - + " — " + + " - " + facts.get("stage", "") )[:512], "body": self._sf_opp_body(facts), @@ -649,7 +644,7 @@ def _sim_event_to_row( } ) - # ── Internal event row — ALWAYS emitted, even when artifact rows exist ─ + # ── Internal event row - ALWAYS emitted, even when artifact rows exist ─ # Preserves the full ground-truth facts (stress snapshots, similarity # scores, coverage percentages, departure edge snapshots, etc.) as a # separately retrievable document. Filter by category == "sim_event" @@ -776,7 +771,7 @@ def _sf_account_body(self, facts: dict, acc_id: str) -> str: accounts_lapsed = facts.get("accounts_lapsed", []) if acc_id in accounts_lapsed: parts.append(f"account_id: {acc_id}") - parts.append("status: ownership lapsed — pending reassignment") + parts.append("status: ownership lapsed - pending reassignment") opps_lapsed = facts.get("opportunities_lapsed", []) if opps_lapsed: parts.append( @@ -812,7 +807,7 @@ def _plans_to_corpus_rows(self) -> List[dict]: { "doc_id": plan_id, "doc_type": "dept_plan", - "title": f"{dept} plan — Day {day}", + "title": f"{dept} plan - Day {day}", "body": body, "day": day, "date": plan["date"], @@ -835,7 +830,7 @@ def _plans_to_corpus_rows(self) -> List[dict]: { "doc_id": f"{plan_id}-reasoning", "doc_type": "dept_plan_reasoning", - "title": f"{dept} planner reasoning — Day {day}", + "title": f"{dept} planner reasoning - Day {day}", "body": reasoning, "day": day, "date": plan["date"], @@ -1133,7 +1128,7 @@ def _sf_account_body(doc): f"primary_contact: {doc.get('primary_contact', '')}", ] if doc.get("risk_flag"): - parts.append("risk_flag: true — ownership lapsed or at-risk") + parts.append("risk_flag: true - ownership lapsed or at-risk") return "\n".join(p for p in parts if p) jira_rich, jira_meta = self._build_rich_and_meta( @@ -1383,7 +1378,7 @@ def _make_orphan_row( ) existing_ids.add(aid) - # slack_messages — bucket by thread_id + # slack_messages - bucket by thread_id thread_buckets: Dict[str, dict] = {} for msg in self._mem._db["slack_messages"].find({}, {"embedding": 0}): tid = msg.get("thread_id", "") @@ -1441,7 +1436,7 @@ def _compute_corpus_stats(corpus: List[dict], cfg: dict, mem=None) -> dict: """ Derives everything the dataset card needs from the corpus + config. - mem is optional — if provided, the raw SimEvent count is read from MongoDB + mem is optional - if provided, the raw SimEvent count is read from MongoDB so the card shows total events alongside deduplicated corpus documents. """ sim_cfg = cfg.get("simulation", {}) @@ -1478,7 +1473,7 @@ def _compute_corpus_stats(corpus: List[dict], cfg: dict, mem=None) -> dict: # Most SimEvents are internal state-machine events (day_summary, # knowledge_gap_detected, proposed_event_rejected, etc.) that do not # map 1:1 to a corpus artifact. The corpus is the deduplicated set of - # *artifacts* those events produced — which is why corpus doc count + # *artifacts* those events produced - which is why corpus doc count # will always be much smaller than the raw event count. sim_events_total = None sim_days_actual = None @@ -1580,7 +1575,7 @@ class DatasetCardWriter: """ Produces the HuggingFace README.md dataset card. - Tells the story of the corpus first — what it is, why the ground truth + Tells the story of the corpus first - what it is, why the ground truth is trustworthy, and what makes this dataset structurally different from other synthetic benchmarks. Artifact counts and schema follow. """ @@ -1662,6 +1657,10 @@ def _render(self, stats: dict, cfg: dict) -> str: " data_files:\n" " - split: train\n" ' path: "corpus/*.parquet"\n' + "- config_name: questions\n" + " data_files:\n" + " - split: test\n" + ' path: "questions/*.jsonl"\n' "task_categories:\n" "- question-answering\n" "- text-retrieval\n" @@ -1688,7 +1687,8 @@ def _render(self, stats: dict, cfg: dict) -> str: "- temporal-reasoning\n" "- knowledge-graphs\n" "- agentic-eval\n" - f'pretty_name: "OrgForge — {company} Enterprise Corpus"\n' + "- benchmark\n" + 'pretty_name: "OrgForge EpistemicBench"\n' "size_categories:\n" "- 1K str: sections = [] # ── Title + pitch ────────────────────────────────────────────────────── - sections.append(f"# OrgForge — {company} Enterprise Corpus") + sections.append("# OrgForge EpistemicBench") sections.append("") sections.append("![OrgForge corpus overview](orgforge_dataset_hero.png)") sections.append("") sections.append( "OrgForge generates synthetic but causally grounded enterprise corpora from a\n" - "deterministic simulation engine. Every artifact in this dataset — Jira tickets,\n" + "deterministic simulation engine. Every artifact in this dataset, Jira tickets,\n" "Slack threads, Confluence pages, customer emails, Zendesk tickets, invoices, Zoom\n" - "transcripts, Datadog alerts — traces back to a single event log. No LLM invented\n" + "transcripts, Datadog alerts, traces back to a single event log. No LLM invented\n" "any facts. The state machine controls what happened; LLMs only wrote the prose." ) sections.append("") @@ -1726,7 +1726,7 @@ def _render(self, stats: dict, cfg: dict) -> str: sections.append( f"This dataset is the output of a **{num_days}-day simulation** of **{company}**, a\n" f"{industry} company which {company_description} with ~{org_size} employees. It is not a random walk through\n" - "enterprise activity — it was seeded with specific organizational crises and simulated\n" + "enterprise activity - it was seeded with specific organizational crises and simulated\n" "through to their resolution." ) sections.append("") @@ -1738,7 +1738,7 @@ def _render(self, stats: dict, cfg: dict) -> str: "**Causal grounding.** Every artifact is downstream of a SimEvent. A Jira ticket,\n" "the Slack thread that opened alongside it, the Confluence postmortem written the\n" "next day, and the Zendesk tickets that escalated from the same incident all share\n" - "a causal ancestor. Cross-referencing between artifact types is not coincidental —\n" + "a causal ancestor. Cross-referencing between artifact types is not coincidental -\n" "it reflects the actual information flow the simulation produced." ) sections.append("") @@ -1754,9 +1754,9 @@ def _render(self, stats: dict, cfg: dict) -> str: sections.append( "**Verifiable ground truth.** The simulation snapshot and domain registry ship\n" "alongside the corpus as structured reference files (see Supplemental Files). For\n" - "any question the corpus can raise — who owned this domain when this incident fired,\n" + "any question the corpus can raise, who owned this domain when this incident fired,\n" "which customer was affected, what was the system health on the day this postmortem\n" - "was written — the answer exists as a queryable record independent of the text. You\n" + "was written, the answer exists as a queryable record independent of the text. You\n" "do not need to parse the corpus to build your eval set." ) sections.append("") @@ -1773,7 +1773,7 @@ def _render(self, stats: dict, cfg: dict) -> str: "**State-driven external communication.** Customer emails, vendor alerts, and\n" "Zendesk tickets are generated from actual simulation conditions, not randomly\n" "sampled. Each external contact has a `depends_on_components` list mapped to the\n" - "tech stack — an outage to a component a customer depends on is what triggers their\n" + "tech stack, an outage to a component a customer depends on is what triggers their\n" "email. Approximately 15% of customer emails are deliberately dropped with no action,\n" "leaving ground-truth absences in the event log that an agent must detect through\n" "negative evidence rather than positive retrieval." @@ -1792,17 +1792,17 @@ def _render(self, stats: dict, cfg: dict) -> str: sections.append("## Use cases") sections.append("") sections.append( - "- **Agentic reasoning** — tasks that require traversing causal chains across\n" + "- **Agentic reasoning** - tasks that require traversing causal chains across\n" " artifact types, time, and org boundaries rather than finding a single relevant\n" " document\n" - "- **Multi-hop question answering** — questions whose correct answer requires\n" + "- **Multi-hop question answering** - questions whose correct answer requires\n" " joining facts from Jira, Confluence, Slack, CRM, and the simulation ground truth\n" - "- **Temporal reasoning** — questions where the correct answer depends on what day\n" + "- **Temporal reasoning** - questions where the correct answer depends on what day\n" " they are asked relative to the simulation timeline\n" - "- **RAG pipeline evaluation** — a corpus with known causal structure allows\n" + "- **RAG pipeline evaluation** - a corpus with known causal structure allows\n" " precise measurement of what a retrieval system found versus what it needed to\n" " find to answer correctly\n" - "- **Org dynamics and knowledge loss research** — the simulation snapshot exposes\n" + "- **Org dynamics and knowledge loss research** - the simulation snapshot exposes\n" " how knowledge concentration, engineer departure, and incident causation interact\n" " over time in a controlled, reproducible setting" ) @@ -1814,8 +1814,8 @@ def _render(self, stats: dict, cfg: dict) -> str: sections.append( "This is not a dataset of real corporate communications. The company, employees,\n" "customers, and vendors are entirely fictional. The simulation models organizational\n" - "behavior at the structural level — stress, knowledge concentration, incident\n" - "causation, relationship graph dynamics — but does not model everything. Affect,\n" + "behavior at the structural level - stress, knowledge concentration, incident\n" + "causation, relationship graph dynamics - but does not model everything. Affect,\n" "politics, ambiguity, and the texture of real human communication are present only\n" "to the extent that the persona and mood system introduces them through LLM-generated\n" "prose. Researchers should treat this as a controlled benchmark environment, not a\n" @@ -1831,7 +1831,7 @@ def _render(self, stats: dict, cfg: dict) -> str: sections.append( "These gaps pre-date the simulation. They are the structural cause of the\n" "organizational narrative in this corpus. Each departed employee's domains entered\n" - "Day 1 as orphaned — undocumented, unowned, and detectable only through semantic\n" + "Day 1 as orphaned - undocumented, unowned, and detectable only through semantic\n" "similarity when new incidents touch the same systems." ) sections.append("") @@ -1909,7 +1909,7 @@ def _render(self, stats: dict, cfg: dict) -> str: ) sections.append("") sections.append( - "**`simulation_snapshot.json`** — Full org state at simulation end: incidents with\n" + "**`simulation_snapshot.json`** - Full org state at simulation end: incidents with\n" "open/resolve timestamps, morale curve, daily system health scores, relationship\n" "graph edge weights, departed employees, new hires, and knowledge gap events. This\n" "is the oracle for eval construction. Use it to build questions with verifiable\n" @@ -1917,7 +1917,7 @@ def _render(self, stats: dict, cfg: dict) -> str: ) sections.append("") sections.append( - "**`assignment_scores.parquet`** — Per-sprint ticket assignment decisions with full\n" + "**`assignment_scores.parquet`** - Per-sprint ticket assignment decisions with full\n" "scoring breakdown: skill match (embedding cosine similarity), inverse stress, \n" "betweenness centrality penalty, recency bonus, and composite score. One row per\n" "(engineer, ticket, day) triple. Useful for eval questions about whether assignments\n" @@ -1925,21 +1925,21 @@ def _render(self, stats: dict, cfg: dict) -> str: ) sections.append("") sections.append( - "**`domain_registry.json`** — Snapshot of all knowledge domains: owner history,\n" + "**`domain_registry.json`** - Snapshot of all knowledge domains: owner history,\n" "documentation coverage scores at each sim day, orphan status, and which incidents\n" "triggered semantic similarity matches against each domain. Joinable to corpus rows\n" "via the Confluence `doc_id` values that cover each domain." ) sections.append("") sections.append( - "**`sim_config.json`** — Reference record for the org configuration: full customer\n" + "**`sim_config.json`** - Reference record for the org configuration: full customer\n" "and vendor profiles (including `depends_on_components`, `sentiment_baseline`,\n" "`trigger_on` conditions, and `persona_archetype`), tech stack, and org structure.\n" "Useful for understanding why specific external communications were generated." ) sections.append("") sections.append( - "**`datadog_metrics.parquet`** — Time-series telemetry at 15-minute intervals\n" + "**`datadog_metrics.parquet`** - Time-series telemetry at 15-minute intervals\n" "across the simulation. Schema: `timestamp`, `metric_name`, `value`, `day`,\n" "`alert_firing` (bool). Kept separate from the corpus because individual metric\n" "ticks are not retrievable text documents. Datadog *alerts* are in the main corpus\n" @@ -2016,11 +2016,15 @@ def _render(self, stats: dict, cfg: dict) -> str: # ── Citation + license ───────────────────────────────────────────────── sections.append("## Citation") sections.append("") - sections.append("If you use the OrgForge methodology or simulator, cite the paper:") + sections.append( + "If you use the OrgForge methodology or simulator, cite the paper:" + ) sections.append("") sections.append("```bibtex") sections.append("@misc{flynt2026orgforge,") - sections.append(" title = {OrgForge: A Multi-Agent Simulation Framework for Verifiable Synthetic Corporate Corpora},") + sections.append( + " title = {OrgForge: A Multi-Agent Simulation Framework for Verifiable Synthetic Corporate Corpora}," + ) sections.append(" author = {Jeffrey Flynt},") sections.append(" year = {2026},") sections.append(" url = {https://arxiv.org/abs/2603.14997},") @@ -2032,10 +2036,12 @@ def _render(self, stats: dict, cfg: dict) -> str: sections.append("") sections.append("```bibtex") sections.append("@misc{flynt2026orgforgedata,") - sections.append(f" title = {{OrgForge — {company} Enterprise Corpus}},") + sections.append(" title = {OrgForge EpistemicBench},") sections.append(" author = {Jeffrey Flynt},") sections.append(" year = {2026},") - sections.append(" url = {https://huggingface.co/datasets/aeriesec/orgforge},") + sections.append( + " url = {https://huggingface.co/datasets/aeriesec/orgforge}," + ) sections.append(" note = {Dataset generated by the OrgForge simulator}") sections.append("}") sections.append("```") @@ -2096,7 +2102,7 @@ def _corpus_schema_table(self, sim_event_counts: Dict[str, int] = None) -> str: | Column | Type | Description | |---|---|---| | `doc_id` | str | Unique artifact ID (e.g. `IT-042`, `CONF-ENG-007`, `PR-031`) | - | `doc_type` | str | Artifact: {artifact_types} — SimEvent: {sim_event_types} | + | `doc_type` | str | Artifact: {artifact_types} - SimEvent: {sim_event_types} | | `category` | str | `artifact` \\| `sim_event` \\| `sim_config` | | `title` | str | Human-readable title or subject line | | `body` | str | Full text content | @@ -2123,7 +2129,7 @@ def _write_parquet(rows: List[dict], out_dir: Path, stem: str = "corpus-00000") with open(out_path, "w") as f: json.dump(rows, f, indent=2, default=str) logger.info( - f" → {out_path} (JSON fallback — install pandas+pyarrow for Parquet)" + f" → {out_path} (JSON fallback - install pandas+pyarrow for Parquet)" ) return @@ -2160,9 +2166,20 @@ def _write_hero_image(self, out_dir: Path) -> None: logger.info(" → orgforge_dataset_hero.png") else: logger.warning( - " orgforge_dataset_hero.png not found next to script — skipping" + " orgforge_dataset_hero.png not found next to script - skipping" ) + def _write_questions(self, out_dir: Path) -> None: + questions_dir = out_dir / "questions" + questions_dir.mkdir(parents=True, exist_ok=True) + + src = BASE / "eval" / "eval_questions.jsonl" + if src.exists(): + shutil.copy2(src, questions_dir / "eval_questions.jsonl") + logger.info(" → questions/eval_questions.jsonl") + else: + logger.warning(" eval_questions.jsonl not found next to script - skipping") + def _write_supplemental(self, mem, out_dir: Path) -> None: supp_dir = out_dir / "supplemental" supp_dir.mkdir(parents=True, exist_ok=True) @@ -2170,9 +2187,9 @@ def _write_supplemental(self, mem, out_dir: Path) -> None: snap_src = BASE / "simulation_snapshot.json" if snap_src.exists(): shutil.copy2(snap_src, supp_dir / "simulation_snapshot.json") - logger.info(f" → supplemental/simulation_snapshot.json") + logger.info(" → supplemental/simulation_snapshot.json") else: - logger.warning(" simulation_snapshot.json not found — skipping") + logger.warning(" simulation_snapshot.json not found - skipping") if mem is not None: try: @@ -2199,7 +2216,7 @@ def _write_supplemental(self, mem, out_dir: Path) -> None: ) elif dd_metrics.exists(): shutil.copy2(dd_metrics, supp_dir / "datadog_metrics.jsonl") - logger.info(f" → supplemental/datadog_metrics.jsonl (parquet unavailable)") + logger.info(" → supplemental/datadog_metrics.jsonl (parquet unavailable)") try: scores = list( @@ -2223,7 +2240,7 @@ def _write_supplemental(self, mem, out_dir: Path) -> None: (supp_dir / "sim_config.json").write_text( json.dumps(sim_config_out, indent=2, default=str) ) - logger.info(f" → supplemental/sim_config.json") + logger.info(" → supplemental/sim_config.json") except Exception as exc: logger.warning(f" sim_config export failed: {exc}") @@ -2247,7 +2264,7 @@ def run(self) -> None: ) corpus = corpus_builder.build() if not corpus: - logger.warning(" Empty corpus — check that flow.py has run first.") + logger.warning(" Empty corpus - check that flow.py has run first.") return counts = corpus_builder.artifact_counts(corpus) @@ -2261,6 +2278,8 @@ def run(self) -> None: self._write_hero_image(HF_DIR) + self._write_questions(HF_DIR) + DatasetCardWriter().write( out_path=HF_DIR / "README.md", corpus=corpus, diff --git a/eval/graph_eval_track.py b/eval/graph_eval_track.py new file mode 100644 index 0000000..6afbbb2 --- /dev/null +++ b/eval/graph_eval_track.py @@ -0,0 +1,1192 @@ +""" +graph_eval_track.py +=================== +TRACK 4 — GRAPH + +Eval questions sourced from the social graph and stress system. +This module is designed to be imported by eval_harness.py and +agentic_eval_harness.py — it does not run standalone. + +Ground truth sources (all from MongoDB `checkpoints` collection): + - checkpoint["stress"] → {name: int} stress score per actor per day + - checkpoint["graph"] → nx.node_link_data() — full edge-weight graph + - checkpoint["state"]["morale"] → org morale float + - checkpoint["state"]["health"] → system health int + +Three question subtypes: + + GRAPH/BURNOUT_PROPAGATION + "Given the stress scores and graph weights on Day N, which actor + absorbed the most stress bleed from a burnt-out key player?" + Ground truth: propagation_result derived from PropagationResult-equivalent + logic run against the checkpoint graph. No LLM inference needed. + + GRAPH/CENTRALITY_SHIFT + "Between Day A and Day B, which actor's betweenness centrality changed + the most, and what event explains the shift?" + Ground truth: delta = |centrality_B[actor] - centrality_A[actor]|, actor + with max delta. Corroborating event from SimEvent log (departure, hire, + incident that boosted edges). + + GRAPH/ESTRANGEMENT + "Which pair of actors had the lowest relationship weight by Day N, and + were they ever on the same incident?" + Ground truth: estranged pair = argmin(edge weight), corroborated by + checking incident_opened events for shared actors. + + GRAPH/ESCALATION_PATH + "On Day N, if a P1 incident had been assigned to actor X, who would + have been the next node in the Dijkstra escalation chain?" + Ground truth: re-run Dijkstra on checkpoint graph, return second node + in chain (the first hop from X to the nearest Lead). + +Scoring weights (registered in agentic_eval_harness.py): + answer 0.50 — the graph math is exact; a wrong answer is wrong + trajectory 0.50 — agent must retrieve checkpoint/day data, not guess + +Agent tool available for this track: + get_graph_snapshot(day: int) → checkpoint["graph"] + checkpoint["stress"] + This is added to GatedTools in agentic_eval_harness.py via the + _register_graph_tool() hook below. +""" + +from __future__ import annotations + +import json +import logging +import random +from collections import defaultdict +from dataclasses import dataclass, asdict +from pathlib import Path +from typing import Any, Dict, List, Optional, Set, Tuple + +logger = logging.getLogger("orgforge.eval.graph") + +# ── burnout threshold mirrors graph_dynamics.py DEFAULT_CFG ────────────────── +_BURNOUT_THRESHOLD = 72 +_STRESS_BLEED_RATE = 0.25 +_KEY_PLAYER_MULTIPLIER = 2.0 +_EDGE_WEIGHT_FLOOR = 0.5 + +_MAX_QUESTIONS_PER_SUBTYPE = 4 + + +# ───────────────────────────────────────────────────────────────────────────── +# DATA CLASSES +# ───────────────────────────────────────────────────────────────────────────── + + +@dataclass +class GraphSnapshot: + """ + One checkpoint reconstructed as a queryable in-memory graph. + We import networkx lazily so this module works even if nx is not + installed at eval-generation time (the evaluator doesn't need it). + """ + + day: int + stress: Dict[str, int] # {name: score} + edges: List[Tuple[str, str, float]] # (u, v, weight) + nodes: List[str] + morale: float + health: int + + @classmethod + def from_checkpoint(cls, doc: dict) -> "GraphSnapshot": + graph_data = doc.get("graph", {}) + stress = doc.get("stress", {}) + state = doc.get("state", {}) + + nodes: List[str] = [] + for n in graph_data.get("nodes", []): + nid = n.get("id") or n.get("name") + if nid: + nodes.append(str(nid)) + + edges: List[Tuple[str, str, float]] = [] + for e in graph_data.get("links", []) or graph_data.get("edges", []): + # node_link_data stores source/target as node-list indices or ids + src = e.get("source") + tgt = e.get("target") + if src is None or tgt is None: + continue + # If stored as int indices, resolve to names + if isinstance(src, int) and src < len(nodes): + src = nodes[src] + if isinstance(tgt, int) and tgt < len(nodes): + tgt = nodes[tgt] + w = float(e.get("weight", 1.0)) + edges.append((str(src), str(tgt), w)) + + return cls( + day=doc.get("day", 0), + stress={str(k): int(v) for k, v in stress.items()}, + edges=edges, + nodes=nodes, + morale=float(state.get("morale", 0.5)), + health=int(state.get("health", 80)), + ) + + def centrality(self) -> Dict[str, float]: + """ + Betweenness centrality computed from stored edge weights. + Falls back to degree-based approximation if networkx is unavailable. + """ + try: + import networkx as nx + + G = self._to_nx() + return nx.betweenness_centrality(G, weight="weight", normalized=True) + except ImportError: + # Degree-based proxy: (sum of neighbour weights) / total weight + neighbour_weight: Dict[str, float] = defaultdict(float) + total_weight = 0.0 + for u, v, w in self.edges: + neighbour_weight[u] += w + neighbour_weight[v] += w + total_weight += w + if total_weight == 0: + return {n: 0.0 for n in self.nodes} + return {n: neighbour_weight[n] / total_weight for n in self.nodes} + + def dijkstra_next_hop( + self, + source: str, + leads: List[str], + ) -> Optional[str]: + """ + Return the first hop from source toward the nearest lead. + Returns None if no path exists. + """ + try: + import networkx as nx + + G = self._to_nx() + best_hop: Optional[str] = None + best_cost = float("inf") + for lead in leads: + if lead == source or lead not in G: + continue + if source not in G: + continue + try: + path = nx.dijkstra_path( + G, + source, + lead, + weight=lambda u, v, d: 1.0 / max(d.get("weight", 1.0), 0.01), + ) + cost = nx.dijkstra_path_length( + G, + source, + lead, + weight=lambda u, v, d: 1.0 / max(d.get("weight", 1.0), 0.01), + ) + if len(path) >= 2 and cost < best_cost: + best_cost = cost + best_hop = path[1] + except (nx.NetworkXNoPath, nx.NodeNotFound): + continue + return best_hop + except ImportError: + # Fallback: return highest-weight neighbour of source that is a lead + neighbour_weights = {v: w for u, v, w in self.edges if u == source} + neighbour_weights.update({u: w for u, v, w in self.edges if v == source}) + lead_neighbours = {n: w for n, w in neighbour_weights.items() if n in leads} + if lead_neighbours: + return max(lead_neighbours, key=lead_neighbours.get) + # No direct lead neighbour — return highest-weight neighbour + if neighbour_weights: + return max(neighbour_weights, key=neighbour_weights.get) + return None + + def estranged_pairs(self, top_n: int = 5) -> List[Tuple[str, str, float]]: + """Pairs with the lowest edge weight (decay near floor).""" + sorted_edges = sorted(self.edges, key=lambda x: x[2]) + # Exclude self-loops and external nodes with no internal-only edges + internal = [ + (u, v, w) + for u, v, w in sorted_edges + if u != v and u in self.stress and v in self.stress + ] + return internal[:top_n] + + def simulate_stress_propagation( + self, + ) -> Dict[str, Any]: + """ + One tick of PropagationResult-equivalent logic derived from + graph_dynamics.py propagate_stress(). Returns a dict with: + key_players — actors above the centrality cutoff + burnt_out — actors whose stress >= _BURNOUT_THRESHOLD + bleed_targets — {actor: stress_absorbed_from_key_players} + max_bleed_target — actor that absorbed the most stress + """ + centrality = self.centrality() + scores = sorted(centrality.values()) + if not scores: + return { + "key_players": [], + "burnt_out": [], + "bleed_targets": {}, + "max_bleed_target": None, + } + + median = scores[len(scores) // 2] + cutoff = median * _KEY_PLAYER_MULTIPLIER + key_players = [n for n, c in centrality.items() if c >= cutoff] + burnt_out = [n for n, s in self.stress.items() if s >= _BURNOUT_THRESHOLD] + + # Build adjacency for bleed computation + adj: Dict[str, List[Tuple[str, float]]] = defaultdict(list) + for u, v, w in self.edges: + adj[u].append((v, w)) + adj[v].append((u, w)) + + bleed_targets: Dict[str, float] = defaultdict(float) + + for kp in key_players: + kp_stress = self.stress.get(kp, 0) + if kp_stress < _BURNOUT_THRESHOLD: + continue + excess = kp_stress - _BURNOUT_THRESHOLD + neighbours = adj[kp] + total_w = sum(w for _, w in neighbours) + if total_w == 0: + continue + for nb, w in neighbours: + bleed = excess * _STRESS_BLEED_RATE * (w / total_w) + if bleed > 0: + bleed_targets[nb] += bleed + + max_bleed_target = ( + max(bleed_targets, key=bleed_targets.get) if bleed_targets else None + ) + + return { + "key_players": sorted(key_players), + "burnt_out": sorted(burnt_out), + "bleed_targets": dict(bleed_targets), + "max_bleed_target": max_bleed_target, + "max_bleed_amount": round(bleed_targets.get(max_bleed_target, 0), 2) + if max_bleed_target + else 0.0, + } + + def _to_nx(self): + import networkx as nx + + G = nx.Graph() + G.add_nodes_from(self.nodes) + for u, v, w in self.edges: + G.add_edge(u, v, weight=w) + return G + + def to_export_dict(self) -> dict: + """Serializable form for export/eval/graph_snapshots.json.""" + return { + "day": self.day, + "stress": self.stress, + "edges": [{"u": u, "v": v, "weight": w} for u, v, w in self.edges], + "nodes": self.nodes, + "morale": self.morale, + "health": self.health, + } + + +@dataclass +class GraphQuestion: + question_id: str + question_type: str # always "GRAPH" + graph_subtype: ( + str # BURNOUT_PROPAGATION | CENTRALITY_SHIFT | ESTRANGEMENT | ESCALATION_PATH + ) + difficulty: str # "medium" | "hard" + question_prose: str + ground_truth: Dict[str, Any] + expected_tool_calls: List[str] # tools the agent must use + as_of_day: int # the day whose snapshot is the primary source + secondary_day: Optional[int] # for CENTRALITY_SHIFT questions + actors_involved: List[str] + corroborating_event_id: Optional[str] # SimEvent mongo_id that explains the answer + cross_subsystem: bool + + def to_dict(self) -> dict: + return asdict(self) + + +# ───────────────────────────────────────────────────────────────────────────── +# SNAPSHOT LOADER +# ───────────────────────────────────────────────────────────────────────────── + + +class GraphSnapshotBuilder: + """ + Reads all checkpoints from MongoDB and reconstructs GraphSnapshot objects. + Snapshots are written to export/eval/graph_snapshots.json for agent retrieval. + """ + + def __init__(self, mem): + self._mem = mem + + def build_all(self) -> List[GraphSnapshot]: + checkpoints = list(self._mem._db["checkpoints"].find({}).sort("day", 1)) + if not checkpoints: + logger.warning( + "[graph_eval] No checkpoints found — GRAPH track will be empty." + ) + return [] + + snapshots = [] + for doc in checkpoints: + if not doc.get("graph") or not doc.get("stress"): + logger.debug( + f"[graph_eval] Checkpoint day={doc.get('day')} missing graph or stress — skipping." + ) + continue + try: + snap = GraphSnapshot.from_checkpoint(doc) + if snap.nodes: + snapshots.append(snap) + except Exception as e: + logger.warning( + f"[graph_eval] Failed to parse checkpoint day={doc.get('day')}: {e}" + ) + + logger.info(f"[graph_eval] Loaded {len(snapshots)} graph snapshots.") + return snapshots + + +# ───────────────────────────────────────────────────────────────────────────── +# QUESTION GENERATORS (one per subtype) +# ───────────────────────────────────────────────────────────────────────────── + + +class GraphQuestionGenerator: + """ + Generates GRAPH-track eval questions from checkpoints and the SimEvent log. + All ground truth is derived deterministically — no LLM inference. + + The LLM is used only to write the prose question (same pattern as the + three existing tracks in eval_harness.py). + """ + + def __init__( + self, + mem, + snapshots: List[GraphSnapshot], + leads: List[str], + worker_llm, + ): + self._mem = mem + self._snapshots = snapshots + self._leads = leads + self._worker_llm = worker_llm + + # Pre-load events once for corroboration lookups + self._events = list( + self._mem._db["events"] + .find( + {}, + { + "_id": 1, + "type": 1, + "day": 1, + "actors": 1, + "facts": 1, + "artifact_ids": 1, + "summary": 1, + }, + ) + .sort("day", 1) + ) + + # ── public entry point ──────────────────────────────────────────────────── + + def generate(self) -> List[GraphQuestion]: + questions: List[GraphQuestion] = [] + + questions.extend(self._gen_burnout_propagation()) + questions.extend(self._gen_centrality_shift()) + questions.extend(self._gen_estrangement()) + questions.extend(self._gen_escalation_path()) + + logger.info( + f"[graph_eval] Generated {len(questions)} GRAPH questions " + f"({sum(1 for q in questions if q.difficulty == 'hard')} hard)." + ) + return questions + + # ── BURNOUT_PROPAGATION ─────────────────────────────────────────────────── + + def _gen_burnout_propagation(self) -> List[GraphQuestion]: + """ + Find snapshots where at least one key player was burnt out and + stress bleed was non-trivial. Ask which actor absorbed the most. + """ + results: List[GraphQuestion] = [] + + candidates = [] + for snap in self._snapshots: + prop = snap.simulate_stress_propagation() + if prop["max_bleed_target"] and prop["max_bleed_amount"] >= 2.0: + candidates.append((snap, prop)) + + random.shuffle(candidates) + for snap, prop in candidates[:_MAX_QUESTIONS_PER_SUBTYPE]: + max_target = prop["max_bleed_target"] + max_amount = prop["max_bleed_amount"] + key_players = prop["key_players"] + burnt_out = prop["burnt_out"] + + if not key_players or not max_target: + continue + + # Find the burnt-out key player that contributed the most bleed + # (the one with the highest stress above the threshold) + kp_stresses = {kp: snap.stress.get(kp, 0) for kp in key_players} + primary_kp = ( + max(kp_stresses, key=kp_stresses.get) if kp_stresses else key_players[0] + ) + + # Corroborate with an incident or gap event involving the key player + corroborating = self._find_corroborating_event(primary_kp, snap.day) + + qid = f"GRAPH-BURNOUT-day{snap.day}-{primary_kp.replace(' ', '_')}" + prose = self._write_prose( + subtype="BURNOUT_PROPAGATION", + template=( + f"Write a natural-language question asking: " + f"On Day {snap.day}, given the stress levels and social graph, " + f"which team member absorbed the most stress from burnt-out key players? " + f"Do not name the answer ({max_target}) in the question. " + f"Reference the key player(s) by role or context, not name. " + f"The question must mention Day {snap.day} and reference stress propagation. " + f"End with a question mark. 15-100 words." + ), + ground_truth_str=max_target, + ) + if not prose: + continue + + results.append( + GraphQuestion( + question_id=qid, + question_type="GRAPH", + graph_subtype="BURNOUT_PROPAGATION", + difficulty="hard" if len(key_players) > 1 else "medium", + question_prose=prose, + ground_truth={ + "max_bleed_target": max_target, + "max_bleed_amount": max_amount, + "key_players": key_players, + "burnt_out": burnt_out, + "primary_source": primary_kp, + "stress_snapshot": { + k: v + for k, v in snap.stress.items() + if k in key_players + [max_target] + }, + }, + expected_tool_calls=["get_graph_snapshot", "get_stress_snapshot"], + as_of_day=snap.day, + secondary_day=None, + actors_involved=[primary_kp, max_target], + corroborating_event_id=corroborating, + cross_subsystem=False, + ) + ) + + return results + + # ── CENTRALITY_SHIFT ────────────────────────────────────────────────────── + + def _gen_centrality_shift(self) -> List[GraphQuestion]: + """ + Find day-pairs where one actor's betweenness centrality changed + significantly. Ask which actor and why. + """ + results: List[GraphQuestion] = [] + + if len(self._snapshots) < 2: + return results + + # Compare consecutive snapshots + shift_candidates = [] + for i in range(len(self._snapshots) - 1): + snap_a = self._snapshots[i] + snap_b = self._snapshots[i + 1] + + cent_a = snap_a.centrality() + cent_b = snap_b.centrality() + + # Only consider actors present in both + common = set(cent_a) & set(cent_b) + if not common: + continue + + deltas = {actor: abs(cent_b[actor] - cent_a[actor]) for actor in common} + max_actor = max(deltas, key=deltas.get) + max_delta = deltas[max_actor] + + if max_delta >= 0.05: # meaningful shift + shift_candidates.append( + (snap_a, snap_b, max_actor, max_delta, cent_a, cent_b) + ) + + random.shuffle(shift_candidates) + for snap_a, snap_b, actor, delta, cent_a, cent_b in shift_candidates[ + :_MAX_QUESTIONS_PER_SUBTYPE + ]: + direction = "increased" if cent_b[actor] > cent_a[actor] else "decreased" + + # Find the event that explains the shift (departure, hire, incident) + corroborating = self._find_centrality_event(actor, snap_a.day, snap_b.day) + + qid = f"GRAPH-CENTRALITY-day{snap_a.day}-{snap_b.day}-{actor.replace(' ', '_')}" + prose = self._write_prose( + subtype="CENTRALITY_SHIFT", + template=( + f"Write a natural-language question asking: " + f"Between Day {snap_a.day} and Day {snap_b.day}, which team member " + f"experienced the largest change in their betweenness centrality " + f"in the collaboration graph, and what organisational event explains it? " + f"Do not name the answer ({actor}) or the direction ({direction}). " + f"Reference the day range and the concept of centrality or influence. " + f"End with a question mark. 15-100 words." + ), + ground_truth_str=actor, + ) + if not prose: + continue + + results.append( + GraphQuestion( + question_id=qid, + question_type="GRAPH", + graph_subtype="CENTRALITY_SHIFT", + difficulty="hard", + question_prose=prose, + ground_truth={ + "actor": actor, + "centrality_day_a": round(cent_a[actor], 4), + "centrality_day_b": round(cent_b[actor], 4), + "delta": round(delta, 4), + "direction": direction, + "day_a": snap_a.day, + "day_b": snap_b.day, + }, + expected_tool_calls=["get_graph_snapshot", "get_events_for_day"], + as_of_day=snap_b.day, + secondary_day=snap_a.day, + actors_involved=[actor], + corroborating_event_id=corroborating, + cross_subsystem=True, + ) + ) + + return results + + # ── ESTRANGEMENT ────────────────────────────────────────────────────────── + + def _gen_estrangement(self) -> List[GraphQuestion]: + """ + Find the most estranged internal pair on a given day. + Ask whether they ever collaborated on the same incident. + """ + results: List[GraphQuestion] = [] + + # Use later snapshots where decay has had time to act + late_snaps = self._snapshots[len(self._snapshots) // 2 :] + if not late_snaps: + late_snaps = self._snapshots + + random.shuffle(late_snaps) + seen_pairs: Set[Tuple[str, str]] = set() + + for snap in late_snaps[: _MAX_QUESTIONS_PER_SUBTYPE * 2]: + pairs = snap.estranged_pairs(top_n=3) + if not pairs: + continue + + for u, v, w in pairs: + pair_key = (min(u, v), max(u, v)) + if pair_key in seen_pairs: + continue + seen_pairs.add(pair_key) + + shared_incident = self._shared_incident(u, v, up_to_day=snap.day) + + qid = f"GRAPH-ESTRANGE-day{snap.day}-{u.replace(' ', '_')}-{v.replace(' ', '_')}" + answer_str = f"{u} and {v}" + prose = self._write_prose( + subtype="ESTRANGEMENT", + template=( + f"Write a natural-language question asking: " + f"By Day {snap.day}, which pair of team members had the " + f"weakest collaboration relationship based on their interaction " + f"history, and had they ever worked together on the same incident? " + f"Do not name the pair ({u} and {v}) in the question. " + f"Reference the day and the concept of relationship decay or " + f"collaboration frequency. End with a question mark. 15-100 words." + ), + ground_truth_str=answer_str, + ) + if not prose: + continue + + results.append( + GraphQuestion( + question_id=qid, + question_type="GRAPH", + graph_subtype="ESTRANGEMENT", + difficulty="medium", + question_prose=prose, + ground_truth={ + "estranged_pair": [u, v], + "edge_weight": round(w, 4), + "shared_incident": shared_incident, + "answer": answer_str, + }, + expected_tool_calls=[ + "get_graph_snapshot", + "get_events_for_day", + "get_ticket", + ], + as_of_day=snap.day, + secondary_day=None, + actors_involved=[u, v], + corroborating_event_id=None, + cross_subsystem=False, + ) + ) + + if len(results) >= _MAX_QUESTIONS_PER_SUBTYPE: + return results + + return results + + # ── ESCALATION_PATH ─────────────────────────────────────────────────────── + + def _gen_escalation_path(self) -> List[GraphQuestion]: + """ + Hypothetical: on Day N, if actor X were the first responder, + who would be the next hop in the Dijkstra escalation chain? + """ + results: List[GraphQuestion] = [] + + if not self._leads: + logger.warning( + "[graph_eval] No leads configured — skipping ESCALATION_PATH." + ) + return results + + for snap in random.sample( + self._snapshots, min(len(self._snapshots), _MAX_QUESTIONS_PER_SUBTYPE * 3) + ): + internal_actors = [ + n for n in snap.nodes if n in snap.stress and n not in self._leads + ] + if not internal_actors: + continue + + actor = random.choice(internal_actors) + next_hop = snap.dijkstra_next_hop(actor, self._leads) + + if not next_hop: + continue + + # Find if there was an actual incident on this day involving actor + corroborating = self._find_corroborating_event(actor, snap.day) + + qid = f"GRAPH-ESCALATION-day{snap.day}-{actor.replace(' ', '_')}" + prose = self._write_prose( + subtype="ESCALATION_PATH", + template=( + f"Write a natural-language question asking: " + f"On Day {snap.day}, if {actor} had been the first responder " + f"to a P1 incident, who would have been the next person in their " + f"escalation chain based on the collaboration graph at that time? " + f"Do not name the answer ({next_hop}) in the question. " + f"Reference the day and the concept of escalation or incident response. " + f"End with a question mark. 15-100 words." + ), + ground_truth_str=next_hop, + ) + if not prose: + continue + + results.append( + GraphQuestion( + question_id=qid, + question_type="GRAPH", + graph_subtype="ESCALATION_PATH", + difficulty="medium", + question_prose=prose, + ground_truth={ + "first_responder": actor, + "next_hop": next_hop, + "leads": self._leads, + "day": snap.day, + }, + expected_tool_calls=["get_graph_snapshot"], + as_of_day=snap.day, + secondary_day=None, + actors_involved=[actor, next_hop], + corroborating_event_id=corroborating, + cross_subsystem=False, + ) + ) + + if len(results) >= _MAX_QUESTIONS_PER_SUBTYPE: + break + + return results + + # ── helpers ─────────────────────────────────────────────────────────────── + + def _find_corroborating_event(self, actor: str, day: int) -> Optional[str]: + """Find a SimEvent on or just before `day` involving `actor`.""" + _CORROBORATING_TYPES = { + "incident_opened", + "incident_resolved", + "knowledge_gap_detected", + "employee_departed", + "employee_hired", + "postmortem_created", + } + candidates = [ + e + for e in self._events + if actor in (e.get("actors") or []) + and abs(e.get("day", 0) - day) <= 2 + and e.get("type") in _CORROBORATING_TYPES + ] + if candidates: + # Prefer same-day, then nearest + candidates.sort(key=lambda e: abs(e.get("day", 0) - day)) + return str(candidates[0].get("_id", "")) + return None + + def _find_centrality_event( + self, actor: str, day_a: int, day_b: int + ) -> Optional[str]: + """Find the org event between day_a and day_b that explains the centrality shift.""" + _CENTRALITY_TYPES = { + "employee_departed", + "employee_hired", + "incident_opened", + "centrality_vacuum", + "knowledge_gap_detected", + } + candidates = [ + e + for e in self._events + if day_a <= e.get("day", 0) <= day_b + and e.get("type") in _CENTRALITY_TYPES + and ( + actor in (e.get("actors") or []) + or actor == e.get("facts", {}).get("name") + or actor == e.get("facts", {}).get("departed") + ) + ] + if candidates: + return str(candidates[0].get("_id", "")) + + # Also accept any departure/hire in the window even if not directly involving the actor + any_org_event = next( + ( + e + for e in self._events + if day_a <= e.get("day", 0) <= day_b + and e.get("type") in {"employee_departed", "employee_hired"} + ), + None, + ) + return str(any_org_event["_id"]) if any_org_event else None + + def _shared_incident(self, u: str, v: str, up_to_day: int) -> Optional[str]: + """Return the Jira ID of a shared incident, or None.""" + for e in self._events: + if e.get("type") != "incident_opened": + continue + if e.get("day", 0) > up_to_day: + continue + actors = set(e.get("actors") or []) + if u in actors and v in actors: + return e.get("artifact_ids", {}).get("jira") + return None + + def _write_prose( + self, subtype: str, template: str, ground_truth_str: str + ) -> Optional[str]: + """ + Ask the worker LLM to write the question prose. + Validates and retries up to 3 times (same pattern as eval_harness.py). + """ + try: + from agent_factory import make_agent + from crewai import Crew, Task + except ImportError: + # If crewai is not available (e.g. unit test), return a template placeholder + return f"[{subtype} question about {ground_truth_str} — prose generation unavailable]" + + agent = make_agent( + role="Eval Dataset Author", + goal="Write natural-sounding evaluation questions for AI agent benchmarks.", + backstory=( + "You write clear, specific questions for evaluating AI agents on reasoning tasks. " + "Questions must reference the social graph, stress levels, or collaboration patterns " + "of the simulated organisation. Questions must be unambiguous and answerable only " + "through careful analysis of graph snapshots and event data." + ), + llm=self._worker_llm, + ) + + for attempt in range(3): + retry_note = ( + " Previous attempt failed validation. Make sure the question: " + "ends with '?', does not reveal the answer, and is 15-100 words long." + if attempt > 0 + else "" + ) + task = Task( + description=template + retry_note, + expected_output="One question ending with a question mark. No preamble or explanation.", + agent=agent, + ) + try: + result = str( + Crew(agents=[agent], tasks=[task], verbose=False).kickoff() + ).strip() + if self._validate_prose(result, ground_truth_str): + return result + logger.debug( + f"[graph_eval] Prose validation failed (attempt {attempt + 1}): {result[:80]}" + ) + except Exception as exc: + logger.warning( + f"[graph_eval] Prose generation error (attempt {attempt + 1}): {exc}" + ) + + return None + + def _validate_prose(self, text: str, ground_truth_str: str) -> bool: + import re + + normalized = text.replace("\u2013", "-").replace("\u2014", "-") + + if not normalized.endswith("?"): + return False + + words = normalized.split() + if len(words) < 10 or len(words) > 150: + return False + + gt_lower = ground_truth_str.lower() + if gt_lower in normalized.lower() and len(gt_lower) > 4: + return False + + if re.search(r"\b[A-Z]{1,4}-\d{2,6}\b", normalized): + return False + + if not re.search(r"day\s+\d+", normalized, re.IGNORECASE): + return False + + return True + + +def register_graph_tool(gated_tools_instance, mem, question: dict): + """ + Monkey-patches a get_graph_snapshot() and get_stress_snapshot() method + onto an existing GatedTools instance for GRAPH-track questions. + + Call this in AgentaticEvalHarness._run_question() when question_type == "GRAPH": + + from graph_eval_track import register_graph_tool + register_graph_tool(tools, self._mem, question) + + The agent then has access to: + tools.get_graph_snapshot(day: int) -> dict + tools.get_stress_snapshot(day: int) -> dict + """ + import time + + _mem = mem + _as_of_day = question.get("as_of_day", 9999) + + def get_graph_snapshot(day: int) -> dict: + t0 = time.time() + horizon_violation = day > _as_of_day + effective_day = min(day, _as_of_day) + doc = _mem._db["checkpoints"].find_one( + {"day": effective_day}, + {"_id": 0, "graph": 1, "stress": 1, "state": 1, "day": 1}, + ) + if not doc: + nearest = _mem._db["checkpoints"].find_one( + {"day": {"$lte": effective_day}}, + {"_id": 0, "graph": 1, "stress": 1, "state": 1, "day": 1}, + sort=[("day", -1)], + ) + doc = nearest or {} + + g = doc.get("graph", {}) + node_list = [ + n.get("id") or n.get("name") + for n in g.get("nodes", []) + if n.get("id") or n.get("name") + ] + edge_list = [ + { + "source": e.get("source"), + "target": e.get("target"), + "weight": round(float(e.get("weight", 1.0)), 4), + } + for e in (g.get("links") or g.get("edges", [])) + ] + result = { + "day": doc.get("day", effective_day), + "nodes": node_list, + "edges": edge_list, + "stress": doc.get("stress", {}), + "state": doc.get("state", {}), + } + gated_tools_instance._record( + tool_name="get_graph_snapshot", + arguments={"day": day}, + results=[result], + t0=t0, + horizon_violation=horizon_violation, + timestamp_applied=str(effective_day), + ) + return result + + def get_stress_snapshot(day: int) -> dict: + t0 = time.time() + horizon_violation = day > _as_of_day + effective_day = min(day, _as_of_day) + doc = _mem._db["checkpoints"].find_one( + {"day": effective_day}, + {"_id": 0, "stress": 1, "day": 1}, + ) + if not doc: + nearest = _mem._db["checkpoints"].find_one( + {"day": {"$lte": effective_day}}, + {"_id": 0, "stress": 1, "day": 1}, + sort=[("day", -1)], + ) + doc = nearest or {} + + result = { + "day": doc.get("day", effective_day), + "stress": doc.get("stress", {}), + } + gated_tools_instance._record( + tool_name="get_stress_snapshot", + arguments={"day": day}, + results=[result], + t0=t0, + horizon_violation=horizon_violation, + timestamp_applied=str(effective_day), + ) + return result + + import types + + gated_tools_instance.get_graph_snapshot = types.MethodType( + lambda self, day: get_graph_snapshot(day), gated_tools_instance + ) + gated_tools_instance.get_stress_snapshot = types.MethodType( + lambda self, day: get_stress_snapshot(day), gated_tools_instance + ) + + +# ───────────────────────────────────────────────────────────────────────────── +# GRAPH TRAJECTORY SCORER +# ───────────────────────────────────────────────────────────────────────────── + + +def score_graph_trajectory( + trajectory, # AgentTrajectory from agentic_eval_harness.py + question: dict, + ground_truth: dict, +) -> float: + """ + Trajectory score for GRAPH questions (weight: 0.50). + + Rules: + 1. Agent must have called get_graph_snapshot (or get_stress_snapshot) + at least once — otherwise score = 0.0 regardless of answer. + 2. Agent must have called it for the correct day (as_of_day ± 1). + 3. For CENTRALITY_SHIFT: agent must also query the secondary_day. + 4. Penalty for horizon violations (checking future checkpoints). + + Returns a float in [0.0, 1.0]. + """ + required_tools = set(question.get("expected_tool_calls", [])) + as_of_day = question.get("as_of_day", 0) + secondary_day = question.get("secondary_day") + + tool_names_called = {tc.tool_name for tc in trajectory.tool_calls} + graph_calls = [ + tc + for tc in trajectory.tool_calls + if tc.tool_name in ("get_graph_snapshot", "get_stress_snapshot") + ] + + # Rule 1: must have called a graph tool + if not graph_calls: + return 0.0 + + # Rule 2: must have queried the correct day + def _day_from_args(tc) -> Optional[int]: + return tc.arguments.get("day") + + days_queried = { + _day_from_args(tc) for tc in graph_calls if _day_from_args(tc) is not None + } + correct_day_queried = any(abs(d - as_of_day) <= 1 for d in days_queried) + + if not correct_day_queried: + return 0.2 # queried graph but wrong day + + score = 0.7 + + # Rule 3: CENTRALITY_SHIFT requires both days + if secondary_day is not None: + secondary_queried = any(abs(d - secondary_day) <= 1 for d in days_queried) + if secondary_queried: + score += 0.15 + else: + score -= 0.2 + + # Horizon violations (future checkpoints queried) + horizon_violations = sum(1 for tc in graph_calls if tc.horizon_violation) + if horizon_violations: + score -= 0.1 * min(horizon_violations, 3) + + # Bonus: called all expected tools + if required_tools.issubset(tool_names_called): + score += 0.15 + + return round(max(0.0, min(1.0, score)), 4) + + +def score_graph_answer( + agent_answer: dict, + ground_truth: dict, + graph_subtype: str, +) -> Tuple[float, bool]: + """ + Answer score for GRAPH questions (weight: 0.50). + Returns (score: float, correct: bool). + + Accepts fuzzy name matching (case-insensitive, partial name match) + because agents often abbreviate names. + """ + + def _name_match(a: str, b: str) -> bool: + a_tok = set(a.lower().split()) + b_tok = set(b.lower().split()) + if not a_tok or not b_tok: + return False + jaccard = len(a_tok & b_tok) / len(a_tok | b_tok) + return jaccard >= 0.5 + + extracted = agent_answer.get("answer", "") or agent_answer.get("actor", "") or "" + + if graph_subtype == "BURNOUT_PROPAGATION": + gt_actor = ground_truth.get("max_bleed_target", "") + if _name_match(str(extracted), gt_actor): + return 1.0, True + # Partial credit: named someone who was in bleed_targets at all + bleed_targets = ground_truth.get("bleed_targets", {}) + if any(_name_match(str(extracted), n) for n in bleed_targets): + return 0.4, False + return 0.0, False + + elif graph_subtype == "CENTRALITY_SHIFT": + gt_actor = ground_truth.get("actor", "") + if _name_match(str(extracted), gt_actor): + gt_dir = ground_truth.get("direction", "") + agent_dir = agent_answer.get("direction", "") + if gt_dir and agent_dir and gt_dir.lower() in agent_dir.lower(): + return 1.0, True + return 0.8, True + return 0.0, False + + elif graph_subtype == "ESTRANGEMENT": + pair = ground_truth.get("estranged_pair", []) + if not pair: + return 0.0, False + full_answer = json.dumps(agent_answer).lower() + both_mentioned = all(p.lower() in full_answer for p in pair) + if both_mentioned: + gt_incident = ground_truth.get("shared_incident") + agent_incident = agent_answer.get("shared_incident") + if gt_incident is not None and agent_incident is not None: + incident_match = str(gt_incident) in str(agent_incident) + return (1.0 if incident_match else 0.7), True + return 0.8, True + return 0.0, False + + elif graph_subtype == "ESCALATION_PATH": + gt_hop = ground_truth.get("next_hop", "") + if _name_match(str(extracted), gt_hop): + return 1.0, True + return 0.0, False + + return 0.0, False + + +def build_graph_track( + mem, + worker_llm, + leads: List[str], + eval_dir: Path, +) -> Tuple[List[GraphSnapshot], List[dict]]: + """ + Full pipeline: load snapshots → generate questions → write outputs. + + Returns (snapshots, questions_as_dicts) so EvalHarness can merge them + into the main eval_questions.json. + + Writes two side-car files: + eval_dir/graph_snapshots.json — all snapshots (agent retrieval corpus) + eval_dir/graph_questions.json — GRAPH questions only + """ + logger.info("[graph_eval] Building GRAPH track...") + + snapshot_builder = GraphSnapshotBuilder(mem) + snapshots = snapshot_builder.build_all() + + if not snapshots: + logger.warning("[graph_eval] No snapshots — GRAPH track skipped.") + return [], [] + + snap_path = eval_dir / "graph_snapshots.json" + with open(snap_path, "w") as f: + json.dump( + [s.to_export_dict() for s in snapshots], + f, + indent=2, + default=str, + ) + logger.info(f" → {snap_path} ({len(snapshots)} snapshots)") + + generator = GraphQuestionGenerator( + mem=mem, + snapshots=snapshots, + leads=leads, + worker_llm=worker_llm, + ) + questions = generator.generate() + + q_dicts = [q.to_dict() for q in questions] + graph_q_path = eval_dir / "graph_questions.json" + with open(graph_q_path, "w") as f: + json.dump(q_dicts, f, indent=2, default=str) + logger.info(f" → {graph_q_path} ({len(questions)} questions)") + + return snapshots, q_dicts diff --git a/eval/org_dynamics_question_builder.py b/eval/org_dynamics_question_builder.py new file mode 100644 index 0000000..554cfbc --- /dev/null +++ b/eval/org_dynamics_question_builder.py @@ -0,0 +1,1305 @@ +""" +org_dynamics_question_builder.py +================================= +OrgForge Organizational Dynamics Question Builder + +Generates questions that require multi-step reasoning over the full sim corpus — +the kind of questions a C-suite executive, new manager, or board analyst would +actually ask. These cannot be answered from question text alone or by defaulting +to a safe answer. Corpus access is mandatory. + +Five question categories: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +CATEGORY 1 — ATTENTION COST +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +How much productive time was lost to distractions, context switching, +and non-primary activities? + +All ground truth is derived from the dept_plans collection. The dept_plan +agenda is the authoritativerecord of how each engineer's day was actually +structured — it captures ticket_progress, deep_work, async_question, +design_discussion, pr_review, 1on1, and deferred items with estimated_hrs for each. + +Three question sub-types: + Q1a — activity_type breakdown by dept per week (ticket vs non-ticket split) + Q1b — sprint alignment: does the team's activity mix match the sprint theme? + Q1c — collaborator demand: which engineer appeared most as a collaborator + in others' agendas, and how did that affect their own ticket output? + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +CATEGORY 2 — RESOURCE PRESSURE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Who was over-capacity, under-utilized, or misaligned with their +assigned work? Requires dept_plans capacity_by_member vs actual load. + +Also uses is_on_call + agenda load to flag engineers carrying both +on-call responsibility and heavy scheduled work on the same day. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +CATEGORY 3 — CAUSAL PRESSURE PROPAGATION +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +How did external pressure (customer complaints, vendor alerts) ripple +through the org into planning decisions and engineering priorities? +Requires tracing inbound_external_email → cross_dept_signals → dept themes. + +Linkage uses explicit causal flags (jira_from_customer_email, +customer_escalation_relayed) and artifact-ID cross-reference rather than +substring matching against the full facts blob, to avoid false chains. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +CATEGORY 4 — ASSIGNMENT QUALITY +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Were tickets assigned to the right engineers given stress, expertise, +and capacity? Requires assignment_scores collection (must be persisted +in sim) + stress history + ticket metadata. + +Now includes opportunity-cost questions: given all candidate scores for +a ticket (was_assigned=false rows), how much better was the optimal +assignment vs. what actually happened? + +Questions do NOT reveal the stress/capacity finding in the question text; +the agent must discover it from the corpus. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +CATEGORY 5 — ORGANIZATIONAL FRICTION +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Where did cross-dept tension surface, who was involved, and what +drove it? Requires collision events + stress snapshots + dept themes. + +Now stores event_summary in ground_truth so the embedding scorer in +org_dynamics_scorer.py can compare against SimEvent.summary rather than +the LLM-generated rationale field. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Scoring model (all categories) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Each question has a structured ground_truth with multiple components. +Answer scoring: weighted sum across components (partial credit). +Trajectory scoring: did the agent retrieve the right artifacts before answering? +Combined: 0.50 answer + 0.50 trajectory (both matter equally). + +""" + +from __future__ import annotations + +import logging +import random +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, Dict, List, Optional, Set, Tuple + +from memory import Memory, SimEvent + +logger = logging.getLogger("orgforge.org_dynamics_eval") + + +# ───────────────────────────────────────────────────────────────────────────── +# CONSTANTS +# ───────────────────────────────────────────────────────────────────────────── + +MAX_ATTENTION_COST = 15 +MAX_RESOURCE_PRESSURE = 15 +MAX_CAUSAL_PRESSURE = 10 +MAX_ASSIGNMENT_QUALITY = 10 +MAX_ORG_FRICTION = 10 + +_TRACK_WEIGHTS = { + "ATTENTION_COST": {"answer": 0.50, "trajectory": 0.50}, + "RESOURCE_PRESSURE": {"answer": 0.50, "trajectory": 0.50}, + "CAUSAL_PRESSURE": {"answer": 0.50, "trajectory": 0.50}, + "ASSIGNMENT_QUALITY": {"answer": 0.50, "trajectory": 0.50}, + "ORG_FRICTION": {"answer": 0.50, "trajectory": 0.50}, +} + +# activity_type values from the dept_plan agenda schema +_TICKET_TYPES = {"ticket_progress"} +_NON_TICKET_TYPES = { + "deep_work", + "async_question", + "design_discussion", + "pr_review", + "1on1", + "mentoring", + "watercooler_chat", +} + +# Delivery-oriented keywords used to detect sprint theme misalignment +_DELIVERY_KEYWORDS = { + "fix", + "ship", + "deliver", + "complete", + "close", + "resolve", + "deploy", + "release", + "ticket", + "sprint", +} + +# Explicit causal flags that reliably link an inbound email to a downstream event +_EMAIL_CAUSAL_FLAGS = { + "jira_from_customer_email", + "customer_escalation_relayed", + "customer_email_routed", + "zd_escalation_source", + "incident_triggers_risk_flag", +} + + +# ───────────────────────────────────────────────────────────────────────────── +# DATA CLASSES +# ───────────────────────────────────────────────────────────────────────────── + + +@dataclass +class OrgDynamicsQuestion: + question_id: str + category: str + difficulty: str # "medium" | "hard" + day_range: Tuple[int, int] + question_text: str + ground_truth: Dict[str, Any] + evidence_search_space: List[str] + evidence_plan_ids: List[str] = field(default_factory=list) + # ^^^ PLAN-{day}-{dept} IDs separated out so the harness can route them to + # get_dept_plan rather than treating them as generic artifact IDs. + requires_reasoning: bool = True + + +# ───────────────────────────────────────────────────────────────────────────── +# BUILDER +# ───────────────────────────────────────────────────────────────────────────── + + +class OrgDynamicsQuestionBuilder: + """ + Builds OrgDynamicsQuestion objects from live MongoDB sim data. + Called from eval_harness.py after the sim has completed. + """ + + def __init__(self, mem: Memory, sim_start: datetime, config: dict): + self._mem = mem + self._sim_start = sim_start + self._config = config + self._org_chart: Dict[str, List[str]] = config.get("org_chart", {}) + self._leads: Dict[str, str] = config.get("leads", {}) + self._events = mem.get_event_log(from_db=True) + self._max_day = max((e.day for e in self._events), default=1) + + # Pre-build email_id → downstream event index for CAUSAL_PRESSURE. + self._email_to_downstream: Dict[str, List[SimEvent]] = ( + self._build_email_downstream_index() + ) + + # ── Public entry point ──────────────────────────────────────────────────── + + def build_all(self) -> List[Dict]: + questions: List[OrgDynamicsQuestion] = [] + questions += self._attention_cost_questions() + questions += self._resource_pressure_questions() + questions += self._causal_pressure_questions() + questions += self._assignment_quality_questions() + questions += self._org_friction_questions() + logger.info( + f"[org_dynamics] {len(questions)} questions built — " + f"Types: { {q.category for q in questions} }" + ) + return [self._to_dict(q) for q in questions] + + # ───────────────────────────────────────────────────────────────────────── + # CAUSAL INDEX — built once, used by CAUSAL_PRESSURE + # ───────────────────────────────────────────────────────────────────────── + + def _build_email_downstream_index(self) -> Dict[str, List[SimEvent]]: + """ + Maps each email artifact ID to downstream events that explicitly + reference it via a causal flag or shared artifact_ids entry. + Uses explicit criteria instead of substring-matching the facts blob. + """ + index: Dict[str, List[SimEvent]] = {} + + for email_ev in (e for e in self._events if e.type == "inbound_external_email"): + email_id = (email_ev.artifact_ids or {}).get("email", "") + if not email_id: + continue + + downstream: List[SimEvent] = [] + for ev in self._events: + if ev.day < email_ev.day or ev.day > email_ev.day + 5: + continue + + if ev.type in _EMAIL_CAUSAL_FLAGS: + artifact_vals: Set[str] = set() + for v in (ev.artifact_ids or {}).values(): + if isinstance(v, list): + artifact_vals.update(str(x) for x in v) + elif v: + artifact_vals.add(str(v)) + if email_id in artifact_vals or email_id in str( + ev.facts.get("source_email_id", "") + ): + downstream.append(ev) + continue + + for v in (ev.artifact_ids or {}).values(): + if email_id in str(v): + downstream.append(ev) + break + + if downstream: + index[email_id] = downstream + + return index + + # ───────────────────────────────────────────────────────────────────────── + # SHARED PLAN HELPERS + # ───────────────────────────────────────────────────────────────────────── + + def _agenda_hours( + self, ep: dict, activity_types: Optional[Set[str]] = None + ) -> float: + """Sum estimated_hrs for non-deferred agenda items, optionally filtered by type.""" + return sum( + float(item.get("estimated_hrs", 0)) + for item in ep.get("agenda", []) + if not item.get("deferred") + and (activity_types is None or item.get("activity_type") in activity_types) + ) + + # ───────────────────────────────────────────────────────────────────────── + # CATEGORY 1 — ATTENTION COST + # ───────────────────────────────────────────────────────────────────────── + + def _attention_cost_questions(self) -> List[OrgDynamicsQuestion]: + questions: List[OrgDynamicsQuestion] = [] + questions += self._attention_ticket_split_questions() + questions += self._attention_sprint_alignment_questions() + questions += self._attention_collaborator_demand_questions() + random.shuffle(questions) + return questions[:MAX_ATTENTION_COST] + + # Q1a — ticket vs non-ticket split by dept/week + + def _attention_ticket_split_questions(self) -> List[OrgDynamicsQuestion]: + questions: List[OrgDynamicsQuestion] = [] + cap = MAX_ATTENTION_COST // 3 + + for dept in self._org_chart: + for day_start in range(1, self._max_day - 4, 7): + day_end = min(day_start + 6, self._max_day) + result = self._compute_activity_breakdown(dept, day_start, day_end) + if not result: + continue + + by_type, plan_ids = result + total_hrs = sum(by_type.values()) + ticket_hrs = by_type.get("ticket_progress", 0.0) + non_ticket = total_hrs - ticket_hrs + + if total_hrs < 5: + continue + pct_non_ticket = round(non_ticket / total_hrs * 100, 1) + if pct_non_ticket < 20: + continue + + questions.append( + OrgDynamicsQuestion( + question_id=f"attention_split_{dept}_D{day_start}_D{day_end}", + category="ATTENTION_COST", + difficulty="hard", + day_range=(day_start, day_end), + question_text=( + f"During the week of Day {day_start} to Day {day_end}, " + f"what fraction of the {dept} team's planned working hours " + f"were allocated to activities other than direct ticket " + f"progress, and what does this suggest about the team's " + f"capacity that week?" + ), + ground_truth={ + "dept": dept, + "ticket_hours": round(ticket_hrs, 2), + "non_ticket_hours": round(non_ticket, 2), + "pct_non_ticket": pct_non_ticket, + "activity_breakdown": { + k: round(v, 2) for k, v in by_type.items() + }, + "day_range": [day_start, day_end], + }, + evidence_search_space=plan_ids, + evidence_plan_ids=plan_ids, + ) + ) + + if len(questions) >= cap: + break + if len(questions) >= cap: + break + + return questions + + def _compute_activity_breakdown( + self, dept: str, day_start: int, day_end: int + ) -> Optional[Tuple[Dict[str, float], List[str]]]: + """ + Returns ({activity_type: total_hrs}, plan_ids) for a dept/window. + Reads directly from dept_plans engineer_plans[].agenda[]. + Deferred items are excluded. + """ + plans = list( + self._mem._db["dept_plans"].find( + {"dept": dept, "day": {"$gte": day_start, "$lte": day_end}}, + {"_id": 0, "engineer_plans": 1, "day": 1, "dept": 1}, + ) + ) + if not plans: + return None + + by_type: Dict[str, float] = {} + plan_ids = [f"PLAN-{p['day']}-{p['dept']}" for p in plans] + + for plan in plans: + for ep in plan.get("engineer_plans", []): + for item in ep.get("agenda", []): + if item.get("deferred"): + continue + atype = item.get("activity_type", "other") + hrs = float(item.get("estimated_hrs", 0)) + by_type[atype] = by_type.get(atype, 0.0) + hrs + + return by_type, plan_ids + + # Q1b — sprint theme alignment + + def _attention_sprint_alignment_questions(self) -> List[OrgDynamicsQuestion]: + """ + Flags weeks where the sprint theme implies delivery work but the actual + activity breakdown shows little or no ticket_progress. + + The agent must read both the dept_plan theme and the agenda breakdown + to assess alignment — neither alone is sufficient. + """ + questions: List[OrgDynamicsQuestion] = [] + cap = MAX_ATTENTION_COST // 3 + + for dept in self._org_chart: + for day_start in range(1, self._max_day - 4, 7): + day_end = min(day_start + 6, self._max_day) + result = self._compute_activity_breakdown(dept, day_start, day_end) + if not result: + continue + + by_type, plan_ids = result + total_hrs = sum(by_type.values()) + ticket_hrs = by_type.get("ticket_progress", 0.0) + if total_hrs < 5: + continue + + plan_doc = self._mem._db["dept_plans"].find_one( + {"dept": dept, "day": {"$gte": day_start, "$lte": day_end}}, + {"_id": 0, "theme": 1}, + ) + theme = (plan_doc or {}).get("theme", "") + if not theme: + continue + + theme_implies_delivery = any( + kw in theme.lower() for kw in _DELIVERY_KEYWORDS + ) + pct_ticket = ( + round(ticket_hrs / total_hrs * 100, 1) if total_hrs else 0.0 + ) + + if not (theme_implies_delivery and pct_ticket < 15.0): + continue + + no_ticket_count = self._count_engineers_without_ticket_work( + dept, day_start, day_end + ) + + questions.append( + OrgDynamicsQuestion( + question_id=f"attention_alignment_{dept}_D{day_start}_D{day_end}", + category="ATTENTION_COST", + difficulty="hard", + day_range=(day_start, day_end), + question_text=( + f"The {dept} team's sprint theme for the week of " + f"Day {day_start} to Day {day_end} implied delivery work. " + f"Looking at how the team actually spent their time that week, " + f"how well did their planned activities align with that goal, " + f"and how many engineers had no ticket work scheduled at all?" + ), + ground_truth={ + "dept": dept, + "sprint_theme": theme, + "pct_ticket_hours": pct_ticket, + "ticket_hours": round(ticket_hrs, 2), + "total_hours": round(total_hrs, 2), + "engineers_with_no_ticket_work": no_ticket_count, + "aligned": False, + "day_range": [day_start, day_end], + }, + evidence_search_space=plan_ids, + evidence_plan_ids=plan_ids, + ) + ) + + if len(questions) >= cap: + break + if len(questions) >= cap: + break + + return questions + + def _count_engineers_without_ticket_work( + self, dept: str, day_start: int, day_end: int + ) -> int: + plans = list( + self._mem._db["dept_plans"].find( + {"dept": dept, "day": {"$gte": day_start, "$lte": day_end}}, + {"_id": 0, "engineer_plans": 1}, + ) + ) + all_engineers: Set[str] = set() + with_ticket_work: Set[str] = set() + + for plan in plans: + for ep in plan.get("engineer_plans", []): + name = ep.get("name", "") + if not name: + continue + all_engineers.add(name) + if any( + item.get("activity_type") == "ticket_progress" + and not item.get("deferred") + for item in ep.get("agenda", []) + ): + with_ticket_work.add(name) + + return len(all_engineers - with_ticket_work) + + # Q1c — collaborator demand + + def _attention_collaborator_demand_questions(self) -> List[OrgDynamicsQuestion]: + """ + Reads the collaborator[] field on each agenda item. + Finds who was most requested as a collaborator by colleagues, + then checks how many ticket_progress hours they had themselves. + """ + questions: List[OrgDynamicsQuestion] = [] + cap = MAX_ATTENTION_COST // 3 + + for dept in self._org_chart: + for day_start in range(1, self._max_day - 4, 7): + day_end = min(day_start + 6, self._max_day) + result = self._compute_collaborator_demand(dept, day_start, day_end) + if not result: + continue + + top_collab, demand_count, own_ticket_hrs, plan_ids = result + if demand_count < 2: + continue + + questions.append( + OrgDynamicsQuestion( + question_id=f"attention_collab_{top_collab}_D{day_start}_D{day_end}", + category="ATTENTION_COST", + difficulty="hard", + day_range=(day_start, day_end), + question_text=( + f"Between Day {day_start} and Day {day_end} in the {dept} team, " + f"which engineer was most frequently listed as a collaborator " + f"in their colleagues' planned work, and how many hours did " + f"that engineer themselves spend on direct ticket progress?" + ), + ground_truth={ + "engineer": top_collab, + "dept": dept, + "collaborator_demand_count": demand_count, + "own_ticket_hours": round(own_ticket_hrs, 2), + "day_range": [day_start, day_end], + }, + evidence_search_space=plan_ids, + evidence_plan_ids=plan_ids, + ) + ) + + if len(questions) >= cap: + break + if len(questions) >= cap: + break + + return questions + + def _compute_collaborator_demand( + self, dept: str, day_start: int, day_end: int + ) -> Optional[Tuple[str, int, float, List[str]]]: + """ + Returns (top_collaborator, demand_count, own_ticket_hrs, plan_ids). + demand_count = number of times an engineer appears in others' collaborator lists. + """ + plans = list( + self._mem._db["dept_plans"].find( + {"dept": dept, "day": {"$gte": day_start, "$lte": day_end}}, + {"_id": 0, "engineer_plans": 1, "day": 1, "dept": 1}, + ) + ) + if not plans: + return None + + plan_ids: List[str] = [f"PLAN-{p['day']}-{p['dept']}" for p in plans] + collab_demand: Dict[str, int] = {} + own_ticket_hrs: Dict[str, float] = {} + + for plan in plans: + for ep in plan.get("engineer_plans", []): + owner = ep.get("name", "") + if not owner: + continue + for item in ep.get("agenda", []): + if item.get("deferred"): + continue + for collab in item.get("collaborator", []): + if collab and collab != owner: + collab_demand[collab] = collab_demand.get(collab, 0) + 1 + if item.get("activity_type") == "ticket_progress": + own_ticket_hrs[owner] = own_ticket_hrs.get(owner, 0.0) + float( + item.get("estimated_hrs", 0) + ) + + if not collab_demand: + return None + + top = max(collab_demand, key=lambda e: collab_demand[e]) + return top, collab_demand[top], own_ticket_hrs.get(top, 0.0), plan_ids + + # ───────────────────────────────────────────────────────────────────────── + # CATEGORY 2 — RESOURCE PRESSURE + # ───────────────────────────────────────────────────────────────────────── + + def _resource_pressure_questions(self) -> List[OrgDynamicsQuestion]: + questions: List[OrgDynamicsQuestion] = [] + questions += self._resource_overcapacity_questions() + questions += self._resource_crossdept_questions() + questions += self._resource_oncall_overload_questions() + random.shuffle(questions) + return questions[:MAX_RESOURCE_PRESSURE] + + def _resource_overcapacity_questions(self) -> List[OrgDynamicsQuestion]: + questions: List[OrgDynamicsQuestion] = [] + cap = MAX_RESOURCE_PRESSURE // 3 + + for day in range(1, self._max_day + 1): + result = self._find_most_over_capacity_engineer(day) + if not result: + continue + + engineer, capacity, actual_load, dept, plan_id = result + overflow = round(actual_load - capacity, 2) + if overflow < 0.5: + continue + + questions.append( + OrgDynamicsQuestion( + question_id=f"resource_overcapacity_{engineer}_D{day}", + category="RESOURCE_PRESSURE", + difficulty="medium", + day_range=(day, day), + question_text=( + f"On Day {day}, which engineer was most over their available " + f"capacity based on their planned workload, and by how many hours?" + ), + ground_truth={ + "engineer": engineer, + "dept": dept, + "day": day, + "capacity_hours": round(capacity, 2), + "planned_hours": round(actual_load, 2), + "overflow_hours": overflow, + "plan_artifact_id": plan_id, + }, + evidence_search_space=[plan_id], + evidence_plan_ids=[plan_id], + ) + ) + + if len(questions) >= cap: + break + + return questions + + def _resource_crossdept_questions(self) -> List[OrgDynamicsQuestion]: + questions: List[OrgDynamicsQuestion] = [] + cap = MAX_RESOURCE_PRESSURE // 3 + + for day_start in range(1, self._max_day - 4, 7): + day_end = min(day_start + 6, self._max_day) + result = self._find_most_cross_dept_actor(day_start, day_end) + if not result: + continue + + actor, cross_events, ticket_hrs, event_ids = result + if not cross_events: + continue + + questions.append( + OrgDynamicsQuestion( + question_id=f"resource_crossdept_{actor}_D{day_start}_D{day_end}", + category="RESOURCE_PRESSURE", + difficulty="hard", + day_range=(day_start, day_end), + question_text=( + f"Between Day {day_start} and Day {day_end}, which team member " + f"appeared in the most cross-department discussions relative to " + f"their ticket workload, and what departments were involved?" + ), + ground_truth={ + "actor": actor, + "cross_dept_event_count": len(cross_events), + "ticket_hours": round(ticket_hrs, 2), + "cross_dept_events": event_ids, + "day_range": [day_start, day_end], + }, + evidence_search_space=event_ids + ["dept_plans"], + ) + ) + + if len(questions) >= cap: + break + + return questions + + def _resource_oncall_overload_questions(self) -> List[OrgDynamicsQuestion]: + """ + Find days where an on-call engineer also had >= 5h of scheduled work. + Uses is_on_call from the dept_plan engineer_plans entry. + An engineer carrying on-call duty alongside heavy scheduled work is a + capacity risk the assignment scorer may not fully capture. + """ + questions: List[OrgDynamicsQuestion] = [] + cap = MAX_RESOURCE_PRESSURE // 3 + + for day in range(1, self._max_day + 1): + plans = list( + self._mem._db["dept_plans"].find( + {"day": day}, {"_id": 0, "engineer_plans": 1, "dept": 1} + ) + ) + for plan in plans: + dept = plan.get("dept", "") + plan_id = f"PLAN-{day}-{dept}" + for ep in plan.get("engineer_plans", []): + if not ep.get("is_on_call"): + continue + name = ep.get("name", "") + total_load = self._agenda_hours(ep) + if total_load < 5.0: + continue + + questions.append( + OrgDynamicsQuestion( + question_id=f"resource_oncall_{name}_D{day}", + category="RESOURCE_PRESSURE", + difficulty="medium", + day_range=(day, day), + question_text=( + f"On Day {day}, which engineer in the {dept} team was " + f"carrying on-call responsibility, and how did that " + f"interact with their scheduled workload that day?" + ), + ground_truth={ + "engineer": name, + "dept": dept, + "day": day, + "is_on_call": True, + "scheduled_hours": round(total_load, 2), + "plan_artifact_id": plan_id, + }, + evidence_search_space=[plan_id], + evidence_plan_ids=[plan_id], + ) + ) + + if len(questions) >= cap: + break + + return questions + + def _find_most_over_capacity_engineer( + self, day: int + ) -> Optional[Tuple[str, float, float, str, str]]: + plans = list( + self._mem._db["dept_plans"].find( + {"day": day}, + {"_id": 0, "engineer_plans": 1, "dept": 1, "capacity_by_member": 1}, + ) + ) + if not plans: + return None + + worst = None + worst_overflow = 0.0 + + for plan in plans: + dept = plan.get("dept", "") + capacity_map = plan.get("capacity_by_member", {}) + plan_id = f"PLAN-{day}-{dept}" + + for ep in plan.get("engineer_plans", []): + name = ep.get("name", "") + capacity = float(capacity_map.get(name, 6.0)) + # Exclude deferred items — they don't consume real capacity + actual = self._agenda_hours(ep) + overflow = actual - capacity + if overflow > worst_overflow: + worst_overflow = overflow + worst = (name, capacity, actual, dept, plan_id) + + return worst + + def _find_most_cross_dept_actor( + self, day_start: int, day_end: int + ) -> Optional[Tuple[str, List, float, List[str]]]: + cross_events = [ + e + for e in self._events + if day_start <= e.day <= day_end + and e.type + in ( + "async_question", + "design_discussion", + "org_collision_tension", + "leadership_sync", + "feature_request_from_sales", + "stability_update_to_sales", + ) + and len( + set( + dept + for dept, members in self._org_chart.items() + for actor in (e.actors or []) + if actor in members + ) + ) + > 1 + ] + if not cross_events: + return None + + actor_cross: Dict[str, List] = {} + for ev in cross_events: + for actor in ev.actors or []: + actor_cross.setdefault(actor, []).append(ev) + + if not actor_cross: + return None + + top_actor = max(actor_cross, key=lambda a: len(actor_cross[a])) + events = actor_cross[top_actor] + + plans = list( + self._mem._db["dept_plans"].find( + { + "day": {"$gte": day_start, "$lte": day_end}, + "engineer_plans.name": top_actor, + }, + {"_id": 0, "engineer_plans": 1}, + ) + ) + ticket_hrs = sum( + self._agenda_hours(ep, activity_types=_TICKET_TYPES) + for plan in plans + for ep in plan.get("engineer_plans", []) + if ep.get("name") == top_actor + ) + + event_ids = [ + str((ev.artifact_ids or {}).get("slack_thread", ev.mongo_id or "")) + for ev in events + ] + return top_actor, events, ticket_hrs, event_ids + + # ───────────────────────────────────────────────────────────────────────── + # CATEGORY 3 — CAUSAL PRESSURE PROPAGATION + # ───────────────────────────────────────────────────────────────────────── + + def _causal_pressure_questions(self) -> List[OrgDynamicsQuestion]: + questions: List[OrgDynamicsQuestion] = [] + + complaint_emails = [ + e + for e in self._events + if e.type == "inbound_external_email" + and e.facts.get("email_type") in ("complaint", "escalation") + and e.facts.get("high_priority", False) + ] + + for email_event in complaint_emails[:MAX_CAUSAL_PRESSURE]: + day = email_event.day + source = email_event.facts.get("source", "an external contact") + org = email_event.facts.get("org", "") + subject = email_event.facts.get("subject", "") + email_id = (email_event.artifact_ids or {}).get("email", "") + + downstream_signals = self._email_to_downstream.get(email_id, []) + if not downstream_signals: + continue + + theme_shifts = self._find_theme_shifts_after(day, day + 3) + if not theme_shifts: + continue + + signal_ids = [ + str( + (ev.artifact_ids or {}).get("slack_thread", "") + or (ev.artifact_ids or {}).get("jira", "") + ) + for ev in downstream_signals + ] + signal_ids = [s for s in signal_ids if s] + + plan_ids = [f"PLAN-{t['day']}-{t['dept']}" for t in theme_shifts] + + questions.append( + OrgDynamicsQuestion( + question_id=f"causal_pressure_{email_id or 'email'}_D{day}", + category="CAUSAL_PRESSURE", + difficulty="hard", + day_range=(day, day + 3), + question_text=( + f"A complaint email arrived from {source}" + + (f" ({org})" if org else "") + + f" on Day {day}" + + (f" regarding '{subject[:60]}'" if subject else "") + + ". How did this external pressure propagate internally " + "over the following days, and which departments adjusted " + "their priorities as a result?" + ), + ground_truth={ + "trigger_email": email_id, + "trigger_day": day, + "source": source, + "org": org, + "downstream_events": signal_ids, + "theme_shifts": theme_shifts, + "propagation_chain": ([email_id] + signal_ids) + if email_id + else signal_ids, + }, + evidence_search_space=([email_id] if email_id else []) + + signal_ids + + plan_ids, + evidence_plan_ids=plan_ids, + ) + ) + + random.shuffle(questions) + return questions[:MAX_CAUSAL_PRESSURE] + + def _find_theme_shifts_after(self, day_start: int, day_end: int) -> List[Dict]: + plans = list( + self._mem._db["dept_plans"].find( + {"day": {"$gte": day_start, "$lte": day_end}}, + {"_id": 0, "day": 1, "dept": 1, "theme": 1, "raw.planner_reasoning": 1}, + ) + ) + return [ + { + "day": p["day"], + "dept": p.get("dept", ""), + "theme": p.get("theme", ""), + "reasoning": p.get("raw", {}).get("planner_reasoning", ""), + } + for p in plans + ] + + # ───────────────────────────────────────────────────────────────────────── + # CATEGORY 4 — ASSIGNMENT QUALITY + # ───────────────────────────────────────────────────────────────────────── + + def _assignment_quality_questions(self) -> List[OrgDynamicsQuestion]: + questions: List[OrgDynamicsQuestion] = [] + + has_scores = self._mem._db["assignment_scores"].count_documents({}) > 0 + + if has_scores: + questions += self._assignment_skill_mismatch_questions() + questions += self._assignment_opportunity_cost_questions() + questions += self._assignment_stress_questions() + else: + logger.warning( + "[org_dynamics] assignment_scores collection is empty — " + "falling back to stress-based assignment questions only. " + "Persist scores in ticket_assigner._hungarian_assign() for full coverage." + ) + questions += self._assignment_stress_questions() + + random.shuffle(questions) + return questions[:MAX_ASSIGNMENT_QUALITY] + + def _assignment_skill_mismatch_questions(self) -> List[OrgDynamicsQuestion]: + """Find tickets where skill_score was low but assignment happened anyway.""" + questions: List[OrgDynamicsQuestion] = [] + + poor_matches = list( + self._mem._db["assignment_scores"] + .find( + {"was_assigned": True, "skill_score": {"$lt": 0.6}}, + { + "_id": 0, + "engineer": 1, + "ticket_id": 1, + "skill_score": 1, + "stress_score": 1, + "composite_score": 1, + "day": 1, + }, + ) + .sort("skill_score", 1) + .limit(MAX_ASSIGNMENT_QUALITY) + ) + + for match in poor_matches: + plan_ids = [ + f"PLAN-{match['day']}-Engineering_Backend", + f"PLAN-{match['day']}-Engineering_Mobile", + ] + questions.append( + OrgDynamicsQuestion( + question_id=f"assignment_mismatch_{match['ticket_id']}_D{match['day']}", + category="ASSIGNMENT_QUALITY", + difficulty="hard", + day_range=(match["day"], match["day"]), + question_text=( + f"On Day {match['day']}, was {match['engineer']} the best " + f"available engineer for ticket {match['ticket_id']}? " + f"What does the assignment data suggest about the quality " + f"of this match given the team's current state?" + ), + ground_truth={ + "ticket_id": match["ticket_id"], + "assigned_engineer": match["engineer"], + "skill_score": round(match["skill_score"], 3), + "stress_score": round(match["stress_score"], 3), + "composite_score": round(match["composite_score"], 3), + "assessment": "poor_skill_match" + if match["skill_score"] < 0.5 + else "suboptimal_skill_match", + "day": match["day"], + }, + evidence_search_space=[match["ticket_id"]] + plan_ids, + evidence_plan_ids=plan_ids, + ) + ) + + return questions + + def _assignment_opportunity_cost_questions(self) -> List[OrgDynamicsQuestion]: + """ + Compares assigned engineer's composite_score against the best available + candidate (was_assigned=false rows for the same ticket). + + Only generates a question when the gap is meaningful (> 0.10). + The agent must retrieve all candidate rows, compare scores, and quantify + the cost of the actual decision. + """ + questions: List[OrgDynamicsQuestion] = [] + + assigned_rows = list( + self._mem._db["assignment_scores"].find( + {"was_assigned": True}, + { + "_id": 0, + "engineer": 1, + "ticket_id": 1, + "composite_score": 1, + "skill_score": 1, + "stress_score": 1, + "centrality_factor": 1, + "day": 1, + }, + ) + ) + + for assigned in assigned_rows: + ticket_id = assigned["ticket_id"] + assigned_score = assigned["composite_score"] + day = assigned["day"] + + candidates = list( + self._mem._db["assignment_scores"].find( + {"ticket_id": ticket_id, "was_assigned": False}, + { + "_id": 0, + "engineer": 1, + "composite_score": 1, + "skill_score": 1, + "stress_score": 1, + "centrality_factor": 1, + }, + ) + ) + if not candidates: + continue + + best = max(candidates, key=lambda c: c["composite_score"]) + opp_cost = round(best["composite_score"] - assigned_score, 4) + + if opp_cost <= 0.10: + continue + + plan_ids = [ + f"PLAN-{day}-Engineering_Backend", + f"PLAN-{day}-Engineering_Mobile", + ] + + questions.append( + OrgDynamicsQuestion( + question_id=f"assignment_opcost_{ticket_id}_D{day}", + category="ASSIGNMENT_QUALITY", + difficulty="hard", + day_range=(day, day), + question_text=( + f"On Day {day}, ticket {ticket_id} was assigned to " + f"{assigned['engineer']}. Looking at all the engineers " + f"who were evaluated for this ticket, was this the optimal " + f"choice, and if not, what was the best available alternative " + f"and how large was the gap?" + ), + ground_truth={ + "ticket_id": ticket_id, + "assigned_engineer": assigned["engineer"], + "assigned_composite": round(assigned_score, 4), + "best_candidate": best["engineer"], + "best_composite": round(best["composite_score"], 4), + "opportunity_cost": opp_cost, + "assessment": "suboptimal", + "day": day, + }, + evidence_search_space=[ticket_id] + plan_ids, + evidence_plan_ids=plan_ids, + ) + ) + + if len(questions) >= MAX_ASSIGNMENT_QUALITY // 2: + break + + return questions + + def _assignment_stress_questions(self) -> List[OrgDynamicsQuestion]: + """ + Find cases where a high-stress engineer was assigned a critical ticket. + Falls back to dept_plans capacity data when assignment_scores unavailable. + Question text does NOT reveal the stress finding. + """ + questions: List[OrgDynamicsQuestion] = [] + + incidents = [e for e in self._events if e.type == "incident_opened"] + + for incident in incidents[:MAX_ASSIGNMENT_QUALITY]: + day = incident.day + ticket_id = (incident.artifact_ids or {}).get("jira", "") + if not ticket_id: + continue + + ticket = ( + self._mem._db["jira_tickets"].find_one( + {"id": ticket_id}, {"_id": 0, "assignee": 1, "title": 1} + ) + or {} + ) + assignee = ticket.get("assignee", "") + if not assignee: + continue + + plan = self._mem._db["dept_plans"].find_one( + {"day": day, "engineer_plans.name": assignee}, + {"_id": 0, "engineer_plans": 1, "capacity_by_member": 1, "dept": 1}, + ) + if not plan: + continue + + capacity = plan.get("capacity_by_member", {}).get(assignee, 6.0) + if float(capacity) >= 5.0: + continue + + plan_id = f"PLAN-{day}-{plan.get('dept', '')}" + + questions.append( + OrgDynamicsQuestion( + question_id=f"assignment_stress_{ticket_id}_D{day}", + category="ASSIGNMENT_QUALITY", + difficulty="medium", + day_range=(day, day), + question_text=( + f"On Day {day}, ticket {ticket_id} was assigned to {assignee}. " + f"Based on the team's state that day, was this the right " + f"assignment, and who — if anyone — might have been a better choice?" + ), + ground_truth={ + "ticket_id": ticket_id, + "assigned_engineer": assignee, + "capacity_that_day": round(float(capacity), 2), + "stress_indicator": "high" + if float(capacity) < 4.0 + else "elevated", + "day": day, + "plan_artifact_id": plan_id, + }, + evidence_search_space=[ + ticket_id, + plan_id, + (incident.artifact_ids or {}).get("slack_thread", ""), + ], + evidence_plan_ids=[plan_id], + ) + ) + + return questions + + # ───────────────────────────────────────────────────────────────────────── + # CATEGORY 5 — ORGANIZATIONAL FRICTION + # ───────────────────────────────────────────────────────────────────────── + + def _org_friction_questions(self) -> List[OrgDynamicsQuestion]: + questions: List[OrgDynamicsQuestion] = [] + + tension_events = [ + e + for e in self._events + if e.type + in ( + "org_collision_tension", + "leadership_sync", + "assignment_domain_mismatch", + ) + and e.facts.get("tension_level") in ("high", "medium") + ] + + for ev in tension_events[:MAX_ORG_FRICTION]: + day = ev.day + actors = ev.actors or [] + tension = ev.facts.get("tension_level", "medium") + rationale = ev.facts.get("rationale", "") + slack_id = (ev.artifact_ids or {}).get("slack_thread", "") + + actor_depts = { + actor: next( + ( + dept + for dept, members in self._org_chart.items() + if actor in members + ), + "Unknown", + ) + for actor in actors + } + depts_involved = list(set(actor_depts.values())) + plan_ids = [f"PLAN-{day}-{dept}" for dept in depts_involved] + + questions.append( + OrgDynamicsQuestion( + question_id=f"friction_{ev.mongo_id or 'evt'}_D{day}", + category="ORG_FRICTION", + difficulty="hard" if tension == "high" else "medium", + day_range=(day, day), + question_text=( + f"On Day {day}, there was a {tension}-tension cross-department " + f"interaction involving {' and '.join(depts_involved)}. " + f"What caused it, who was involved, and what were each " + f"department's competing priorities at the time?" + ), + ground_truth={ + "day": day, + "tension_level": tension, + "actors": actors, + "depts_involved": depts_involved, + "rationale": rationale, + # event_summary is the state-machine-grounded string used by + # the embedding scorer in org_dynamics_scorer.py as ground truth. + # Falls back to rationale if SimEvent.summary is not present. + "event_summary": getattr(ev, "summary", None) or rationale, + "event_id": ev.mongo_id or "", + "slack_artifact": slack_id, + "plan_artifacts": plan_ids, + }, + evidence_search_space=( + [slack_id] + plan_ids if slack_id else plan_ids + ), + evidence_plan_ids=plan_ids, + ) + ) + + # Week-level friction pattern + week_tension = self._compute_weekly_tension() + if week_tension: + worst_week_start, tension_count, week_event_ids = week_tension + worst_week_end = min(worst_week_start + 6, self._max_day) + + questions.append( + OrgDynamicsQuestion( + question_id=f"friction_worst_week_D{worst_week_start}", + category="ORG_FRICTION", + difficulty="hard", + day_range=(worst_week_start, worst_week_end), + question_text=( + "Which week of the simulation had the highest cross-department " + "tension, what drove it, and which departments were most involved?" + ), + ground_truth={ + "week_start_day": worst_week_start, + "week_end_day": worst_week_end, + "tension_event_count": tension_count, + "evidence_events": week_event_ids, + }, + evidence_search_space=week_event_ids, + ) + ) + + random.shuffle(questions) + return questions[:MAX_ORG_FRICTION] + + def _compute_weekly_tension(self) -> Optional[Tuple[int, int, List[str]]]: + tension_by_week: Dict[int, List] = {} + + for ev in self._events: + if ev.type not in ( + "org_collision_tension", + "assignment_domain_mismatch", + "morale_intervention", + ): + continue + week_start = ((ev.day - 1) // 5) * 5 + 1 + tension_by_week.setdefault(week_start, []).append(ev) + + if not tension_by_week: + return None + + worst = max(tension_by_week, key=lambda w: len(tension_by_week[w])) + events = tension_by_week[worst] + event_ids = [ + str((ev.artifact_ids or {}).get("slack_thread", ev.mongo_id or "")) + for ev in events + ] + return worst, len(events), event_ids + + # ───────────────────────────────────────────────────────────────────────── + # SERIALISATION + # ───────────────────────────────────────────────────────────────────────── + + def _to_dict(self, q: OrgDynamicsQuestion) -> Dict: + return { + "question_id": q.question_id, + "question_type": "ORG_DYNAMICS", + "category": q.category, + "difficulty": q.difficulty, + "day_range": list(q.day_range), + "question_text": q.question_text, + "ground_truth": q.ground_truth, + "evidence_search_space": [s for s in q.evidence_search_space if s], + # Separated so the harness can route these to get_dept_plan tool + "evidence_plan_ids": [s for s in q.evidence_plan_ids if s], + "requires_reasoning": q.requires_reasoning, + "track_weights": _TRACK_WEIGHTS[q.category], + } diff --git a/eval/org_dynamics_scorer.py b/eval/org_dynamics_scorer.py new file mode 100644 index 0000000..86d371f --- /dev/null +++ b/eval/org_dynamics_scorer.py @@ -0,0 +1,545 @@ +""" +org_dynamics_scorer.py +======================= +Scoring logic for OrgForge Organizational Dynamics questions. + +Drop this alongside agentic_eval_harness.py and import it. + +Each category has its own answer scorer and trajectory scorer. +All scorers follow the same pattern as PerspectiveScorer / CounterfactualScorer: + + score_answer(final_answer, ground_truth) → (float, bool) + score_trajectory(trajectory, question) → OrgDynamicsTrajectoryScore + +Answer scoring uses partial credit across structured components. +Trajectory scoring checks whether the agent retrieved the right artifacts +before answering — the evidence_search_space from the question defines +what "right" means. + +Changes from v1: +- no_hallucination check is now real: flags agent-cited names not in GT or org chart +- correct_tools_used penalty for zero tool use is now 0.0 (was 0.3) +- Unified correct threshold of 0.55 across all categories +- OrgFriction root cause scoring uses token-overlap only on content words (len>4), + with a note that semantic similarity would be more robust +- CAUSAL_PRESSURE downstream_signals linkage tightened (see question builder) +""" + +from __future__ import annotations + +import logging +import re +from dataclasses import dataclass +from typing import Dict, Set, Tuple + +logger = logging.getLogger("orgforge.org_dynamics_scorer") + +# Shared org-chart actor allowlist — populated once at import time via +# _configure_known_actors(). Scorers will work without it (hallucination +# check is skipped if the set is empty) but calling _configure_known_actors() +# from eval harness setup gives the best signal. +_KNOWN_ACTORS: Set[str] = set() + + +def configure_known_actors(names: Set[str]) -> None: + """Call once from harness setup with the full set of employee names.""" + global _KNOWN_ACTORS + _KNOWN_ACTORS = {n.lower() for n in names} + + +# ───────────────────────────────────────────────────────────────────────────── +# TRAJECTORY SCORE DATACLASS +# ───────────────────────────────────────────────────────────────────────────── + + +@dataclass +class OrgDynamicsTrajectoryScore: + search_coverage: float # fraction of evidence_search_space the agent checked + correct_tools_used: float # did agent use appropriate tools for the category + no_hallucination: float # agent didn't fabricate actors/numbers not in corpus + composite: float + + +# ───────────────────────────────────────────────────────────────────────────── +# ANSWER SCORERS BY CATEGORY +# ───────────────────────────────────────────────────────────────────────────── + +# Unified passing threshold — all categories use this. +_CORRECT_THRESHOLD = 0.55 + + +class AttentionCostScorer: + """ + Scores ATTENTION_COST answers. + + Expected answer schema: + { + "actor": "string", + "total_distraction_hours": float, + "reasoning": "string" + } + OR for overhead questions: + { + "pct_non_ticket": float, + "ticket_hours": float, + "non_ticket_hours": float, + "reasoning": "string" + } + """ + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + score = 0.0 + + # Actor identification (if applicable) + gt_actor = ground_truth.get("actor", "") + if gt_actor: + agent_actor = str(final_answer.get("actor", "")).strip() + if agent_actor.lower() == gt_actor.lower(): + score += 0.40 + elif gt_actor.lower() in agent_actor.lower(): + score += 0.20 + + # Numeric accuracy — distraction hours or pct_non_ticket + gt_hours = ground_truth.get("total_distraction_hours") + gt_pct = ground_truth.get("pct_non_ticket") + + if gt_hours is not None: + try: + agent_hours = float(final_answer.get("total_distraction_hours", -1)) + if abs(agent_hours - gt_hours) <= 0.25: + score += 0.40 + elif abs(agent_hours - gt_hours) <= 0.75: + score += 0.20 + elif agent_hours > 0: + score += 0.10 # found something, wrong magnitude + except (ValueError, TypeError): + pass + + elif gt_pct is not None: + try: + agent_pct = float(final_answer.get("pct_non_ticket", -1)) + if abs(agent_pct - gt_pct) <= 5.0: + score += 0.40 + elif abs(agent_pct - gt_pct) <= 15.0: + score += 0.20 + except (ValueError, TypeError): + pass + + # Day range awareness + gt_range = ground_truth.get("day_range", []) + agent_reasoning = str(final_answer.get("reasoning", "")).lower() + if gt_range and str(gt_range[0]) in agent_reasoning: + score += 0.20 + + correct = score >= _CORRECT_THRESHOLD + return round(min(score, 1.0), 4), correct + + def score_trajectory( + self, trajectory, question: Dict + ) -> OrgDynamicsTrajectoryScore: + return _score_trajectory_generic( + trajectory, + question, + required_tools={"get_events_for_day", "search_artifacts"}, + ) + + +class ResourcePressureScorer: + """ + Scores RESOURCE_PRESSURE answers. + + Expected answer schema: + { + "engineer": "string", + "dept": "string", + "overflow_hours": float, OR "cross_dept_event_count": int, + "reasoning": "string" + } + """ + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + score = 0.0 + + gt_engineer = ground_truth.get("engineer", "") + gt_actor = ground_truth.get("actor", gt_engineer) + + agent_engineer = str( + final_answer.get("engineer", final_answer.get("actor", "")) + ).strip() + + if gt_actor and agent_engineer.lower() == gt_actor.lower(): + score += 0.45 + elif gt_actor and gt_actor.lower() in agent_engineer.lower(): + score += 0.25 + + # Dept identification + gt_dept = ground_truth.get("dept", "") + if gt_dept: + agent_dept = str(final_answer.get("dept", "")).lower() + if gt_dept.lower() in agent_dept or agent_dept in gt_dept.lower(): + score += 0.20 + + # Numeric accuracy + gt_overflow = ground_truth.get("overflow_hours") + if gt_overflow is not None: + try: + agent_overflow = float(final_answer.get("overflow_hours", -1)) + if abs(agent_overflow - gt_overflow) <= 0.5: + score += 0.35 + elif abs(agent_overflow - gt_overflow) <= 1.5: + score += 0.15 + except (ValueError, TypeError): + pass + + correct = score >= _CORRECT_THRESHOLD + return round(min(score, 1.0), 4), correct + + def score_trajectory( + self, trajectory, question: Dict + ) -> OrgDynamicsTrajectoryScore: + return _score_trajectory_generic( + trajectory, + question, + required_tools={"get_events_for_day", "search_artifacts"}, + ) + + +class CausalPressureScorer: + """ + Scores CAUSAL_PRESSURE answers. + + Expected answer schema: + { + "trigger_source": "string", + "downstream_depts": ["string"], + "propagation_chain": ["artifact_id"], + "theme_shifts": [{"dept": "string", "day": int, "theme": "string"}], + "reasoning": "string" + } + """ + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + score = 0.0 + + # Source identification + gt_source = str(ground_truth.get("source", "")).lower() + gt_org = str(ground_truth.get("org", "")).lower() + agent_reasoning = str(final_answer.get("reasoning", "")).lower() + agent_source = str(final_answer.get("trigger_source", "")).lower() + + if gt_source and (gt_source in agent_source or gt_source in agent_reasoning): + score += 0.25 + elif gt_org and gt_org in agent_reasoning: + score += 0.15 + + # Downstream dept identification + gt_theme_shifts = ground_truth.get("theme_shifts", []) + gt_depts = {t["dept"].lower() for t in gt_theme_shifts} + agent_depts_raw = final_answer.get("downstream_depts", []) + agent_depts = {str(d).lower() for d in agent_depts_raw} + + if gt_depts: + overlap = len(gt_depts & agent_depts) / len(gt_depts) + score += 0.35 * overlap + + # Propagation chain — did agent find downstream artifacts? + gt_chain = set(ground_truth.get("propagation_chain", [])) + agent_chain = set(final_answer.get("propagation_chain", [])) + if gt_chain: + chain_overlap = len(gt_chain & agent_chain) / len(gt_chain) + score += 0.40 * chain_overlap + + correct = score >= _CORRECT_THRESHOLD + return round(min(score, 1.0), 4), correct + + def score_trajectory( + self, trajectory, question: Dict + ) -> OrgDynamicsTrajectoryScore: + return _score_trajectory_generic( + trajectory, + question, + required_tools={"get_email", "search_artifacts", "get_events_for_day"}, + ) + + +class AssignmentQualityScorer: + """ + Scores ASSIGNMENT_QUALITY answers. + + Expected answer schema: + { + "ticket_id": "string", + "assigned_engineer": "string", + "assessment": "optimal | suboptimal | poor", + "alternative_engineer": "string", (optional) + "reasoning": "string" + } + """ + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + score = 0.0 + + # Ticket identification + gt_ticket = ground_truth.get("ticket_id", "") + agent_ticket = str(final_answer.get("ticket_id", "")) + if gt_ticket and agent_ticket == gt_ticket: + score += 0.20 + + # Engineer identification + gt_engineer = ground_truth.get("assigned_engineer", "") + agent_engineer = str(final_answer.get("assigned_engineer", "")).strip() + if gt_engineer and agent_engineer.lower() == gt_engineer.lower(): + score += 0.25 + + # Assessment accuracy + gt_assessment = ground_truth.get("assessment", "") + agent_assessment = str(final_answer.get("assessment", "")).lower() + gt_is_poor = "poor" in gt_assessment or "mismatch" in gt_assessment + agent_is_poor = "poor" in agent_assessment or "suboptimal" in agent_assessment + + if gt_is_poor == agent_is_poor: + score += 0.35 + + # Stress indicator awareness + gt_stress = ground_truth.get("stress_indicator", "") + agent_reasoning = str(final_answer.get("reasoning", "")).lower() + if gt_stress and (gt_stress in agent_reasoning or "stress" in agent_reasoning): + score += 0.20 + + correct = score >= _CORRECT_THRESHOLD + return round(min(score, 1.0), 4), correct + + def score_trajectory( + self, trajectory, question: Dict + ) -> OrgDynamicsTrajectoryScore: + return _score_trajectory_generic( + trajectory, + question, + required_tools={"get_ticket", "search_artifacts"}, + ) + + +class OrgFrictionScorer: + """ + Scores ORG_FRICTION answers. + + Expected answer schema: + { + "tension_level": "high | medium | low", + "actors": ["string"], + "depts_involved": ["string"], + "root_cause": "string", + "reasoning": "string" + } + + Root cause scoring uses content-word overlap (words > 4 chars) between the + ground truth rationale and the agent's root_cause + reasoning fields. + This is a rough proxy — semantic similarity scoring would be more robust + but requires an embedding call. The overlap is capped at 1.0 and weighted + generously (2× multiplier) because prose paraphrasing naturally reduces + exact token overlap. + """ + + def score_answer( + self, final_answer: Dict, ground_truth: Dict + ) -> Tuple[float, bool]: + if not final_answer: + return 0.0, False + + score = 0.0 + + # Tension level + gt_tension = ground_truth.get("tension_level", "").lower() + agent_tension = str(final_answer.get("tension_level", "")).lower() + if gt_tension and agent_tension == gt_tension: + score += 0.20 + elif gt_tension == "high" and agent_tension in ("high", "medium"): + score += 0.10 + + # Actor identification + gt_actors = {a.lower() for a in ground_truth.get("actors", [])} + agent_actors = {str(a).lower() for a in final_answer.get("actors", [])} + if gt_actors: + actor_overlap = len(gt_actors & agent_actors) / len(gt_actors) + score += 0.30 * actor_overlap + + # Dept identification + gt_depts = {d.lower() for d in ground_truth.get("depts_involved", [])} + agent_depts = {str(d).lower() for d in final_answer.get("depts_involved", [])} + if gt_depts: + dept_overlap = len(gt_depts & agent_depts) / len(gt_depts) + score += 0.25 * dept_overlap + + # Root cause / rationale — content-word overlap only (skip stop-words). + # Words ≤ 4 chars are excluded to avoid matching "this", "that", "with" etc. + gt_rationale = ground_truth.get("rationale", "").lower() + agent_cause = str(final_answer.get("root_cause", "")).lower() + agent_reasoning = str(final_answer.get("reasoning", "")).lower() + + if gt_rationale: + gt_words = set(re.findall(r"[a-z]+", gt_rationale)) + agent_words = set( + re.findall(r"[a-z]+", agent_cause + " " + agent_reasoning) + ) + content_words = {w for w in gt_words if len(w) > 4} + if content_words: + word_overlap = len(content_words & agent_words) / len(content_words) + score += 0.25 * min(word_overlap * 2, 1.0) + + correct = score >= _CORRECT_THRESHOLD + return round(min(score, 1.0), 4), correct + + def score_trajectory( + self, trajectory, question: Dict + ) -> OrgDynamicsTrajectoryScore: + return _score_trajectory_generic( + trajectory, + question, + required_tools={ + "get_slack_thread", + "get_events_for_day", + "search_artifacts", + }, + ) + + +# ───────────────────────────────────────────────────────────────────────────── +# SHARED TRAJECTORY SCORER +# ───────────────────────────────────────────────────────────────────────────── + + +def _score_trajectory_generic( + trajectory, + question: Dict, + required_tools: set, +) -> OrgDynamicsTrajectoryScore: + """ + Generic trajectory scorer used by all ORG_DYNAMICS categories. + + Three components: + 1. search_coverage — fraction of evidence_search_space the agent retrieved + 2. correct_tools_used — 1.0 if at least one required tool was used, 0.0 otherwise + (was 0.3 for zero-tool case in v1 — fixed) + 3. no_hallucination — penalises agent for citing names that are not in the + ground truth actor list and not in the known org roster. + Returns 1.0 when _KNOWN_ACTORS is not configured (safe + default) or when no suspicious names are found. + """ + calls = trajectory.tool_calls or [] + + if not calls: + return OrgDynamicsTrajectoryScore( + search_coverage=0.0, + correct_tools_used=0.0, # no tools → no credit (was 0.3) + no_hallucination=1.0, # no claims → no hallucinations + composite=0.0, + ) + + # ── 1. Search coverage ─────────────────────────────────────────────────── + expected_space = set(question.get("evidence_search_space", [])) + retrieved_ids: set = set() + for call in calls: + retrieved_ids.update(call.result_ids or []) + for arg_val in (call.arguments or {}).values(): + if isinstance(arg_val, str) and len(arg_val) > 3: + retrieved_ids.add(arg_val) + + coverage = ( + len(expected_space & retrieved_ids) / len(expected_space) + if expected_space + else (1.0 if calls else 0.0) + ) + + # ── 2. Correct tools used ──────────────────────────────────────────────── + used_tools = {c.tool_name for c in calls} + tools_score = 1.0 if required_tools & used_tools else 0.0 # was 0.3 + + # ── 3. Hallucination check ─────────────────────────────────────────────── + # Collect the ground-truth actor allowlist for this question. + gt = question.get("ground_truth", {}) + gt_actors: Set[str] = set() + for field in ("actors", "depts_involved", "trigger_actors"): + for name in gt.get(field, []): + gt_actors.add(str(name).lower()) + for field in ("actor", "engineer", "assigned_engineer"): + val = gt.get(field, "") + if val: + gt_actors.add(str(val).lower()) + + hallucination_score = 1.0 # default: clean + + if _KNOWN_ACTORS and gt_actors: + # Extract all capitalised tokens from the agent's final answer that look + # like proper nouns (first-letter uppercase, ≥ 2 chars). If any appear + # in the corpus-wide actor list but NOT in the GT allowlist for this + # question, they are suspicious but not conclusive (e.g. a mentioned + # bystander). We only penalise if they are wholly absent from the known + # org roster — those are genuine fabrications. + final_str = str(trajectory.final_answer or "") + cited_names = { + tok.lower() for tok in re.findall(r"\b[A-Z][a-z]{1,}\b", final_str) + } + # Remove names that are legitimately in the ground truth or org + suspicious = cited_names - gt_actors - _KNOWN_ACTORS + if suspicious: + # Each fabricated name deducts 0.15, floor 0.0 + penalty = min(len(suspicious) * 0.15, 1.0) + hallucination_score = round(max(0.0, 1.0 - penalty), 4) + logger.debug( + f"[org_dynamics] Hallucination flag: suspicious names={suspicious} " + f"→ score {hallucination_score}" + ) + + # ── Composite ──────────────────────────────────────────────────────────── + composite = round( + 0.50 * coverage + 0.30 * tools_score + 0.20 * hallucination_score, + 4, + ) + + return OrgDynamicsTrajectoryScore( + search_coverage=round(coverage, 4), + correct_tools_used=tools_score, + no_hallucination=hallucination_score, + composite=composite, + ) + + +# ───────────────────────────────────────────────────────────────────────────── +# DISPATCHER — maps category → scorer instance +# ───────────────────────────────────────────────────────────────────────────── + +_SCORERS = { + "ATTENTION_COST": AttentionCostScorer(), + "RESOURCE_PRESSURE": ResourcePressureScorer(), + "CAUSAL_PRESSURE": CausalPressureScorer(), + "ASSIGNMENT_QUALITY": AssignmentQualityScorer(), + "ORG_FRICTION": OrgFrictionScorer(), +} + + +def get_scorer(category: str): + """Returns the appropriate scorer for a given category.""" + scorer = _SCORERS.get(category) + if not scorer: + raise ValueError(f"Unknown ORG_DYNAMICS category: {category}") + return scorer diff --git a/eval/questions_jsonl.py b/eval/questions_jsonl.py new file mode 100644 index 0000000..2e42845 --- /dev/null +++ b/eval/questions_jsonl.py @@ -0,0 +1,14 @@ +import json +from pathlib import Path + +src = Path("export/eval/eval_questions.json") +out = Path("export/eval/eval_questions.jsonl") + +data = json.loads(src.read_text()) +questions = data["questions"] + +with open(out, "w") as f: + for q in questions: + f.write(json.dumps(q) + "\n") + +print(f"Wrote {len(questions)} questions to {out}") diff --git a/pyproject.toml b/pyproject.toml index 7d0972c..6945cf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,13 +3,40 @@ requires = ["setuptools>=64", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] -name = "flow" +name = "orgforge" dynamic = ["version"] readme = "README.md" description = "Synthetic corporate dataset generator for AI agent evaluation." +requires-python = ">=3.13" +dependencies = [ + "crewai>=1.14.5", + "crewai-tools>=1.14.5", + "pymongo>=4.6.0", + "requests>=2.31.0", + "pyyaml>=6.0", + "pydantic>=2.0.0", + "networkx>=3.0", + "vaderSentiment>=3.3.2", + "rich>=13.0.0", + "boto3==1.42.63", + "scipy>=1.17.1", + "json-repair>=0.58.6", + +] + +[dependency-groups] +dev = [ + "mongomock>=4.3.0", + "pytest>=9.1.1", + "ruff>=0.4.0", +] [tool.setuptools_scm] +[tool.uv] +override-dependencies = [ + "json-repair>=0.58.6", +] [tool.setuptools.packages.find] where = ["src"] @@ -19,4 +46,10 @@ testpaths = ["tests"] pythonpath = ["src"] filterwarnings = [ "ignore:deprecated:DeprecationWarning:crewai.crew", -] \ No newline at end of file +] + +[tool.ruff] +src = ["src"] + +[tool.ruff.lint] +select = ["E", "F", "B"] diff --git a/requirements-cloud.txt b/requirements-cloud.txt deleted file mode 100644 index 68660c7..0000000 --- a/requirements-cloud.txt +++ /dev/null @@ -1,5 +0,0 @@ --r requirements.txt - -boto3>=1.34.0 -langchain-aws>=0.1.0 -openai>=1.12.0 \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index 9832960..0000000 --- a/requirements-test.txt +++ /dev/null @@ -1,5 +0,0 @@ --r requirements.txt - -pytest>=7.4.0 -pytest-cov>=4.1.0 -mongomock==4.3.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7b898a5..0000000 --- a/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -crewai>=0.28.0 -crewai-tools>=0.1.0 -langchain-ollama>=0.1.0 -pymongo>=4.6.0 -requests>=2.31.0 -pyyaml>=6.0 -pydantic>=2.0.0 -networkx>=3.0 -vaderSentiment>=3.3.2 -rich>=13.0.0 -ollama>=0.1.7 -boto3==1.42.63 -scipy>=1.17.1 -json-repair>=0.58.6 \ No newline at end of file diff --git a/simulation.log.txt b/simulation.log.txt deleted file mode 100644 index 3f4320d..0000000 --- a/simulation.log.txt +++ /dev/null @@ -1,5758 +0,0 @@ -2026-04-02 19:53:48: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 19:57:16: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:06:32: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:13:50: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:15:54: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:16:45: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:17:58: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:19:59: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:21:49: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:33:53: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:35:21: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:35:26: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:56:57: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:57:05: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "There is no explicit evidence in the available documents that the Day 8 incident directly triggered the creation of a postmortem page in Confluence or a root cause analysis entry in Jira or Slack. Without a documented causal link, we cannot assert that the absence of the incident would have prevented those artifacts from being created.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "The search results returned incident records (e.g., ENG-111) and various Confluence pages, but none explicitly reference a 'Day 8' incident, its postmortem, or a root cause analysis tied to that incident. Therefore, the required causal connection is missing, indicating a knowledge gap rather than a confirmed causal relationship." -}" -2026-04-02 20:57:05: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:57:12: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any record of Taylor's review comments, approval status, or a request for changes on Day 6 for Alex's pull request. Consequently, there is no documented causal link showing that Taylor's actions caused Alex to revise the implementation before merge.", - "causal_mechanism": "involves_gap", - "actors": ["Taylor", "Alex"], - "reasoning": "Since the data lacks any evidence of Taylor approving the PR without changes or Alex making revisions in response, we cannot assert that removing Taylor's approval would have prevented Alex's revision. The missing information represents a knowledge gap, so the outcome would not be shown to change based on the current records." -}" -2026-04-02 20:57:13: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:57:20: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the document corpus for any records dated Day 14 that mention an inbound email involving Ravi and Nadia, as well as any related internal routing entries (including Jira tickets). The only Day 14 record found is an outbound email (ack_vendor_ravi_14) where Nadia replies to an earlier inbound message from Ravi. No inbound email on Day 14 involving Ravi and Nadia, nor any internal routing configuration for such an email, was located. Therefore, there is no evidence that an internal routing for the inbound email on Day 14 involving Ravi and Nadia was created." -}" -2026-04-02 20:57:20: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:57:26: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched all Confluence artifacts created after Day 11 for any mention of a knowledge gap, Sam, or Chloe. The only Confluence creation on Day 11 was CONF-ENG-042 by Jax, unrelated to Sam or Chloe. No other Confluence pages were found, and no events indicate a knowledge gap involving Sam and Chloe that prompted a documentation page. Therefore, no Confluence documentation page was created in response to that knowledge gap." -}" -2026-04-02 20:57:26: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:57:33: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The inbound email from Ravi on Day 4 (artifact ext_email_ravi_4_6) is present in the email system, but there is no evidence that John was a recipient or that he accessed it. The only response recorded is an acknowledgment sent by Nadia (artifact ack_vendor_ravi_4), indicating the email was handled by another liaison. Therefore, John would not have been aware of the email as of Day 4.", - "evidence_artifacts": ["ext_email_ravi_4_6", "ack_vendor_ravi_4"], - "blocked_subsystems": [] -}" -2026-04-02 20:57:33: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:57:38: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "No inbound email from Gridiron Pro Football with the title “Re: Final proposal & next steps for Gridiron Pro Football” is present in the email artifacts accessible to Patty. Searches of the email corpus did not return any matching email, and there is no evidence from Slack, Confluence, or Zoom indicating that Patty was notified about such an email. Therefore, as of Day 29, Patty could not have known about it.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 20:57:38: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:57:44: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "A search of Jordan's accessible systems (Confluence, Jira, Slack, Email, Git, Datadog, Zoom) for any mention of a design discussion about the 'Define automation framework roadmap' on Day 2 returned no matching artifacts. The only Day 2 items found relate to other design documents and Jira tickets, none referencing the Define automation framework roadmap. Therefore Jordan would not have had knowledge of that specific discussion as of Day 2.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 20:57:45: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:57:50: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available documents include Vince's expertise profile but contain no explicit record of ticket or escalation reassignment tied to his departure on Day 3. Without a documented link, we cannot establish that his departure caused the reassignment, so removing the departure would not be shown to change the reassignment outcome.", - "causal_mechanism": "involves_gap", - "actors": ["Vince"], - "reasoning": "Search results reveal Vince's role and departure timing but lack any statement that his active tickets or escalation duties were transferred after he left. Since the causal connection is undocumented, the question cannot be answered affirmatively; the effect (reassignment) is not proven to depend on the cause (departure)." -}" -2026-04-02 20:57:51: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:57:56: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available records show Maya's emails on days 2, 17, and 18 each resulted in a corresponding Jira ticket (ORG-100, ORG-137). No email from Maya is recorded on day 21, nor is there a Jira ticket linked to a day‑21 email. Because the documentation does not contain any evidence that a day‑21 email existed or that it triggered a ticket, we cannot establish a causal link between a hypothetical day‑21 email and the creation of a Jira ticket.", - "causal_mechanism": "involves_gap", - "actors": ["Maya", "Jax"], - "reasoning": "The question asks whether the absence of a day‑21 email would prevent a ticket from being created. The data set does not include any day‑21 email from Maya nor a ticket tied to such an email. Without explicit documentation of that email-to-ticket relationship, there is a knowledge gap, preventing us from asserting that the ticket’s existence depends on the day‑21 email. Hence, removing the cause (the email) cannot be shown to change the effect (ticket creation)." -}" -2026-04-02 20:57:56: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:58:02: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of all artifacts accessible to Umji (Confluence pages, emails, Slack messages, and Zendesk tickets) up to Day 6 returned no mention of a knowledge gap in the undocumented domain, the unknown issue left by the departed employee, or the reviewer_audit detection. The reviewer_audit system and related code repositories (Git) are outside Umji's access, so any information originating from those sources would be unavailable to her.", - "evidence_artifacts": [], - "blocked_subsystems": ["git", "reviewer_audit"] -}" -2026-04-02 20:58:02: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 16 involving Kaitlyn, Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:58:09: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 25 involving Zoe, Ben? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:58:15: task_name="None", task="As of Day 18, would Sanjay have known about the inbound email from Ingrid (GitHub) regarding “Enterprise Audit Log Access & Retention – Next Steps”? - -IMPORTANT: You are answering from the perspective of Sanjay as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:58:23: task_name="None", task="As of Day 18, would Sanjay have known about the inbound email from Ingrid (GitHub) regarding “Enterprise Audit Log Access & Retention – Next Steps”? - -IMPORTANT: You are answering from the perspective of Sanjay as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Search of Sanjay's accessible email artifacts (the only system where an inbound email from Ingrid would appear) returned no results matching the subject “Enterprise Audit Log Access & Retention – Next Steps”. No other accessible subsystem (confluence, datadog, git, jira, slack, zoom) contains evidence of this email. Therefore Sanjay could not have known about it by Day 18.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 20:58:24: task_name="None", task="As of Day 23, could Tom have been aware of the design discussion about Ops sync on handoff, given that he has access to Confluence, email, Slack, and Zoom, but such design discussion events are visible only to direct participants and he was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Tom as of Day 23. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:58:30: task_name="None", task="As of Day 23, could Tom have been aware of the design discussion about Ops sync on handoff, given that he has access to Confluence, email, Slack, and Zoom, but such design discussion events are visible only to direct participants and he was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Tom as of Day 23. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Tom's accessible systems (Confluence, Email, Slack, Zoom) contain no records of a design discussion titled 'Ops sync on handoff' after Day 20, and the policy states such design discussion events are visible only to direct participants. Since Tom was not listed as a participant, he could not have become aware of the discussion.", - "evidence_artifacts": [], - "blocked_subsystems": ["design_discussion"] -}" -2026-04-02 20:58:30: task_name="None", task="As of Day 9, could Umji—who has access to Confluence, email, Slack, and Zendesk but not Git—have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 9. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:58:36: task_name="None", task="As of Day 9, could Umji—who has access to Confluence, email, Slack, and Zendesk but not Git—have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 9. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of all artifacts accessible to Umji (Confluence, email, Slack, Zendesk) up to Day 9 returned no mention of the undocumented domain knowledge gap, the triggering unknown issue, the departed employee, or the reviewer_audit detection. Without any documented references in these systems, Umji could not have become aware of the gap. Access to Git, where related code or version‑control comments might reside, is blocked, further limiting visibility.", - "evidence_artifacts": [], - "blocked_subsystems": ["git"] -}" -2026-04-02 20:58:37: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 3 involving Yusuf and Marcus? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:58:45: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 3 involving Yusuf and Marcus? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I performed multiple searches across the Confluence corpus focusing on day 3 and the keywords \"knowledge gap\", \"Yusuf\", and \"Marcus\". Searches for \"knowledge gap Yusuf Marcus\" after day 3 returned no results. Separate searches for \"knowledge gap\" after day 3, for \"Yusuf\" after day 3, and for \"Marcus\" after day 3 also returned no relevant Confluence pages. The broader searches for \"Yusuf\" and \"Marcus\" returned only day 1 documents unrelated to a knowledge gap on day 3. Since no Confluence documentation page linking a knowledge gap detected on day 3 involving Yusuf and Marcus was found, the answer is that such a page does not exist." -}" -2026-04-02 20:58:46: task_name="None", task="Did we create a Salesforce risk flag on the related deals in response to the incident on Day 24 involving Jax, Reese? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:58:50: task_name="None", task="Given that Janice has access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but inbound external email events are visible only to direct participants and she was not listed as a participant, would Janice have known about the inbound email from Gridiron Pro Football titled “Re: Re: Re: Re: Urgent Request: Real-Time Load Monitoring Upgrade Needed” as of Day 25? - -IMPORTANT: You are answering from the perspective of Janice as of Day 25. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:58:55: task_name="None", task="Given that Janice has access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but inbound external email events are visible only to direct participants and she was not listed as a participant, would Janice have known about the inbound email from Gridiron Pro Football titled “Re: Re: Re: Re: Urgent Request: Real-Time Load Monitoring Upgrade Needed” as of Day 25? - -IMPORTANT: You are answering from the perspective of Janice as of Day 25. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "No email artifact matching the title “Re: Re: Re: Re: Urgent Request: Real-Time Load Monitoring Upgrade Needed” is present in Janice's accessible email collection, and inbound external emails are only visible to direct participants. Janice was not a participant, so even if the email existed, she would not have been able to see it.", - "evidence_artifacts": [], - "blocked_subsystems": ["email"] -}" -2026-04-02 20:58:55: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 25 involving Jordan? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:59:01: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 25 involving Jordan? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the corpus for any Confluence documentation created after Day 25 that mentions Jordan, and also queried events for Day 25 for any knowledge‑gap detection involving Jordan. Both searches returned no results. No event on Day 25 references a knowledge gap involving Jordan, and there is no Confluence page linked to such a gap. Therefore, there is no evidence that a Confluence documentation page was created in response to a knowledge gap detected on Day 25 involving Jordan." -}" -2026-04-02 20:59:02: task_name="None", task="Can you confirm whether an internal routing was created for the inbound email on Day 19 involving Ethan and Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:59:08: task_name="None", task="Was an internal routing created for the inbound email on Day 28 involving Ethan and Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:59:13: task_name="None", task="Did we create an internal routing for the customer email in response to the inbound email on Day 19 involving Oliver and Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:59:18: task_name="None", task="As of Day 18, would Morgan have known about the inbound email from Ingrid (GitHub) regarding “Enterprise Audit Log Access & Retention – Next Steps”? - -IMPORTANT: You are answering from the perspective of Morgan as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:59:25: task_name="None", task="As of Day 18, would Morgan have known about the inbound email from Ingrid (GitHub) regarding “Enterprise Audit Log Access & Retention – Next Steps”? - -IMPORTANT: You are answering from the perspective of Morgan as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Search of Morgan's email records (the only subsystem where the inbound email would appear) up to Day 18 returned no messages from Ingrid (GitHub) with the subject “Enterprise Audit Log Access & Retention – Next Steps”. Since Morgan has no visibility to other systems that could contain this email, there is no evidence that Morgan could have known about it.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 20:59:25: task_name="None", task="If Miki had approved the pull request on Day 6 without requesting changes, would Jordan not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:59:31: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 6 involving Chris and Jamie? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:59:37: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 8, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:59:42: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 8, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The records show an inbound email from Maya on Day 8 (ext_email_maya_8_6) and an outbound acknowledgment (ack_vendor_maya_8), but no associated Jira ticket is documented (jira_ref is null and no Jira issues were found referencing this email). Because there is no evidence that the Day 8 email triggered a Jira ticket, the absence of that email would not have altered the creation of a ticket.", - "causal_mechanism": "involves_gap", - "actors": ["Maya", "Jax"], - "reasoning": "The corpus contains the Day 8 email and its acknowledgment but lacks any Jira ticket linking to that communication. Without a documented ticket, we cannot infer that the email caused ticket creation. The missing ticket information represents a knowledge gap, so removing the email would not change an effect that is not evidenced to exist." -}" -2026-04-02 20:59:47: task_name="None", task="As of Day 17, given that Jenna can access Confluence, email, Salesforce, Slack, and Zoom but not Git, would she have been aware of the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected through a reviewer audit? - -IMPORTANT: You are answering from the perspective of Jenna as of Day 17. This actor only has access to: confluence, email, salesforce, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 20:59:52: task_name="None", task="As of Day 17, given that Jenna can access Confluence, email, Salesforce, Slack, and Zoom but not Git, would she have been aware of the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected through a reviewer audit? - -IMPORTANT: You are answering from the perspective of Jenna as of Day 17. This actor only has access to: confluence, email, salesforce, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Within Jenna's visibility cone (Confluence, email, Salesforce, Slack, Zoom) there are no artifacts that reference a knowledge gap in an undocumented domain, an unknown issue left by a departed employee, or a reviewer audit detection. The only retrieved artifact (CONF-ENG-006) is unrelated. Therefore Jenna would not have been aware of that specific knowledge gap.", - "evidence_artifacts": [], - "blocked_subsystems": ["git"] -}" -2026-04-02 20:59:53: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 1 involving Deepa and Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:00:00: task_name="None", task="Did we create a Confluence documentation page in response to the knowledge gap detected on Day 27 involving Patty, Dave? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:00:07: task_name="None", task="Did we create a Confluence documentation page in response to the knowledge gap detected on Day 27 involving Patty, Dave? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the corpus for Confluence documents created after Day 27 that mention a knowledge gap, Patty, or Dave. No results were returned for any query limited to after Day 27. A broader search of all Confluence pages for 'Patty' and 'Dave' returned only entries dated Day 2, 10, 12, 22, and 25, none of which reference a knowledge gap on Day 27. Therefore, there is no evidence that a Confluence documentation page was created in response to the Day 27 knowledge gap involving Patty and Dave." -}" -2026-04-02 21:00:07: task_name="None", task="If the email from Ravi (CloudBees) had not arrived on Day 17, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:00:13: task_name="None", task="If the email from Ravi (CloudBees) had not arrived on Day 17, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The vendor email sent by Ravi on Day 17 was recorded as the source of Jira ticket ORG-135, which was created to track the plugin compatibility request. The ticket’s metadata explicitly links it to a vendor_email from Ravi, indicating the email triggered the ticket creation. Without that email, the ticket would not have been opened.", - "causal_mechanism": "incident_coordination", - "actors": ["Ravi", "Apex Athletics"], - "reasoning": "The Jira ticket ORG-135 (day 17) includes metadata showing its source as a vendor_email from Ravi. This provides a documented causal link: the external contact (Ravi’s email) caused the incident (ticket) to be created. Removing the email eliminates the trigger, so the ticket would not exist." -}" -2026-04-02 21:00:13: task_name="None", task="If the incident had not occurred, would the postmortem page not have been written and the root cause analysis not exist on Day 24? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:00:25: task_name="None", task="If the incident had not occurred, would the postmortem page not have been written and the root cause analysis not exist on Day 24? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The incident triggered the creation of a postmortem page on Confluence, which included a root cause analysis. The postmortem page (a Confluence document) was authored directly in response to the incident, so without the incident the page would not have been written and the analysis would not exist on Day 24.", - "causal_mechanism": "spawned_doc", - "actors": ["Jax", "Incident Response Team"], - "reasoning": "The available Confluence records show postmortem pages titled \"Postmortem: P1 incident ...\" that were authored after each incident (e.g., ENG‑128, ENG‑118, ENG‑130). These pages are explicitly linked to the incidents they analyze, indicating a direct causal relationship: the incident caused the documentation effort. Therefore, if the incident had not occurred, the associated postmortem page and its root‑cause analysis would not have been produced on Day 24." -}" -2026-04-02 21:00:26: task_name="None", task="Did we create an internal routing for the customer email received on Day 25 involving Ethan and Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:00:31: task_name="None", task="By Day 18, would Deepa have known about the design discussion on the QA automation roadmap, given that she has access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but such design discussion events are visible only to direct participants and she was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Deepa as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:00:35: task_name="None", task="By Day 18, would Deepa have known about the design discussion on the QA automation roadmap, given that she has access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but such design discussion events are visible only to direct participants and she was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Deepa as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The design discussion on the QA automation roadmap was conducted in a Zoom meeting whose participant list (Nadia, Zoe, Ben, Yusuf) does not include Deepa, and the related Confluence design page was authored by Nadia without any indication that Deepa was a reviewer or collaborator. Since such design discussion events are visible only to direct participants, Deepa would not have been granted visibility through her allowed systems (Confluence, Slack, etc.). Therefore, by Day 18 Deepa could not have known about the discussion.", - "evidence_artifacts": [ - "zoom_2026-03-03_723459ae", - "CONF-ENG-019" - ], - "blocked_subsystems": [] -}" -2026-04-02 21:00:35: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 28 involving Morgan? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:00:43: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 28 involving Morgan? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I examined all events for Day 28 and performed searches for Confluence artifacts, knowledge gaps, and references to Morgan. The only Confluence page created on Day 28 (CONF-MKT-005) pertains to telemetry service credential rotation and is unrelated to any knowledge gap involving Morgan. No other Confluence pages were found, and no event indicates a knowledge gap detection involving Morgan that triggered documentation creation. Therefore, no Confluence documentation page was created in response to such a gap." -}" -2026-04-02 21:00:43: task_name="None", task="As of Day 11, would Priya have known about the inbound email from Ethan (Peak Performance Labs) regarding the “Request for Enhanced Real-Time Performance Metrics Dashboard”? - -IMPORTANT: You are answering from the perspective of Priya as of Day 11. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:00:50: task_name="None", task="As of Day 11, would Priya have known about the inbound email from Ethan (Peak Performance Labs) regarding the “Request for Enhanced Real-Time Performance Metrics Dashboard”? - -IMPORTANT: You are answering from the perspective of Priya as of Day 11. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Priya's accessible systems include email. The email artifacts show three inbound messages from Ethan (Peak Performance Labs): ext_email_ethan_2_9 (Request for Enhanced Data Integration Capabilities, Day 2), ext_email_ethan_3_9 (Request for Enhanced Data Export Capability, Day 3), and ext_email_ethan_7_6 (RDS Performance Review & Recommendations, Day 7). None of these correspond to a “Request for Enhanced Real-Time Performance Metrics Dashboard”. No email with that subject or topic is present in the searchable email corpus up to Day 11, so Priya could not have known about it.", - "evidence_artifacts": [ - "ext_email_ethan_2_9", - "ext_email_ethan_3_9", - "ext_email_ethan_7_6" - ], - "blocked_subsystems": [] -}" -2026-04-02 21:00:50: task_name="None", task="If Chloe had approved the pull request on Day 21 without requesting changes, would Miki not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:00:57: task_name="None", task="Was an internal routing of the customer email created in response to the inbound email on Day 21 involving Lucia and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:01:03: task_name="None", task="Was an internal routing of the customer email created in response to the inbound email on Day 21 involving Lucia and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "Searched the corpus for any artifacts after Day 21 mentioning Lucia, Chloe, inbound email, and internal routing across email, Jira, and Slack documents. No records were found that reference an internal routing created in response to a Day 21 inbound email involving Lucia and Chloe. The only related items were emails on Days 14 and 19, but none on Day 21 or any internal routing entries. Therefore, such an internal routing does not exist in the available data." -}" -2026-04-02 21:01:03: task_name="None", task="As of Day 12, would Mike have known about the inbound email from Ravi (CloudBees) regarding “Urgent: Jenkins Plugin Compatibility Update Required for Apex Athletics”? - -IMPORTANT: You are answering from the perspective of Mike as of Day 12. This actor only has access to: confluence, email, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:01:09: task_name="None", task="As of Day 12, would Mike have known about the inbound email from Ravi (CloudBees) regarding “Urgent: Jenkins Plugin Compatibility Update Required for Apex Athletics”? - -IMPORTANT: You are answering from the perspective of Mike as of Day 12. This actor only has access to: confluence, email, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of Mike's accessible systems (email, slack, confluence) for the specific inbound email from Ravi with the subject “Urgent: Jenkins Plugin Compatibility Update Required for Apex Athletics” returned no matching artifacts, and no related references were found in other accessible sources. Therefore, there is no evidence that Mike could have known about this email as of Day 12.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 21:01:09: task_name="None", task="As of Day 18, would Miki have been aware of the design discussion about the Office space redesign, given that Miki has access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but the discussion was only visible to direct participants and Miki was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Miki as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:01:14: task_name="None", task="As of Day 18, would Miki have been aware of the design discussion about the Office space redesign, given that Miki has access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but the discussion was only visible to direct participants and Miki was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Miki as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The design discussion about the Office space redesign was only visible to direct participants. Searches of Miki's accessible systems (Confluence, Slack, Zoom, Jira, Email, Git, Datadog) returned no artifacts referencing the discussion, and Miki is not listed as a participant. Therefore Miki would not have been aware of the discussion as of Day 18.", - "evidence_artifacts": [], - "blocked_subsystems": ["slack", "zoom", "confluence"] -}" -2026-04-02 21:01:14: task_name="None", task="If the postmortem for the original incident resolved on Day 15 had included preventive action items in git, jira, and slack, would the subsequent incident would likely not have occurred? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:01:19: task_name="None", task="If the postmortem for the original incident resolved on Day 15 had included preventive action items in git, jira, and slack, would the subsequent incident would likely not have occurred? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The postmortem for the Day 15 incident (ENG-111) did not generate concrete preventive action items in version control, Jira tickets, or Slack notifications. Consequently, the underlying misconfiguration remained unaddressed, leading to a recurrence (ENG-118) that is explicitly labeled as a recurrence of ENG-111.", - "causal_mechanism": "recurrence_of", - "actors": ["Jax"], - "reasoning": "The ENG-118 Jira entry contains the tag '[recurrence of ENG-111]', directly linking the later incident to the earlier one. This indicates the root cause was known but not remedied. Implementing preventive actions in git, Jira, and Slack as part of the original postmortem would have broken this causal chain, so the subsequent incident would likely not have occurred." -}" -2026-04-02 21:01:19: task_name="None", task="As of Day 14, would Vince have been aware of the inbound email from Lucia (Google) regarding “Integrating Google Crash Reporting with Apex Athletics’ Mobile Apps”? - -IMPORTANT: You are answering from the perspective of Vince as of Day 14. This actor only has access to: confluence, email, salesforce, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:01:27: task_name="None", task="As of Day 14, would Vince have been aware of the inbound email from Lucia (Google) regarding “Integrating Google Crash Reporting with Apex Athletics’ Mobile Apps”? - -IMPORTANT: You are answering from the perspective of Vince as of Day 14. This actor only has access to: confluence, email, salesforce, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Vince’s accessible email archive (search results for inbound messages from Lucia) contains no email matching the subject “Integrating Google Crash Reporting with Apex Athletics’ Mobile Apps”. The only related entries are outbound acknowledgments (ack_vendor_lucia_6, ack_vendor_lucia_7) sent by Chloe, indicating that the original inbound message was handled by Chloe, not Vince. Since Vince’s visibility cone (email, confluence, salesforce, slack, zoom) does not include this inbound email, he would not have been aware of it by Day 14.", - "evidence_artifacts": [ - "ack_vendor_lucia_6", - "ack_vendor_lucia_7" - ], - "blocked_subsystems": [] -}" -2026-04-02 21:01:27: task_name="None", task="As of Day 29, would Jax have been aware of the inbound email from Lucia (Google) regarding “Enhancing Firebase Cloud Messaging Reliability for Apex Athletics,” given his access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but that inbound external email events are visible only to direct participants and Jax was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Jax as of Day 29. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:01:30: task_name="None", task="As of Day 29, would Jax have been aware of the inbound email from Lucia (Google) regarding “Enhancing Firebase Cloud Messaging Reliability for Apex Athletics,” given his access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but that inbound external email events are visible only to direct participants and Jax was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Jax as of Day 29. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Inbound external emails are only visible to the direct participants. The email from Lucia (Google) about “Enhancing Firebase Cloud Messaging Reliability for Apex Athletics” does not list Jax as a participant, and no email artifact appears in Jax's accessible email store (search returned no results). Therefore Jax would not have been able to see or know about the email by Day 29.", - "evidence_artifacts": [], - "blocked_subsystems": ["email"] -}" -2026-04-02 21:01:31: task_name="None", task="Was a warmup 1‑on‑1 meeting created in response to the hiring of Riley Thompson on Day 17 involving Riley Thompson? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:01:37: task_name="None", task="As of Day 22, would Jax have known about the design discussion regarding the Align Japan proposal strategy? - -IMPORTANT: You are answering from the perspective of Jax as of Day 22. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:01:43: task_name="None", task="As of Day 22, would Jax have known about the design discussion regarding the Align Japan proposal strategy? - -IMPORTANT: You are answering from the perspective of Jax as of Day 22. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches across all systems Jax can access (Confluence, Slack, Jira, Email, Zoom) returned no artifacts mentioning the Align Japan proposal design discussion. Without any record of such a discussion in Jax's accessible sources, it is unlikely Jax would have known about it by Day 22.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 21:01:43: task_name="None", task="If the email from Gabriel (Confluent) had not arrived on Day 23, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:01:49: task_name="None", task="If the email from Gabriel (Confluent) had not arrived on Day 23, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The inbound email from Gabriel (Confluent) on Day 23 was logged as a vendor email, which automatically triggered the creation of Jira ticket ORG-152 to track the request. Without that email, the automated workflow would not have initiated the ticket.", - "causal_mechanism": "incident_coordination", - "actors": ["Gabriel"], - "reasoning": "The artifact set contains a Jira ticket (ORG-152) dated Day 23 with metadata indicating it originated from a vendor email from Gabriel. This explicit linkage shows the ticket was created as a direct response to the email. Therefore, if the email had not arrived, the ticket would not have been created, changing the outcome." -}" -2026-04-02 21:01:49: task_name="None", task="As of Day 18, would Mike have been aware of the design discussion about the QA automation roadmap? - -IMPORTANT: You are answering from the perspective of Mike as of Day 18. This actor only has access to: confluence, email, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:01:55: task_name="None", task="Given that inbound external email events are visible only to direct participants and Karen was not listed as a participant, as of Day 19 would Karen have known about the inbound email from Gabriel (Confluent) regarding “Urgent: Kafka Connector Health Below Threshold – Action Needed”? - -IMPORTANT: You are answering from the perspective of Karen as of Day 19. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:01: task_name="None", task="Given that inbound external email events are visible only to direct participants and Karen was not listed as a participant, as of Day 19 would Karen have known about the inbound email from Gabriel (Confluent) regarding “Urgent: Kafka Connector Health Below Threshold – Action Needed”? - -IMPORTANT: You are answering from the perspective of Karen as of Day 19. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Inbound external email events are only visible to direct participants. Karen was not listed as a participant on the email from Gabriel. Searches of Karen's accessible email, Slack, and Confluence records up to Day 19 returned no evidence of the email or any related discussion, indicating she had no exposure to that inbound message.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 21:02:01: task_name="None", task="Given that Alex has access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but design discussion events are visible only to direct participants and Alex was not listed as a participant, would Alex have known about the design discussion about ops workflow design as of Day 25? - -IMPORTANT: You are answering from the perspective of Alex as of Day 25. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:04: task_name="None", task="Given that Alex has access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but design discussion events are visible only to direct participants and Alex was not listed as a participant, would Alex have known about the design discussion about ops workflow design as of Day 25? - -IMPORTANT: You are answering from the perspective of Alex as of Day 25. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": true, - "reasoning": "Alex authored a Confluence page titled 'Design: ops process redesign' on Day 18 (CONF-ENG-075). This document demonstrates that Alex possessed knowledge of the ops workflow design discussion prior to Day 25, despite not being listed as a direct participant in the private design discussion event.", - "evidence_artifacts": ["CONF-ENG-075"], - "blocked_subsystems": [] -}" -2026-04-02 21:02:05: task_name="None", task="Was a Zendesk support ticket created in response to the routing of the customer email on Day 11 involving Gridiron Pro Football, Marcus? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:11: task_name="None", task="Was an internal routing created for the inbound email on Day 6 involving Gabriel and Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:16: task_name="None", task="As of Day 18, would Miki have been aware of the design discussion about the Review ops KPI dashboard? - -IMPORTANT: You are answering from the perspective of Miki as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:22: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 25 involving Raj and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:28: task_name="None", task="As of Day 8, would Hanna have known about the design discussion titled “Discuss QA process gaps,” given that she has access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but such design discussion events are visible only to direct participants and she was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Hanna as of Day 8. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:32: task_name="None", task="As of Day 8, would Hanna have known about the design discussion titled “Discuss QA process gaps,” given that she has access to Confluence, Datadog, email, Git, Jira, Slack, and Zoom, but such design discussion events are visible only to direct participants and she was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Hanna as of Day 8. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The design discussion titled “Discuss QA process gaps” is restricted to direct participants only. Hanna is not listed as a participant, and none of the systems she can access (Confluence, Datadog, email, Git, Jira, Slack, Zoom) contain any record or notification of this discussion. Consequently, she could not have known about it by Day 8.", - "evidence_artifacts": [], - "blocked_subsystems": ["zoom"] -}" -2026-04-02 21:02:32: task_name="None", task="If Janice had not been hired to fill the gap in TitanDB, legacy auth service, AWS cost structure on Day 7, would the knowledge vacuum left by Bill’s departure remain unaddressed in the company’s Confluence and Jira systems? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:39: task_name="None", task="If Janice had not been hired to fill the gap in TitanDB, legacy auth service, AWS cost structure on Day 7, would the knowledge vacuum left by Bill’s departure remain unaddressed in the company’s Confluence and Jira systems? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "Bill's departure created a knowledge gap regarding TitanDB, legacy auth service, and AWS cost structure. Although Janice was hired on Day 7, the available records show no explicit documentation or Jira updates authored by her that address this gap. Consequently, the knowledge vacuum would remain unaddressed in Confluence and Jira even if Janice had not been hired.", - "causal_mechanism": "involves_gap", - "actors": ["Bill", "Janice"], - "reasoning": "The data includes Bill's expertise profile indicating his knowledge area, Janice's hiring date, and several Confluence pages and Jira tickets related to TitanDB. However, none of these artifacts attribute the creation or filling of the knowledge gap to Janice. Therefore, the causal link between Janice's hiring and remediation of the gap is not documented, meaning the gap would persist regardless of her presence." -}" -2026-04-02 21:02:39: task_name="None", task="By the end of Day 5, would Janice have been aware of the inbound email from Ravi (CloudBees) titled “Urgent: Action Required on Docker Registry Credentials – Apex Athletics”? - -IMPORTANT: You are answering from the perspective of Janice as of Day 5. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:45: task_name="None", task="If the Day 16 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira, given the usual Slack notifications? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:51: task_name="None", task="If the Day 16 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira, given the usual Slack notifications? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available records show that the Confluence postmortem page (CONF-ENG-036) and the Jira root‑cause analysis (ENG‑111) were created in response to the P1 incident ENG‑111 (Kafka consumer group lag) that occurred on day 6. There is no explicit evidence linking the Day 16 incident to the creation of those artefacts; the data does not mention a Day 16 incident triggering Slack notifications that led to the postmortem or the Jira analysis. Consequently, removing the Day 16 incident would not change the existence of the postmortem page or the Jira root‑cause analysis.", - "causal_mechanism": "involves_gap", - "actors": ["Slack (notification system)", "Engineering team", "Jax (author of postmortem)"], - "reasoning": "Because the documentation events are explicitly tied to incident ENG‑111 and no record connects them to a Day 16 incident, the causal chain from Day 16 incident to the documentation is missing. Hence the outcome would remain unchanged, and the missing link constitutes a knowledge gap." -}" -2026-04-02 21:02:51: task_name="None", task="If the Day 4 incident reported in Slack had not occurred, would the postmortem page in Confluence not have been written and the root cause analysis in Jira not exist? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:57: task_name="None", task="If the Day 4 incident reported in Slack had not occurred, would the postmortem page in Confluence not have been written and the root cause analysis in Jira not exist? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "No explicit documentation links the Day 4 incident reported in Slack to the creation of a postmortem page in Confluence or a root‑cause analysis ticket in Jira. The available artifacts do not contain any reference that the incident triggered those documents, so removing the incident would not be shown to affect their existence.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "A thorough search of Slack messages, Confluence pages, and Jira tickets for references to a \"Day 4 incident\" and any subsequent postmortem or RCA produced no evidence of a causal relationship. Because the data do not document that the incident led to the documentation, we must conclude that the outcome would not change based on the available information." -}" -2026-04-02 21:02:57: task_name="None", task="Would Jenna have known about the inbound email from Peak Performance Labs titled “Re: Re: Request for Enhanced Real‑Time Performance Data Integration” by Day 19, given that she has access to Confluence, email, Salesforce, Slack, and Zoom but inbound external email events are visible only to direct participants and she was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Jenna as of Day 19. This actor only has access to: confluence, email, salesforce, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:02:59: task_name="None", task="Would Jenna have known about the inbound email from Peak Performance Labs titled “Re: Re: Request for Enhanced Real‑Time Performance Data Integration” by Day 19, given that she has access to Confluence, email, Salesforce, Slack, and Zoom but inbound external email events are visible only to direct participants and she was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Jenna as of Day 19. This actor only has access to: confluence, email, salesforce, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Jenna does not have visibility into inbound external email events unless she is a direct participant. The email titled “Re: Re: Request for Enhanced Real‑Time Performance Data Integration” was sent to internal recipients not including Jenna, so she would not have seen it in her email system by Day 19.", - "evidence_artifacts": [], - "blocked_subsystems": ["email"] -}" -2026-04-02 21:02:59: task_name="None", task="Would Vince have known about the knowledge gap in the undocumented domain (triggered by an unknown issue left by a departed employee and detected via reviewer_audit) as of Day 25, given his access to Confluence, email, Salesforce, Slack, and Zoom but not to Git? - -IMPORTANT: You are answering from the perspective of Vince as of Day 25. This actor only has access to: confluence, email, salesforce, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:03:06: task_name="None", task="Would Vince have known about the knowledge gap in the undocumented domain (triggered by an unknown issue left by a departed employee and detected via reviewer_audit) as of Day 25, given his access to Confluence, email, Salesforce, Slack, and Zoom but not to Git? - -IMPORTANT: You are answering from the perspective of Vince as of Day 25. This actor only has access to: confluence, email, salesforce, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Within Vince's visibility cone (Confluence, email, Salesforce, Slack, Zoom) there are no artifacts indicating awareness of a knowledge gap in the undocumented domain, nor any mention of a reviewer_audit detection of an issue left by a departed employee. The only accessible artifact found (CONF-ENG-007) is a design document authored by Vince on Day 1 and does not reference the gap or audit. Therefore, as of Day 25 Vince would not have known about this knowledge gap.", - "evidence_artifacts": ["CONF-ENG-007"], - "blocked_subsystems": ["Git"] -}" -2026-04-02 21:03:06: task_name="None", task="If Jordan had not departed the company on Day 12, would their active tickets in the internal ticketing system and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:03:11: task_name="None", task="If Jordan had not departed the company on Day 12, would their active tickets in the internal ticketing system and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available documents do not contain any explicit statement that Jordan's departure on Day 12 triggered the reassignment of their active tickets or escalation responsibilities. No ticketing system logs, handover notes, or communication records link the departure event to a reassignment action.", - "causal_mechanism": "involves_gap", - "actors": ["Jordan"], - "reasoning": "Because the data lacks a documented causal link between Jordan's departure and ticket/escalation reassignment, we cannot assert that removing the departure would have prevented the reassignment. The absence of evidence constitutes a knowledge gap, so the counterfactual outcome cannot be confirmed." -}" -2026-04-02 21:03:11: task_name="None", task="As of Day 29, would Desmond have known about the inbound email from Ethan (Amazon) regarding “Optimizing VPC Networking for Apex Athletics” Current Workloads? - -IMPORTANT: You are answering from the perspective of Desmond as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:03:17: task_name="None", task="As of Day 29, would Desmond have known about the inbound email from Ethan (Amazon) regarding “Optimizing VPC Networking for Apex Athletics” Current Workloads? - -IMPORTANT: You are answering from the perspective of Desmond as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of all data sources available to Desmond (email, Slack, Confluence, Zoom) for any record of an inbound email from Ethan (Amazon) about “Optimizing VPC Networking for Apex Athletics” returned no results. Because the email does not appear in Desmond's visible inbox or related communications, he would not have been aware of it on Day 29.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 21:03:17: task_name="None", task="If the postmortem completed on Day 7 had included preventive action items in the git, Jira, and Slack processes, would the subsequent incident likely not have occurred? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:03:22: task_name="None", task="As of Day 18, could Umji have known about the design discussion about address Kafka throughput, given that she only has access to Confluence, email, Slack, and Zendesk, and the discussion was visible only to its direct participants, with Umji not listed as a participant? - -IMPORTANT: You are answering from the perspective of Umji as of Day 18. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:03:26: task_name="None", task="As of Day 18, could Umji have known about the design discussion about address Kafka throughput, given that she only has access to Confluence, email, Slack, and Zendesk, and the discussion was visible only to its direct participants, with Umji not listed as a participant? - -IMPORTANT: You are answering from the perspective of Umji as of Day 18. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The design discussion about addressing Kafka throughput was limited to its direct participants, and Umji is not listed as a participant. A search of the artifacts Umami can access (Confluence, email, Slack, Zendesk) up to Day 18 returned no record of this discussion. Therefore, with the available information, Umji could not have known about it.", - "evidence_artifacts": [], - "blocked_subsystems": ["jira", "pr", "zoom", "datadog", "salesforce", "zendesk"] -}" -2026-04-02 21:03:26: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 27 involving Kaitlyn and Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:03:32: task_name="None", task="Was an internal routing of the customer email created in response to the inbound email on Day 27 involving Ingrid and Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:03:37: task_name="None", task="If the incident on Day 4 had not occurred, would no coordination with Amazon Web Services have been needed at all? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:03:41: task_name="None", task="As of Day 11, would Umji have known about the inbound email from Peak Performance Labs titled “Re: Re: Request for Enhanced Real-Time Performance Metrics Dashboard,” given that inbound external email events are visible only to direct participants and Umji was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Umji as of Day 11. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:03:44: task_name="None", task="As of Day 11, would Umji have known about the inbound email from Peak Performance Labs titled “Re: Re: Request for Enhanced Real-Time Performance Metrics Dashboard,” given that inbound external email events are visible only to direct participants and Umji was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Umji as of Day 11. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Inbound external email events are only visible to direct participants. Umji was not listed as a participant on the email from Peak Performance Labs titled “Re: Re: Request for Enhanced Real-Time Performance Metrics Dashboard.” A search of Umji's accessible email artifacts returned no matching email, indicating Umji did not receive or see this message.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 21:03:44: task_name="None", task="Was a Zendesk support ticket created in response to the routing of the customer email on Day 8 involving Darius and Marcus? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:03:51: task_name="None", task="Was a Zendesk support ticket created in response to the routing of the customer email on Day 8 involving Darius and Marcus? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the corpus for Zendesk tickets, emails, and other records referencing Day 8, Darius, and Marcus. The email records show Darius‑related messages on Day 1 and Day 3, and a Day 8 email from Maya (not Darius). No Zendesk ticket entries were found linking Darius and Marcus, nor any ticket created on Day 8 in response to a routed customer email. Therefore, there is no evidence that a Zendesk support ticket was created for that scenario." -}" -2026-04-02 21:03:52: task_name="None", task="Was a Confluence documentation page created to address the knowledge gap detected on Day 19 involving Jordan? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:04:00: task_name="None", task="Was a Confluence documentation page created to address the knowledge gap detected on Day 19 involving Jordan? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I examined all events for Day 19 using get_events_for_day and found no mention of Jordan. I then searched the artifact corpus for any Jordan-related items after Day 19 and for any Confluence pages mentioning Jordan; both searches returned no results. The only Confluence page created on Day 19 (CONF-ENG-078) was authored by Jax and does not involve Jordan. Therefore, there is no evidence that a Confluence documentation page was created to address a knowledge gap involving Jordan on Day 19." -}" -2026-04-02 21:04:00: task_name="None", task="As of Day 20, would Jordan have known about the inbound email from the Japan Olympic Committee titled “Re: Re: Re: Re: Request for Real‑Time Alert Capability Enhancement”? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 20. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:04:06: task_name="None", task="As of Day 20, would Jordan have known about the inbound email from the Japan Olympic Committee titled “Re: Re: Re: Re: Request for Real‑Time Alert Capability Enhancement”? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 20. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Within Jordan's visibility cone (email, slack, confluence, datadog, git, jira, zoom) there is no inbound email from the Japan Olympic Committee with the exact title \"Re: Re: Re: Re: Request for Real-Time Alert Capability Enhancement\". Searches of the email repository for relevant keywords and the specific subject line returned no matching artifacts. Therefore Jordan could not have been aware of such an email by Day 20.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 21:04:06: task_name="None", task="If Morgan had not departed the company on Day 24, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:04:11: task_name="None", task="If the incident on Day 24 had not occurred, would no coordination with Jenkins CI have been needed at all? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:04:17: task_name="None", task="If the incident on Day 24 had not occurred, would no coordination with Jenkins CI have been needed at all? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available records show separate communications about Jenkins CI issues (e.g., emails on days 6, 9, 11, 12) that are unrelated to the Day 24 incident, which concerns a policy‑as‑code discussion with HashiCorp. No document explicitly links the Day 24 incident to the need for coordination with Jenkins CI. Therefore, even if the Day 24 incident had not occurred, coordination with Jenkins CI would still have been required based on the other independent issues.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "A causal link of the form 'incident → coordination with Jenkins CI' is not present in the data. The incident on Day 24 is documented only as a vendor inquiry about Terraform and CI/CD pipelines, while Jenkins‑related coordination appears in earlier, distinct communications. Because the coordination originates from separate incidents, removing the Day 24 incident would not eliminate the need for Jenkins CI coordination." -}" -2026-04-02 21:04:17: task_name="None", task="As of Day 14, would Blake have been aware of the inbound email from Ethan (Amazon) concerning “Optimizing VPC Networking for Apex Athletics”? - -IMPORTANT: You are answering from the perspective of Blake as of Day 14. This actor only has access to: confluence, email, salesforce, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:04:24: task_name="None", task="As of Day 14, would Blake have been aware of the inbound email from Ethan (Amazon) concerning “Optimizing VPC Networking for Apex Athletics”? - -IMPORTANT: You are answering from the perspective of Blake as of Day 14. This actor only has access to: confluence, email, salesforce, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The inbound email titled “Optimizing VPC Networking for Apex Athletics” (artifact ext_email_ethan_6_6) was addressed to Jax, not Blake. There is no evidence in Blake's accessible email, Slack, Confluence, Salesforce, or Zoom records that Blake was CC'd, forwarded the email, or otherwise received it. Therefore, by Day 14 Blake would not have been aware of this email.", - "evidence_artifacts": ["ext_email_ethan_6_6"], - "blocked_subsystems": [] -}" -2026-04-02 21:04:24: task_name="None", task="If the Day 12 incident reported in Jira and discussed on Slack had not occurred, would no coordination with the Firebase SDK have been needed at all? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:04:29: task_name="None", task="If the Day 12 incident reported in Jira and discussed on Slack had not occurred, would no coordination with the Firebase SDK have been needed at all? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any explicit statement that the Day 12 incident caused the need for coordination with the Firebase SDK. Without a documented causal link, we cannot conclude that eliminating the incident would have removed the need for SDK coordination.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "Searches of Jira, Slack, and other documents for references to a Day 12 incident and Firebase SDK yielded no records establishing a direct cause‑effect relationship. The absence of such evidence represents a knowledge gap, meaning the claim that the incident necessitated SDK coordination is not substantiated by the data." -}" -2026-04-02 21:04:29: task_name="None", task="As of Day 28, would Bill have known about the inbound email from Ethan (Peak Performance Labs) regarding “Request for Enhanced Data Integration Capability”? - -IMPORTANT: You are answering from the perspective of Bill as of Day 28. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:04:36: task_name="None", task="As of Day 28, would Bill have known about the inbound email from Ethan (Peak Performance Labs) regarding “Request for Enhanced Data Integration Capability”? - -IMPORTANT: You are answering from the perspective of Bill as of Day 28. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The inbound email from Ethan (Peak Performance Labs) titled 'Request for Enhanced Data Integration Capability' is addressed to Marcus, the liaison, as indicated by the email content and metadata. Bill's accessible systems include email, but there is no evidence that this email was forwarded to or visible in Bill's mailbox. Consequently, Bill would not have known about this email by Day 28.", - "evidence_artifacts": ["ext_email_ethan_10_9"], - "blocked_subsystems": [] -}" -2026-04-02 21:04:36: task_name="None", task="Did we create a Confluence documentation page in response to the knowledge gap detected on Day 6 involving Sam? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:13:33: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:13:41: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain an explicit statement that the Day 8 incident directly triggered the creation of a postmortem page in Confluence or a root‑cause analysis entry in Jira or Slack. Without documented evidence of that causal chain, we cannot conclude that removing the incident would have prevented those documents from existing.", - "causal_mechanism": "involves_gap", - "actors": [ - "Hanna", - "Engineering team" - ], - "reasoning": "Search results show a Day 8 incident (ENG‑106) but no linked postmortem page or analysis record. Since the required causal link is missing, the counterfactual effect cannot be established, leading to a knowledge gap classification." -}" -2026-04-02 21:13:41: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:13:49: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available records show Alex authored PR‑105 on Day 5, but there is no documented review comment or change request from Taylor. Consequently, the revision Alex made before the merge cannot be directly linked to a request from Taylor; the causal link is absent in the data.", - "causal_mechanism": "involves_gap", - "actors": ["Alex", "Taylor"], - "reasoning": "Since the dataset contains no explicit evidence that Taylor requested changes on the pull request, we cannot establish that Taylor's actions caused Alex's revision. Removing a non‑existent cause would not alter the observed effect, indicating a knowledge gap rather than a confirmed causal relationship." -}" -2026-04-02 21:13:49: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:13:55: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:14:01: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:14:11: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The inbound email from Ravi was sent to Nadia, not John, and there is no evidence that it was forwarded to John or that John was directly notified. While a Jira ticket (ORG-103) referencing the email exists, there is no indication that John was assigned, mentioned, or otherwise alerted about this ticket. Therefore, based on the available artifacts, John would not have been aware of the email on Day 4.", - "evidence_artifacts": ["ext_email_ravi_4_6", "ORG-103"], - "blocked_subsystems": [] -}" -2026-04-02 21:14:11: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:14:17: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of Patty's email, Slack, and Confluence artifacts up to Day 29 returned no record of an inbound email titled “Re: Final proposal & next steps for Gridiron Pro Football”. Without any evidence of receipt or discussion of that email in the systems Patty can access, we conclude she would not have known about it by Day 29.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 21:14:17: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:14:24: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of Jordan's accessible systems (Confluence, Jira, Slack, etc.) for any mention of a design discussion about the Define automation framework roadmap on Day 2 returned no results. No artifacts reference that discussion, so Jordan would not have been aware of it.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 21:14:24: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:14:29: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any documented event linking Vince's departure on Day 3 to a reassignment of his active tickets or escalation responsibilities. No record of a departure event, ticket hand‑off, or escalation transfer involving Vince is present.", - "causal_mechanism": "involves_gap", - "actors": ["Vince"], - "reasoning": "A thorough search of the knowledge base returned no entries describing Vince's departure, nor any subsequent ticket or escalation reassignment. Because the causal link is undocumented, we cannot assert that removing the departure would have prevented a reassignment; thus the outcome would not be shown to change." -}" -2026-04-02 21:14:29: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:14:33: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The inbound email from Maya (Datadog) on Day 21 was the trigger for creating Jira ticket ORG‑145, which records the vendor request to optimise alert thresholds. Without that email, the ticket would not have been opened.", - "causal_mechanism": "email_dropped", - "actors": ["Maya", "Jax"], - "reasoning": "Artifact data shows an email (ext_email_maya_21_6) received on Day 21 and a Jira ticket (ORG‑145) created on the same day with metadata linking it to a vendor email from Maya. This explicit linkage indicates the ticket was generated because of the email. Therefore, removing the email would have prevented the ticket’s creation." -}" -2026-04-02 21:14:33: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 21:14:39: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of all artifacts accessible to Umji (Confluence, email, Slack, Zendesk) returned no records mentioning the knowledge gap in the undocumented domain, the unknown issue left by a departed employee, or the reviewer_audit detection. Without any documented evidence in the systems Umji can access, she would not have been aware of this gap as of Day 6.", - "evidence_artifacts": [], - "blocked_subsystems": ["git"] -}" -2026-04-02 21:14:40: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 16 involving Kaitlyn, Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:07:26: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:07:33: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:07:41: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available records show Alex submitted PR-105 on Day 5, but there is no documented evidence of Taylor reviewing the PR on Day 6 or requesting changes that prompted Alex to revise the implementation. Without an explicit link between Taylor's review actions and Alex's subsequent revision, we cannot assert that Taylor's approval (or lack of change requests) would have prevented Alex's revision before merge.", - "causal_mechanism": "involves_gap", - "actors": ["Taylor", "Alex"], - "reasoning": "The data set contains the PR metadata but no Slack messages, review comments, or change‑request logs tying Taylor's actions to Alex's revision. This missing documentation constitutes a knowledge gap, meaning the causal relationship is undocumented. Consequently, removing the supposed cause (Taylor approving without changes) cannot be shown to alter the effect (Alex revising), leading to a determination that the outcome would not have changed based on the available evidence." -}" -2026-04-02 22:07:42: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:07:52: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I queried all events for Day 14 and searched the artifact corpus for any mention of routing involving Ravi and Nadia. The Day 14 event list includes an inbound external email from Ravi (with Nadia listed as an actor) but the entry is of type \"inbound_external_email\" and lacks any \"routed\" tag or separate routing record. No additional artifacts or events indicate that an internal routing was created for this email. Therefore, there is no evidence that an internal routing for the inbound email involving Ravi and Nadia was created." -}" -2026-04-02 22:07:53: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:08:00: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I examined all Day 11 events and artifacts. The only Confluence page created on Day 11 was CONF-ENG-042 by Jax, unrelated to Sam or Chloe. Searches for Confluence artifacts authored by Sam after Day 11, and for any artifacts mentioning a 'knowledge gap' involving Sam and Chloe, returned no results. No record shows a Confluence documentation page created specifically to address a knowledge gap detected on Day 11 with Sam and Chloe." -}" -2026-04-02 22:08:00: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:08:09: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The only evidence is the inbound email artifact (ID ext_email_ravi_4_6) showing Ravi's message arrived on Day 4. The artifact does not specify the recipient, and there is an outbound acknowledgment sent by Nadia, indicating the email was likely addressed to her or a shared vendor inbox. No records show John receiving, forwarding, or discussing this email in Slack, Jira, or any other system he can access. Therefore, based on available evidence, John would not have been aware of the email as of Day 4.", - "evidence_artifacts": ["ext_email_ravi_4_6"], - "blocked_subsystems": [] -}" -2026-04-02 22:08:09: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:08:15: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "No inbound email from Gridiron Pro Football with the title “Re: Final proposal & next steps for Gridiron Pro Football” exists in the email artifacts accessible to Patty up to Day 29. The only inbound email from Gridiron Pro Football found is titled “Re: Next steps on Apex Athletics proposal for Gridiron Pro”, which does not match the specified title, indicating Patty could not have known about the requested email.", - "evidence_artifacts": [ - "customer_reply_gridiron_pro_football_OPP-1001_17" - ], - "blocked_subsystems": [] -}" -2026-04-02 22:08:15: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:08:22: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "A search of all artifacts accessible to Jordan on Day 2 (Confluence pages, Jira tickets, and Slack messages) yielded no documents or communications containing the phrase 'Define automation framework roadmap' or any discussion of a design related to that roadmap. The only design-related items found were unrelated Confluence design docs (CONF-ENG-022, CONF-ENG-023) and Jira tickets about automated tests. Since no evidence of the specific roadmap discussion exists in Jordan's visibility cone, Jordan could not have known about it on Day 2.", - "evidence_artifacts": [ - "CONF-ENG-022", - "CONF-ENG-023" - ], - "blocked_subsystems": [] -}" -2026-04-02 22:08:22: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:08:28: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available documents do not contain any explicit statement linking Vince's departure on Day 3 to the reassignment of active tickets or escalation responsibilities. No record of ticket ownership changes, escalation handoffs, or departure notices referencing Vince was found.", - "causal_mechanism": "involves_gap", - "actors": ["Vince"], - "reasoning": "Because the corpus lacks a documented incident describing Vince's departure and subsequent ticket/escalation reassignment, the causal relationship cannot be established. Therefore, we cannot conclude that the outcome would have differed if Vince had not left." -}" -2026-04-02 22:08:28: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:08:33: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The Jira ticket ORG-145 was created on Day 21 with metadata indicating it originated from a vendor email sent by Maya (Datadog). The ticket’s existence is directly tied to the receipt of that email; without the email, there would have been no trigger to open the ticket.", - "causal_mechanism": "email_dropped", - "actors": ["Maya"], - "reasoning": "The artifact data shows a clear causal chain: Maya’s email (the communication) → creation of Jira ticket ORG-145. Since the ticket’s metadata cites the vendor email as its source, removing the email would have prevented the ticket from being generated." -}" -2026-04-02 22:08:33: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:29:23: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:29:34: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "There is no documented evidence that the Day 8 incident (ENG-118) directly triggered the creation of a postmortem page in Confluence or a root cause analysis entry in Jira or Slack. The available event logs show the incident opening and related communications, but no subsequent postmortem artifact or explicit reference linking the incident to such documentation.", - "causal_mechanism": "involves_gap", - "actors": ["Jax", "Alex", "Deepa", "Jordan", "Morgan", "Chloe", "others involved in incident communications"], - "reasoning": "Since the data does not contain a postmortem page or root cause analysis artifact tied to the Day 8 incident, we cannot establish a concrete causal link. Therefore, removing the incident would not be shown to change the existence of those documents, making the outcome unchanged." -}" -2026-04-02 22:29:34: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:29:40: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any explicit record that Taylor's approval of the pull request on Day 6 (without requesting changes) prevented Alex from revising the implementation before merge. Without documented evidence of this causal relationship, we cannot assert that removing Taylor's approval would have altered Alex's behavior.", - "causal_mechanism": "involves_gap", - "actors": ["Taylor", "Alex"], - "reasoning": "Searches of confluence pages, Slack threads, PR records, and persona skill profiles returned no mention of a Day 6 pull request, Taylor's approval action, or Alex's subsequent revision. The absence of this information constitutes a knowledge gap, preventing confirmation of a causal link. Therefore, the outcome (Alex revising the implementation) would not be shown to change if the cause (Taylor's approval) were removed." -}" -2026-04-02 22:29:41: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:29:48: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the document corpus for any records after Day 14 that mention both Ravi and Nadia together with terms like \"inbound email\" and \"routing\". No such artifacts were found. I also queried the events for Day 14 specifically for email events and found none. A broader search for the keyword \"routing\" after Day 14 also returned no results. Therefore, there is no evidence that an internal routing for an inbound email involving Ravi and Nadia was created on Day 14." -}" -2026-04-02 22:29:49: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:29:55: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:40:26: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:40:33: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "There is no explicit evidence in the available artifacts linking the Day 8 incident to the creation of a postmortem page in Confluence or a root‑cause analysis in Jira or Slack. Without a documented causal chain, we cannot conclude that the absence of the incident would have prevented those documents from being authored.", - "causal_mechanism": "involves_gap", - "actors": ["Day 8 incident"], - "reasoning": "The search returned only a Jira ticket (ENG‑106) describing a deployment task on Day 8, with no mention of a postmortem, Confluence page, or Slack discussion. Since the required causal link is missing, the counterfactual claim cannot be supported; thus we treat the outcome as unchanged (i.e., we cannot assert it would have been different)." -}" -2026-04-02 22:40:33: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:40:41: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "There is no documented evidence that Taylor requested changes on the pull request; the available artifacts only show the PR authored by Alex (PR-105) and no review comments or Slack messages indicating a change request. Without an explicit causal link, we cannot conclude that Taylor's request caused Alex to revise the implementation, so removing a non‑existent cause would not change the outcome.", - "causal_mechanism": "involves_gap", - "actors": ["Alex", "Taylor"], - "reasoning": "The data set includes the PR details but lacks any record of Taylor's review actions. Since the causal chain (Taylor requesting changes → Alex revising) is not documented, the link is a knowledge gap. Consequently, we cannot assert that approving without changes would have prevented Alex's revision." -}" -2026-04-02 22:40:41: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:40:47: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I queried the day‑14 events and found an inbound_external_email entry involving Ravi (and Nadia listed as an actor) but there is no separate event indicating an internal routing action for that email. No record of a routing step (e.g., a 'email_routed' or similar internal routing type) was found, so the internal routing was not created." -}" -2026-04-02 22:40:48: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:40:50: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="I apologize, but I couldn't generate a proper response. Please try again." -2026-04-02 22:40:50: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:40:57: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic” exists on Day 4 (artifact ext_email_ravi_4_6). However, there is no evidence that John was the recipient or that the email was forwarded to him. The only related activity is an outbound acknowledgment sent by Nadia, not John. Since John’s visibility cone includes email but we lack any record of him receiving or reading this message, we conclude he would not have been aware of it on Day 4.", - "evidence_artifacts": ["ext_email_ravi_4_6"], - "blocked_subsystems": [] -}" -2026-04-02 22:40:57: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:41:05: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Patty's accessible sources (email, confluence, slack, zoom) contain no inbound email from Gridiron Pro Football with the exact title “Re: Final proposal & next steps for Gridiron Pro Football” up to Day 29. The only inbound emails from Gridiron Pro Football in the search results have different subjects (e.g., “Re: Next steps on Apex Athletics proposal for Gridiron Pro”). Therefore, Patty would not have known about the specified email as of Day 29.", - "evidence_artifacts": [ - "sales_email_OUTREACH-17-marcus_17", - "customer_reply_gridiron_pro_football_OPP-1001_17", - "reply_customer_gridiron_pro_football_17", - "sales_email_OUTREACH-8-marcus_8", - "sales_email_SALES-103_2", - "sales_email_SALES-109_12" - ], - "blocked_subsystems": [] -}" -2026-04-02 22:41:05: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:41:13: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of Jordan's accessible systems (Confluence, Jira, Slack, etc.) for any mention of a design discussion about the Define automation framework roadmap up to Day 2 returned no relevant artifacts. The available documents on Day 2 pertain to other design topics and tasks, but none reference the Define automation framework roadmap, indicating Jordan would not have been aware of that discussion.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 22:41:13: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:41:18: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:41:22: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The inbound email from Maya (Datadog) on Day 21 requested optimization of alert thresholds. This external vendor request triggered the creation of Jira ticket ORG‑145 to track and fulfill the request.", - "causal_mechanism": "incident_coordination", - "actors": ["Maya", "Jax"], - "reasoning": "The Jira ticket ORG‑145 (day 21) is explicitly linked to a vendor email from Maya on the same day, indicating the ticket was opened as a direct response to that email. If the email had not been received, there would be no documented trigger for the ticket, so the ticket would not have been created." -}" -2026-04-02 22:41:22: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:41:28: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of all systems that Umji can access (Confluence, email, Slack, Zendesk) returned no records mentioning a knowledge gap in the undocumented domain, the unknown issue left by a departed employee, or the reviewer_audit detection. Without any documented communication, ticket, or discussion in these channels, Umji would have had no way to become aware of the gap by Day 6. The missing information resides in systems outside her visibility (e.g., Git or other internal audit logs), which she cannot access.", - "evidence_artifacts": [], - "blocked_subsystems": ["git"] -}" -2026-04-02 22:41:29: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 16 involving Kaitlyn, Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:41:34: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 16 involving Kaitlyn, Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the corpus for Confluence pages created after Day 16 that mention Kaitlyn, Jax, or a knowledge gap. The only Confluence artifact on Day 16 is CONF-ENG-071, created by Jax (with Morgan, Priya, etc.), but its summary does not reference Kaitlyn or a knowledge gap. No other Confluence pages referencing Kaitlyn or a knowledge gap were found. Therefore, there is no evidence that a Confluence documentation page was created in response to the knowledge gap involving Kaitlyn and Jax." -}" -2026-04-02 22:41:34: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 25 involving Zoe, Ben? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:41:40: task_name="None", task="As of Day 18, would Sanjay have known about the inbound email from Ingrid (GitHub) regarding “Enterprise Audit Log Access & Retention – Next Steps”? - -IMPORTANT: You are answering from the perspective of Sanjay as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:41:47: task_name="None", task="As of Day 18, would Sanjay have known about the inbound email from Ingrid (GitHub) regarding “Enterprise Audit Log Access & Retention – Next Steps”? - -IMPORTANT: You are answering from the perspective of Sanjay as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of Sanjay's accessible email artifacts for keywords related to the inbound email from Ingrid (GitHub) titled “Enterprise Audit Log Access & Retention – Next Steps” returned no matching messages. Therefore Sanjay would not have been aware of this email by Day 18.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 22:41:47: task_name="None", task="As of Day 23, could Tom have been aware of the design discussion about Ops sync on handoff, given that he has access to Confluence, email, Slack, and Zoom, but such design discussion events are visible only to direct participants and he was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Tom as of Day 23. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:48:15: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:48:24: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "There is no documented evidence that a postmortem page was created in Confluence or that a root cause analysis was recorded in Jira or Slack as a result of the Day 8 incident. Searches of Confluence, Slack, and Jira after Day 8 returned no such artifacts, so the existence of those documents cannot be confirmed, and thus we cannot assert that the incident caused them.", - "causal_mechanism": "involves_gap", - "actors": ["Incident Team"], - "reasoning": "The available data includes only a Jira ticket (ENG-106) related to a deployment on Day 8, with no associated postmortem or analysis artifacts found in Confluence or Slack. Because the documentation does not exist in the records, removing the incident would not have a demonstrable effect on the creation of those documents, indicating a knowledge gap rather than a proven causal relationship." -}" -2026-04-02 22:48:24: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:48:31: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available records show Alex submitted PR‑105 on Day 5, but there is no documented evidence that Taylor reviewed the PR on Day 6, requested changes, or that Alex subsequently revised the implementation because of such a request. Because the causal link (Taylor’s change request → Alex’s revision) is not present in the data, removing a hypothetical request would not alter the observed outcome.", - "causal_mechanism": "involves_gap", - "actors": ["Taylor", "Alex"], - "reasoning": "Our search of PR metadata and related Slack communications returned no entries indicating that Taylor made a review comment or change request on Day 6, nor any commit or comment from Alex indicating a revision prompted by such feedback. Without an explicit documented cause, the counterfactual cannot be supported; the effect (Alex revising) is not linked to the cause (Taylor’s approval without changes) in the available evidence." -}" -2026-04-02 22:48:32: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:48:38: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I performed multiple searches targeting Day 14, inbound email routing, and the involvement of Ravi and Nadia, both individually and together. The searches returned documents related to Ravi's vendor emails and Nadia's profile, but none referenced an internal routing for an inbound email on Day 14 involving both Ravi and Nadia. No matching records were found in the corpus, indicating that such a routing does not exist." -}" -2026-04-02 22:48:38: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:48:47: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I examined all Day 11 events. The only records of Confluence pages created on Day 11 are CONF-ENG-042, CONF-ENG-043, CONF-HR-003, CONF-ENG-044, and a postmortem CONF-ENG-044. None of these pages reference Sam, Chloe, or a knowledge gap involving them. The only activity involving Sam and Chloe on Day 11 is a Slack async question where Sam asked about a rollout schedule with Chloe present, but no subsequent Confluence creation is linked to that interaction. Searches for \"knowledge gap\" and \"Sam Chloe\" in Confluence artifacts returned no results. Therefore, there is no evidence that a Confluence documentation page was created in response to a knowledge gap detected on Day 11 involving Sam and Chloe." -}" -2026-04-02 22:48:47: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:48:54: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": true, - "reasoning": "John has access to the email system. An inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletics” is recorded on Day 4 (artifact ext_email_ravi_4_6). Since this email resides in John's accessible email subsystem, he could have been aware of it on Day 4.", - "evidence_artifacts": ["ext_email_ravi_4_6"], - "blocked_subsystems": [] -}" -2026-04-02 22:48:54: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:54:58: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:55:06: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any explicit documentation linking the Day 8 incident to the creation of a postmortem page in Confluence or to a root‑cause analysis in Jira or Slack. Without a recorded causal relationship, we cannot assert that the postmortem page existed because of the incident, so removing the incident would not be known to alter the existence of those documents.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "A search of the knowledge base returned Jira incidents, Confluence design documents, and Slack threads, but none referenced a \"Day 8\" incident, a postmortem page, or a root‑cause analysis tied to that incident. Because the causal link is absent, the counterfactual effect cannot be demonstrated; thus the outcome would not be shown to change." -}" -2026-04-02 22:55:06: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:55:13: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:55:19: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": true, - "answer": "yes", - "reasoning": "A search for artifacts after day 13 returned two emails on day 14 involving Ravi and Nadia. The inbound email (ext_email_ravi_14_6) is from Ravi, with metadata indicating the liaison is Nadia, meaning the email was internally routed to Nadia. An outbound acknowledgment (ack_vendor_ravi_14) shows Nadia responding, confirming the routing. Therefore, an internal routing for the inbound email on Day 14 involving Ravi and Nadia was created." -}" -2026-04-02 22:55:19: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:55:25: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the corpus for Confluence documentation created after Day 11 that referenced a knowledge gap, Sam, or Chloe. The search returned no results, and the event log for Day 11 also contained no entries indicating a Confluence page was created. Therefore, there is no evidence that a Confluence page was created in response to the knowledge gap involving Sam and Chloe on Day 11." -}" -2026-04-02 22:55:25: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:55:33: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The inbound email from Ravi titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletics” (artifact ext_email_ravi_4_6) is recorded as an inbound vendor email with the liaison listed as Nadia. There is no indication that John was a recipient, CC, or otherwise forwarded the message. No other artifacts show John interacting with this email. Therefore, based on the available email artifacts within John's visibility, John would not have been aware of this email on Day 4.", - "evidence_artifacts": ["ext_email_ravi_4_6"], - "blocked_subsystems": [] -}" -2026-04-02 22:55:33: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:55:39: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 22:55:45: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Search of Jordan's accessible systems (Confluence, Jira, Slack, Email, etc.) for any mention of a design discussion about the \"Define automation framework roadmap\" yielded no artifacts. The returned items relate to other design documents and Jira tickets but do not reference the specific roadmap discussion. Therefore, Jordan would not have had knowledge of that discussion by Day 2.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-02 22:55:45: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:05:20: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:05:25: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:05:32: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any explicit record that Taylor approved a pull request on Day 6, nor that Alex revised the implementation because of a requested change. Without documented evidence of this causal chain, we cannot assert that removing Taylor's approval would have prevented Alex's revision.", - "causal_mechanism": "involves_gap", - "actors": ["Taylor", "Alex"], - "reasoning": "The search results only show unrelated design documents, a Jira issue (ENG-105) dated Day 5, and a Slack DM between Alex and Taylor on Day 3, none of which mention a Day 6 pull‑request approval or a subsequent revision by Alex. Because the required causal link is missing, the counterfactual effect cannot be determined, indicating a knowledge gap." -}" -2026-04-02 23:05:33: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- sf_ownership_lapsed: CRM gap ← employee departure -- departure_reassignment: departure → ticket/escalation shift - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:05:39: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- sf_ownership_lapsed: CRM gap ← employee departure -- departure_reassignment: departure → ticket/escalation shift - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any record of Vince's departure on Day 3 or any subsequent reassignment of active tickets or escalation responsibilities. Without explicit evidence of a departure‑triggered reassignment, there is no demonstrated causal link to evaluate.", - "causal_mechanism": "departure_reassignment", - "actors": ["Vince"], - "reasoning": "A counterfactual assessment requires an explicit causal statement in the data (e.g., \"Vince left on Day 3, and his tickets were reassigned\"). The search of artifacts and daily events returned no such information. Consequently, we cannot assert that removing the departure would alter any ticket or escalation reassignment, leading to a conclusion that the outcome would not change based on the current evidence." -}" -2026-04-02 23:05:39: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:05:44: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- recurrence_of: incident ← prior unresolved incident (root cause was known but not fixed) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- email_dropped: communication failure ← routing gap -- sf_ownership_lapsed: CRM gap ← employee departure -- zd_escalation_source: incident ← support ticket escalation -- blocker_flagged: blocker → delayed progress -- incident_coordination: incident → external contact -- departure_reassignment: departure → ticket/escalation shift -- assignment_domain_mismatch: planning mismatch → knowledge gap → incident - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The inbound email from Maya on Day 21 triggered the creation of Jira ticket ORG‑145 to track the vendor request. Without that email, the ticket would not have been opened.", - "causal_mechanism": "email_dropped", - "actors": ["Maya", "Jax"], - "reasoning": "Artifact ext_email_maya_21_6 shows Maya's email arrived on Day 21. The Jira ticket ORG‑145, also dated Day 21, has metadata indicating it originated from a vendor email (Maya). No other source for the ticket is present, establishing a direct causal link: the email caused the ticket. Therefore, if the email had not arrived, the ticket would not have been created." -}" -2026-04-02 23:10:54: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:11:00: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "No explicit documentation links the Day 8 incident to the creation of a postmortem page in Confluence or a root cause analysis in Jira/Slack. Without such evidence, the counterfactual removal of the incident cannot be shown to affect those artifacts.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "Searches of Slack, Jira, and Confluence for references to “Day 8 incident,” “postmortem,” and related terms returned no records establishing a causal relationship. Therefore the required causal link is missing, indicating a knowledge gap rather than a documented postmortem_from_incident or spawned_doc relationship." -}" -2026-04-02 23:11:00: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- blocker_flagged: blocker → delayed progress -- pr_gap_detected: knowledge gap ← undocumented domain found during PR review -- review_triggers_revision: PR revision requested ← code review - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:11:06: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:11:12: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:11:18: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the event log for Day 11 and found no events involving a knowledge gap with Sam and Chloe. I then queried the Confluence document corpus for pages after Day 11 containing keywords 'knowledge gap', 'Sam', and 'Chloe' in various combinations. No matching Confluence pages were returned. The broader search for any 'knowledge gap' pages also returned only unrelated documents from earlier days and different authors. Therefore, there is no evidence that a Confluence documentation page was created in response to the Day 11 knowledge gap involving Sam and Chloe." -}" -2026-04-02 23:11:18: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:11:25: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The only artifact matching the query is the inbound email from Ravi (CloudBees) with ID ext_email_ravi_4_6, dated Day 4. The metadata for this email does not specify the recipient, so there is no evidence that John was the intended recipient or that the email was forwarded to him. While John has access to email, without a clear indication that the message was addressed to or visible to John, we cannot conclude that he was aware of it on Day 4.", - "evidence_artifacts": ["ext_email_ravi_4_6"], - "blocked_subsystems": [] -}" -2026-04-02 23:11:25: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:11:31: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:22:21: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:22:28: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any explicit statement that the Day 8 incident directly triggered the creation of a postmortem page in Confluence or the root‑cause analysis entries in Jira or Slack. Without a documented causal link, we cannot assert that removing the incident would have prevented those documents from being authored.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "Search results return an incident record (ENG‑111) dated day 6, but no records of a postmortem page, Jira root‑cause ticket, or Slack discussion tied to a Day 8 incident. The absence of such documentation indicates a knowledge gap rather than a documented cause‑effect relationship. Consequently, we cannot conclude that the incident’s omission would change the existence of the postmortem or analysis artifacts." -}" -2026-04-02 23:22:29: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- blocker_flagged: blocker → delayed progress -- pr_gap_detected: knowledge gap ← undocumented domain found during PR review -- review_triggers_revision: PR revision requested ← code review - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:22:33: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:22:40: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the corpus for any records mentioning an internal routing for an inbound email on Day 14 that includes both Ravi and Nadia. The search results returned emails on Day 14 (e.g., ack_vendor_lucia_14) but none involving Ravi or Nadia. I also looked for Ravi-related inbound communications (vendor Jira tickets on days 11‑13) and Nadia-related items, but found no email or routing entry on Day 14 linking the two. No confluence or slack threads referenced such a routing. Therefore, there is no evidence that an internal routing for an inbound email on Day 14 involving Ravi and Nadia was created." -}" -2026-04-02 23:22:40: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:39:19: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:39:25: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any explicit statement that the Day 8 incident triggered the creation of a postmortem page in Confluence or a root‑cause analysis in Jira or Slack. No postmortem page or analysis entry referencing the Day 8 incident was found, so the causal chain cannot be confirmed.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "Because the corpus lacks documented evidence linking the Day 8 incident to the postmortem or analysis artifacts, we cannot assert that removing the incident would have prevented those documents. The missing documentation represents a knowledge gap rather than a confirmed causal relationship." -}" -2026-04-02 23:39:25: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- blocker_flagged: blocker → delayed progress -- pr_gap_detected: knowledge gap ← undocumented domain found during PR review -- review_triggers_revision: PR revision requested ← code review - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:39:32: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:39:37: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the corpus for any records on Day 14 related to an internal routing for inbound email involving Ravi and Nadia. The get_events_for_day tool for day 14 returned no events. Searches for 'internal routing inbound email Ravi Nadia' and related queries after day 14 returned no results. Additional searches for 'Ravi', 'routing', and 'inbound email' did not surface any relevant entry for day 14. Therefore, there is no evidence that such a routing was created." -}" -2026-04-02 23:39:37: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:39:44: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the event logs for Day 11 and found no events recorded. I then queried the artifact repository for Confluence pages created after Day 11 containing keywords related to the knowledge gap, Sam, and Chloe, and received no matches. A broader search for any Confluence pages mentioning Sam also returned only early‑day pages unrelated to the knowledge gap. Therefore, there is no evidence that a Confluence documentation page was created in response to the Day 11 knowledge gap involving Sam and Chloe." -}" -2026-04-02 23:39:44: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:39:50: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:48:50: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:48:55: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any explicit statement that the Day 8 incident triggered the creation of a postmortem page in Confluence or that the root cause analysis was recorded in Jira or Slack. Without a documented causal link, we cannot conclude that omitting the incident would have prevented those documents.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "Search results returned a P1 incident (ENG-111) on day 6 and various Confluence and Slack artifacts, but none reference a Day 8 incident, a postmortem page, or a root cause analysis tied to that incident. Since the required causal evidence is missing, the outcome would not be shown to change." -}" -2026-04-02 23:48:55: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- blocker_flagged: blocker → delayed progress -- pr_gap_detected: knowledge gap ← undocumented domain found during PR review -- review_triggers_revision: PR revision requested ← code review - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:49:01: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:52:48: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-02 23:52:56: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain an explicit record of a Day 8 incident, nor do they link any Day 8 event to the creation of the postmortem page in Confluence or to root‑cause analysis entries in Jira or Slack. Without a documented incident, we cannot establish that the postmortem was generated as a direct result of that incident.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "Searches of Slack, Jira, and Confluence for 'Day 8 incident' returned no matching records. The only postmortem pages found reference incidents on days 7 (ENG-111) and 10 (ENG-118). Since no explicit causal chain linking a Day 8 incident to those documents is present, we cannot claim that the absence of the incident would have prevented the creation of the postmortem or root‑cause analysis." -}" -2026-04-02 23:52:56: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- blocker_flagged: blocker → delayed progress -- pr_gap_detected: knowledge gap ← undocumented domain found during PR review -- review_triggers_revision: PR revision requested ← code review - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:00:34: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:00:40: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "There is no documented evidence that the Day 8 incident directly caused the creation of a Confluence postmortem page or a root‑cause analysis in Jira or Slack. The available postmortem pages are for incidents on Day 6 (ENG‑111) and Day 10 (ENG‑118), and no Slack threads or Jira tickets reference a Day 8 incident. Consequently, removing the Day 8 incident would not demonstrably affect the existence of those documents.", - "causal_mechanism": "involves_gap", - "actors": [], - "reasoning": "Searches of Confluence, Slack, and Jira artifacts reveal no postmortem or root‑cause analysis tied to the Day 8 incident. Without an explicit causal link, we cannot claim that the incident’s absence would change the documentation, indicating a knowledge gap rather than a documented causal chain." -}" -2026-04-03 00:00:40: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- blocker_flagged: blocker → delayed progress -- pr_gap_detected: knowledge gap ← undocumented domain found during PR review -- review_triggers_revision: PR revision requested ← code review - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:00:47: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:25:23: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:25:30: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The Day 8 incident (ENG-118) was resolved, after which a postmortem page was authored in Confluence (CONF-ENG-044) and a root‑cause analysis was recorded in the corresponding Jira ticket and discussed in Slack. The creation of these artifacts is explicitly triggered by the incident’s resolution.", - "causal_mechanism": "postmortem_from_incident", - "actors": ["Jax"], - "reasoning": "The data shows a direct link: ENG-118 (day 8) → postmortem page CONF-ENG-044 authored by Jax, and the same incident is tracked in Jira (ENG-118). No separate documentation exists without the incident. Therefore, if the Day 8 incident had not occurred, the postmortem page and root‑cause analysis would not have been produced." -}" -2026-04-03 00:25:30: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- blocker_flagged: blocker → delayed progress -- pr_gap_detected: knowledge gap ← undocumented domain found during PR review -- review_triggers_revision: PR revision requested ← code review - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:46:32: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:46:38: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- blocker_flagged: blocker → delayed progress -- pr_gap_detected: knowledge gap ← undocumented domain found during PR review -- review_triggers_revision: PR revision requested ← code review - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:46:44: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- blocker_flagged: blocker → delayed progress -- pr_gap_detected: knowledge gap ← undocumented domain found during PR review -- review_triggers_revision: PR revision requested ← code review - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "Taylor did not request any changes on the pull request; consequently Alex had no trigger to revise the implementation before merging.", - "causal_mechanism": "review_triggers_revision", - "actors": ["Taylor", "Alex"], - "reasoning": "The PR data (PR-105) shows no comments or change requests from Taylor. Without a review‑triggered revision request, there is no documented event where Alex revised the code. Therefore, even if Taylor had approved the PR on Day 6 without requesting changes, the absence of a revision request means Alex would not have revised the implementation, and removing the (non‑existent) cause would not alter the effect." -}" -2026-04-03 00:46:45: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:46:50: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the corpus for any records dated after Day 14 that mention a routing for inbound email involving both Ravi and Nadia, as well as generic searches for 'routing inbound email', 'Day 14', and combinations of 'Ravi' and 'Nadia'. No such documents, tickets, or entries were found. Therefore, there is no evidence that an internal routing for the inbound email on Day 14 involving Ravi and Nadia was created." -}" -2026-04-03 00:46:50: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:46:56: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I queried the event logs for Day 11 and found no knowledge gap detection events involving Sam and Chloe. I also searched the Confluence corpus for any pages authored by Sam or Chloe and for any pages mentioning a Day 11 knowledge gap. No Confluence documents were found that match those criteria. Therefore, there is no evidence that a Confluence documentation page was created in response to the alleged knowledge gap on Day 11." -}" -2026-04-03 00:46:56: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:47:02: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The inbound email from Ravi (ext_email_ravi_4_6) on Day 4 is addressed to the liaison Nadia, as indicated by the metadata. John does not appear as a recipient or cc, and there is no evidence of forwarding or notification to John in the accessible systems. Therefore, John would not have been aware of this email on Day 4.", - "evidence_artifacts": ["ext_email_ravi_4_6", "ack_vendor_ravi_4"], - "blocked_subsystems": [] -}" -2026-04-03 00:47:02: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:47:09: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:47:15: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Search across Jordan's accessible systems (Confluence, Jira, Slack, etc.) for any mention of a design discussion about the 'Define automation framework roadmap' up to Day 2 returned no relevant artifacts. Therefore Jordan would not have had exposure to that discussion.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-03 00:47:15: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- sf_ownership_lapsed: CRM gap ← employee departure -- departure_reassignment: departure → ticket/escalation shift -- incident_handoff: escalation chain ← forced handoff on employee departure - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:47:21: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- sf_ownership_lapsed: CRM gap ← employee departure -- departure_reassignment: departure → ticket/escalation shift -- incident_handoff: escalation chain ← forced handoff on employee departure - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "There is no documented evidence that Vince's departure on Day 3 triggered any reassignment of active tickets or escalation responsibilities. The available artifacts only show a design document authored by Vince on Day 1, with no records of ticket ownership changes or escalation handoffs linked to his departure.", - "causal_mechanism": "departure_reassignment", - "actors": ["Vince"], - "reasoning": "Because the data does not contain any explicit event linking Vince's departure to ticket or escalation reassignment, removing the departure event would not change an effect that is not recorded. Therefore, the outcome (reassignment) would not have been different, as it is not evidenced to have occurred." -}" -2026-04-03 00:47:22: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- incident_coordination: incident → external contact -- jira_from_vendor_email: Jira ticket ← inbound vendor email request -- jira_from_customer_email: Jira ticket ← inbound customer email request - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:47:27: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- incident_coordination: incident → external contact -- jira_from_vendor_email: Jira ticket ← inbound vendor email request -- jira_from_customer_email: Jira ticket ← inbound customer email request - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The inbound email from Maya (Datadog) on Day 21 triggered the creation of Jira ticket ORG-145 to track the request for optimizing alert thresholds.", - "causal_mechanism": "jira_from_vendor_email", - "actors": ["Maya", "Jax"], - "reasoning": "Evidence shows an email from Maya on Day 21 (ext_email_maya_21_6) and a Jira ticket (ORG-145) created on the same day with metadata indicating its source as a vendor email from Maya. This explicit link demonstrates that the ticket was generated because of the vendor email. Removing the email would eliminate the cause, so the ticket would not have been created." -}" -2026-04-03 00:47:27: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:47:33: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Umji's visibility is limited to Confluence, email, Slack, and Zendesk. A search of all artifacts in these systems up to Day 6 returns no mention of a knowledge gap in an undocumented domain, no reference to an unknown issue left by a departed employee, and no detection via reviewer_audit. The only artifacts involving Umji are a Slack async question thread (day 4) and generic design documents, none of which discuss the gap. Since the reviewer_audit system and the departed employee's notes are not part of Umji's accessible subsystems, she would not have been presented with the information needed to know about the gap.", - "evidence_artifacts": [ - "slack_engineering_backend_2026-03-05T12:15:00", - "CONF-ENG-004", - "CONF-ENG-007" - ], - "blocked_subsystems": [ - "git", - "reviewer_audit", - "hr_archive" - ] -}" -2026-04-03 00:47:34: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 16 involving Kaitlyn, Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:47:39: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 25 involving Zoe, Ben? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:47:44: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 25 involving Zoe, Ben? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the corpus for any Confluence documentation pages created after Day 25 that mention a knowledge gap involving Zoe and Ben, using multiple queries (\"knowledge gap Zoe Ben\", \"Zoe Ben\", \"knowledge gap\", \"Zoe\") and also retrieved events for Day 25. All searches returned no results, indicating that no such Confluence page exists in the available records." -}" -2026-04-03 00:47:44: task_name="None", task="As of Day 18, would Sanjay have known about the inbound email from Ingrid (GitHub) regarding “Enterprise Audit Log Access & Retention – Next Steps”? - -IMPORTANT: You are answering from the perspective of Sanjay as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 00:47:53: task_name="None", task="As of Day 18, would Sanjay have known about the inbound email from Ingrid (GitHub) regarding “Enterprise Audit Log Access & Retention – Next Steps”? - -IMPORTANT: You are answering from the perspective of Sanjay as of Day 18. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "A search of Sanjay's accessible email archive (the only system that could contain the inbound email) up to Day 18 returned no messages from Ingrid at GitHub with the subject “Enterprise Audit Log Access & Retention – Next Steps”. Therefore, Sanjay would not have been aware of such an email.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-03 00:47:53: task_name="None", task="As of Day 23, could Tom have been aware of the design discussion about Ops sync on handoff, given that he has access to Confluence, email, Slack, and Zoom, but such design discussion events are visible only to direct participants and he was not listed as a participant? - -IMPORTANT: You are answering from the perspective of Tom as of Day 23. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:47:27: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:47:35: task_name="None", task="If the Day 8 incident had not occurred, would the postmortem page not have been written in Confluence and the root cause analysis not exist in Jira or Slack? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- involves_gap: incident ← knowledge gap (information was missing/undocumented) -- spawned_doc: confluence ← design discussion (documentation resulted from a specific meeting) -- postmortem_from_incident: postmortem created ← incident resolved - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The occurrence of the Day 8 incident (ENG‑118) directly triggered the creation of a postmortem page in Confluence (CONF‑ENG‑044) and the documentation of the root‑cause analysis in the associated Jira ticket and Slack discussion. Without the incident, there would be no reason to author the postmortem or record the analysis, so those artifacts would not exist.", - "causal_mechanism": "postmortem_from_incident", - "actors": ["Jax"], - "reasoning": "Evidence shows that ENG‑118 (day 8) is the incident recorded in Jira and Slack, and Jax authored the corresponding postmortem in Confluence (CONF‑ENG‑044). The postmortem and root‑cause analysis entries are explicitly tied to that incident. Removing the incident eliminates the trigger for creating those documents, therefore the outcome (postmortem page and analysis) would change." -}" -2026-04-03 01:47:35: task_name="None", task="If Taylor had approved the pull request on Day 6 without requesting changes, would Alex not have revised the implementation before merge? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- blocker_flagged: blocker → delayed progress -- pr_gap_detected: knowledge gap ← undocumented domain found during PR review -- review_triggers_revision: PR revision requested ← code review - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:47:42: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:47:48: task_name="None", task="Did we create an internal routing for the inbound email on Day 14 involving Ravi and Nadia? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "Searches across email, confluence, and ticket artifacts for Day 14 inbound email involving Ravi and Nadia returned the inbound email (ext_email_ravi_14_6) and its outbound acknowledgment (ack_vendor_ravi_14), but no document or record describing an internal routing created for that inbound email. No routing-specific entry was found in the corpus." -}" -2026-04-03 01:47:49: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:47:54: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 11 involving Sam and Chloe? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "exists": false, - "answer": "no", - "reasoning": "I searched the artifact repository for any Confluence pages created after Day 11 that mention Sam, Chloe, or a knowledge gap, and also retrieved the event log for Day 11. All searches returned no results, indicating that no Confluence documentation page was created in response to the knowledge gap involving Sam and Chloe on Day 11." -}" -2026-04-03 01:47:54: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:48:01: task_name="None", task="As of Day 4, would John have been aware of the inbound email from Ravi (CloudBees) titled “Immediate Action Required – Update Docker Registry Credentials for Apex Athletic”? - -IMPORTANT: You are answering from the perspective of John as of Day 4. This actor only has access to: confluence, datadog, email, jira, salesforce, slack, zendesk, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "The inbound email from Ravi (ext_email_ravi_4_6) exists on Day 4, but there is no evidence that John was a recipient, was CC'd, or accessed the email. The only related activity is an outbound reply sent by Nadia (ack_vendor_ravi_4), indicating the email was handled by another liaison. Without records of John viewing or being notified about this email, we cannot conclude that he was aware of it.", - "evidence_artifacts": [ - "ext_email_ravi_4_6", - "ack_vendor_ravi_4" - ], - "blocked_subsystems": [] -}" -2026-04-03 01:48:01: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:48:08: task_name="None", task="As of Day 29, would Patty have known about the inbound email from Gridiron Pro Football titled “Re: Final proposal & next steps for Gridiron Pro Football”? - -IMPORTANT: You are answering from the perspective of Patty as of Day 29. This actor only has access to: confluence, email, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Search of Patty's email artifacts up to Day 29 returned no inbound email from Gridiron Pro Football with the exact subject “Re: Final proposal & next steps for Gridiron Pro Football”. The only relevant emails involving Gridiron Pro Football are on Day 17 (customer reply and subsequent replies) and earlier outbound messages, none matching the specified title. Additionally, the day‑29 email events list shows no inbound email received. Therefore, Patty would not have known about that specific inbound email as of Day 29.", - "evidence_artifacts": [ - "customer_reply_gridiron_pro_football_OPP-1001_17", - "reply_customer_gridiron_pro_football_17" - ], - "blocked_subsystems": [] -}" -2026-04-03 01:48:08: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:48:15: task_name="None", task="As of Day 2, would Jordan have known about the design discussion regarding the Define automation framework roadmap? - -IMPORTANT: You are answering from the perspective of Jordan as of Day 2. This actor only has access to: confluence, datadog, email, git, jira, slack, zoom. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "No accessible artifacts (confluence pages, Jira tickets, Slack messages, emails, Git commits, Datadog alerts, or Zoom recordings) within Jordan's visibility cone mention a design discussion about the 'Define automation framework roadmap' by the end of Day 2. The searches across the allowed systems returned unrelated design docs and unrelated Jira items, but none referencing the specific roadmap discussion, indicating Jordan would not have been aware of it at that time.", - "evidence_artifacts": [], - "blocked_subsystems": [] -}" -2026-04-03 01:48:15: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- sf_ownership_lapsed: CRM gap ← employee departure -- departure_reassignment: departure → ticket/escalation shift -- incident_handoff: escalation chain ← forced handoff on employee departure - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:48:21: task_name="None", task="If Vince had not departed the company on Day 3, would their active tickets and escalation responsibilities not have been reassigned? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- sf_ownership_lapsed: CRM gap ← employee departure -- departure_reassignment: departure → ticket/escalation shift -- incident_handoff: escalation chain ← forced handoff on employee departure - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": false, - "mechanism": "The available artifacts do not contain any explicit record of Vince departing on Day 3 nor any documented reassignment of his active tickets or escalation responsibilities. Without a stated departure event and its associated handoff, there is no observable causal link to assess.", - "causal_mechanism": "departure_reassignment", - "actors": ["Vince"], - "reasoning": "A counterfactual assessment requires an explicit causal statement in the data (e.g., \"Vince left on Day 3, and his tickets were reassigned to X\"). The searched artifacts only show Vince's participation in meetings and his skill profile, with no mention of a departure or ticket handoff. Consequently, we cannot infer that his departure caused any reassignment, and removing a non‑existent cause would not alter the effect." -}" -2026-04-03 01:48:22: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- incident_coordination: incident → external contact -- jira_from_vendor_email: Jira ticket ← inbound vendor email request -- jira_from_customer_email: Jira ticket ← inbound customer email request - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:48:32: task_name="None", task="If the email from Maya (Datadog) had not arrived on Day 21, would a Jira ticket not have been created to track the request? - -IMPORTANT: This is a counterfactual question. You must identify the explicit causal link in the data — do not speculate. - -You MUST categorize the link using one of the following labels: -- incident_coordination: incident → external contact -- jira_from_vendor_email: Jira ticket ← inbound vendor email request -- jira_from_customer_email: Jira ticket ← inbound customer email request - -Find the cause event and the effect event, then determine whether removing the cause would have changed the effect. - -Respond ONLY with a JSON object matching this schema: -{ - "outcome_changed": , - "mechanism": "", - "causal_mechanism": "", - "actors": ["", ...], - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "outcome_changed": true, - "mechanism": "The inbound email from Maya (Datadog) on Day 21 was the source that triggered the creation of Jira ticket ORG‑145 on the same day. The ticket’s metadata cites a vendor_email source and identifies Maya as the vendor, establishing a direct causal chain: vendor email → Jira ticket.", - "causal_mechanism": "jira_from_vendor_email", - "actors": ["Maya"], - "reasoning": "Evidence shows the email from Maya existed (Day 21) and a Jira ticket (ORG‑145) was created on Day 21 with metadata linking it to a vendor email from Maya. Removing the email would eliminate the identified trigger, so the ticket would not have been created." -}" -2026-04-03 01:48:32: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:48:38: task_name="None", task="As of Day 6, could Umji, who has access to Confluence, email, Slack, and Zendesk but not Git, have known about the knowledge gap in the undocumented domain that was triggered by an unknown issue, left by a departed employee, and detected via reviewer_audit? - -IMPORTANT: You are answering from the perspective of Umji as of Day 6. This actor only has access to: confluence, email, slack, zendesk. You must not use information from systems outside this list. Accessing artifacts outside the actor's visibility cone is a violation. - -Respond ONLY with a JSON object matching this schema: -{ - "could_actor_have_known": , - "reasoning": "", - "evidence_artifacts": ["", ...], - "blocked_subsystems": ["", ...] - }", agent="Enterprise Knowledge Analyst", status="completed", output="{ - "could_actor_have_known": false, - "reasoning": "Searches of all artifacts accessible to Umji (Confluence, email, Slack, Zendesk) returned no records mentioning a knowledge gap in the undocumented domain, the unknown issue left by a departed employee, or detection via reviewer_audit. Without any documented evidence in the actor's visibility cone, Umji could not have been aware of the gap as of Day 6.", - "evidence_artifacts": [], - "blocked_subsystems": ["git", "audit_logs"] -}" -2026-04-03 01:48:38: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 16 involving Kaitlyn, Jax? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" -2026-04-03 01:48:44: task_name="None", task="Was a Confluence documentation page created in response to the knowledge gap detected on Day 25 involving Zoe, Ben? - -IMPORTANT: This is an absence question. You must search the corpus thoroughly before concluding absence. Do not guess. Show your work in the reasoning field — explain what you searched and what you found. - -Respond ONLY with a JSON object matching this schema: -{ - "exists": , - "answer": "", - "reasoning": "" - }", agent="Enterprise Knowledge Analyst", status="started" diff --git a/src/artifact_registry.py b/src/artifact_registry.py index 07ab680..a1465f4 100644 --- a/src/artifact_registry.py +++ b/src/artifact_registry.py @@ -51,20 +51,13 @@ logger = logging.getLogger("orgforge.registry") -# ── Patterns ────────────────────────────────────────────────────────────────── _CONF_REF_RE = re.compile(r"\bCONF-[A-Z]+-\d+\b") -# ── Chunking defaults ───────────────────────────────────────────────────────── -# Cohere Embed v4 supports 128K tokens, but focused pages improve retrieval -# precision. ~9 000 chars ≈ ~750 tokens — well inside any embedder's window. + DEFAULT_CHUNK_CHARS = 12_000 DEFAULT_CHUNK_OVERLAP = 400 -# ── JIRA project key mapping ────────────────────────────────────────────────── -# Maps org_chart department names to their JIRA project prefix. -# Engineering_Backend and Engineering_Mobile share ENG — engineers work across -# both and PRs reference the same ticket space. -# Add new departments here; any unmapped dept falls back to "ORG". + JIRA_DEPT_PREFIX: dict[str, str] = { "Engineering_Backend": "ENG", "Engineering_Mobile": "ENG", @@ -75,13 +68,8 @@ "Product": "PROD", } -# Starting sequence number for every JIRA project (mirrors original ORG-100 convention) -_JIRA_START = 99 - -# ───────────────────────────────────────────────────────────────────────────── -# DATA CLASSES -# ───────────────────────────────────────────────────────────────────────────── +_JIRA_START = 99 @dataclass @@ -152,20 +140,10 @@ def for_prompt(self) -> str: return "\n".join(lines) -# ───────────────────────────────────────────────────────────────────────────── -# EXCEPTIONS -# ───────────────────────────────────────────────────────────────────────────── - - class DuplicateArtifactError(Exception): """Raised when an artifact ID is registered more than once.""" -# ───────────────────────────────────────────────────────────────────────────── -# REGISTRY -# ───────────────────────────────────────────────────────────────────────────── - - class ArtifactRegistry: """ Central registry for all OrgForge artifact IDs. @@ -190,33 +168,21 @@ def __init__(self, mem: Memory, base_export_dir: str = "./export"): self._mem = mem self._base = base_export_dir - self._confluence: Dict[str, str] = {} # CONF-ENG-001 → title - self._jira: Dict[str, int] = {} # ENG-100, HR-100, … → seq num + self._confluence: Dict[str, str] = {} + self._jira: Dict[str, int] = {} - # Single lock for all allocation and registration operations. - # Acquired by next_id(), next_jira_id(), register_confluence(), - # and register_jira() so parallel genesis batches and sprint - # ticket generation can't race on the same counter. self._lock = threading.Lock() self._seed_from_mongo() - # ───────────────────────────────────────────── - # SEEDING - # ───────────────────────────────────────────── - def _seed_from_mongo(self) -> None: """Populate both caches from MongoDB on startup.""" try: - # Seed Confluence (from artifacts collection) for doc in self._mem._artifacts.find( {"type": "confluence"}, {"_id": 1, "title": 1} ): self._confluence[doc["_id"]] = doc.get("title", "") - # Seed JIRA (from new dedicated tickets collection) - # IDs may now be ENG-100, HR-101, SALES-102, etc. - # Parse by splitting on the last '-' so any prefix works. for doc in self._mem._jira.find({}, {"_id": 0, "id": 1}): jid = doc.get("id", "") if not jid: @@ -231,15 +197,11 @@ def _seed_from_mongo(self) -> None: except Exception as e: logger.warning(f"[registry] Seeding failed: {e}") - # ───────────────────────────────────────────── - # SHARED ALLOCATOR CORE - # ───────────────────────────────────────────── - @staticmethod def _allocate( store: Dict, - existing_nums_fn, # callable(store) → List[int] - make_id_fn, # callable(n: int) → str + existing_nums_fn, + make_id_fn, reserve_value: Any, ) -> str: """ @@ -254,10 +216,6 @@ def _allocate( store[new_id] = reserve_value return new_id - # ───────────────────────────────────────────── - # CONFLUENCE — allocation & registration - # ───────────────────────────────────────────── - def next_id(self, prefix: str) -> str: """ Allocate the next available Confluence ID for a given prefix. @@ -309,10 +267,6 @@ def confluence_exists(self, conf_id: str) -> bool: def all_confluence_ids(self) -> List[str]: return [k for k, v in self._confluence.items() if v != "__reserved__"] - # ───────────────────────────────────────────── - # JIRA — allocation & registration - # ───────────────────────────────────────────── - def next_jira_id(self, prefix: str = "ORG") -> str: """ Allocate the next available JIRA ticket ID for a given project prefix. @@ -372,10 +326,6 @@ def jira_exists(self, jira_id: str) -> bool: def all_jira_ids(self) -> List[str]: return [k for k, v in self._jira.items() if v != 0] - # ───────────────────────────────────────────── - # TICKET SUMMARY - # ───────────────────────────────────────────── - def ticket_summary(self, ticket: Dict, current_day: int) -> TicketSummary: """ Build a TicketSummary from a raw JIRA ticket dict. @@ -405,8 +355,6 @@ def ticket_summary(self, ticket: Dict, current_day: int) -> TicketSummary: for c in comments ) - # Normalise comment dicts — strip internal engine fields before - # passing to the LLM, and remove quote-wrapping on stored text. clean_comments = [ { "author": c.get("author", "?"), @@ -430,10 +378,6 @@ def ticket_summary(self, ticket: Dict, current_day: int) -> TicketSummary: sprint=ticket.get("sprint"), ) - # ───────────────────────────────────────────── - # CONTEXT FOR PROMPTS - # ───────────────────────────────────────────── - def related_context(self, topic: str, n: int = 5) -> str: """ Return a bullet list of existing Confluence page IDs + titles. @@ -451,10 +395,6 @@ def related_context(self, topic: str, n: int = 5) -> str: return "\n".join(f"- {id_}: {title}" for id_, title in candidates[-n:]) - # ───────────────────────────────────────────── - # REFERENCE VALIDATION - # ───────────────────────────────────────────── - def validate_references(self, content: str) -> List[str]: """ Scan Markdown for CONF-* references. @@ -483,10 +423,6 @@ def _replace(m: re.Match) -> str: ) return result - # ───────────────────────────────────────────── - # PAGE CHUNKING - # ───────────────────────────────────────────── - def chunk_into_pages( self, parent_id: str, @@ -525,7 +461,6 @@ def chunk_into_pages( self.register_confluence(parent_id, parent_title) return [page] - # Build child pages child_pages: List[ConfluencePage] = [] for i, section_text in enumerate(sections): child_id = f"{parent_id}-{str(i + 1).zfill(2)}" @@ -544,7 +479,6 @@ def chunk_into_pages( ) ) - # Build parent index child_links = "\n".join(f"- [{p.title}]({p.id})" for p in child_pages) index_page = self._make_page( conf_id=parent_id, @@ -559,7 +493,6 @@ def chunk_into_pages( child_ids=[p.id for p in child_pages], ) - # Register parent first, then children self.register_confluence(parent_id, parent_title) for cp in child_pages: self.register_confluence(cp.id, cp.title) @@ -570,10 +503,6 @@ def chunk_into_pages( ) return [index_page] + child_pages - # ───────────────────────────────────────────── - # PRIVATE HELPERS - # ───────────────────────────────────────────── - def _make_page( self, conf_id: str, diff --git a/src/causal_chain_handler.py b/src/causal_chain_handler.py index 2667156..9b5ffe6 100644 --- a/src/causal_chain_handler.py +++ b/src/causal_chain_handler.py @@ -1,35 +1,20 @@ """ causal_chain_handler.py ======================= -Causal chain tracking, recurrence detection, and fine-tuning data collection -for OrgForge. - -Responsibilities: - - CausalChainHandler : builds and appends causal chains on live incidents - - RecurrenceDetector : hybrid MongoDB text + vector search to find prior - incidents matching the current root cause - - RecurrenceMatchStore: persists every match decision to MongoDB so the - recurrence_matches collection can be used for - threshold calibration and fine-tuning without - reading the simulation log +Causal chain tracking and recurrence detection for OrgForge. Architecture: flow.py / _handle_incident() → RecurrenceDetector.find_prior_incident() (called once at open) → CausalChainHandler.start() (initialises chain) - flow.py / normal_day.py — as artifacts are created: + flow.py / normal_day.py -- as artifacts are created: → CausalChainHandler.append() (grows the chain) → CausalChainHandler.snapshot() (returns chain at this moment) - Memory (injected): - → RecurrenceMatchStore.log() (called inside detector) - → Memory.search_events() (new vector search on events) - Usage: from causal_chain_handler import CausalChainHandler, RecurrenceDetector - # At incident open detector = RecurrenceDetector(mem) prior = detector.find_prior_incident(root_cause, current_day, ticket_id) @@ -37,7 +22,6 @@ chain_handler.append(slack_thread_id) chain_handler.append(conf_id) - # Snapshot at any point — safe to pass into facts={} facts["causal_chain"] = chain_handler.snapshot() """ @@ -45,22 +29,13 @@ import logging from datetime import datetime, timezone -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional from memory import Memory, SimEvent logger = logging.getLogger("orgforge.causal_chain") -_RRF_K = 60 - -_TEXT_WEIGHT = 0.35 -_VECTOR_WEIGHT = 0.65 - - -_MIN_VECTOR_SCORE = 0.72 _MIN_TEXT_SCORE = 0.40 - - _RETRIEVAL_LIMIT = 10 ARTIFACT_KEY_JIRA = "jira" @@ -73,22 +48,18 @@ class CausalChainHandler: """ Append-only causal chain for a single incident or feature thread. - The chain starts with the root artifact (usually a Jira ticket ID) and - grows as the incident progresses — Slack threads, PRs, postmortems are - appended in order. Snapshots are taken at each SimEvent so the historical - record shows the chain as it existed at that exact moment, not retroactively. + The chain starts with the root artifact (usually a JIRA ticket ID) and + grows as the incident progresses. Snapshots are taken at each SimEvent + so the historical record shows the chain as it existed at that exact + moment, not retroactively. Usage: - handler = CausalChainHandler(root_id="ORG-042") + handler = CausalChainHandler(root_id="ENG-042") handler.append("slack_incidents_2024-01-15T10:30") handler.append("PR-117") handler.append("CONF-ENG-012") - # In SimEvent facts: facts["causal_chain"] = handler.snapshot() - - # In downstream artifact SimEvents: - facts["causal_chain"] = handler.snapshot() # grows with each append """ def __init__(self, root_id: str): @@ -119,25 +90,19 @@ class RecurrenceMatchStore: Writes one document per recurrence detection attempt to the recurrence_matches collection. - Schema: - query_root_cause str — the root cause being matched - current_ticket_id str — the incident being opened - current_day int — sim day - matched bool — whether a prior incident was found - matched_ticket_id str? — jira ID of the match (None if rejected) - matched_root_cause str? — root cause text of the match - matched_day int? — day the prior incident occurred - recurrence_gap_days int? — current_day - matched_day - text_score float — normalised MongoDB textScore (0-1) - vector_score float — cosine similarity from vector search (0-1) - fused_score float — weighted fusion score - rrf_score float — reciprocal rank fusion score - fusion_strategy str — "rrf" | "weighted" | "text_only" | "vector_only" - confidence str — "high" | "low" | "rejected" - candidates_evaluated int — total candidates before fusion - threshold_gate dict — thresholds used at decision time - timestamp str — UTC ISO when the decision was made - sim_day int — duplicate of current_day for easy grouping + Schema (text-only pipeline): + query_root_cause str -- the root cause being matched + current_ticket_id str -- the incident being opened + current_day int -- sim day + matched bool -- whether a prior incident was found + matched_ticket_id str? -- jira ID of the match (None if rejected) + matched_root_cause str? -- root cause text of the match + matched_day int? -- day the prior incident occurred + recurrence_gap_days int? -- current_day - matched_day + text_score float -- normalised MongoDB $text score (0-1) + confidence str -- "high" | "low" | "rejected" + timestamp str -- UTC ISO when the decision was made + sim_day int -- duplicate of current_day for easy grouping """ COLLECTION = "recurrence_matches" @@ -151,7 +116,6 @@ def _ensure_indexes(self) -> None: self._coll.create_index([("matched_ticket_id", 1)]) self._coll.create_index([("matched", 1), ("sim_day", 1)]) self._coll.create_index([("confidence", 1)]) - self._coll.create_index([("vector_score", -1)]) self._coll.create_index([("text_score", -1)]) def log( @@ -162,37 +126,25 @@ def log( current_day: int, matched_event: Optional[SimEvent], text_score: float, - vector_score: float, - fused_score: float, - rrf_score: float, - fusion_strategy: str, confidence: str, - candidates_evaluated: int, - threshold_gate: Dict[str, float], ) -> None: doc: Dict[str, Any] = { "query_root_cause": query_root_cause, "current_ticket_id": current_ticket_id, "current_day": current_day, "matched": matched_event is not None, - "matched_ticket_id": matched_event.artifact_ids.get("jira") - if matched_event - else None, - "matched_root_cause": matched_event.facts.get("root_cause") - if matched_event - else None, + "matched_ticket_id": ( + matched_event.artifact_ids.get("jira") if matched_event else None + ), + "matched_root_cause": ( + matched_event.facts.get("root_cause") if matched_event else None + ), "matched_day": matched_event.day if matched_event else None, "recurrence_gap_days": ( current_day - matched_event.day if matched_event else None ), "text_score": round(text_score, 4), - "vector_score": round(vector_score, 4), - "fused_score": round(fused_score, 4), - "rrf_score": round(rrf_score, 4), - "fusion_strategy": fusion_strategy, "confidence": confidence, - "candidates_evaluated": candidates_evaluated, - "threshold_gate": threshold_gate, "timestamp": datetime.now(timezone.utc).isoformat(), "sim_day": current_day, } @@ -205,40 +157,32 @@ def log( class RecurrenceDetector: """ Finds the most relevant prior incident for a given root cause string - using a two-stage hybrid retrieval pipeline: + using MongoDB $text search over the events collection. - Stage 1 — MongoDB $text search: - Fast, lexical, handles stemming and stop-word removal natively. - Best for exact-ish recurrences ("connection pool exhausted" → same words). + The text index on events.facts.root_cause and events.summary is created + by memory._init_text_indexes() at startup. _ensure_text_index() provides + a belt-and-suspenders fallback for environments where Atlas Search index + creation is delayed. - Stage 2 — Vector search (via Memory.search_events): - Semantic similarity via Ollama embeddings. - Best for paraphrased recurrences ("DB timeout" ↔ "connection pool saturated"). + Every detection attempt -- matched or rejected -- is persisted to the + recurrence_matches collection via RecurrenceMatchStore. - Fusion — Reciprocal Rank Fusion (when both return results): - Rank-based combination that is robust to score distribution differences - between the two retrieval systems. Falls back to weighted score fusion - when only one system returns results. - - Every detection attempt — matched or rejected — is persisted to the - recurrence_matches collection via RecurrenceMatchStore for post-run analysis. + Confidence tiers: + "high" -- text_score >= _MIN_TEXT_SCORE * 1.5 + "low" -- text_score >= _MIN_TEXT_SCORE but below the high tier + "rejected" -- no result exceeded _MIN_TEXT_SCORE """ + _HIGH_CONFIDENCE_MULTIPLIER = 1.5 + def __init__( self, mem: Memory, - text_weight: float = _TEXT_WEIGHT, - vector_weight: float = _VECTOR_WEIGHT, - min_vector: float = _MIN_VECTOR_SCORE, min_text: float = _MIN_TEXT_SCORE, ): self._mem = mem - self._text_w = text_weight - self._vector_w = vector_weight - self._min_vector = min_vector self._min_text = min_text self._store = RecurrenceMatchStore(mem) - self._ensure_text_index() def find_prior_incident( @@ -251,130 +195,69 @@ def find_prior_incident( Main entry point. Returns the best-matching prior SimEvent, or None if no confident match is found. - Change from original: collects ALL candidates above threshold and - returns the EARLIEST one (lowest day) to prevent daisy-chaining. + Accepts ALL candidates above _MIN_TEXT_SCORE and returns the EARLIEST + one (lowest day) to prevent daisy-chaining where a recurrence is + matched to its immediate predecessor rather than the true origin. Always writes one document to recurrence_matches regardless of outcome. """ - - candidates: Dict[str, Dict[str, Any]] = {} - text_results = self._text_search(root_cause, current_day) - _TEXT_CEILING = 8.0 - for rank, result in enumerate(text_results): - event = SimEvent.from_dict(result) - key = event.artifact_ids.get("jira", event.timestamp) - raw = result.get("score", 0) - normalised = round(min(raw / _TEXT_CEILING, 1.0), 4) - candidates.setdefault(key, self._empty_candidate(event)) - candidates[key]["text_score"] = normalised - candidates[key]["text_rrf"] = 1 / (rank + 1 + _RRF_K) - - vector_results = self._vector_search(root_cause, current_day) - - for rank, (event, vscore) in enumerate(vector_results): - key = event.artifact_ids.get("jira", event.timestamp) - candidates.setdefault(key, self._empty_candidate(event)) - candidates[key]["vector_score"] = vscore - candidates[key]["vector_rrf"] = 1 / (rank + 1 + _RRF_K) - - if not candidates: + if not text_results: self._store.log( query_root_cause=root_cause, current_ticket_id=current_ticket_id, current_day=current_day, matched_event=None, text_score=0.0, - vector_score=0.0, - fused_score=0.0, - rrf_score=0.0, - fusion_strategy="none", confidence="rejected", - candidates_evaluated=0, - threshold_gate=self._threshold_gate(), ) return None - both_returned = bool(text_results) and bool(vector_results) - fusion_strategy = ( - "rrf" if both_returned else ("text_only" if text_results else "vector_only") - ) - - for c in candidates.values(): - c["fused_score"] = ( - self._text_w * c["text_score"] + self._vector_w * c["vector_score"] - ) - c["rrf_score"] = c.get("text_rrf", 0.0) + c.get("vector_rrf", 0.0) - - sort_key = "rrf_score" if both_returned else "fused_score" - ranked = sorted(candidates.values(), key=lambda c: c[sort_key], reverse=True) - accepted = [ - c - for c in ranked - if c["vector_score"] >= self._min_vector - or c["text_score"] >= self._min_text + r for r in text_results if r.get("text_score", 0.0) >= self._min_text ] if not accepted: - best = ranked[0] + best_score = max(r.get("text_score", 0.0) for r in text_results) self._store.log( query_root_cause=root_cause, current_ticket_id=current_ticket_id, current_day=current_day, matched_event=None, - text_score=best["text_score"], - vector_score=best["vector_score"], - fused_score=best["fused_score"], - rrf_score=best["rrf_score"], - fusion_strategy=fusion_strategy, + text_score=best_score, confidence="rejected", - candidates_evaluated=len(candidates), - threshold_gate=self._threshold_gate(), ) logger.debug( - f"[causal_chain] No prior incident — " - f"best vector={best['vector_score']:.3f}, " - f"text={best['text_score']:.3f}" + f"[causal_chain] No prior incident -- best text={best_score:.3f} " + f"(threshold={self._min_text})" ) return None - best = min(accepted, key=lambda c: c["event"].day) + best_result = min(accepted, key=lambda r: SimEvent.from_dict(r).day) + best_event = SimEvent.from_dict(best_result) + best_score = best_result.get("text_score", 0.0) - confidence = ( - "high" - if ( - best["vector_score"] >= self._min_vector - and best["text_score"] >= self._min_text - ) - else "low" - ) + high_threshold = self._min_text * self._HIGH_CONFIDENCE_MULTIPLIER + confidence = "high" if best_score >= high_threshold else "low" self._store.log( query_root_cause=root_cause, current_ticket_id=current_ticket_id, current_day=current_day, - matched_event=best["event"], - text_score=best["text_score"], - vector_score=best["vector_score"], - fused_score=best["fused_score"], - rrf_score=best["rrf_score"], - fusion_strategy=fusion_strategy, + matched_event=best_event, + text_score=best_score, confidence=confidence, - candidates_evaluated=len(candidates), - threshold_gate=self._threshold_gate(), ) logger.info( f"[causal_chain] Recurrence matched ({confidence}): " - f"{best['event'].artifact_ids.get('jira', '?')} " - f"(vector={best['vector_score']:.3f}, " - f"text={best['text_score']:.3f}, " - f"gap={current_day - best['event'].day}d)" + f"{best_event.artifact_ids.get('jira', '?')} " + f"(text={best_score:.3f}, " + f"gap={current_day - best_event.day}d)" ) - return best["event"] + return best_event def find_postmortem_for_ticket(self, ticket_id: str) -> Optional[str]: """ @@ -422,7 +305,6 @@ def get_causal_chain(self, artifact_id: str) -> List[SimEvent]: for parent in event.facts.get("causal_chain", []): if parent not in visited: queue.append(parent) - prior = event.facts.get("recurrence_of") if prior and prior not in visited: queue.append(prior) @@ -440,7 +322,7 @@ def get_causal_chain(self, artifact_id: str) -> List[SimEvent]: def get_recurrence_history(self, ticket_id: str) -> List[SimEvent]: """ Returns all SimEvents that reference ticket_id as a recurrence_of - parent — i.e. every time this class of problem has recurred. + parent -- every time this class of problem has recurred. """ return [ e @@ -450,7 +332,16 @@ def get_recurrence_history(self, ticket_id: str) -> List[SimEvent]: ] def _text_search(self, root_cause: str, current_day: int) -> List[Dict[str, Any]]: - """MongoDB $text search — returns results with normalised scores.""" + """ + MongoDB $text search over the events collection. + + Scores are normalised against the maximum raw score in the result set + using log1p scaling so the distribution is comparable across queries + of different lengths. Results are filtered to incident event types only + and scoped to days prior to the current incident. + """ + import math + try: results = list( self._mem._events.find( @@ -474,8 +365,6 @@ def _text_search(self, root_cause: str, current_day: int) -> List[Dict[str, Any] raw_scores = [r.get("score", 0.0) for r in results] max_score = max(raw_scores) if raw_scores else 1.0 - import math - normalised = [] for r, raw in zip(results, raw_scores): norm_score = ( @@ -486,104 +375,22 @@ def _text_search(self, root_cause: str, current_day: int) -> List[Dict[str, Any] return normalised - def _vector_search( - self, root_cause: str, current_day: int - ) -> List[Tuple[SimEvent, float]]: + def _ensure_text_index(self) -> None: """ - Vector search over the events collection scoped to incident types. - Uses Memory.search_events() which runs $vectorSearch on _events. + Belt-and-suspenders fallback. memory._init_text_indexes() creates the + $text index at startup. This guard catches cases where the Atlas Search + index creation is delayed or the local MongoDB instance is running + without Atlas Search support. """ - try: - return self._mem.search_events( - query=root_cause, - event_types=["incident_opened", "incident_resolved"], - n=_RETRIEVAL_LIMIT, - as_of_day=current_day - 1, - ) - except Exception as e: - logger.warning(f"[causal_chain] Vector search failed: {e}") - return [] - - def _ensure_text_index(self) -> None: - """Create the text index on events if it doesn't already exist.""" try: existing = self._mem._events.index_information() if not any("text" in str(v.get("key")) for v in existing.values()): self._mem._events.create_index( [("facts.root_cause", "text"), ("summary", "text")], - name="event_text_search", + name="event_text_search_legacy", ) - logger.info("[causal_chain] Created text index on events collection") + logger.info("[causal_chain] Created $text index on events collection.") except Exception as e: - logger.warning(f"[causal_chain] Could not create text index: {e}") - - @staticmethod - def _empty_candidate(event: SimEvent) -> Dict[str, Any]: - return { - "event": event, - "text_score": 0.0, - "vector_score": 0.0, - "text_rrf": 0.0, - "vector_rrf": 0.0, - "fused_score": 0.0, - "rrf_score": 0.0, - } - - def _threshold_gate(self) -> Dict[str, float]: - return { - "min_vector": self._min_vector, - "min_text": self._min_text, - } - - -def search_events( - mem: Memory, - query: str, - event_types: Optional[List[str]] = None, - n: int = 10, - as_of_day: Optional[int] = None, -) -> List[Tuple[SimEvent, float]]: - """ - Vector search over the events collection. - Returns (SimEvent, cosine_score) pairs sorted by descending relevance. - - Add this as a method on Memory: - Memory.search_events = lambda self, **kw: search_events(self, **kw) - Or copy the body directly into the Memory class. - """ - query_vector = mem._embedder.embed(query) - if not query_vector: - return [] - - pipeline_filter: Dict[str, Any] = {} - if event_types: - pipeline_filter["type"] = {"$in": event_types} - if as_of_day is not None: - pipeline_filter["day"] = {"$lte": as_of_day} - - pipeline: List[Dict] = [ - { - "$vectorSearch": { - "index": "vector_index", - "path": "embedding", - "queryVector": query_vector, - "numCandidates": n * 10, - "limit": n, - } - }, - {"$addFields": {"vector_score": {"$meta": "vectorSearchScore"}}}, - {"$project": {"_id": 0, "embedding": 0}}, - ] - - if pipeline_filter: - pipeline[0]["$vectorSearch"]["filter"] = pipeline_filter - - try: - results = list(mem._events.aggregate(pipeline)) - return [ - (SimEvent.from_dict(r), round(r.get("vector_score", 0.0), 4)) - for r in results - ] - except Exception as e: - logger.warning(f"[causal_chain] Event vector search failed: {e}") - return [] + logger.warning( + f"[causal_chain] Could not create $text index on events: {e}" + ) diff --git a/src/config_loader.py b/src/config_loader.py index 341a8b5..5b818e0 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -7,6 +7,8 @@ from __future__ import annotations +from dotenv import load_dotenv + from pathlib import Path from typing import Any, Dict @@ -16,6 +18,8 @@ SRC_DIR = Path(__file__).resolve().parent PROJECT_ROOT = SRC_DIR.parent +load_dotenv(PROJECT_ROOT / ".env") + CONFIG_PATH = PROJECT_ROOT / "config" / "config.yaml" EXPORT_DIR = PROJECT_ROOT / "export" diff --git a/src/confluence_writer.py b/src/confluence_writer.py index e4b1202..65e5f1c 100644 --- a/src/confluence_writer.py +++ b/src/confluence_writer.py @@ -1,39 +1,24 @@ """ confluence_writer.py -===================== Single source of truth for all Confluence page generation in OrgForge. -Every path that produces a Confluence artifact — genesis, postmortems, -design doc stubs, ad-hoc pages — runs through this module. - -Responsibilities: - - ID allocation (Python owns the namespace, never the LLM) - - Single-page LLM generation (one task per page, no PAGE BREAK parsing) - - Reference injection (LLM is told which pages already exist) - - Reference validation + broken-ref stripping (via ArtifactRegistry) - - Chunking of long content into focused child pages - - Embedding and SimEvent logging - -Callers (flow.py, normal_day.py) import ConfluenceWriter and call the -appropriate method. They no longer manage conf_id allocation or embedding -directly for Confluence artifacts. - -Usage: - from confluence_writer import ConfluenceWriter - - writer = ConfluenceWriter( - mem=self._mem, - registry=self._registry, - state=self.state, - config=CONFIG, - worker_llm=WORKER_MODEL, - planner_llm=PLANNER_MODEL, - clock=self._clock, - lifecycle=self._lifecycle, - persona_helper=persona_backstory, - graph_dynamics=self.graph_dynamics, - base_export_dir=BASE, - ) +Every path that produces a Confluence artifact (genesis, postmortems, +design doc stubs, ad-hoc pages) runs through this module. + +Knowledge gap detection is fully deterministic and engine-controlled: + - _compute_domain_fit() checks the author's expertise against orphaned + domains in the registry using system_tags and documentation_coverage + thresholds. No LLM involvement. + - scan_for_knowledge_gaps() (on org_lifecycle.py) uses BM25 text search + over persona_skill artifacts cross-referenced with the DomainRegistry. + No LLM involvement. + +The LLM's only job is to produce prose and alias vocabulary. It never +assesses its own knowledge gaps, domain fit, or confidence. + +Alias vocabulary is emitted by the LLM and stored as a List[str] of domain +terms. Each term is indexed by Atlas Search for BM25 retrieval so vocabulary +captured at write time grows with each artifact. """ from __future__ import annotations @@ -70,6 +55,27 @@ "QA_Support": "QA", } +_ALIAS_INSTRUCTION = ( + "\n\n### ALIAS VOCABULARY\n" + "List 4-10 short lowercase domain terms that someone searching for this \n" + "page's topic would use (system names, component names, protocol names,\n" + "abbreviated names. NOT generic words like 'overview' or 'documentation').\n" +) + +_ALIAS_JSON_FIELDS = ' "aliases": ["string"]\n' + + +def _extract_aliases(parsed: dict) -> Optional[List[str]]: + """ + Pull the aliases array from a parsed LLM JSON response. + Returns a list of lowercase terms, or None if absent/malformed. + """ + raw = parsed.get("aliases") + if not isinstance(raw, list): + return None + clean_terms = [t.lower().strip() for t in raw if isinstance(t, str) and t.strip()] + return clean_terms if clean_terms else None + class ConfluenceWriter: def __init__( @@ -117,26 +123,9 @@ def write_genesis_batch( ) -> List[str]: """ Generate *count* independent genesis Confluence pages for a given prefix. - - Python allocates all IDs upfront. Each page is generated in a separate - LLM call so max_tokens truncation only ever affects one page, not the - whole batch. Later pages in the batch receive the IDs of earlier ones as - allowed references so cross-links are always resolvable. - - Args: - prefix: ID namespace, e.g. "ENG" or "MKT". - count: Number of pages to generate. - prompt_tpl: Single-page prompt template. Available placeholders: - {id}, {company}, {industry}, {legacy_system}, - {project_name}, {author}, {related_pages} - author: Author of the page. - extra_vars: Any additional {placeholder} → value substitutions. - subdir: Export subdirectory under confluence/. - - Returns: - List of registered conf_ids in generation order. + Each page is generated in a separate LLM call. The alias vocabulary is + extracted from the same JSON output as the page content. """ - genesis_time = self._clock.now("system").isoformat() queue = [self._registry.next_id(prefix) for _ in range(count)] registered_ids: List[str] = [] @@ -172,41 +161,57 @@ def write_genesis_batch( historian = make_agent( role=f"{author}, {prefix} Department", - goal="Write one authentic internal Confluence page as yourself. Write with real insider detail.", + goal="Write one authentic internal Confluence page as yourself.", backstory=self._persona(author, mem=self._mem, graph_dynamics=self._gd), llm=self._planner, ) task = Task( - description=prompt, - expected_output=( - f"A single Markdown Confluence page with ID {conf_id}. " - f"No separators. No preamble. " - f"Do not include a main # title or a metadata block at the top. " - f"Start directly with the first paragraph or ## section." + description=( + f"{prompt}\n\n" + f"{_ALIAS_INSTRUCTION}" + f"\nRespond ONLY with valid JSON:\n" + f"{{\n" + f' "markdown_doc": "full Markdown page content, no # title",\n' + f"{_ALIAS_JSON_FIELDS}" + f"}}" ), + expected_output=(f"Valid JSON with markdown_doc and aliases keys."), agent=historian, ) - raw = str( + + raw_output = str( Crew(agents=[historian], tasks=[task], verbose=False).kickoff() ).strip() + clean = raw_output.replace("```json", "").replace("```", "").strip() + content = raw_output + aliases: Optional[List[str]] = None + + try: + parsed = json.loads(clean) + content = parsed.get("markdown_doc", raw_output) + aliases = _extract_aliases(parsed) + except (json.JSONDecodeError, ValueError): + content = raw_output + resolved_tags = tags or ["genesis", "confluence"] conf_ids = self._finalize_page( - raw_content=raw, + raw_content=content, conf_id=conf_id, - title=self._extract_title(raw, conf_id), + title=self._extract_title(content, conf_id), author=author, date_str=str(self._state.current_date.date()), timestamp=genesis_time, subdir=subdir, tags=resolved_tags, facts={"phase": "genesis"}, + aliases=aliases, ) registered_ids.extend(conf_ids) logger.info( - f"[confluence] ✓ Genesis batch complete ({prefix}): " + f"[confluence] Genesis batch complete ({prefix}): " f"{len(registered_ids)} page(s) registered." ) return registered_ids @@ -217,36 +222,12 @@ def write_genesis_batches_parallel( ) -> Dict[str, List[str]]: """ Run multiple independent genesis batches concurrently. - - Each batch is a dict with the same kwargs as write_genesis_batch(): - prefix, count, prompt_tpl, author, extra_vars, subdir, tags - - Pages WITHIN a batch remain sequential (each page references prior - pages in the same batch via related_pages — that dependency is - load-bearing and cannot be parallelised). - - Pages ACROSS batches (e.g. ENG vs MKT) are completely independent - and safe to run in parallel. - - Args: - batches: list of kwarg dicts, one per independent batch. - - Returns: - Dict mapping prefix → list of registered conf_ids. - - Example: - results = writer.write_genesis_batches_parallel([ - {"prefix": "ENG", "count": 3, "prompt_tpl": ..., "author": eng_member, - "subdir": "archives", "extra_vars": {"tech_stack": tech_context}}, - {"prefix": "MKT", "count": 2, "prompt_tpl": ..., "author": sale_member, - "subdir": "archives", "tags": ["genesis"]}, - ]) + Pages within a batch remain sequential; batches across prefixes are parallel. """ prefixes = [b["prefix"] for b in batches] if len(prefixes) != len(set(prefixes)): raise ValueError( - f"[confluence] Duplicate prefixes in parallel genesis batches: {prefixes}. " - f"Each batch must have a unique prefix." + f"[confluence] Duplicate prefixes in parallel genesis batches: {prefixes}." ) results: Dict[str, List[str]] = {} lock = threading.Lock() @@ -263,12 +244,12 @@ def write_genesis_batches_parallel( with lock: results[prefix] = ids logger.info( - f"[confluence] ✓ Parallel genesis batch done ({prefix}): " + f"[confluence] Parallel genesis batch done ({prefix}): " f"{len(ids)} page(s)" ) except Exception as e: logger.error( - f"[confluence] ✗ Parallel genesis batch failed ({prefix}): {e}" + f"[confluence] Parallel genesis batch failed ({prefix}): {e}" ) with lock: results[prefix] = [] @@ -286,8 +267,7 @@ def write_postmortem( ) -> tuple: """ Generate a postmortem Confluence page for a resolved incident. - - Returns the registered conf_id. + The LLM emits alias vocabulary alongside Markdown in a single JSON response. """ conf_id = self._registry.next_id("ENG") date_str = str(self._state.current_date.date()) @@ -314,7 +294,7 @@ def write_postmortem( f" - Code review / PR: {eng_peer}\n" f" - Test coverage / QA: {_qa_lead}\n" f" - Infra / alerting: {_infra_lead}\n" - f"Do NOT use role labels like 'backend lead' or 'qa lead' — use the names above." + f"Do NOT use role labels. Use the names above." ) writer = make_agent( @@ -325,33 +305,52 @@ def write_postmortem( ) task = Task( description=( - f"Write a Confluence postmortem page with ID {conf_id} " - f"for incident {incident_id}.\n" + f"Write a postmortem for incident {incident_id}.\n" f"Title: Postmortem: {incident_title}\n" f"Root Cause: {root_cause}\n" f"Duration: {days_active} days.\n" f"You may reference these existing pages if relevant:\n{related}\n" - f"{_action_owners}\n" - f"Format as Markdown. Do NOT write a main # title — start directly with ## Executive Summary. " + f"{_action_owners}\n\n" + f"Format as Markdown. Do NOT write a main # title. " + f"Start directly with ## Executive Summary. " f"Include: Executive Summary, Timeline, Root Cause, Impact, " - f"What Went Wrong, What Went Right, Action Items." + f"What Went Wrong, What Went Right, Action Items.\n\n" + f"{_ALIAS_INSTRUCTION}" + f"\nRespond ONLY with valid JSON:\n" + f"{{\n" + f' "markdown_doc": "full postmortem Markdown, no # title",\n' + f"{_ALIAS_JSON_FIELDS}" + f"}}" ), - expected_output=f"A single Markdown postmortem page with ID {conf_id}.", + expected_output="Valid JSON with markdown_doc and aliases keys.", agent=writer, ) - raw = str(Crew(agents=[writer], tasks=[task], verbose=False).kickoff()).strip() + raw_output = str( + Crew(agents=[writer], tasks=[task], verbose=False).kickoff() + ).strip() + + clean = raw_output.replace("```json", "").replace("```", "").strip() + content = raw_output + aliases: Optional[List[str]] = None + try: + parsed = json.loads(clean) + content = parsed.get("markdown_doc", raw_output) + aliases = _extract_aliases(parsed) + except (json.JSONDecodeError, ValueError): + content = raw_output self._lifecycle.scan_for_knowledge_gaps( - text=raw, + text=content, triggered_by=conf_id, day=self._state.day, date_str=date_str, state=self._state, timestamp=timestamp, + author=on_call, ) conf_ids = self._finalize_page( - raw_content=raw, + raw_content=content, conf_id=conf_id, title=f"Postmortem: {incident_title}", author=on_call, @@ -361,16 +360,13 @@ def write_postmortem( tags=["postmortem", "confluence"], facts={"root_cause": root_cause, "incident_id": incident_id}, extra_artifact_ids={"jira": incident_id}, + aliases=aliases, ) - logger.info(f" [green]📄 Postmortem:[/green] {conf_ids[0]}") + logger.info(f" [green]Postmortem:[/green] {conf_ids[0]}") for inc in self._state.active_incidents: if inc.ticket_id == incident_id and getattr(inc, "causal_chain", None): inc.causal_chain.append(conf_ids[0]) - logger.info( - f" [dim]🔗 Postmortem {conf_ids[0]} appended to " - f"{incident_id} causal chain[/dim]" - ) break return conf_ids[0], timestamp @@ -385,47 +381,36 @@ def write_design_doc( ) -> Optional[str]: """ Generate a design doc Confluence page from a Slack discussion. - Also spawns 1 JIRA ticket from the action items in the chat. - Returns the registered conf_id, or None on failure. + The LLM produces prose and a self_audit block containing raw + observations only. All gap classification and domain_fit scoring + is computed deterministically by the engine from those observations, + consistent with the physics-cognition boundary. """ - conf_id = self._registry.next_id("ENG") write_delay_hours = random.uniform(0.5, 1.5) artifact_time, _ = self._clock.advance_actor(author, hours=write_delay_hours) timestamp = artifact_time.isoformat() chat_log = "\n".join(f"{m['user']}: {m['text']}" for m in slack_transcript) - ctx = self._mem.recall_with_rewrite(raw_query=topic, n=3, as_of_time=timestamp) - related = self._registry.related_context(topic=topic, n=3) + backstory = persona_utils.get_voice_card( - author, "design", mem=self._mem, graph_dynamics=self._gd + author, "design", mem=self._mem, graph_dynamics=self._gd, include_expertise=False ) - persona = self._config.get("personas", {}).get(author, {}) - expertise_list = persona.get("expertise", ["general tasks"]) - expertise_str = ", ".join(str(e) for e in expertise_list[:5]) - author_dept = next( - (d for d, members in self._org_chart.items() if author in members), - "Unknown", + related_pages = self._mem.search_artifacts_text( + query=topic, + n=5, + type_filter="confluence", + as_of_time=timestamp, ) + related = "\n".join( + f"- {r['id']}: {r['title']}" + for r in related_pages + ) or "None yet." - orphaned_domain_context = "" - all_domains = list( - self._mem._db["domain_registry"].find({"primary_owner": None}) - ) - for rec in all_domains: - tags = rec.get("system_tags", []) - topic_lower = topic.lower() - if any(tag in topic_lower for tag in tags): - pct = int(rec.get("documentation_coverage", 0) * 100) - known_by = rec.get("known_by", []) - orphaned_domain_context += ( - f"\n⚠ '{rec['domain']}' is an orphaned domain: " - f"former owner={rec.get('former_owner', 'unknown')}, " - f"documentation={pct}%, " - f"partial knowledge held by: {known_by or 'nobody'}." - ) + expertise_tokens = self._mem.get_author_domain_tokens(author) + expertise_str = ", ".join(sorted(expertise_tokens)) agent = make_agent( role="Technical Lead", @@ -436,48 +421,31 @@ def write_design_doc( task = Task( description=( f"You just had this Slack discussion about '{topic}':\n\n{chat_log}\n\n" - f"Background context: {ctx}\n" f"Existing pages you may reference:\n{related}\n\n" f"Write a design doc Confluence page with ID {conf_id}.\n" f"Also extract 1 concrete next step as a JIRA ticket.\n\n" - + ( - f"DOMAIN CONTEXT:{orphaned_domain_context}\n\n" - if orphaned_domain_context - else "" - ) - + "### SELF-AUDIT (fill metadata objectively, not in character)\n" + f"\n\n### SELF-AUDIT\n" f"Your expertise on record: [{expertise_str}]\n" - f"Your department: {author_dept}\n" - "Compare every topic in your doc against that expertise list.\n" - "If the doc discusses areas NOT in that list, name them in " - "'topics_beyond_author_expertise'.\n" - "If you had to guess, hedge, or hand-wave on any claim, list it " - "in 'hedged_claims'.\n" - "If you deferred or left incomplete any section you know should " - "exist, list it in 'deferred_or_incomplete'.\n\n" - "Use these criteria:\n" - " author_domain_fit:\n" - " 'high' — doc demonstrates fluency: correct abstractions, aware of edge cases\n" - " 'medium' — doc is functional but shows shallow understanding or minor missteps\n" - " 'low' — doc shows clear unfamiliarity: wrong patterns, missing fundamentals\n\n" - " gap_classification:\n" - f" 'none' — {author}'s expertise aligns with all domains in this doc\n" - f" 'possible' — doc touches 1-2 domains outside {author}'s expertise but content looks adequate\n" - f" 'likely' — doc touches domains outside {author}'s expertise AND the content shows it\n\n" + f"After writing the doc, fill the self_audit block objectively, not in character.\n" + f" topics_in_doc: every distinct technical domain, system, or component this doc discusses.\n" + f" topics_outside_my_expertise: copy terms from topics_in_doc only — " + f"do not add new terms — where the topic is NOT in your expertise list above.\n" + f" claims_i_approximated: specific sentences or values where you inferred, " + f"generalized, or were uncertain rather than stating a known fact.\n" + f" sections_i_left_thin: section headers (## only) where you wrote less " + f"than the section warrants because you lacked detail.\n\n" f"Respond ONLY with valid JSON:\n" f"{{\n" - f' "markdown_doc": "full Markdown, no # title, start with ' - f'## Problem Statement",\n' + f' "markdown_doc": "full Markdown, no # title, start with ## Problem Statement",\n' f' "new_tickets": [\n' - f' {{"title": "string", "assignee": "{author}", ' - f'"story_points": 1|2|3|5|8}}\n' + f' {{"title": "string", "assignee": "{author}", "story_points": 1}}\n' f" ],\n" - f' "metadata": {{\n' - f' "author_domain_fit": "low | medium | high",\n' - f' "gap_classification": "none | possible | likely",\n' - f' "topics_beyond_author_expertise": ["string"],\n' - f' "hedged_claims": ["string"],\n' - f' "deferred_or_incomplete": ["string"]\n' + f"{_ALIAS_JSON_FIELDS}" + f' "self_audit": {{\n' + f' "topics_in_doc": ["string"],\n' + f' "topics_outside_my_expertise": ["subset of topics_in_doc only"],\n' + f' "claims_i_approximated": ["string"],\n' + f' "sections_i_left_thin": ["string"]\n' f" }}\n" f"}}" ), @@ -492,30 +460,35 @@ def write_design_doc( brace_end = clean.rindex("}") + 1 clean = clean[brace_start:brace_end] except ValueError: - logger.warning( - f"[confluence] No JSON object found in design doc response — " - f"raw output: {clean[:200]!r}" - ) clean = "{}" try: parsed = json.loads(clean) content = parsed.get("markdown_doc", "Draft pending.") new_tickets = parsed.get("new_tickets", []) - metadata = parsed.get("metadata", {}) + aliases = _extract_aliases(parsed) + audit = parsed.get("self_audit", {}) except json.JSONDecodeError as e: - logger.warning( - f"[confluence] JSON parse failed for design doc: {e} — " - f"raw JSON attempt: {clean[:200]!r}" - ) + logger.warning(f"[confluence] JSON parse failed for design doc: {e}") content = raw new_tickets = [] - metadata = {} + aliases = None + audit = {} + + topics_in_doc = set(audit.get("topics_in_doc", [])) + topics_outside = [ + t for t in audit.get("topics_outside_my_expertise", []) + if t in topics_in_doc and t not in expertise_tokens + ] + claims_approximated = audit.get("claims_i_approximated", []) + sections_thin = audit.get("sections_i_left_thin", []) + + conf_ids = self._finalize_page( raw_content=content, conf_id=conf_id, - title=f"Design: {topic[:70]}", + title=f"Design: {topic[:80]}", author=author, date_str=date_str, timestamp=timestamp, @@ -523,6 +496,7 @@ def write_design_doc( tags=["confluence", "design_doc"], facts={"title": f"Design: {topic[:80]}", "type": "design_doc"}, skip_event=True, + aliases=aliases, ) _updated_domains = [ @@ -532,60 +506,18 @@ def write_design_doc( ) ] - domain_fit = metadata.get("author_domain_fit", "high") - gap_class = metadata.get("gap_classification", "none") - beyond_expertise = metadata.get("topics_beyond_author_expertise", []) - hedged = metadata.get("hedged_claims", []) - deferred = metadata.get("deferred_or_incomplete", []) - - gap_detected = ( - domain_fit == "low" - or gap_class == "likely" - or (gap_class == "possible" and len(beyond_expertise) > 0) + self._lifecycle.scan_for_knowledge_gaps( + text=content, + triggered_by=conf_id, + day=self._state.day, + date_str=date_str, + state=self._state, + timestamp=timestamp, + author=author, + topics_outside_expertise=topics_outside, + claims_approximated=claims_approximated, ) - if gap_detected: - self._mem.log_event( - SimEvent( - type="knowledge_gap_detected", - timestamp=timestamp, - day=self._state.day, - date=date_str, - actors=[author], - artifact_ids={"confluence": conf_id}, - facts={ - "detection_method": "author_self_audit", - "topic": topic, - "author_domain_fit": domain_fit, - "author_expertise": expertise_list, - "gap_classification": gap_class, - "topics_beyond_expertise": beyond_expertise, - "hedged_claims": hedged, - "deferred_sections": deferred, - }, - summary=( - f"Knowledge gap detected in {conf_id}: " - f"{author} (expertise: {expertise_str}) wrote about '{topic}' " - f"with fit={domain_fit}, gap={gap_class}" - ), - tags=["knowledge_gap", "confluence", "design_doc"], - ) - ) - - if beyond_expertise: - targeted_text = ". ".join(beyond_expertise) - if hedged: - targeted_text += ". " + ". ".join(hedged) - - self._lifecycle.scan_for_knowledge_gaps( - text=targeted_text, - triggered_by=conf_id, - day=self._state.day, - date_str=date_str, - state=self._state, - timestamp=timestamp, - ) - created_ticket_ids = self._spawn_tickets( new_tickets, author, participants, date_str, timestamp ) @@ -610,26 +542,25 @@ def write_design_doc( "type": "design_doc", "spawned_tickets": created_ticket_ids, "causal_chain": chain.snapshot(), - "author_domain_fit": metadata.get("author_domain_fit", "high"), - "gap_classification": metadata.get("gap_classification", "none"), + "author_domain_fit": domain_fit, + "gap_classification": gap_classification, + "topics_outside_author_expertise": topics_outside, + "claims_approximated": claims_approximated, + "sections_left_thin": sections_thin, "domains_updated": _updated_domains, }, summary=( f"{author} created {conf_ids[0]} and spawned " f"{len(created_ticket_ids)} ticket(s): {', '.join(created_ticket_ids)}" ), - tags=[ - "confluence", - "design_doc", - "jira", - "causal_chain", - ], + tags=["confluence", "design_doc", "jira", "causal_chain"], ) ) logger.info( - f" [dim]📄 Design doc: {conf_ids[0]} " - f"(spawned {len(created_ticket_ids)} ticket(s))[/dim]" + f" [dim]Design doc: {conf_ids[0]} " + f"(spawned {len(created_ticket_ids)} ticket(s), " + f"domain_fit={domain_fit}, gap={gap_classification})[/dim]" ) return conf_ids[0] @@ -637,16 +568,10 @@ def write_adhoc_page( self, author: Optional[str] = None, backstory: Optional[str] = None, - ) -> None: + ) -> Optional[tuple]: """ Generate a character-accurate ad-hoc Confluence page. - - Topic and ID prefix are derived from the author's persona expertise via - a fast LLM call — no hardcoded topic lists required. The author is drawn - from state.daily_active_actors so every page is organically tied to - someone who was actually working today. - - Falls back to a random org member only if no active actors exist yet. + The writer task emits alias vocabulary alongside the page content. """ active_today: List[str] = list( dict.fromkeys(getattr(self._state, "daily_active_actors", [])) @@ -703,9 +628,9 @@ def write_adhoc_page( expertise_str = ", ".join(expertise_list) seed_query = f"{resolved_author} {expertise_str} {daily_theme}" - topic_ctx = self._mem.recall_with_rewrite( - raw_query=seed_query, - n=3, + topic_ctx = self._mem.context_for_prompt( + seed_query, + n=5, as_of_time=self._clock.now(resolved_author).isoformat(), ) backstory = persona_utils.get_voice_card( @@ -726,12 +651,12 @@ def write_adhoc_page( f"EXISTING DOCUMENTATION (Do NOT duplicate or overlap significantly):\n" f"{history_str}\n\n" f"TASK:\n" - f"Based on your expertise ({expertise_str}), propose ONE specific Confluence page title " - f"you would plausibly write TODAY. \n\n" + f"Based on your expertise ({expertise_str}), propose ONE specific " + f"Confluence page title you would plausibly write TODAY.\n\n" f"Rules:\n" - f"- The topic MUST fall within your area of expertise ({expertise_str}). " - f"Do NOT propose engineering, infrastructure, or backend topics unless that is your expertise.\n" - f"- Find a specific 'gap'. If a topic is already documented, look for a sub-topic or angle not yet covered.\n" + f"- The topic MUST fall within your area of expertise ({expertise_str}).\n" + f"- Find a specific gap. If a topic is already documented, " + f"look for a sub-topic or angle not yet covered.\n" f"- Be specific and realistic based on the current Org Theme.\n" f"- Return ONLY the page title string. No explanation. No quotes." ), @@ -745,8 +670,6 @@ def write_adhoc_page( .strip('"') .strip("'") ) - - # Sanity-trim in case the LLM adds extra prose title = title.splitlines()[0][:120] conf_id = self._registry.next_id(prefix) @@ -777,31 +700,47 @@ def write_adhoc_page( f"Existing pages you may reference (and ONLY these):\n{related}\n\n" f"Rules:\n" f"- Use your specific technical expertise and typing style.\n" - f"- If stressed, the doc may be shorter or more blunt.\n" f"- Do not invent any CONF-* IDs not listed above.\n" - f"- Format as Markdown. Do not write a main # title or metadata block (like Author/Date) at the top.\n" - f"- Start directly with the first paragraph or ## section." + f"- Format as Markdown. Do not write a main # title or metadata block.\n" + f"- Start directly with the first paragraph or ## section.\n\n" + f"{_ALIAS_INSTRUCTION}" + f"\nRespond ONLY with valid JSON:\n" + f"{{\n" + f' "markdown_doc": "full Markdown page content",\n' + f"{_ALIAS_JSON_FIELDS}" + f"}}" ), - expected_output=f"A single Markdown Confluence page with ID {conf_id}.", + expected_output="Valid JSON with markdown_doc and aliases keys.", agent=writer_agent, ) raw = str( Crew(agents=[writer_agent], tasks=[task], verbose=False).kickoff() ).strip() - raw += self._knowledge_gap_warning(title) - # Lifecycle scan before chunking + clean = raw.replace("```json", "").replace("```", "").strip() + content = raw + aliases: Optional[List[str]] = None + try: + parsed = json.loads(clean) + content = parsed.get("markdown_doc", raw) + aliases = _extract_aliases(parsed) + except (json.JSONDecodeError, ValueError): + content = raw + self._lifecycle.scan_for_knowledge_gaps( - text=raw, + text=content, triggered_by=conf_id, day=self._state.day, date_str=date_str, state=self._state, timestamp=timestamp, + author=resolved_author, ) + content += self._knowledge_gap_warning(title) + self._finalize_page( - raw_content=raw, + raw_content=content, conf_id=conf_id, title=title, author=resolved_author, @@ -810,8 +749,11 @@ def write_adhoc_page( subdir="general", tags=["confluence", "adhoc"], facts={"title": title, "adhoc": True}, + aliases=aliases, ) + return (conf_id, resolved_author, title) + def _finalize_page( self, raw_content: str, @@ -825,17 +767,12 @@ def _finalize_page( facts: Dict, extra_artifact_ids: Optional[Dict[str, str]] = None, skip_event: bool = False, + aliases: Optional[List[str]] = None, ) -> List[str]: """ - Common finalization pipeline for every Confluence page: - 1. Strip broken cross-references - 2. Register ID (raises DuplicateArtifactError — caller handles) - 3. Chunk into child pages if content is long - 4. Save .md files, embed, log SimEvents - - Returns list of all conf_ids created (parent + children). + Common finalization pipeline for every Confluence page. + Writes aliases as a list so Atlas Search can index them for BM25 retrieval. """ - clean_content = self._registry.strip_broken_references(raw_content) pages: List[ConfluencePage] = self._registry.chunk_into_pages( @@ -850,13 +787,10 @@ def _finalize_page( created_ids: List[str] = [] for page in pages: - logger.info( - f"[finalize] embedding page.id={page.id} parent={page.parent_id or 'ROOT'} content_len={len(page.content)}" - ) try: final_content = self._registry.strip_broken_references(page.content) except Exception as e: - logger.info(f"[finalize] Caught exception {e}") + logger.warning(f"[finalize] strip_broken_references failed: {e}") final_content = page.content self._save_md(page.path, final_content) @@ -870,6 +804,8 @@ def _finalize_page( if tags and "genesis" in tags: meta["phase"] = "genesis" + page_aliases = aliases if page.parent_id is None else None + self._mem.embed_artifact( id=page.id, type="confluence", @@ -879,6 +815,7 @@ def _finalize_page( date=date_str, timestamp=timestamp, metadata=meta, + aliases=page_aliases, ) if page.parent_id is None and author and "genesis" not in (tags or []): @@ -891,18 +828,9 @@ def _finalize_page( if domains_updated: meta["domains_updated"] = domains_updated - if page.parent_id is None and author: - self._mem._db["author_expertise"].update_one( - {"author": author}, - {"$addToSet": {"topics": page.title.lower().strip()}}, - upsert=True, - ) - self._state.daily_artifacts_created += 1 - logger.debug(f"[finalize] pre-facts page.id={page.id}") page_facts = dict(facts) - page_facts.update( { "parent_id": page.parent_id or "", @@ -912,26 +840,10 @@ def _finalize_page( } ) - logger.info(f"[finalize] page facts {page_facts}") - logger.debug(f"[finalize] pre-artifact-ids page.id={page.id}") artifact_ids = {"confluence": page.id} if extra_artifact_ids: artifact_ids.update(extra_artifact_ids) - logger.debug(f"[finalize] pre-log-event page.id={page.id}") - logger.debug( - f"[finalize] SimEvent fields — " - f"type=confluence_created " - f"timestamp={timestamp} " - f"day={self._state.day} " - f"date={date_str} " - f"actors={[author]} " - f"artifact_ids={artifact_ids} " - f"facts={page_facts} " - f"summary={'Child' if page.parent_id else 'Page'} {page.id} created: {page.title} " - f"tags={tags}" - ) - if not skip_event: self._mem.log_event( SimEvent( @@ -943,19 +855,71 @@ def _finalize_page( artifact_ids=artifact_ids, facts=page_facts, summary=( - f"{'Child' if page.parent_id else 'Page'} {page.id} created: {page.title}" + f"{'Child' if page.parent_id else 'Page'} " + f"{page.id} created: {page.title}" ), tags=tags, ) ) - logger.debug(f"[finalize] post-log-event page.id={page.id}") - - logger.info(f"[confluence] _finalize_page complete: {page.id}") created_ids.append(page.id) return created_ids + def generate_tech_stack(self) -> dict: + """ + Ask the LLM to invent a plausible tech stack for this company and industry. + Persists to MongoDB immediately so every subsequent LLM call can reference it. + """ + existing = self._mem.get_tech_stack() + if existing: + logger.info("[confluence] Tech stack already exists. Skipping generation.") + return existing + + agent = make_agent( + role="Principal Engineer", + goal="Define the canonical technology stack for this company.", + backstory=( + f"You are a principal engineer at {self._company}, " + f"a {self._industry} company. You are documenting the actual " + f"technologies in use. Not aspirational, not greenfield." + ), + llm=self._planner, + ) + task = Task( + description=( + f"Define the canonical tech stack for {self._company} " + f"which {COMPANY_DESCRIPTION}\n\n" + f"The legacy system is called '{self._legacy.get('name', '')}' " + f"({self._legacy.get('description', '')}).\n\n" + f"Respond ONLY with a JSON object with these keys:\n" + f" database, backend_language, frontend_language, mobile, " + f" infra, message_queue, source_control, ci_cd, " + f" monitoring, notable_quirks\n\n" + f"Each value is a short string (1-2 sentences max). " + f"Include at least one legacy wart or technical debt item. " + f"No preamble, no markdown fences." + ), + expected_output="A single JSON object. No preamble.", + agent=agent, + ) + + raw = str(Crew(agents=[agent], tasks=[task], verbose=False).kickoff()).strip() + + try: + stack = json.loads(raw.replace("```json", "").replace("```", "").strip()) + except json.JSONDecodeError: + logger.warning( + "[confluence] Tech stack JSON parse failed. Using minimal fallback." + ) + stack = { + "notable_quirks": "Stack unknown. Legacy system predates documentation." + } + + self._mem.save_tech_stack(stack) + logger.info(f"[confluence] Tech stack established: {list(stack.keys())}") + return stack + def _spawn_tickets( self, new_tickets: List[Dict], @@ -1027,129 +991,142 @@ def _update_domain_registry_on_write( content: str, day: int, coverage_delta: float = 0.10, + known_by_tag_threshold: int = 2, ) -> List[str]: """ - After any Confluence page is finalised, check whether the page title or - content touches any registered domain in the DomainRegistry. If it does, - increment documentation_coverage by coverage_delta (default +10%) and - add the author to known_by. - - This is the recovery arc: every page written against an orphaned domain - nudges coverage upward. The planner prompt and gap detection both read - live coverage, so this produces visible narrative improvement over time. - - Matching is done against system_tags so variant spellings still resolve - (e.g. "titan" matches "TitanDB", "auth" matches "legacy auth service"). - - Returns: - List of domain names that were updated. + After any Confluence page is finalised, increment documentation_coverage + for any domain whose system_tags appear in the page content. + + known_by is only updated when the domain is clearly a primary topic: + either the domain name itself appears in the content, or at least + known_by_tag_threshold distinct system_tags match. A single incidental + tag mention (e.g. 'auth' in a page about something else) does not + qualify. """ updated: List[str] = [] - all_domains = list(self._mem._db["domain_registry"].find({})) if not all_domains: return updated - search_text = f"{title} {content[:500]}".lower() + search_text = f"{title} {content}".lower() + title_lower = title.lower() for rec in all_domains: tags = rec.get("system_tags", []) - if not any(tag in search_text for tag in tags): + matched_tags = [tag for tag in tags if tag in search_text] + if not matched_tags: continue old_coverage = rec.get("documentation_coverage", 0.0) new_coverage = min(1.0, old_coverage + coverage_delta) + + is_primary_topic = ( + rec["domain"].lower() in title_lower + or len(matched_tags) >= known_by_tag_threshold + ) + + update: dict = { + "$set": { + "documentation_coverage": round(new_coverage, 3), + "last_updated_day": day, + } + } + if is_primary_topic: + update["$addToSet"] = {"known_by": author} + self._mem._db["domain_registry"].update_one( {"_id": rec["_id"]}, - { - "$set": { - "documentation_coverage": round(new_coverage, 3), - "last_updated_day": day, - }, - "$addToSet": {"known_by": author}, - }, + update, ) updated.append(rec["domain"]) logger.info( - f" [dim]→ Domain registry: '{rec['domain']}' coverage " - f"{int(old_coverage * 100)}% → {int(new_coverage * 100)}% " - f"(author: {author})[/dim]" + f" [dim]Domain registry: '{rec['domain']}' coverage " + f"{int(old_coverage * 100)}% to {int(new_coverage * 100)}%" + f"{' (known_by: ' + author + ')' if is_primary_topic else ''}" + f" (author: {author})[/dim]" ) return updated def _pick_dept_author(self, prefix: str) -> str: - """Return a random member of the department matching prefix, fallback to any employee.""" for dept, members in self._org_chart.items(): if prefix.upper() in dept.upper() and members: return random.choice(members) - return random.choice(self._all_names) def _conf_prefix_for(self, author: str) -> str: dept = next( (d for d, members in self._org_chart.items() if author in members), - None, + "", ) return _CONF_PREFIX_MAP.get(dept, "ENG") - def generate_tech_stack(self) -> dict: - """ - Ask the LLM to invent a plausible tech stack for this company and industry. - Persists to MongoDB immediately so every subsequent LLM call can reference it. - Returns the stack as a dict. + def _compute_domain_fit(self, author: str, topic: str) -> str: """ + Deterministically compute how well the author's expertise covers the + topic, using the domain_registry's orphan status and system_tags. - existing = self._mem.get_tech_stack() - if existing: - logger.info("[confluence] Tech stack already exists — skipping generation.") - return existing + Returns "high", "medium", or "low" based on documentation_coverage + thresholds (same thresholds used in scan_for_knowledge_gaps): + - No orphaned domains touched by topic -> "high" + - Author is in known_by for all touched orphans -> "high" + - avg coverage < 0.3 -> "low" + - avg coverage < 0.6 -> "medium" + - else -> "high" - agent = make_agent( - role="Principal Engineer", - goal="Define the canonical technology stack for this company.", - backstory=( - f"You are a principal engineer at {self._company}, " - f"a {self._industry} company. You are documenting the actual " - f"technologies in use — not aspirational, not greenfield. " - f"This is a company with years of history and legacy decisions." - ), - llm=self._planner, - ) - task = Task( - description=( - f"Define the canonical tech stack for {self._company} " - f"which {COMPANY_DESCRIPTION}\n\n" - f"The legacy system is called '{self._legacy.get('name', '')}' " - f"({self._legacy.get('description', '')}).\n\n" - f"Respond ONLY with a JSON object with these keys:\n" - f" database, backend_language, frontend_language, mobile, " - f" infra, message_queue, source_control, ci_cd, " - f" monitoring, notable_quirks\n\n" - f"Each value is a short string (1-2 sentences max). " - f"Include at least one legacy wart or technical debt item. " - f"No preamble, no markdown fences." - ), - expected_output="A single JSON object. No preamble.", - agent=agent, - ) - - raw = str(Crew(agents=[agent], tasks=[task], verbose=False).kickoff()).strip() - - try: - stack = json.loads(raw.replace("```json", "").replace("```", "").strip()) - except json.JSONDecodeError: - logger.warning( - "[confluence] Tech stack JSON parse failed — using minimal fallback." - ) - stack = { - "notable_quirks": "Stack unknown — legacy system predates documentation." - } - - self._mem.save_tech_stack(stack) - logger.info(f"[confluence] ✓ Tech stack established: {list(stack.keys())}") - return stack + """ + orphaned = list(self._mem._db["domain_registry"].find({"primary_owner": None})) + topic_lower = topic.lower() + touched_orphans = [ + rec + for rec in orphaned + if any(tag in topic_lower for tag in rec.get("system_tags", [])) + ] + if not touched_orphans: + return "high" + covered = all(author in rec.get("known_by", []) for rec in touched_orphans) + if covered: + return "high" + avg_coverage = sum( + r.get("documentation_coverage", 0) for r in touched_orphans + ) / len(touched_orphans) + if avg_coverage < 0.3: + return "low" + if avg_coverage < 0.6: + return "medium" + return "high" + + def _compute_gap_classification(self, author: str, topic: str) -> str: + """ + Deterministically classify whether this topic/author combination + represents a knowledge gap, using the same thresholds as + scan_for_knowledge_gaps in org_lifecycle.py: + - live_coverage < 0.3 -> "likely" + - live_coverage < 0.6 -> "possible" + - else -> "none" + + This runs BEFORE the LLM writes anything, so the engine knows at + planning time whether this document enters a gap zone. + """ + orphaned = list(self._mem._db["domain_registry"].find({"primary_owner": None})) + topic_lower = topic.lower() + touched_orphans = [ + rec + for rec in orphaned + if any(tag in topic_lower for tag in rec.get("system_tags", [])) + ] + if not touched_orphans: + return "none" + covered = all(author in rec.get("known_by", []) for rec in touched_orphans) + if covered: + return "none" + min_coverage = min(r.get("documentation_coverage", 0) for r in touched_orphans) + if min_coverage < 0.3: + return "likely" + if min_coverage < 0.6: + return "possible" + return "none" @staticmethod def _render(template: str, vars_: Dict[str, str]) -> str: @@ -1158,7 +1135,6 @@ def _render(template: str, vars_: Dict[str, str]) -> str: return template def _render_template(self, template: str) -> str: - """Apply simulation-level placeholder substitutions.""" return ( template.replace("{legacy_system}", self._legacy.get("name", "")) .replace("{project_name}", self._legacy.get("project_name", "")) @@ -1168,9 +1144,7 @@ def _render_template(self, template: str) -> str: @staticmethod def _extract_title(content: str, fallback: str) -> str: - clean = re.sub(r"```.*?```", "", content, flags=re.DOTALL) - m = re.search(r"^#\s+(.+)", clean, re.MULTILINE) if m: return m.group(1).strip() @@ -1181,18 +1155,11 @@ def _extract_title(content: str, fallback: str) -> str: @staticmethod def _id_prefix_from_id(conf_id: str) -> str: - """Extract prefix from a conf_id like CONF-ENG-003 → ENG.""" parts = conf_id.split("-") return parts[1] if len(parts) >= 3 else "GEN" def _knowledge_gap_warning(self, topic: str) -> str: - """ - Append a knowledge-gap warning if the topic touches a registered orphaned domain. - Uses live documentation_coverage from DomainRegistry rather than the static - config value so the warning reflects any recovery that has happened since genesis. - """ topic_lower = topic.lower() - all_domains = list( self._mem._db["domain_registry"].find({"primary_owner": None}) ) @@ -1208,7 +1175,7 @@ def _knowledge_gap_warning(self, topic: str) -> str: else " No current owner." ) return ( - f"\n\n> ⚠️ **Knowledge Gap**: This area ({rec['domain']}) was owned by " + f"\n\n> **Knowledge Gap**: This area ({rec['domain']}) was owned by " f"{former}. Only ~{pct}% documented.{known_str}" ) @@ -1217,7 +1184,7 @@ def _knowledge_gap_warning(self, topic: str) -> str: hits = [k for k in emp.get("knew_about", []) if k.lower() in topic_lower] if hits: return ( - f"\n\n> ⚠️ **Knowledge Gap**: This area ({', '.join(hits)}) was owned by " + f"\n\n> **Knowledge Gap**: This area ({', '.join(hits)}) was owned by " f"{emp['name']} (ex-{emp['role']}, left {emp['left']}). " f"Only ~{int(emp.get('documented_pct', 0.2) * 100)}% documented." ) @@ -1241,14 +1208,6 @@ def _save_json(self, path: str, data: Any) -> None: def _tenure_at_date( tenure_str: str, sim_start: datetime, page_date: datetime ) -> str: - """ - Back-calculate what an employee's tenure label should read - on a historical page_date, given their tenure string at sim_start. - - "5yr" at 2026-03-02, page dated 2024-03-02 → "3yr" - "2yr" at 2026-03-02, page dated 2025-09-02 → "6mo" - "new" / unparseable → returned unchanged - """ import re from dateutil.relativedelta import relativedelta diff --git a/src/day_planner.py b/src/day_planner.py index 3ae4d06..1b04091 100644 --- a/src/day_planner.py +++ b/src/day_planner.py @@ -40,9 +40,11 @@ from ticket_assigner import TicketAssigner from external_email_ingest import ExternalEmailSignal from config_loader import ( + DEFAULT_PERSONA, LEADS, LIVE_ORG_CHART, COMPANY_DESCRIPTION, + PERSONAS, resolve_role, ) from utils.persona_utils import persona_utils @@ -367,7 +369,7 @@ def plan( raw = str(Crew(agents=[agent], tasks=[task], verbose=False).kickoff()).strip() result, raw_data = self._parse_plan( - raw, org_theme, day, date, cross_signals, sprint_context + raw, org_theme, day, date, cross_signals, sprint_context, graph_dynamics ) mem.log_dept_plan( @@ -412,6 +414,7 @@ def _parse_plan( date: str, cross_signals: List[CrossDeptSignal], sprint_context: Optional[SprintContext] = None, + graph_dynamics: Optional[GraphDynamics] = None, ) -> Tuple[DepartmentDayPlan, dict]: """ Parse the LLM JSON response into a DepartmentDayPlan. @@ -492,12 +495,19 @@ def _parse_plan( ) ] + p = PERSONAS.get(name, DEFAULT_PERSONA) + current_stress = ( + graph_dynamics._stress.get(name, p.get("stress", 50)) + if graph_dynamics + else p.get("stress", 50) + ) + eng_plans.append( EngineerDayPlan( name=name, dept=self.dept, agenda=agenda, - stress_level=0, + stress_level=current_stress, focus_note=ep.get("focus_note", ""), ) ) @@ -825,10 +835,12 @@ def coordinate( other_plans_str = "" for dept, plan in dept_plans.items(): lead_name = self._config.get("leads", {}).get(dept) - stress = state.persona_stress.get(lead_name, 50) if lead_name else 50 + lead_stress = state.persona_stress.get(lead_name, 50) if lead_name else 50 members = self._config.get("org_chart", {}).get(dept, []) other_plans_str += ( - f"- {dept}: Theme='{plan.theme}'. Lead={lead_name} (Stress: {stress}/100). " + f"- {dept}: Theme='{plan.theme}'. " + f"Lead={lead_name} (Stress: {lead_stress}/100). " + f"Team avg stress: {plan.avg_stress}/100. " f"Members: {members}. " f"Events planned: {[e.event_type for e in plan.proposed_events[:2]]}\n" ) @@ -912,7 +924,6 @@ def _format_other_plans( if dept == eng_key: continue events_str = ", ".join(e.event_type for e in plan.proposed_events[:2]) - # Extract the names of the people in this department names = ", ".join(ep.name for ep in plan.engineer_plans) lines.append( @@ -927,10 +938,8 @@ class DayPlannerOrchestrator: Called once per day from flow.py's daily_cycle(), replacing _generate_theme(). Usage in flow.py: - # In __init__: self._day_planner = DayPlannerOrchestrator(CONFIG, WORKER_MODEL, PLANNER_MODEL) - # In daily_cycle(), replacing _generate_theme(): org_plan = self._day_planner.plan( state=self.state, mem=self._mem, @@ -1289,6 +1298,12 @@ def _patch_stress_levels( for ep in plan.engineer_plans: ep.stress_level = graph_dynamics._stress.get(ep.name, 30) + if plan.engineer_plans: + plan.avg_stress = round( + sum(ep.stress_level for ep in plan.engineer_plans) + / len(plan.engineer_plans) + ) + def _recent_day_summaries(self, mem: Memory, day: int) -> List[dict]: """Last 7 day_summary facts dicts for the validator. Queries MongoDB.""" return mem.get_recent_day_summaries(current_day=day, window=7) diff --git a/src/embed_worker.py b/src/embed_worker.py deleted file mode 100644 index 08eebb4..0000000 --- a/src/embed_worker.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -embed_worker.py -=============== -Background embedding queue for OrgForge. - -Decouples artifact embedding from LLM generation so Infinity/Ollama inference -runs while the next Bedrock call is in flight, rather than blocking between -each generation step. - -Architecture ------------- -- A ThreadPoolExecutor processes embed tasks concurrently from a Queue. -- Concurrency is tuned via EMBED_WORKER_CONCURRENCY (default: 8 for Infinity, - 1 for Ollama which is serial anyway). -- The main sim loop calls enqueue() instead of mem.embed_artifact() directly. -- Before any vector search (context_for_prompt, recall, search_events) the - caller must call drain() to flush pending embeds — this ensures causal - consistency so searches never miss artifacts that were logically prior. -- At end-of-day, daily_cycle() calls drain() once before the checkpoint write. - -Usage in flow.py ----------------- - # __init__ - from embed_worker import EmbedWorker - self._embed_worker = EmbedWorker(self._mem) - self._embed_worker.start() - - # replacing _embed_and_count - def _embed_and_count(self, **kwargs): - self._embed_worker.enqueue(**kwargs) - self.state.daily_artifacts_created += 1 - - # before any vector search or at end-of-day - self._embed_worker.drain() - - # after simulation completes - self._embed_worker.stop() - -Thread safety -------------- -- Queue is thread-safe by design. -- mem.embed_artifact() writes to MongoDB via PyMongo, which is thread-safe. -- daily_artifacts_created is incremented on the main thread (in enqueue), - so counts remain accurate without locking. -- _errors is guarded by _errors_lock for concurrent appends from the pool. -""" - -from __future__ import annotations - -import logging -import os -import threading -from concurrent.futures import ThreadPoolExecutor, Future -from queue import Queue, Empty -from typing import Any, Dict, List - -logger = logging.getLogger("orgforge.embed_worker") - -_SENTINEL = None - -# How many embed HTTP calls to run in parallel. -# - Infinity: 8–16 is a good starting point on a Xeon 6975P; Infinity's -# dynamic batching coalesces concurrent requests server-side so -# you get throughput gains without hammering the network. -# - Ollama: Keep at 1 — Ollama serialises embeds internally anyway and -# parallel requests just queue up in its HTTP layer. -_DEFAULT_CONCURRENCY = int(os.environ.get("EMBED_WORKER_CONCURRENCY", "8")) - - -class EmbedWorker: - """ - Concurrent background worker that drains an embed task queue using a - thread pool. Works with both Ollama (concurrency=1) and Infinity - (concurrency=8+). - - Parameters - ---------- - mem : Memory - The shared Memory instance. _embed() is called on it from the worker - threads — PyMongo handles connection pooling safely. - concurrency : int - Number of concurrent embed calls. Set via EMBED_WORKER_CONCURRENCY - env var or passed directly. Default: 8. - maxsize : int - Maximum queue depth before enqueue() blocks the caller. Default 0 - (unbounded) is correct for OrgForge since the LLM is always slower - than embedding. - """ - - def __init__(self, mem, concurrency: int = _DEFAULT_CONCURRENCY, maxsize: int = 0): - self._mem = mem - self._concurrency = concurrency - self._queue: Queue[Dict[str, Any] | None] = Queue(maxsize=maxsize) - self._executor = ThreadPoolExecutor( - max_workers=concurrency, - thread_name_prefix="embed-pool", - ) - self._dispatcher = threading.Thread( - target=self._dispatch_loop, - name="embed-dispatcher", - daemon=True, - ) - self._futures: List[Future] = [] - self._futures_lock = threading.Lock() - self._errors: list[Exception] = [] - self._errors_lock = threading.Lock() - - def start(self) -> None: - """Start the background dispatcher thread. Call once from Flow.__init__.""" - self._dispatcher.start() - logger.info( - f"[embed_worker] Background embed queue started " - f"(concurrency={self._concurrency})." - ) - - def stop(self) -> None: - """ - Flush remaining tasks then shut down cleanly. - Call after the simulation loop exits. - """ - self.drain() - self._queue.put(_SENTINEL) - self._dispatcher.join(timeout=60) - self._executor.shutdown(wait=True, cancel_futures=False) - if self._dispatcher.is_alive(): - logger.warning("[embed_worker] Dispatcher thread did not exit within 60s.") - else: - logger.info("[embed_worker] Background embed queue stopped cleanly.") - - def enqueue(self, **kwargs) -> None: - """ - Non-blocking enqueue of an embed task. - - Accepts the same keyword arguments as Memory.embed_artifact(): - id, type, title, content, day, date, timestamp, metadata - Plus the internal routing key: - _target: "artifacts" (default) or "events" - """ - self._queue.put(kwargs) - - def drain(self) -> None: - """ - Block until all currently queued and in-flight embed tasks are complete. - - Call this: - - Before any vector search (recall, context_for_prompt, search_events) - - At end-of-day before the checkpoint write - - Before the simulation's final report - - After drain() returns, MongoDB is consistent with all enqueued artifacts. - Any errors accumulated during background processing are logged here. - """ - # Wait for queue to be fully dispatched to the thread pool - self._queue.join() - - # Wait for all in-flight futures (tasks running in the pool right now) - with self._futures_lock: - futures_snapshot = list(self._futures) - - for fut in futures_snapshot: - try: - fut.result() - except Exception as exc: - with self._errors_lock: - self._errors.append(exc) - - with self._futures_lock: - self._futures.clear() - - if self._errors: - with self._errors_lock: - for err in self._errors: - logger.error(f"[embed_worker] Background embed error: {err}") - self._errors.clear() - - def _dispatch_loop(self) -> None: - """ - Dispatcher thread body. Pulls tasks off the queue and submits them to - the thread pool. Runs until it receives the sentinel value. - """ - while True: - try: - task = self._queue.get(block=True, timeout=5) - except Empty: - continue - - if task is _SENTINEL: - self._queue.task_done() - break - - future = self._executor.submit(self._process_task, task) - with self._futures_lock: - # Prune completed futures to avoid unbounded list growth - self._futures = [f for f in self._futures if not f.done()] - self._futures.append(future) - - # Mark the queue slot as done immediately after dispatch — - # drain() waits on futures directly for in-flight completion. - self._queue.task_done() - - def _process_task(self, task: Dict[str, Any]) -> None: - """ - Executed in a pool thread. Calls the embedder and writes to MongoDB. - This is where actual HTTP calls to Infinity/Ollama happen. - """ - try: - target = task.pop("_target", "artifacts") - - if target == "events": - text = task["content"] - vector = self._mem._embed( - text, - input_type="search_document", - caller="log_event_async", - doc_id=task["id"], - doc_type=task["type"], - ) - if vector: - self._mem._events.update_one( - {"_id": task["id"]}, - {"$set": {"embedding": vector}}, - ) - else: - embed_text = task["content"] - vector = self._mem._embed( - embed_text, - input_type="search_document", - caller="embed_artifact_async", - doc_id=task["id"], - doc_type=task["type"], - ) - if vector: - self._mem._artifacts.update_one( - {"_id": task["id"]}, - {"$set": {"embedding": vector}}, - ) - except Exception as exc: - with self._errors_lock: - self._errors.append(exc) - logger.warning( - f"[embed_worker] embed failed for id={task.get('id')}: {exc}" - ) diff --git a/src/external_email_ingest.py b/src/external_email_ingest.py index e43e969..288772e 100644 --- a/src/external_email_ingest.py +++ b/src/external_email_ingest.py @@ -33,10 +33,10 @@ _PROB_INCIDENT = 0.70 _PROB_INCIDENT_QUIET = 0.10 _HEALTH_THRESHOLD = 60 -_PROB_EMAIL_DROPPED = 0.15 # customer emails dropped with no action -_PROB_CUSTOMER_JIRA = 0.55 # high-priority customer complaint → JIRA -_PROB_VENDOR_JIRA = 0.45 # vendor alert → JIRA task -_HR_EMAIL_WINDOW = (1, 3) # days before hire arrival to send email +_PROB_EMAIL_DROPPED = 0.15 +_PROB_CUSTOMER_JIRA = 0.55 +_PROB_VENDOR_JIRA = 0.45 +_HR_EMAIL_WINDOW = (1, 3) _VALID_EMAIL_TYPES = frozenset( ["complaint", "question", "feature_request", "positive_feedback", "general_inquiry"] ) @@ -78,16 +78,10 @@ def _get_stage_probability(stage: str) -> int: _PROB_NON_COMPLAINT_SALES_FYI = 0.35 -# Email types that trigger a ZD ticket via handle_inbound_customer_email. -# feature_request → Slack FYI to Product only (no ticket). -# positive_feedback → no ticket. _ZD_TICKET_TYPES = frozenset(["complaint", "question", "general_inquiry"]) -# Kept for any import sites not yet updated; mirrors _ZD_TICKET_TYPES. _COMPLAINT_EMAIL_TYPES = frozenset(["complaint"]) -# Probability gate per email type — complaints always get a ticket, -# others are sampled so not every question floods the ZD queue. _ZD_TICKET_PROB: dict = { "complaint": 1.0, "question": 0.70, @@ -1101,9 +1095,6 @@ def _generate_email( ) if category == "customer": - # Customers never see our tech stack. They experience symptoms. - # symptom_context is the customer-facing description of their problem; - # trigger_context tells the LLM why this email is being sent today. symptom_hint = f"\nSITUATION: {symptom}" if symptom else "" email_type_hint = { "complaint": "You are writing to report a problem you are experiencing. Describe the business impact on your organisation. Do NOT name or guess at internal systems.", @@ -1160,7 +1151,6 @@ def _generate_email( return None else: - # Vendors: plain text, tech_ctx injected, first-person framing tech_stack = self._mem.tech_stack_for_prompt() tech_ctx = ( ( @@ -1561,7 +1551,6 @@ def generate_customer_replies(self, state) -> List[Any]: day=state.day, ) - # Embed artifact for RAG. self._mem.embed_artifact( id=embed_id, type="email", @@ -1736,7 +1725,6 @@ def _route_non_complaint_email( signal.internal_liaison, next(iter(self._leads.values())) ) - # Append pre-created ZD ticket to causal chain if present zd_ticket_id = signal.facts.get("zd_ticket_id") if zd_ticket_id: signal.causal_chain.append(zd_ticket_id) @@ -1999,9 +1987,7 @@ def _derive_customer_email_signals(self, state) -> List[dict]: sentiment = source.get("sentiment_baseline", 0.8) tone = source.get("tone", "formal") - # ── Signal 1: Active incident affecting this customer ──────────── for incident in state.active_incidents: - # Skip if already contacted proactively via _handle_external_contact if org_name in getattr(incident, "contacted_customers", []): continue if not self._gd._incident_affects_customer(incident, source): @@ -2024,7 +2010,6 @@ def _derive_customer_email_signals(self, state) -> List[dict]: break else: - # ── Signal 2: Stale deal at Negotiation/Review ─────────────── opp = None if hasattr(self._crm, "_sf_o"): opp = self._crm._sf_o.find_one( @@ -2067,7 +2052,6 @@ def _derive_customer_email_signals(self, state) -> List[dict]: ) continue - # ── Signal 3: Contract renewal within 60 days ──────────────── renewal_str = source.get("contract_renewal_date", "") if renewal_str: try: @@ -2092,7 +2076,6 @@ def _derive_customer_email_signals(self, state) -> List[dict]: except ValueError: pass - # ── Signal 4: Opp has risk notes + low sentiment ───────────── if hasattr(self._crm, "_sf_o"): risky_opp = self._crm._sf_o.find_one( { @@ -2117,11 +2100,10 @@ def _derive_customer_email_signals(self, state) -> List[dict]: ) continue - # ── Signal 5: High expansion potential + healthy system ─────── if ( source.get("expansion_potential", 0) >= 8 and state.system_health >= 80 - and random.random() < 0.25 # not every day — keep it sparse + and random.random() < 0.25 ): topic = "Exploring additional use cases and features for our team" signals.append( @@ -2134,9 +2116,6 @@ def _derive_customer_email_signals(self, state) -> List[dict]: } ) - # ── Signal 6: Chronically low sentiment — unprompted complaint ─ - # Unhappy customers complain regardless of active incidents. - # Fires independently of all other signals as a baseline floor. elif sentiment < 0.45 and random.random() < 0.15: topic = source.get("topics", ["platform reliability concerns"])[0] signals.append( diff --git a/src/flow.py b/src/flow.py index 05ee07a..bf5707e 100644 --- a/src/flow.py +++ b/src/flow.py @@ -62,7 +62,6 @@ from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer from crewai import Process, Task, Crew -from langchain_ollama import OllamaLLM from memory import Memory, SimEvent from graph_dynamics import GraphDynamics @@ -78,7 +77,7 @@ ARTIFACT_KEY_SLACK_THREAD, RecurrenceDetector, ) -from embed_worker import EmbedWorker + os.makedirs("./export", exist_ok=True) @@ -151,7 +150,6 @@ def patched_parse_native_tool_call( func_name = sanitize_tool_name( func_info.get("name", "") or tool_call.get("name", "") ) - # FIX: use None default so falsy check correctly falls through to input func_args = func_info.get("arguments") or tool_call.get("input") or {} return call_id, func_name, func_args return None @@ -169,59 +167,51 @@ def _bare_model(model_str: str) -> str: return model_str.strip() -def build_llm(model_key: str): +def build_llm(model_key: str, temperature: float = 0.3): """ - Return the correct LangChain LLM for the active quality_preset. + Return a CrewAI or Ollama LLM for the active quality_preset. - preset provider values: - "ollama" → langchain_community.llms.Ollama (local_gpu) - "bedrock" → langchain_aws.ChatBedrock (cloud — AWS Bedrock) + local_gpu → OllamaLLM (local models) + cloud → CrewAI LLM (auto-detects provider from model string) model_key: "planner" or "worker" """ - model_str = _PRESET[model_key] - model = _bare_model(model_str) - - if _PROVIDER == "bedrock": - try: - from crewai import LLM - - region = _PRESET.get( - "aws_region", os.environ.get("AWS_DEFAULT_REGION", "us-east-1") - ) + model = _bare_model(_PRESET[model_key]) - llm_args = { - "model": model, - "region_name": region, - "temperature": 0.7, - "max_tokens": 16384, - } + if _PROVIDER == "ollama": + from crewai import LLM + base_url = os.environ.get("OLLAMA_BASE_URL") or _PRESET.get( + "base_url", "http://localhost:11434" + ) + logger.info(f"[config] {model_key} → Ollama/{model} ({base_url})") + return LLM(model=model, base_url=base_url, temperature=temperature, max_tokens=16384) - llm = LLM(**llm_args) + from crewai import LLM - logger.info(f"[config] {model_key} → Bedrock/{model} (region={region})") - return llm - except ImportError: - raise ImportError( - "langchain-aws is required for the cloud preset. " - "Run: pip install langchain-aws" - ) + kwargs = { + "model": model, + "temperature": temperature, + "max_tokens": 16384, + } - env_base_url = os.environ.get("OLLAMA_BASE_URL") - config_base_url = _PRESET.get("base_url", "http://localhost:11434") + provider = _PRESET.get("provider", "openai") - base_url = env_base_url if env_base_url else config_base_url + if provider == "bedrock": + kwargs["region_name"] = _PRESET.get( + "aws_region", os.environ.get("AWS_DEFAULT_REGION", "us-east-1") + ) + elif provider == "openai": + custom_url = _PRESET.get("base_url") or os.environ.get("OPENAI_BASE_URL") + if custom_url: + kwargs["base_url"] = custom_url - logger.info(f"[config] {model_key} → Ollama/{model} ({base_url})") - return OllamaLLM(model=model, base_url=base_url) + logger.info(f"[config] {model_key} → {provider}/{model}") + return LLM(**kwargs) -PLANNER_MODEL = build_llm("planner") -WORKER_MODEL = build_llm("worker") +PLANNER_MODEL = build_llm("planner", temperature=0.3) +WORKER_MODEL = build_llm("worker", temperature=0.3) -os.environ.setdefault("EMBED_PROVIDER", _PRESET.get("embed_provider", "ollama")) -os.environ.setdefault("EMBED_MODEL", _PRESET.get("embed_model", "mxbai-embed-large")) -os.environ.setdefault("EMBED_DIMS", str(_PRESET.get("embed_dims", 1024))) os.environ.setdefault("DB_NAME", CONFIG["simulation"].get("db_name", "orgforge")) if _PROVIDER == "bedrock": os.environ.setdefault("AWS_DEFAULT_REGION", _PRESET.get("aws_region", "us-east-1")) @@ -314,6 +304,7 @@ class ActiveIncident(BaseModel): on_call: str = "" actors: List[str] = [] contacted_customers: List[str] = [] + stage_advance_probs: Dict[str, float] = Field(default_factory=dict) class SprintState(BaseModel): @@ -367,7 +358,7 @@ class State(BaseModel): daily_active_actors: List[str] = [] daily_event_type_counts: Dict[str, int] = {} departed_employees: Dict[str, Dict] = {} - new_hires: Dict[str, Dict] = {} # name → {joined, role, dept, expertise} + new_hires: Dict[str, Dict] = {} ticket_actors_today: Dict[str, List[str]] = Field(default_factory=dict) @@ -642,6 +633,13 @@ def next_jira_id(state, registry=None, dept: str = "") -> str: } +_STALL_REASONS: Dict[str, str] = { + "detected": "alert noise under review — triage not yet confirmed", + "investigating": "root cause unclear; investigation still in progress", + "fix_in_progress": "fix branch blocked — awaiting test results or dependency", + "review_pending": "reviewers unavailable or requesting changes", +} + _DEPT_COMPLETION_ARTIFACT: dict[str, str] = { "HR_Ops": "confluence", "Sales_Marketing": "email", @@ -666,10 +664,6 @@ def __init__(self, mem: Optional[Memory] = None): self.state = State() self._mem = mem if mem is not None else Memory() - self._embed_worker = EmbedWorker(self._mem) - self._embed_worker.start() - self._mem.set_embed_worker(self._embed_worker) - self.graph_dynamics = GraphDynamics( build_social_graph(self._mem), CONFIG, self._mem ) @@ -757,7 +751,6 @@ def __init__(self, mem: Optional[Memory] = None): confluence_writer=self._confluence, vader=vader, threat_injector=self._threat, - embed_worker=self._embed_worker, lifecycle=self._lifecycle, crm=self._crm, ) @@ -771,7 +764,7 @@ def __init__(self, mem: Optional[Memory] = None): stats = self._mem.stats() logger.info( - f"[dim]Memory: provider={stats['embed_provider']} model={stats['embed_model']} dims={stats['embed_dims']} MongoDB={'✓' if stats['mongodb_ok'] else '⚠'}[/dim]" + f"[dim]Memory: artifacts={stats['artifact_count']} events={stats['event_count']} MongoDB={'✓' if stats['mongodb_ok'] else '⚠'}[/dim]" ) def _is_sprint_planning_day(self) -> bool: @@ -783,13 +776,12 @@ def _is_retro_day(self) -> bool: return self.state.day % sprint_length == (sprint_length - 1) def _is_standup_day(self) -> bool: - # Skip standup if the team is already doing sprint planning today if self._is_sprint_planning_day(): return False return self.state.current_date.weekday() in (0, 2, 4) - def _embed_and_count(self, **kwargs): - self._embed_worker.enqueue(**kwargs) + def _store_and_count(self, **kwargs): + self._mem.embed_artifact(**kwargs) self.state.daily_artifacts_created += 1 def _record_daily_actor(self, *names: str): @@ -820,7 +812,6 @@ def run(self): self.genesis_phase() self.daily_cycle() - # ─── GENESIS ───────────────────────────── def genesis_phase(self): logger.info( Panel.fit( @@ -914,7 +905,6 @@ def _embed_persona(args): f"Memory: {self._mem.stats()['artifact_count']} artifacts embedded.\n" ) - # ─── DAILY LOOP ─────────────────────────── def daily_cycle(self): latest = self._mem.load_latest_checkpoint() if latest: @@ -925,7 +915,6 @@ def daily_cycle(self): self.state.team_morale = latest["state"]["morale"] self.state.system_health = latest["state"]["health"] - # Restore the 'Live' state of the secondary systems self.graph_dynamics._stress = latest["stress"] self.state.actor_cursors = latest["cursors"] @@ -951,7 +940,6 @@ def daily_cycle(self): incident.causal_chain = handler self.state.active_incidents.append(incident) - # Re-sync current_date string back to a datetime object self.state.current_date = datetime.strptime( latest["state"]["date"], "%Y-%m-%d" ) @@ -961,7 +949,6 @@ def daily_cycle(self): self.social_graph.clear() self.social_graph.add_nodes_from(restored_graph.nodes(data=True)) self.social_graph.add_edges_from(restored_graph.edges(data=True)) - # Force graph_dynamics to recalculate betweenness centrality self.graph_dynamics._centrality_dirty = True while self.state.day <= self.state.max_days: @@ -973,7 +960,7 @@ def daily_cycle(self): self.state.current_date += timedelta(days=1) continue - self.state.ticket_actors_today = {} # cleared here; orchestrator re-seeds from SprintContext + self.state.ticket_actors_today = {} self._threat.begin_day(self.state.day, self.state) self._clock.reset_to_business_start(ALL_NAMES) date_str = str(self.state.current_date.date()) @@ -996,17 +983,12 @@ def daily_cycle(self): inc.days_active += 1 if departures or hires: - # Patch the day planner's validator to reflect the new roster patch_validator_for_lifecycle( self._day_planner._validator, self._lifecycle ) vendor_signals = self._email_ingestor.generate_pre_standup(state=self.state) - if self.state.day > 1: - logger.info("[dim] Draining embedding queue[/dim]") - self._embed_worker.drain() - crm_signals = self._crm.planner_context() self.state.persona_stress = dict(self.graph_dynamics._stress) @@ -1078,9 +1060,6 @@ def daily_cycle(self): self._email_ingestor.generate_customer_replies(state=self.state) self._email_ingestor.generate_hr_outbound(state=self.state) - if random.random() < CONFIG["simulation"].get("adhoc_confluence_prob", 0.3): - self._generate_adhoc_confluence_page() - for subject_name in self._threat.active_subject_names(): result = self._threat.inject_host_hoarding( actor=subject_name, @@ -1107,9 +1086,6 @@ def daily_cycle(self): self._advance_incidents() - logger.info("[dim] Draining embedding queue[/dim]") - self._embed_worker.drain() - serialized_incidents = [] for inc in self.state.active_incidents: inc_dict = inc.model_dump() @@ -1148,7 +1124,6 @@ def daily_cycle(self): self.state.day += 1 self.state.current_date += timedelta(days=1) - self._embed_worker.stop() self._print_final_report() run_post_sim(export_dir=Path(BASE), use_llm=True) @@ -1173,9 +1148,6 @@ def _handle_sprint_planning(self): timestamp_str = meeting_time.isoformat() date_str = str(self.state.current_date.date()) - # ── Step 1: LLM generates sprint theme (one call, cheap) ────────────── - # Tier 1: structured MongoDB query — no embedding needed here. - # sprint_theme omitted intentionally; it hasn't been decided yet. ctx = self._mem.context_for_sprint_planning( sprint_num=sprint_num, dept="", @@ -1381,7 +1353,7 @@ def _generate_dept_tickets(dept: str, members: list) -> list: save_json(f"{BASE}/jira/{tid}.json", ticket) self.state.sprint.tickets_in_sprint.append(tid) dept_tickets.append(ticket) - self._embed_and_count( + self._store_and_count( id=tid, type="jira", title=ticket["title"], @@ -1562,7 +1534,7 @@ def _handle_standup(self): if thread_id and messages: full_transcript = "\n".join(f"{m['user']}: {m['text']}" for m in messages) - self._embed_and_count( + self._store_and_count( id=thread_id, type="slack_thread", title=f"Standup Day {self.state.day}", @@ -1594,7 +1566,6 @@ def _handle_standup(self): self._record_daily_actor(*attendees) self._record_daily_event("standup") - # ─── RETROSPECTIVE ──────────────────────── def _handle_retrospective(self): logger.info( f" [bold blue]🔄 Retro — Sprint #{self.state.sprint.sprint_number}[/bold blue]" @@ -1605,7 +1576,6 @@ def _handle_retrospective(self): conf_id = self._registry.next_id("RETRO") self._registry.register_confluence(conf_id, f"Retro Sprint #{sprint_num}") - # ── Attendees: engineering + product only ──────────────────────────────── sprint_depts = {"engineering", "product"} attendees = [ n for n in ALL_NAMES if dept_of_name(n, ORG_CHART).lower() in sprint_depts @@ -1616,21 +1586,18 @@ def _handle_retrospective(self): ) meeting_time_iso = meeting_time.isoformat() - # ── Sprint-bounded context only ────────────────────────────────────────── sprint_length = CONFIG["simulation"].get("sprint_length_days", 10) sprint_start_iso = self.state.sprint.start_date( self.state.current_date - timedelta(days=self.state.day - self.state.sprint.start_day), sprint_length, ).isoformat() - # Tier 1: structured sprint-window query — no embedding. ctx = self._mem.context_for_retrospective( sprint_num=sprint_num, since_iso=sprint_start_iso, as_of_iso=meeting_time_iso, ) - # ── Participants ───────────────────────────────────────────────────────── scrum_master = resolve_role("scrum_master") eng_dept = next((d for d in LEADS if "engineering" in d.lower()), None) eng_lead = LEADS.get(eng_dept) @@ -1639,7 +1606,6 @@ def _handle_retrospective(self): sprint_leads = [p for p in [scrum_master, eng_lead, product_lead] if p] - # ── Per-voice agents ───────────────────────────────────────────────────── agents = [] tasks = [] prev_task = None @@ -1706,11 +1672,10 @@ def _handle_retrospective(self): ).kickoff() ) - # ── Persist ────────────────────────────────────────────────────────────── path = f"{BASE}/confluence/retros/{conf_id}.md" save_md(path, content) - self._embed_and_count( + self._store_and_count( id=conf_id, type="confluence", title=f"Retro Sprint #{sprint_num}", @@ -1739,7 +1704,6 @@ def _handle_retrospective(self): ) ) - # ── Close sprint ───────────────────────────────────────────────────────── self._close_sprint() self._record_daily_actor(*attendees) self._record_daily_event("retrospective") @@ -1750,6 +1714,58 @@ def _close_sprint(self) -> None: self.state.sprint.sprint_number += 1 self.state.sprint.tickets_in_sprint = [] + def _compute_stage_probs( + self, + involves_gap: bool, + recurrence_of: Optional[str], + system_health: int, + ) -> Dict[str, float]: + """ + Compute per-stage advance probabilities for a new incident. + + Base probabilities give an *expected* duration of ~4-5 days for a + clean incident. Severity signals lower them, extending the tail: + + - Knowledge gap → harder to investigate and fix (lower probs on + investigating + fix_in_progress) + - Recurrence → review cycle is more scrutinised (lower prob + on review_pending) + - Low system health → everything is slower (global penalty) + + Stage order: detected → investigating → fix_in_progress → + review_pending → resolved + + A probability of 1.0 means "always advance today". + A probability of 0.6 means "40% chance of stalling for one more day". + """ + cfg = CONFIG.get("simulation", {}) + + base = { + "detected": cfg.get("incident_prob_detected", 1.00), + "investigating": cfg.get("incident_prob_investigating", 0.70), + "fix_in_progress": cfg.get("incident_prob_fix_in_progress", 0.65), + "review_pending": cfg.get("incident_prob_review_pending", 0.75), + } + + if involves_gap: + base["investigating"] = round(base["investigating"] * 0.65, 3) + base["fix_in_progress"] = round(base["fix_in_progress"] * 0.65, 3) + + if recurrence_of: + base["review_pending"] = round(base["review_pending"] * 0.70, 3) + + if system_health < 40: + factor = 0.70 + elif system_health < 65: + factor = 0.85 + else: + factor = 1.00 + + if factor < 1.00: + base = {k: round(v * factor, 3) for k, v in base.items()} + + return {k: max(0.20, v) for k, v in base.items()} + def _handle_incident(self): ticket_id = next_jira_id(self.state, self._registry, dept="Engineering_Backend") date_str = str(self.state.current_date.date()) @@ -1841,6 +1857,7 @@ def _handle_incident(self): date_str=date_str, state=self.state, timestamp=incident_start_iso, + author=on_call, ) involves_gap = len(detected_gaps) > 0 @@ -1921,6 +1938,11 @@ def _handle_incident(self): ), llm=WORKER_MODEL, ) + gap_note = ( + " - Note the knowledge gap and documentation risk explicitly\n" + if gap_areas + else "" + ) desc_task = Task( description=( f"Write a Jira ticket description for this incident.\n\n" @@ -1937,7 +1959,7 @@ def _handle_incident(self): f" - What is broken and how it manifests (1-2 sentences)\n" f" - Which system or component is affected\n" f" - User or business impact\n" - f"{' - Note the knowledge gap and documentation risk explicitly\n' if gap_areas else ''}" + f"{gap_note}" f" - One acceptance criterion for resolution\n\n" f"Keep it under 100 words. Write as {on_call} would in Jira." ), @@ -2012,7 +2034,7 @@ def _handle_incident(self): ) ) - self._embed_and_count( + self._store_and_count( id=ticket_id, type="jira", title=title, @@ -2030,6 +2052,11 @@ def _handle_incident(self): }, ) + stage_probs = self._compute_stage_probs( + involves_gap=involves_gap, + recurrence_of=recurrence_of, + system_health=self.state.system_health, + ) inc = ActiveIncident( ticket_id=ticket_id, title=title, @@ -2038,7 +2065,9 @@ def _handle_incident(self): root_cause=root_cause, causal_chain=chain_handler, recurrence_of=recurrence_of, + on_call=on_call, actors=responders, + stage_advance_probs=stage_probs, ) self.state.active_incidents.append(inc) self.state.daily_incidents_opened += 1 @@ -2137,20 +2166,59 @@ def _handle_incident(self): logger.info(f" [red]🚨 {ticket_id}:[/red] {root_cause[:80]}") def _advance_incidents(self): - still_active = [] - on_call = resolve_role("on_call_engineer") - eng_peer = next( - ( - n - for n in ORG_CHART.get(CONFIG["roles"].get("on_call_engineer", ""), []) - if n != on_call - ), - on_call, - ) + """ + Advance each active incident by one stage — or stall it for the day. + + Each stage has a configured advance probability stored on the incident + at creation time (stage_advance_probs). On a failed roll the incident + stays in its current stage, a stall SimEvent is logged, and the day's + planner context will continue to show the incident as open. This + produces realistic variation in resolution time: + - Clean incident (no gap, healthy system): ~4–6 days + - Gap-knowledge incident: ~6–10 days + - Recurrence + degraded system: can stretch to ~12+ days + """ + still_active = [] cron_time_iso = self._clock.now("system").isoformat() + date_str = str(self.state.current_date.date()) for inc in self.state.active_incidents: + on_call = inc.on_call or resolve_role("on_call_engineer") + eng_peer = next((a for a in inc.actors if a != on_call), on_call) + + advance_prob = inc.stage_advance_probs.get(inc.stage, 1.0) + if random.random() > advance_prob: + stall_reason = _STALL_REASONS.get(inc.stage, "blocked on current stage") + logger.info( + f" [yellow]⏸ {inc.ticket_id} stalled in '{inc.stage}' " + f"(p={advance_prob:.2f}) — {stall_reason}[/yellow]" + ) + self._mem.log_event( + SimEvent( + type="incident_stalled", + timestamp=cron_time_iso, + day=self.state.day, + date=date_str, + actors=[on_call], + artifact_ids={"jira": inc.ticket_id}, + facts={ + "stage": inc.stage, + "days_active": inc.days_active, + "stall_reason": stall_reason, + "advance_prob": advance_prob, + "root_cause": inc.root_cause, + }, + summary=( + f"{inc.ticket_id} stalled in {inc.stage} " + f"(day {inc.days_active}): {stall_reason}." + ), + tags=["incident_stalled", inc.stage], + ) + ) + still_active.append(inc) + continue + if inc.stage == "detected": inc.stage = "investigating" still_active.append(inc) @@ -2215,7 +2283,7 @@ def _advance_incidents(self): self._normal_day._handle_pr_review_for_incident( reviewer=reviewer, pr=pr_doc, - date_str=str(self.state.current_date.date()), + date_str=date_str, timestamp=cron_time_iso, ) except Exception as exc: @@ -2254,7 +2322,7 @@ def _advance_incidents(self): incident_id=inc.ticket_id, postmortem_link=postmortem_link, timestamp=cron_time_iso, - date_str=str(self.state.current_date.date()), + date_str=date_str, day=self.state.day, ) @@ -2265,7 +2333,7 @@ def _advance_incidents(self): type="incident_resolved", timestamp=cron_time_iso, day=self.state.day, - date=str(self.state.current_date.date()), + date=date_str, actors=[on_call, eng_peer], artifact_ids={"jira": inc.ticket_id, "pr": inc.pr_id or ""}, facts={ @@ -2287,21 +2355,15 @@ def _advance_incidents(self): self.state.active_incidents = still_active def _write_postmortem(self, inc: ActiveIncident): - on_call = resolve_role("postmortem_writer") - eng_peer = next( - ( - n - for n in ORG_CHART.get(CONFIG["roles"].get("postmortem_writer", ""), []) - if n != on_call - ), - on_call, - ) + actors = inc.actors or [inc.on_call] + writer = actors[1] if len(actors) > 1 else actors[0] + eng_peer = next((a for a in actors if a != writer), writer) conf_id, timestamp = self._confluence.write_postmortem( incident_id=inc.ticket_id, incident_title=inc.title, root_cause=inc.root_cause, days_active=inc.days_active, - on_call=on_call, + on_call=writer, eng_peer=eng_peer, ) @@ -2349,7 +2411,7 @@ def _emit_bot_message(self, channel: str, bot_name: str, text: str, timestamp: s ) if thread_id: - self._embed_and_count( + self._store_and_count( id=thread_id, type="slack_thread", title=f"Bot message in #{channel}", @@ -2369,13 +2431,6 @@ def _get_next_on_call(self, day: int): index = day % len(engineers) return engineers[index] - def _generate_adhoc_confluence_page( - self, - author: Optional[str] = None, - backstory: Optional[str] = None, - ): - self._confluence.write_adhoc_page(author=author, backstory=backstory) - def _end_of_day(self): date_str = str(self.state.current_date.date()) decay = CONFIG["morale"]["daily_decay"] @@ -2419,7 +2474,9 @@ def _end_of_day(self): event_counts = self.state.daily_event_type_counts dominant_event = ( - max(event_counts, key=event_counts.get) if event_counts else "normal_day" + max(event_counts, key=lambda k: event_counts[k]) + if event_counts + else "normal_day" ) departments_involved = list( @@ -2561,14 +2618,14 @@ def _print_final_report(self): for row in [ ( "Confluence Pages", - str(self._mem._artifacts.count_documents({"type": "confluence"})), + str(self._mem._confluence_pages.count_documents({})), ), ("JIRA Tickets", str(self._mem._jira.count_documents({}))), ("Slack Threads", str(self._mem._slack.count_documents({}))), ("Git PRs", str(self._mem._prs.count_documents({}))), ("Emails", str(self._mem._db["emails"].count_documents({}))), ("Incidents Resolved", str(len(self.state.resolved_incidents))), - ("Embedded Artifacts", str(s["artifact_count"])), + ("Stored Artifacts", str(s["artifact_count"])), ("Employees Departed", str(len(self._lifecycle._departed))), ("Employees Hired", str(len(self._lifecycle._hired))), ("Knowledge Gaps Surfaced", str(len(self._lifecycle._gap_events))), @@ -2581,7 +2638,7 @@ def _print_final_report(self): _proj = {"_id": 0, "embedding": 0} snapshot = { "confluence_pages": list( - self._mem._artifacts.find({"type": "confluence"}, _proj) + self._mem._confluence_pages.find({}, _proj) ), "jira_tickets": list(self._mem._jira.find({}, {"_id": 0})), "emails": list(self._mem._db["emails"].find({}, {"_id": 0})), @@ -2760,7 +2817,7 @@ def _handle_external_contact(self, inc: ActiveIncident, contact: dict) -> None: ) self.state.daily_external_contacts += 1 - self._embed_and_count( + self._store_and_count( id=f"ext_{external_node}_{inc.ticket_id}", type="slack", title=f"External contact summary: {display_name} re {inc.ticket_id}", diff --git a/src/genesis.py b/src/genesis.py index 078fc7d..6431574 100644 --- a/src/genesis.py +++ b/src/genesis.py @@ -49,14 +49,12 @@ def initialize(config, planner_llm, reset=False): return mem -# genesis.py — replace seed_external_sources entirely - - def seed_external_sources(mem: Memory, planner_llm): if mem.get_inbound_email_sources(): return logger.info("[cyan]🌐 Generating inbound email sources...[/cyan]") + tech_stack = mem.tech_stack_for_prompt() vendors = _generate_vendor_sources(mem, planner_llm, tech_stack) @@ -83,12 +81,13 @@ def _generate_vendor_sources(mem: Memory, planner_llm, tech_stack: str) -> List[ agent = make_agent( role="Enterprise IT Architect", - goal=f"Design the vendor email ecosystem for {COMPANY_NAME}.", + goal=f"Design the vendor email ecosystem for {COMPANY_NAME} which {COMPANY_DESCRIPTION}.", backstory=( f"You map communication patterns between a {INDUSTRY} company " f"and its technology vendors." ), llm=planner_llm, + max_execution_time=60, ) task = Task( description=( @@ -215,16 +214,39 @@ def _generate_customer_sources(mem: Memory, planner_llm, tech_stack: str) -> Lis def seed_tech_stack(mem: Memory, planner_llm): """Generates the tech stack ground truth.""" - if mem._artifacts.find_one({"type": "tech_stack"}): + if mem.get_tech_stack(): return logger.info("[genesis] Generating tech stack...") + gap_domains = [] + for gap in CONFIG.get("knowledge_gaps", []): + gap_domains.extend(gap.get("knew_about", [])) + + for dep in CONFIG.get("org_lifecycle", {}).get("scheduled_departures", []): + gap_domains.extend(dep.get("knowledge_domains", [])) + + gap_domains_str = "" + if gap_domains: + unique = sorted(set(gap_domains)) + gap_domains_str = ( + "\n\nCRITICAL — REQUIRED TECHNOLOGY DOMAINS:\n" + "The following systems and domains MUST appear explicitly in your " + "tech stack output. They represent real systems this company runs " + "that have since lost their primary knowledge owner. Do not omit " + "or rename them:\n" + + "\n".join(f" - {d}" for d in unique) + + "\n\nFit them into the appropriate stack categories " + "(e.g. a 'redis-cache' entry belongs under infra or database, " + "'HL7 message routing' belongs under notable_quirks or a " + "dedicated integration_middleware key)." + ) + agent = make_agent( role="Principal Engineer", goal="Define the canonical technology stack for this company.", backstory=( - f"You are a principal engineer at {COMPANY_NAME}, " + f"You are a principal engineer at {COMPANY_NAME} which {COMPANY_DESCRIPTION}, " f"a {INDUSTRY} company. You are documenting the actual " f"technologies in use — not aspirational, not greenfield. " f"This is a company with years of history and legacy decisions." @@ -236,11 +258,13 @@ def seed_tech_stack(mem: Memory, planner_llm): f"Define the canonical tech stack for {COMPANY_NAME} " f"which {COMPANY_DESCRIPTION}\n\n" f"The legacy system is called '{LEGACY['name']}' " - f"({LEGACY['description']}).\n\n" + f"({LEGACY['description']}).{gap_domains_str}\n\n" f"Respond ONLY with a JSON object with these keys:\n" f" database, backend_language, frontend_language, mobile, " f" infra, message_queue, source_control, ci_cd, " f" monitoring, notable_quirks\n\n" + f"You may add additional keys (e.g. 'integration_middleware', " + f"'auth_layer') if needed to accommodate required domains above. " f"Each value is a short string (1-2 sentences max). " f"Include at least one legacy wart or technical debt item. " f"No preamble, no markdown fences." @@ -264,9 +288,9 @@ def seed_tech_stack(mem: Memory, planner_llm): mem.save_tech_stack(stack) logger.info(f"[confluence] ✓ Tech stack established: {list(stack.keys())}") - mem._db["artifacts"].create_index( + mem._db["confluence_pages"].create_index( [("title", "text"), ("content", "text")], - name="artifacts_text_search", + name="confluence_text_search", weights={"title": 3, "content": 1}, ) @@ -439,13 +463,13 @@ def seed_knowledge_gaps(mem: Memory): equal to the normalised domain key. Each document schema: { - "_id": "titandb", # normalised key - "domain": "TitanDB", # display name - "primary_owner": None, # None = orphaned + "_id": "titandb", + "domain": "TitanDB", + "primary_owner": None, "former_owner": "Bill", "documentation_coverage": 0.20, - "last_updated_day": -180, # day relative to sim start - "known_by": [], # engineers with partial knowledge + "last_updated_day": -180, + "known_by": [], "system_tags": ["titandb", "titan", "db"], "dept": "Engineering_Backend", "is_genesis_gap": True, @@ -458,6 +482,12 @@ def seed_knowledge_gaps(mem: Memory): if not CONFIG.get("knowledge_gaps"): return + if not mem.get_tech_stack(): + raise RuntimeError( + "[genesis] seed_knowledge_gaps called before tech stack exists. " + "Call seed_tech_stack first." + ) + logger.info("[genesis] Seeding pre-simulation knowledge gaps...") sim_start = datetime.strptime(CONFIG["simulation"]["start_date"], "%Y-%m-%d") @@ -512,9 +542,6 @@ def seed_knowledge_gaps(mem: Memory): existing = mem._db["domain_registry"].find_one({"_id": key}) if existing: - # Domain already registered (e.g. two departures knew the - # same system). Just ensure former_owners is a list and - # append — don't overwrite coverage. mem._db["domain_registry"].update_one( {"_id": key}, { @@ -533,12 +560,12 @@ def seed_knowledge_gaps(mem: Memory): record = { "_id": key, "domain": domain, - "primary_owner": None, # orphaned from day 0 + "primary_owner": None, "former_owner": name, "former_owners": [name], "documentation_coverage": doc_pct, "last_updated_day": departure_day, - "known_by": [], # no current engineers + "known_by": [], "system_tags": system_tags, "dept": dept, "is_genesis_gap": True, @@ -606,7 +633,6 @@ def _parse_sources(raw: str) -> List[dict]: for s in parsed: if not required.issubset(s.keys()): continue - # Force-correct customer liaison at parse time if s.get("category", "").lower() == "customer": s["internal_liaison"] = "Sales_Marketing" valid.append(s) diff --git a/src/graph_dynamics.py b/src/graph_dynamics.py index 5c9c6af..a62d554 100644 --- a/src/graph_dynamics.py +++ b/src/graph_dynamics.py @@ -17,16 +17,16 @@ DEFAULT_CFG = { - "stress_bleed_rate": 0.25, # fraction of key-player excess that bleeds - "burnout_threshold": 72, # stress score that triggers propagation - "incident_stress_hit": 20, # raw stress added per P1 involvement - "stress_daily_recovery": 3, # flat recovery applied to everyone EOD - "key_player_multiplier": 2.0, # top-N% by betweenness = key players - "edge_decay_rate": 0.97, # multiplicative daily decay - "slack_boost": 1.5, # weight added per shared Slack thread - "pr_review_boost": 3.0, # weight added per PR review pair - "incident_boost": 4.0, # weight added per shared incident - "edge_weight_floor": 0.5, # decay never goes below this + "stress_bleed_rate": 0.25, + "burnout_threshold": 72, + "incident_stress_hit": 20, + "stress_daily_recovery": 3, + "key_player_multiplier": 2.0, + "edge_decay_rate": 0.97, + "slack_boost": 1.5, + "pr_review_boost": 3.0, + "incident_boost": 4.0, + "edge_weight_floor": 0.5, "escalation_max_hops": 6, } @@ -543,7 +543,7 @@ def sync_crm_edge_weights(self, crm) -> Dict[str, float]: if open_opp.get("risk_notes"): risk_penalty += 2.5 elif not open_tickets: - silence_penalty = 0.5 # going quiet + silence_penalty = 0.5 if account and account.get("risk_flag"): risk_penalty += 1.0 @@ -595,7 +595,6 @@ def apply_crm_stress(self, crm) -> Dict[str, int]: deltas: Dict[str, int] = {} - # Build a map: internal_node -> [org_names they're liaison for] liaison_orgs: Dict[str, List[str]] = {} for node, data in self.G.nodes(data=True): if not data.get("external", False): @@ -630,7 +629,7 @@ def apply_crm_stress(self, crm) -> Dict[str, int]: if at_risk_opp: delta += 6 if at_risk_opp.get("stage") == "Negotiation/Review": - delta += 2 # so close, yet so at-risk + delta += 2 self._stress[employee] = max(0, min(100, self._stress[employee] + delta)) if delta != 0: diff --git a/src/insider_threat.py b/src/insider_threat.py index 206a5a9..1a1dc73 100644 --- a/src/insider_threat.py +++ b/src/insider_threat.py @@ -33,76 +33,6 @@ ``true_positive`` flag are buried in a separate ``_ground_truth.jsonl`` file that is structurally distinct from the observable telemetry stream. -Config schema (add to config.yaml) ------------------------------------ - insider_threat: - enabled: false - - mode: "passive" - # passive — behaviors injected into artifacts; no synthetic SIEM events - # active — additionally emits dlp_alert SimEvents with noise mixed in - - # Export format for security_telemetry/access_log files. - # "jsonl" — custom JSONL (original, default for backward compat) - # "cef" — Common Event Format (ArcSight, Splunk, many SIEMs) - # "ecs" — Elastic Common Schema (Elastic SIEM, OpenSearch) - # "leef" — Log Event Extended Format (IBM QRadar) - # "all" — write all three alongside each other - log_format: "jsonl" - - subjects: - - name: "Jordan" - threat_class: "negligent" - # negligent — accidental credential leak in a PR / commit - # disgruntled — data hoarding, sentiment drift, reduced collaboration - # malicious — deliberate exfil via email/Slack to external contact - onset_day: 8 - behaviors: - - "secret_in_commit" # available for: negligent, malicious - - "unusual_hours_access" # available for: malicious, disgruntled - - "excessive_repo_cloning" # available for: malicious - - "sentiment_drift" # available for: disgruntled - - "cross_dept_snooping" # available for: malicious, disgruntled - - "data_exfil_email" # available for: malicious - - "host_data_hoarding" # available for: malicious, disgruntled - - dlp_noise_ratio: 0.4 - # Fraction of dlp_alert events that are false positives (innocent employees). - # Only relevant in "active" mode. Range 0.0–1.0. - - telemetry_dir: "security_telemetry" - # Subdirectory under the simulation export dir for telemetry output. - - idp_logs: true - # When true, emit realistic IDP (Identity Provider) authentication events - # for every active employee each day. These serve as the authentication - # baseline that anomaly detection agents correlate against behavioral signals. - # Anomalous IDP events are injected for threat subjects automatically. - -Public API (called from flow.py) ---------------------------------- - injector = InsiderThreatInjector.from_config(config, export_base, all_names) - - # Top of daily_cycle — decides whether today is an active threat day - injector.begin_day(day, state) - - # After org_plan is built — may mutate agenda items for the subject - injector.inject_pre_planning(day, org_day_plan) - - # Called by GitSimulator.create_pr — may mutate PR description in-place - injector.inject_pr(pr: dict, author: str, day: int) -> dict - - # Called after Slack message generation — may inject anomalous messages - injector.inject_slack(messages: list, channel: str, day: int) -> list - - # Called after email artifact write — may inject data exfil payload - injector.inject_email(eml_path: str, sender: str, day: int) - - # End of day — flush telemetry, maybe emit dlp_alert SimEvent - injector.end_day(day, state, mem, clock, date_str) -> list[SimEvent] - - # Always-safe check: is behavior X active for subject Y today? - injector.is_active(name: str, behavior: str, day: int) -> bool """ from __future__ import annotations @@ -122,13 +52,6 @@ logger = logging.getLogger("orgforge.security") -# ───────────────────────────────────────────────────────────────────────────── -# FAKE SECRET TEMPLATES -# Realistic-looking but obviously synthetic — safe for training corpora. -# Each template has a variable region generated at runtime. -# ───────────────────────────────────────────────────────────────────────────── - - def _rand_upper(n: int) -> str: return "".join(random.choices(string.ascii_uppercase + string.digits, k=n)) @@ -142,7 +65,6 @@ def _rand_b64ish(n: int) -> str: return "".join(random.choices(chars, k=n)) -# Each entry: (env_var_name, value_generator, inline_comment) _SECRET_TEMPLATES = [ ( "AWS_SECRET_ACCESS_KEY", @@ -185,11 +107,6 @@ def _generate_fake_secret() -> tuple[str, str, str]: return tpl[0], tpl[1](), tpl[2] -# ───────────────────────────────────────────────────────────────────────────── -# DATA MODELS -# ───────────────────────────────────────────────────────────────────────────── - - @dataclass class ThreatSubjectConfig: """Parsed from one entry under ``insider_threat.subjects``.""" @@ -199,10 +116,8 @@ class ThreatSubjectConfig: onset_day: int behaviors: List[str] - # ── Runtime state — mutated as simulation runs ─────────────────────────── _active: bool = field(default=False, repr=False) _fired_behaviors: Dict[str, int] = field(default_factory=dict, repr=False) - # {behavior_name: last_day_fired} @dataclass @@ -221,18 +136,11 @@ class TelemetryRecord: actor: str # name only — no role or threat annotation details: Dict[str, Any] # observable facts (repo, file_count, dest, etc.) - # Ground-truth fields — written to _ground_truth.jsonl only _true_positive: bool = False _threat_class: Optional[str] = None _behavior: Optional[str] = None -# ───────────────────────────────────────────────────────────────────────────── -# IDP LOG HELPERS -# ───────────────────────────────────────────────────────────────────────────── - -# Simulated device fingerprints — each employee "owns" a few known devices. -# These are referenced in IDP logs to model normal vs. anomalous device usage. _DEVICE_OS_POOL = [ ("macOS 14.4", "Apple"), ("macOS 13.6", "Apple"), @@ -251,11 +159,10 @@ class TelemetryRecord: _MFA_METHODS = ["totp", "push_notification", "hardware_key", "sms"] -# Known corporate IP ranges (fake RFC-5737 / documentation ranges) _CORP_IP_PREFIXES = ["203.0.113.", "198.51.100.", "192.0.2."] _RESIDENTIAL_IP_PREFIXES = ["10.0.", "172.16.", "100.64."] _VPN_IP_PREFIX = "192.0.2." -_TOR_EXIT_IP_PREFIX = "198.18." # RFC-2544 benchmark range — clearly anomalous +_TOR_EXIT_IP_PREFIX = "198.18." _SSO_APPS = [ "github-enterprise", @@ -313,12 +220,6 @@ def _seed_employee_devices(name: str) -> List[Dict]: return devices -# ───────────────────────────────────────────────────────────────────────────── -# HOST DATA HOARDING HELPERS -# ───────────────────────────────────────────────────────────────────────────── - -# Simulated internal share paths and file categories. -# These are plausible-sounding but obviously synthetic — no real data. _INTERNAL_SHARES = [ "//fileserver01/Finance/Q-reports", "//fileserver01/HR/headcount", @@ -386,11 +287,6 @@ def _gen_file_list(count: int) -> List[str]: ] -# ───────────────────────────────────────────────────────────────────────────── -# INDUSTRY-STANDARD LOG FORMATTERS -# ───────────────────────────────────────────────────────────────────────────── - - class LogFormatter: """ Converts TelemetryRecord instances to industry-standard SIEM log formats. @@ -407,7 +303,6 @@ class LogFormatter: in ``_ground_truth.jsonl``. """ - # CEF severity: 0 (low) → 10 (very high) _CEF_SEVERITY: Dict[str, int] = { "high": 8, "medium": 5, @@ -415,7 +310,6 @@ class LogFormatter: "info": 0, } - # ECS event.category mapping _ECS_CATEGORY: Dict[str, str] = { "commit": "configuration", "repo_access": "file", @@ -426,7 +320,6 @@ class LogFormatter: "slack_message": "process", } - # ECS event.type mapping _ECS_TYPE: Dict[str, str] = { "commit": "change", "repo_access": "access", @@ -492,7 +385,6 @@ def _esc(v: Any) -> str: else: ext_pairs[k] = v - # Map well-known details to standard CEF field names if "to" in rec.details: ext_pairs["dst"] = rec.details["to"] if "access_hour" in rec.details: @@ -554,7 +446,6 @@ def to_ecs(cls, rec: TelemetryRecord, domain: str = "orgforge.internal") -> str: "tags": ["orgforge", "insider_threat_sim", rec.record_type], } - # Record-type specific mappings d = rec.details if rec.record_type == "commit": @@ -701,8 +592,6 @@ def _esc(v: Any) -> str: return f"LEEF:2.0|OrgForge|InsiderThreatSim|1.0|{event_id}|\t{attr_str}" - # ── Private helpers ─────────────────────────────────────────────────────── - @classmethod def _cef_severity_for(cls, rec: TelemetryRecord) -> int: behavior = rec._behavior or rec.details.get("policy_trigger", "") @@ -728,20 +617,12 @@ def _cef_name_for(cls, rec: TelemetryRecord) -> str: return names.get(rec.record_type, "Security Telemetry Event") -# ───────────────────────────────────────────────────────────────────────────── -# BEHAVIOR REGISTRY -# Each behavior is a plain function: (injector, subject, context) → side-effect -# Context is a dict assembled per call-site (keys vary by surface) -# ───────────────────────────────────────────────────────────────────────────── - - class BehaviorRegistry: """ Maps behavior_name → injection function. Functions return a dict of observable changes (for telemetry) or None. """ - # Minimum gap in days before the same behavior fires again _COOLDOWNS: Dict[str, int] = { "secret_in_commit": 4, "unusual_hours_access": 1, @@ -764,11 +645,6 @@ def mark_fired(subject: ThreatSubjectConfig, behavior: str, day: int): subject._fired_behaviors[behavior] = day -# ───────────────────────────────────────────────────────────────────────────── -# MAIN INJECTOR -# ───────────────────────────────────────────────────────────────────────────── - - class InsiderThreatInjector: """ Central coordinator for the insider threat simulation layer. @@ -802,21 +678,15 @@ def __init__( self._domain = domain self._persona_helper = persona_helper self._worker_llm = worker_llm - self._log_format = log_format # "jsonl" | "cef" | "ecs" | "leef" | "all" + self._log_format = log_format self._emit_idp_logs = emit_idp_logs - # Per-employee device profiles — seeded once, stable across the run. self._employee_devices: Dict[str, List[Dict]] = { name: _seed_employee_devices(name) for name in all_names } - # Per-subject host hoarding state — tracks the multi-day staging trail. - # {subject_name: {"staged_files": [...], "stage_day": int, ...}} self._hoarding_state: Dict[str, Dict] = {} - - # Pending telemetry records, flushed at end_day() self._pending_telemetry: List[TelemetryRecord] = [] - # Pending SimEvents to fire (returned from end_day()) self._pending_sim_events: List[Any] = [] telemetry_dir.mkdir(parents=True, exist_ok=True) @@ -829,8 +699,6 @@ def __init__( f"idp_logs={'on' if emit_idp_logs else 'off'}" ) - # ─── FACTORY ───────────────────────────────────────────────────────────── - @classmethod def from_config( cls, @@ -877,8 +745,6 @@ def from_config( emit_idp_logs=cfg.get("idp_logs", True), ) - # ─── DAY LIFECYCLE ─────────────────────────────────────────────────────── - def begin_day(self, day: int, state) -> None: """ Called at the top of daily_cycle(), before planning. @@ -908,8 +774,7 @@ def end_day( 3. In "active" mode: emits dlp_alert SimEvents (with noise). 4. Returns a list of SimEvent objects to be logged by flow.py. """ - # IDP logs first — they represent the authentication baseline for - # the day and should precede behavioral telemetry in timestamp order. + if self._emit_idp_logs: self._emit_idp_day_logs(day, state, date_str) @@ -923,8 +788,6 @@ def end_day( self._pending_sim_events.clear() return sim_events - # ─── PR INJECTION ──────────────────────────────────────────────────────── - def inject_pr(self, pr: dict, author: str, day: int) -> dict: """ Called by GitSimulator.create_pr() after the base PR dict is built. @@ -943,11 +806,8 @@ def inject_pr(self, pr: dict, author: str, day: int) -> dict: var_name, fake_value, comment = _generate_fake_secret() injection_context = random.choice( [ - # Injected as a test fixture reference f'\n\n## Test Configuration\n```python\n# Temporary test setup — remove before prod\n{var_name} = "{fake_value}" {comment}\n```', - # Injected as a config snippet in the PR body f"\n\n> **Local testing note:** Set `{var_name}={fake_value}` in your `.env` to reproduce. {comment}", - # Injected as a diff comment block f"\n\n```diff\n+{var_name}={fake_value} {comment}\n```", ] ) @@ -955,7 +815,6 @@ def inject_pr(self, pr: dict, author: str, day: int) -> dict: original_desc = pr.get("description", "") pr["description"] = original_desc + injection_context - # Record for telemetry self._pending_telemetry.append( TelemetryRecord( record_type="commit", @@ -1039,8 +898,6 @@ def inject_social_engineering( date_str = str(current_date.date()) - # Pick a target — prefer Finance or HR leads; fall back to random - # active employee that isn't the subject themselves high_value_roles = ["finance", "hr", "payroll", "legal", "executive"] target = next( ( @@ -1093,8 +950,6 @@ def inject_social_engineering( return injected - # ─── SLACK INJECTION ───────────────────────────────────────────────────── - def inject_slack( self, messages: List[dict], @@ -1113,7 +968,6 @@ def inject_slack( if not subject._active: continue - # ── sentiment_drift ────────────────────────────────────────────── if "sentiment_drift" in subject.behaviors and BehaviorRegistry.can_fire( subject, "sentiment_drift", day ): @@ -1145,42 +999,12 @@ def inject_slack( BehaviorRegistry.mark_fired(subject, "sentiment_drift", day) break - # ── unusual_hours_access ───────────────────────────────────────── - # Only fires if no messages from subject exist in this channel yet - # (it represents a late-night check-in, not a standup override) - # - # CLOCK NOTE: This behavior intentionally bypasses SimClock entirely, - # and that bypass is load-bearing — not just polite. Here is why: - # - # _enforce_business_hours() is an OVERFLOW HANDLER, not a cap. - # A cursor landing past 17:30 does not clamp to 17:30 — it rolls - # forward to 09:00 the NEXT business day. So calling advance_actor() - # with an off-hours target would silently teleport the subject's cursor - # to tomorrow morning, corrupting every artifact timestamp they produce - # for the rest of today. - # - # Additionally, sync_and_advance() and sync_and_tick() both call - # _sync_time() internally, which pulls ALL participants up to the - # latest cursor among them. An off-hours cursor on the subject would - # drag their colleagues to 02:00 as well — then roll everyone to - # next-day 09:00. - # - # Correct approach: construct the datetime directly from current_date, - # append it to the message list, and never let it near the cursor - # system. The subject's cursor stays on the business-hours chain. - # Their next advance_actor() call produces e.g. 10:35, which is - # temporally BEFORE the 02:14 off-hours message — and that inversion - # is intentional. It reflects reality: the subject acted at 2am, - # then showed up to standup at 9:30 as normal. Both are real events. if ( "unusual_hours_access" in subject.behaviors and BehaviorRegistry.can_fire(subject, "unusual_hours_access", day) and not any(m.get("user") == subject.name for m in messages) and random.random() < 0.35 # not every eligible day ): - # Build an off-hours datetime that is definitely outside the - # reset_to_business_start → advance_actor window (09:00–~18:00). - # We do NOT call clock.advance_actor() — see note above. off_hour = random.choice([1, 2, 3, 22, 23]) off_hours_ts = current_date.replace( hour=off_hour, @@ -1202,10 +1026,6 @@ def inject_slack( "ts": off_hours_ts.isoformat(), "thread_ts": off_hours_ts.isoformat(), "day": day, - # Flag that this message was injected outside business hours. - # Stored in the Slack artifact metadata — NOT in the message text. - # Detection agents reading raw Slack JSON will see this field; - # agents reading only message content will miss it. "_security_injected": True, "is_bot": False, } @@ -1231,8 +1051,6 @@ def inject_slack( return messages - # ─── EMAIL INJECTION ───────────────────────────────────────────────────── - def inject_email( self, eml_path: str, @@ -1261,7 +1079,6 @@ def inject_email( if random.random() > 0.5: return None # probabilistic — doesn't fire every eligible day - # Build a plausible-looking exfil email to a personal/external account external_domains = ["gmail.com", "protonmail.com", "outlook.com", "yahoo.com"] exfil_to = f"{subject.name.lower()}.personal@{random.choice(external_domains)}" exfil_subject = random.choice( @@ -1274,7 +1091,6 @@ def inject_email( ] ) - # Inline "data" is vague enough to be plausible but never genuinely sensitive exfil_snippets = [ "Attaching the internal roadmap notes I mentioned.", "Here's a copy of the access list I was telling you about.", @@ -1287,7 +1103,6 @@ def inject_email( f"-- {subject.name}\nSent from work\n" ) - # Write the injected email alongside the triggering one base_name = os.path.basename(eml_path).replace(".eml", f"_fwd_{day}.eml") exfil_dir = os.path.dirname(eml_path) exfil_path = os.path.join(exfil_dir, base_name) @@ -1300,11 +1115,6 @@ def inject_email( msg["From"] = f"{sender} <{sender.lower()}@{self._domain}>" msg["To"] = exfil_to msg["Subject"] = exfil_subject - # CLOCK NOTE: Intentional SimClock bypass — exfil emails are written - # directly to disk and never routed through advance_actor(). - # Same load-bearing reason as unusual_hours_access: _enforce_business_hours() - # is an overflow handler that rolls past-17:30 cursors to next-day 09:00, - # not a clamp. The subject's business-hours cursor is unaffected. msg["Date"] = current_date.replace( hour=random.choice([22, 23, 0, 1]), minute=random.randint(0, 59), @@ -1344,8 +1154,6 @@ def inject_email( ) return exfil_path - # ─── JIRA / CROSS-DEPT SNOOPING ────────────────────────────────────────── - def inject_jira_access( self, accessor: str, @@ -1385,8 +1193,6 @@ def inject_jira_access( ) ) - # ─── REPO CLONE TELEMETRY ──────────────────────────────────────────────── - def inject_repo_clone( self, actor: str, @@ -1426,8 +1232,6 @@ def inject_repo_clone( ) BehaviorRegistry.mark_fired(subject, "excessive_repo_cloning", day) - # ─── HOST DATA HOARDING ─────────────────────────────────────────────────── - def inject_host_hoarding( self, actor: str, @@ -1460,34 +1264,30 @@ def inject_host_hoarding( if not BehaviorRegistry.can_fire(subject, "host_data_hoarding", day): return None - # Probabilistic: not every eligible day fires. Phase 1 fires more - # rarely than phase 2/3 (which continue the existing trail). state = self._hoarding_state.get(actor, {}) current_phase = state.get("phase", 0) if current_phase == 0 and random.random() > 0.4: - return None # Phase 1 is rare — drives realistic corpus density + return None date_str = str(current_date.date()) - # Advance to next phase, wrapping back to 0 after phase 3 next_phase = (current_phase % 3) + 1 if next_phase == 1: - # ── Phase 1: Internal share access + local staging ──────────────── shares_accessed = random.sample(_INTERNAL_SHARES, random.randint(2, 4)) file_count = random.randint(15, 80) file_list = _gen_file_list(file_count) staged_path = _gen_staging_path(actor, use_cloud=False) - total_bytes = random.randint(50_000_000, 800_000_000) # 50 MB–800 MB + total_bytes = random.randint(50_000_000, 800_000_000) details = { "action": "bulk_file_copy", "source_shares": shares_accessed, "file_count": file_count, - "sample_files": file_list[:5], # only 5 visible in observable stream + "sample_files": file_list[:5], "staged_path": staged_path, "total_bytes": total_bytes, - "access_hour": random.choice(range(18, 22)), # after-hours but not 2am + "access_hour": random.choice(range(18, 22)), "outside_business_hours": True, } @@ -1526,7 +1326,6 @@ def inject_host_hoarding( return {"phase": 1, "actor": actor, "staged_path": staged_path} elif next_phase == 2 and current_phase == 1: - # ── Phase 2: Compression of staging directory ───────────────────── tool = random.choice(_COMPRESSION_TOOLS) archive_name = random.choice(_ARCHIVE_NAMES).replace( "{date}", current_date.strftime("%Y%m%d") @@ -1582,10 +1381,6 @@ def inject_host_hoarding( return {"phase": 2, "actor": actor, "archive_name": archive_name} elif next_phase == 3 and current_phase == 2: - # ── Phase 3: Move archive to cloud-sync or removable media ──────── - # This is the exfil-staging breadcrumb that should correlate - # with a data_exfil_email, unusual_hours_access, or anomalous - # network transfer event on the same or following day. use_cloud = random.random() < 0.7 cloud_sync_dir = _gen_staging_path(actor, use_cloud=use_cloud) archive_name = state.get("archive_name", "backup.zip") @@ -1610,14 +1405,10 @@ def inject_host_hoarding( "removable_media": not use_cloud, "access_hour": random.choice(range(20, 24)), "outside_business_hours": True, - # Correlation hint: this record should be joined with the - # phase 1 host_event (same actor, 2 days prior) and any - # concurrent email_send or network transfer to external IPs. "hoarding_trail_start_day": state.get("start_day", day - 2), "total_bytes_staged": state.get("total_bytes", compressed_bytes), } - # Reset hoarding state — trail is complete, can start a new one self._hoarding_state[actor] = {"phase": 0} rec = TelemetryRecord( @@ -1650,9 +1441,7 @@ def inject_host_hoarding( "destination_path": dst_path, } - return None # Phase mismatch — trail out of order, skip - - # ─── CONVENIENCE CHECK ─────────────────────────────────────────────────── + return None def is_active(self, name: str, behavior: str, day: int) -> bool: """ @@ -1670,8 +1459,6 @@ def active_subject_names(self) -> Set[str]: """Return set of subject names that are currently active.""" return {s.name for s in self._subjects.values() if s._active} - # ─── PRIVATE — IDP LOG EMISSION ────────────────────────────────────────── - def _emit_idp_day_logs(self, day: int, state, date_str: str) -> None: """ Emit IDP (Identity Provider) authentication events for every active @@ -1706,7 +1493,6 @@ def _emit_idp_day_logs(self, day: int, state, date_str: str) -> None: subject = self._subjects.get(name) is_active_subject = subject and subject._active - # ── Normal morning authentication ───────────────────────────────── morning_hour = random.randint(8, 10) morning_min = random.randint(0, 59) auth_ts = current_date.replace( @@ -1742,7 +1528,6 @@ def _emit_idp_day_logs(self, day: int, state, date_str: str) -> None: ) self._pending_telemetry.append(normal_auth) - # Optional mid-day re-auth (session expiry simulation) if random.random() < 0.4: mid_hour = random.randint(12, 15) mid_ts = current_date.replace( @@ -1775,16 +1560,12 @@ def _emit_idp_day_logs(self, day: int, state, date_str: str) -> None: ) self._pending_telemetry.append(mid_auth) - # ── Threat subject anomalous IDP events ─────────────────────────── if not is_active_subject: continue threat_class = subject.threat_class if threat_class == "malicious" and random.random() < 0.45: - # Malicious: off-hours login from an unfamiliar device or - # anomalous IP. Not every eligible day — just plausible enough - # that a behavioral baseline would flag it as unusual. off_hour = random.choice([0, 1, 2, 22, 23]) off_ts = current_date.replace( hour=off_hour, minute=random.randint(0, 59), second=0, microsecond=0 @@ -1803,7 +1584,6 @@ def _emit_idp_day_logs(self, day: int, state, date_str: str) -> None: else: src_ip = corp_ip - # 20% chance of a new/unknown device use_new_device = random.random() < 0.2 if use_new_device: fake_os, fake_vendor = random.choice(_DEVICE_OS_POOL) @@ -1840,10 +1620,6 @@ def _emit_idp_day_logs(self, day: int, state, date_str: str) -> None: "ip_type": ip_type, "access_hour": off_hour, "outside_business_hours": True, - # Key anomaly signal: the subject authenticated at - # this hour but may have no corroborating activity. - # Detection agents should check for work artifacts - # in the same time window. "corroborating_activity_expected": False, }, _true_positive=True, @@ -1858,10 +1634,6 @@ def _emit_idp_day_logs(self, day: int, state, date_str: str) -> None: ) elif threat_class == "disgruntled" and random.random() < 0.3: - # Disgruntled: off-hours login but NO work activity follows. - # This is the "ghost login" scenario — the employee logged in - # but did nothing productive, possibly browsing access lists - # or export settings without leaving standard work artifacts. off_hour = random.choice([6, 7, 19, 20, 21]) off_ts = current_date.replace( hour=off_hour, minute=random.randint(0, 59), second=0, microsecond=0 @@ -1887,9 +1659,6 @@ def _emit_idp_day_logs(self, day: int, state, date_str: str) -> None: "ip_type": "corporate", "access_hour": off_hour, "outside_business_hours": off_hour < 8 or off_hour > 18, - # The detection signal: auth present, no work artifacts - # in the same window. Agents must check for absence - # of correlated Jira, Confluence, or Slack events. "corroborating_activity_expected": False, "ghost_login": True, }, @@ -1899,7 +1668,6 @@ def _emit_idp_day_logs(self, day: int, state, date_str: str) -> None: ) self._pending_telemetry.append(ghost_auth) - # Occasional failed MFA to simulate credential uncertainty if random.random() < 0.15: failed_ts = off_ts + timedelta(seconds=random.randint(30, 120)) failed_auth = TelemetryRecord( @@ -2064,7 +1832,6 @@ def _apply_sentiment_drift( Crew(agents=[agent], tasks=[task], verbose=False).kickoff() ).strip() - # Sanity check — if rewrite is empty or suspiciously short, keep original if len(result) < 10: logger.warning( "[security] sentiment_drift rewrite too short, keeping original" @@ -2079,9 +1846,7 @@ def _apply_sentiment_drift( except Exception as exc: logger.warning(f"[security] sentiment_drift rewrite failed: {exc}") - return text # always fall back gracefully - - # ─── PRIVATE — DLP ALERT EVENTS ────────────────────────────────────────── + return text def _generate_dlp_events(self, day: int, state, clock, date_str: str) -> List[Any]: """ @@ -2102,7 +1867,6 @@ def _generate_dlp_events(self, day: int, state, clock, date_str: str) -> List[An else str(alert_time) ) - # True positives — one alert per true-positive record today true_positive_records = [ r for r in self._pending_telemetry if r._true_positive and r.day == day ] @@ -2118,9 +1882,6 @@ def _generate_dlp_events(self, day: int, state, clock, date_str: str) -> List[An facts={ "alert_type": rec.record_type, "details": rec.details, - # NOTE: true_positive is deliberately absent from SimEvent.facts - # so agents cannot trivially label it. Ground truth lives in - # security_telemetry/_ground_truth.jsonl. "policy_rule": self._policy_rule_for(rec._behavior or ""), "severity": self._severity_for(rec._behavior or ""), }, @@ -2132,7 +1893,6 @@ def _generate_dlp_events(self, day: int, state, clock, date_str: str) -> List[An ) ) - # False positives — noisy alerts for innocent employees if self._innocent_names and random.random() < self._noise_ratio: fp_actor = random.choice(self._innocent_names) fp_behavior = random.choice( @@ -2164,7 +1924,6 @@ def _generate_dlp_events(self, day: int, state, clock, date_str: str) -> List[An tags=["dlp_alert", "security", "false_positive_candidate"], ) ) - # Record the false positive in telemetry too self._pending_telemetry.append( TelemetryRecord( record_type="dlp_alert", @@ -2246,7 +2005,6 @@ def _flush_telemetry(self, day: int, date_str: str) -> None: fmt = self._log_format.lower() gt_path = self._telemetry_dir / "_ground_truth.jsonl" - # Ground-truth is always written as JSONL regardless of format. with open(gt_path, "a") as gt_f: for rec in self._pending_telemetry: observable_base = { @@ -2265,7 +2023,6 @@ def _flush_telemetry(self, day: int, date_str: str) -> None: } gt_f.write(json.dumps(ground_truth) + "\n") - # Observable stream — format-specific write_jsonl = fmt in ("jsonl", "all") write_cef = fmt in ("cef", "all") write_ecs = fmt in ("ecs", "all") @@ -2319,10 +2076,8 @@ def _inject_spear_phish( from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart - # Impersonate IT helpdesk or HR — the two most exploited pretexts pretext_role = random.choice(["IT Helpdesk", "HR Operations", "Security Team"]) spoofed_from_name = f"{pretext_role} " - # Reply-To is the attacker's external address — the tell attacker_reply_to = f"{subject.name.lower()}@{random.choice(['gmail.com', 'outlook.com', 'protonmail.com'])}" subject_lines = [ @@ -2380,7 +2135,7 @@ def _inject_spear_phish( self._pending_telemetry.append( TelemetryRecord( - record_type="email_send", # appears as inbound mail, not a send + record_type="email_send", day=day, date=date_str, timestamp=send_ts.isoformat(), @@ -2393,7 +2148,6 @@ def _inject_spear_phish( "originating_ip": msg["X-Originating-IP"], "eml_path": str(eml_path), "send_hour": send_hour, - # Observable tell: Reply-To domain differs from From domain "reply_to_domain_mismatch": True, }, _true_positive=True, @@ -2425,8 +2179,7 @@ def _inject_slack_pretext( going through advance_actor(), so the subject's business-hours cursor is unaffected. """ - # Time the pretext to land during a busy window — mid-morning or - # right after lunch, when people are context-switching + send_hour = random.choice([9, 10, 13, 14]) send_ts = current_date.replace( hour=send_hour, minute=random.randint(5, 55), second=0, microsecond=0 @@ -2453,7 +2206,7 @@ def _inject_slack_pretext( ] message = { - "user": subject.name, # appears as the subject's real name, not "IT" + "user": subject.name, "email": f"{subject.name.lower()}@{self._domain}", "text": random.choice(pretext_messages), "ts": send_ts.isoformat(), diff --git a/src/memory.py b/src/memory.py index dbaa116..2edbbfd 100644 --- a/src/memory.py +++ b/src/memory.py @@ -1,14 +1,7 @@ """ -memory.py (MongoDB Vector Search) -============================================== -Semantic memory layer for OrgForge. - -Architecture: - - MongoDB (mongodb-atlas-local or Atlas) stores all documents and vectors. - - Embeddings generated by whichever provider matches the active quality_preset: - local_gpu → OllamaEmbedder (fully offline) - cloud → OpenAIEmbedder (text-embedding-3-large) - BedrockEmbedder (Titan Embeddings G1) +memory.py +========= +Structured retrieval layer for OrgForge. Usage: from memory import Memory @@ -18,50 +11,22 @@ from datetime import datetime, timezone import os import json -import hashlib import logging -from abc import ABC, abstractmethod from dataclasses import dataclass, field, asdict -import time +import re from typing import List, Dict, Optional, Any, Tuple, Union -import boto3 +import shutil +from pathlib import Path from pymongo import MongoClient from pymongo.operations import SearchIndexModel -import requests -import shutil -from pathlib import Path - logger = logging.getLogger("orgforge.memory") MONGO_URI = os.environ.get( "MONGO_URI", "mongodb://localhost:27017/?directConnection=true" ) DB_NAME = os.environ.get("DB_NAME", "orgforge") -OLLAMA_HOST = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434") -EMBED_PROVIDER = os.environ.get("EMBED_PROVIDER", "ollama") -EMBED_MODEL = os.environ.get("EMBED_MODEL", "mxbai-embed-large") -EMBED_DIMS = int(os.environ.get("EMBED_DIMS", "1024")) - - -_SKIP_EMBED_TYPES = { - "jira_ticket_created", - "jira_ticket_updated", - "confluence_page_created", - "slack_thread_created", - "slack_message_sent", - "pr_opened", - "pr_merged", - "email_sent", - "ticket_progress", - "novel_event_proposed", - "plan_rejected", - "end_of_day", - "agenda_item_deferred", - "proposed_event_rejected", - "watercooler_chat", -} _TICKET_PROGRESS_PROJECTION = { "_id": 0, @@ -123,299 +88,15 @@ def from_dict(cls, d: Dict) -> "SimEvent": ) -# ───────────────────────────────────────────── -# EMBEDDER INTERFACE -# ───────────────────────────────────────────── -class BaseEmbedder(ABC): - """All embedders must implement embed() and expose dims.""" - - def __init__(self, dims: int): - self.dims = dims - - @abstractmethod - def embed(self, text: str, input_type: str = "search_document") -> List[float]: - """Return a float vector of length self.dims.""" - ... - - def _fallback(self, text: str) -> List[float]: - """Deterministic hash-based pseudo-embedding used when provider is unreachable.""" - h = hashlib.sha256(text.encode()).digest() - vec = [(b / 255.0) - 0.5 for b in h] * (self.dims // 32 + 1) - return vec[: self.dims] - - -# ── LOCAL: Ollama ────────────────────────────── -class OllamaEmbedder(BaseEmbedder): - """Generates embeddings via a local Ollama instance.""" - - def __init__( - self, model: str = EMBED_MODEL, host: str = OLLAMA_HOST, dims: int = EMBED_DIMS - ): - super().__init__(dims) - self._model = model - self._host = host - self._ok = self._check_connection() - - def _check_connection(self) -> bool: - try: - r = requests.get(f"{self._host}/api/tags", timeout=3) - return r.status_code == 200 - except Exception: - logger.warning( - f"[memory] ⚠️ Cannot connect to Ollama at {self._host}. Using fallback hashing." - ) - return False - - def embed(self, text: str, input_type: str = "search_document") -> List[float]: - if not self._ok: - return self._fallback(text) - - _INSTRUCTIONS = { - "search_document": "Represent this sentence for searching relevant passages: ", - "search_query": "Represent this query for retrieving relevant documents: ", - } - prefix = _INSTRUCTIONS.get(input_type, "") - prefixed_text = prefix + text - - try: - r = requests.post( - f"{self._host}/api/embed", - json={"model": self._model, "input": prefixed_text}, - timeout=300, - ) - - r.raise_for_status() - - return r.json()["embeddings"][0] - except requests.exceptions.HTTPError as e: - # 2. Attempt to pull the specific error message from Ollama's JSON - error_details = "" - try: - error_details = f" Details: {e.response.json().get('error', '')}" - except Exception: - pass # Fails gracefully if response wasn't JSON - - logger.warning( - f"[memory] Ollama embedding HTTP error: {e.response.status_code}.{error_details}" - ) - return [] - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: - logger.warning(f"[memory] Ollama connection/timeout error: {e}") - return [] - except (KeyError, IndexError) as e: - logger.warning(f"[memory] Ollama embedding response malformed: {e}") - return [] - - -# ── CLOUD: OpenAI ────────────────────────────── -class OpenAIEmbedder(BaseEmbedder): - """ - Generates embeddings via OpenAI API (text-embedding-3-large = 3072 dims). - Requires: OPENAI_API_KEY environment variable. - """ - - def __init__(self, model: str = EMBED_MODEL, dims: int = EMBED_DIMS): - super().__init__(dims) - self._model = model - try: - from openai import OpenAI - - self._client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) - self._ok = True - logger.info(f"[memory] OpenAI embedder ready ({model}, {dims} dims)") - except ImportError: - logger.error( - "[memory] ⚠️ openai package not installed. Run: pip install openai" - ) - self._ok = False - except Exception as e: - logger.error(f"[memory] ⚠️ OpenAI init failed: {e}") - self._ok = False - - def embed(self, text: str) -> List[float]: - if not self._ok: - return self._fallback(text) - try: - resp = self._client.embeddings.create( - model=self._model, - input=text[:8191], # OpenAI hard token limit - dimensions=self.dims, # text-embedding-3-* supports custom dims - ) - return resp.data[0].embedding - except Exception as e: - logger.error(f"[memory] OpenAI embedding failed: {e}") - return self._fallback(text) - - -class InfinityEmbedder(BaseEmbedder): - """OpenAI-compatible embedder for Infinity server.""" - - _INSTRUCTIONS = { - "search_document": "", # no prefix at index time - "search_query": "Instruct: Given an enterprise knowledge query, retrieve the most relevant document\nQuery: ", - } - - def __init__( - self, model: str = EMBED_MODEL, host: str = OLLAMA_HOST, dims: int = EMBED_DIMS - ): - super().__init__(dims) - self._model = model - self._host = host - self._session = requests.Session() # reuse connections - self._ok = self._check_connection() - - def _check_connection(self) -> bool: - try: - r = self._session.get(f"{self._host}/health", timeout=3) - return r.status_code == 200 - except Exception: - logger.warning( - f"[memory] ⚠️ Cannot connect to Infinity at {self._host}. Using fallback." - ) - return False - - def embed(self, text: str, input_type: str = "search_document") -> List[float]: - if not self._ok: - return self._fallback(text) - - prefix = self._INSTRUCTIONS.get(input_type, "") - try: - r = self._session.post( - f"{self._host}/embeddings", - json={"model": self._model, "input": prefix + text}, - timeout=300, - ) - r.raise_for_status() - return r.json()["data"][0]["embedding"] - except Exception as e: - logger.warning(f"[memory] Infinity embedding failed: {e}") - return [] - - -# ── CLOUD: AWS Bedrock ───────────────────────── -class BedrockEmbedder(BaseEmbedder): - """ - Generates embeddings via AWS Bedrock Embeddings. - Requires: boto3, AWS credentials (IAM role, env vars, or ~/.aws/credentials). - """ - - EMBED_MODEL = "cohere.embed-v4:0" - EMBED_DIMS = 1024 - - def __init__(self, region: str = "us-east-1", dims: int = EMBED_DIMS): - super().__init__(dims) - self._region = region - try: - import boto3 - import json as _json - from botocore.config import Config - - self._client = boto3.client( - service_name="bedrock-runtime", - region_name=region, - config=Config( - read_timeout=60, - connect_timeout=10, - max_pool_connections=50, - retries={"max_attempts": 2}, - ), - ) - self._json = _json - self._ok = True - logger.info( - f"[memory] Bedrock embedder ready (Titan G1, {dims} dims, region={region})" - ) - except ImportError: - logger.error("[memory] ⚠️ boto3 not installed. Run: pip install boto3") - self._ok = False - except Exception as e: - logger.error(f"[memory] ⚠️ Bedrock init failed: {e}") - self._ok = False - - def embed(self, text: str, input_type: str = "search_document") -> List[float]: - if not self._ok: - return self._fallback(text) - - for attempt in range(3): - try: - body = self._json.dumps( - { - "texts": [text], - "input_type": input_type, - "embedding_types": ["float"], - "output_dimension": 1024, - } - ) - resp = self._client.invoke_model( - modelId=self.EMBED_MODEL, - body=body, - contentType="application/json", - accept="application/json", - ) - - raw_body = resp["body"].read() - resp["body"].close() - - result = self._json.loads(raw_body) - vector = result["embeddings"]["float"][0] - return vector - except self._client.exceptions.ThrottlingException: - wait = 6.2 * (attempt + 1) # backoff: 6.2, 12.4, 18.6 - logger.warning(f"[embed] throttled, waiting {wait}s") - time.sleep(wait) - except Exception as e: - logger.error(f"[memory] Bedrock embedding failed: {e}") - return self._fallback(text) - return self._fallback(text) - - -# ───────────────────────────────────────────── -# EMBEDDER FACTORY -# ───────────────────────────────────────────── -def build_embedder( - provider: str = EMBED_PROVIDER, - model: str = EMBED_MODEL, - dims: int = EMBED_DIMS, - **kwargs, -) -> BaseEmbedder: - """ - Return the correct embedder for the active quality_preset. - - provider values: - "ollama" → OllamaEmbedder (local_gpu preset) - "openai" → OpenAIEmbedder (cloud preset, OPENAI_API_KEY required) - "bedrock" → BedrockEmbedder (cloud preset, AWS credentials required) - """ - provider = provider.lower() - if provider == "openai": - return OpenAIEmbedder(model=model, dims=dims) - if provider == "bedrock": - region = kwargs.get( - "aws_region", os.environ.get("AWS_DEFAULT_REGION", "us-east-1") - ) - return BedrockEmbedder(region=region, dims=dims) - if provider == "infinity": - return InfinityEmbedder(model=model, dims=dims) - # Default: Ollama (local) - return OllamaEmbedder(model=model, dims=dims) - - -# ───────────────────────────────────────────── -# MEMORY (MongoDB) -# ───────────────────────────────────────────── class Memory: def __init__( self, mongo_uri: str = MONGO_URI, mongo_client=None, ): - self._embedder = build_embedder() - self._embed_worker = None self._client = mongo_client or MongoClient(mongo_uri) self._db = self._client[DB_NAME] - self._artifacts = self._db["artifacts"] - self._events = self._db["events"] self._artifacts = self._db["artifacts"] self._events = self._db["events"] self._jira = self._db["jira_tickets"] @@ -424,7 +105,9 @@ def __init__( self._slack = self._db["slack_messages"] self._plans = self._db["dept_plans"] self._conversation_summaries = self._db["conversation_summaries"] - + self._confluence_pages = self._db["confluence_pages"] + self._zoom_transcripts = self._db["zoom_transcripts"] + self._jira.create_index([("id", 1)], unique=True) self._jira.create_index([("assignee", 1), ("status", 1)]) self._prs.create_index([("pr_id", 1)], unique=True) @@ -444,77 +127,141 @@ def __init__( self._events.create_index([("type", 1), ("facts.participants", 1)]) self._checkpoints.create_index([("day", -1)]) self._jira.create_index([("dept", 1), ("status", 1)]) + self._events.create_index( + [ + ("type", 1), + ("facts.gap_classification", 1), + ("timestamp", -1), + ] + ) + self._confluence_pages.create_index([("day", 1)]) + self._confluence_pages.create_index([("metadata.author", 1)]) + self._confluence_pages.create_index([("metadata.dept", 1)]) + self._zoom_transcripts.create_index([("day", 1)]) + self._zoom_transcripts.create_index([("metadata.participants", 1)]) + self._current_day: int = 0 self._event_log: List[SimEvent] = [] - self._init_vector_indexes() + for coll_name in ("confluence_pages", "zoom_transcripts", "events"): + if coll_name not in self._db.list_collection_names(): + self._db.create_collection(coll_name) - def _embed( - self, - text: str, - input_type: str = "search_document", - caller: str = "unknown", - doc_id: str = "", - doc_type: str = "", - ) -> list: + self._init_text_indexes() + + def _init_text_indexes(self) -> None: """ - Single internal embed call. All methods route through here so token - logging is guaranteed regardless of which path triggers the embed. + Create the Atlas Search text index on artifacts + and a lightweight text index on events. + + The artifacts index follows the analyzer pattern from indexes.ts [16]: + - canonical_name: whole_name_analyzer on index, lucene.standard on + search, with a phrase multi-path using canonical_query_search_analyzer + - aliases: whole_name_analyzer on index, aliases_light on search, + with a shingle multi-path using alias_search_analyzer + - type, day: filter tokens for scoped queries + + The events index is a lightweight standard-analyzer index over + facts.root_cause and summary for the RecurrenceDetector text search leg. """ - vector = self._embedder.embed(text, input_type=input_type) - - return vector - - def _init_vector_indexes(self): - """Creates the Atlas Vector Search indexes if they don't exist.""" - index_definition = { - "fields": [ - { - "type": "vector", - "path": "embedding", - "numDimensions": EMBED_DIMS, - "similarity": "dotProduct", - "quantization": "scalar", + artifacts_index_definition = { + "analyzer": "lucene.standard", + "mappings": { + "dynamic": False, + "fields": { + "_id": {"type": "token"}, + "title": { + "type": "string", + "analyzer": "lucene.standard", + }, + "aliases": { + "type": "string", + "analyzer": "lucene.standard", + }, + "why_it_matters": { + "type": "string", + "analyzer": "lucene.standard", + }, + "type": {"type": "token"}, + "day": {"type": "number"}, + "metadata.author": {"type": "token"}, + "metadata.dept": {"type": "token"}, }, - {"type": "filter", "path": "type"}, - {"type": "filter", "path": "timestamp"}, - {"type": "filter", "path": "day"}, - ] + }, } - for coll_name in ["artifacts", "events"]: - coll = self._db[coll_name] + events_index_definition = { + "analyzer": "lucene.standard", + "mappings": { + "dynamic": False, + "fields": { + "type": {"type": "token"}, + "day": {"type": "number"}, + "facts.root_cause": { + "type": "string", + "analyzer": "lucene.standard", + }, + "summary": { + "type": "string", + "analyzer": "lucene.standard", + }, + }, + }, + } - existing = list(coll.list_search_indexes()) - vector_idx = next( - (i for i in existing if i.get("name") == "vector_index"), None - ) + index_specs = [ + ("confluence_pages", "confluence_search", artifacts_index_definition), + ("events", "event_text_search", events_index_definition), + ] - if vector_idx: - status = vector_idx.get("status") - if status in ("FAILED", "DOES_NOT_EXIST"): - logger.warning( - f"[memory] Index on {coll_name} is {status}. Dropping..." - ) - coll.drop_search_index("vector_index") - vector_idx = None + for coll_name, index_name, definition in index_specs: + coll = self._db[coll_name] + try: + existing = list(coll.list_search_indexes()) + found = next((i for i in existing if i.get("name") == index_name), None) + if found: + status = found.get("status", "") + if status in ("FAILED", "DOES_NOT_EXIST"): + logger.warning( + f"[memory] Search index {index_name} on {coll_name} " + f"is {status}. Dropping and recreating." + ) + coll.drop_search_index(index_name) + else: + logger.debug( + f"[memory] Search index {index_name} on {coll_name} " + f"already exists (status={status})." + ) + continue - if not vector_idx: - try: - search_index_model = SearchIndexModel( - definition=index_definition, - name="vector_index", - type="vectorSearch", - ) - coll.create_search_index(model=search_index_model) - logger.info(f"[memory] Created vector_index on {coll_name}") - except Exception as e: - logger.error(f"[memory] Failed to create index on {coll_name}: {e}") + model = SearchIndexModel( + definition=definition, + name=index_name, + type="search", + ) + coll.create_search_index(model=model) + logger.info( + f"[memory] Created search index {index_name} on {coll_name}." + ) + except Exception as e: + logger.error( + f"[memory] Failed to create search index {index_name} " + f"on {coll_name}: {e}" + ) - # ─── WRITE ──────────────────────────────── + try: + existing_idx = self._events.index_information() + if not any("text" in str(v.get("key")) for v in existing_idx.values()): + self._events.create_index( + [("facts.root_cause", "text"), ("summary", "text")], + name="event_text_search_legacy", + ) + logger.info("[memory] Created $text index on events collection.") + except Exception as e: + logger.warning(f"[memory] Could not create $text index on events: {e}") - def embed_artifact( + def store_artifact( self, id: str, type: str, @@ -524,10 +271,9 @@ def embed_artifact( date: str, timestamp: str, metadata: Optional[Dict] = None, - ): - """Upsert artifact into MongoDB immediately. Embedding is deferred to the - background queue if set_embed_worker() has been called, otherwise synchronous.""" - + aliases: Optional[List[str]] = None, + why_it_matters: Optional[str] = None, + ) -> None: doc = { "_id": id, "type": type, @@ -536,44 +282,189 @@ def embed_artifact( "day": day, "date": date, "timestamp": timestamp, - "embedding": None, + "aliases": aliases or [], + "why_it_matters": why_it_matters or "", "metadata": metadata or {}, } self._artifacts.update_one({"_id": id}, {"$set": doc}, upsert=True) - embed_text = f"{title}\n\n{content}" - if self._embed_worker is not None: - self._embed_worker.enqueue( - id=id, - type=type, - title=title, - content=embed_text, - day=day, - date=date, - timestamp=timestamp, - metadata=metadata, - _target="artifacts", - ) - else: - vector = self._embed( - embed_text, - input_type="search_document", - caller="embed_artifact", - doc_id=id, - doc_type=type, - ) - if vector: - self._artifacts.update_one( - {"_id": id}, - {"$set": {"embedding": vector}}, + + def store_confluence_page( + self, + id: str, + title: str, + content: str, + day: int, + date: str, + timestamp: str, + metadata: Optional[Dict] = None, + aliases: Optional[List[str]] = None, + why_it_matters: Optional[str] = None, + ) -> None: + """Store a Confluence page in its dedicated collection.""" + doc = { + "_id": id, + "title": title, + "content": content, + "day": day, + "date": date, + "timestamp": timestamp, + "aliases": aliases or [], + "why_it_matters": why_it_matters or "", + "metadata": metadata or {}, + } + self._confluence_pages.update_one({"_id": id}, {"$set": doc}, upsert=True) + + def store_zoom_transcript( + self, + id: str, + title: str, + content: str, + day: int, + date: str, + timestamp: str, + metadata: Optional[Dict] = None, + ) -> None: + """Store a Zoom transcript in its dedicated collection.""" + doc = { + "_id": id, + "title": title, + "content": content, + "day": day, + "date": date, + "timestamp": timestamp, + "metadata": metadata or {}, + } + self._zoom_transcripts.update_one({"_id": id}, {"$set": doc}, upsert=True) + + def embed_artifact( + self, + id: str, + type: str, + title: str, + content: str, + day: int, + date: str, + timestamp: str, + metadata: Optional[Dict] = None, + aliases: Optional[List[str]] = None, + why_it_matters: Optional[str] = None, + ) -> None: + """ + Routing function. Sends documents to their dedicated collections. + Replaces the old pattern of dumping everything into 'artifacts'. + + Types that already live in their own collection (jira, pr, email, + slack_thread, zd_ticket, sf_opportunity) are no-ops here since + they are persisted by their own upsert methods. + """ + if type == "confluence": + self.store_confluence_page( + id=id, + title=title, + content=content, + day=day, + date=date, + timestamp=timestamp, + metadata=metadata, + aliases=aliases, + why_it_matters=why_it_matters, + ) + elif type == "persona_skill": + name = (metadata or {}).get("name", id) + dept = (metadata or {}).get("dept", "") + self.store_persona_skills( + name=name, + data={"expertise": aliases or [], "social_role": "", "style": ""}, + dept=dept, + day=day, + timestamp_iso=timestamp, + ) + elif type == "zoom_transcript": + self.store_zoom_transcript( + id=id, + title=title, + content=content, + day=day, + date=date, + timestamp=timestamp, + metadata=metadata, + ) + elif type in ("jira", "jira_comment", "pr", "email", "slack_thread", + "slack", "zd_ticket", "sf_opportunity"): + pass + else: + logger.warning( + f"[memory] embed_artifact called with unrouted type='{type}' " + f"(id={id}). Skipping storage." ) - def log_event(self, event: SimEvent): - """Insert SimEvent into MongoDB. Embedding is deferred to the background - queue if one is attached via set_embed_worker(); otherwise synchronous.""" + def store_persona_skills( + self, + name: str, + data: Dict[str, Any], + dept: str, + day: int, + timestamp_iso: str, + ) -> None: + raw_expertise = data.get("expertise", []) + if isinstance(raw_expertise, str): + raw_expertise = [raw_expertise] + expertise_tags = [e.lower() for e in raw_expertise] + alias_terms = list(dict.fromkeys(expertise_tags + [dept.lower()])) + + expertise_val = data.get("expertise", []) + if isinstance(expertise_val, str): + expertise_val = [expertise_val] + skill_text = ( + f"Employee: {name}. Dept: {dept}. " + f"Expertise: {', '.join(expertise_val)}. " + f"Role: {data.get('social_role', '')}. " + f"Style: {data.get('style', '')}" + ) + + self.store_artifact( + id=f"skill_{name.lower().replace(' ', '_')}", + type="persona_skill", + title=f"Expertise Profile: {name}", + content=skill_text, + day=day, + date=timestamp_iso, + timestamp=timestamp_iso, + metadata={"name": name, "dept": dept}, + aliases=alias_terms, + why_it_matters=f"{name} owns {dept} domain knowledge: {', '.join(expertise_tags[:4])}", + ) + + def embed_persona_skills( + self, + name: str, + data: Dict[str, Any], + dept: str, + day: int, + timestamp_iso: str, + ) -> None: + """ + Backward-compatible alias for store_persona_skills(). + Called from flow.py genesis phase and org_lifecycle.py hire path. + """ + self.store_persona_skills( + name=name, + data=data, + dept=dept, + day=day, + timestamp_iso=timestamp_iso, + ) + + def log_event(self, event: SimEvent) -> None: + """ + Insert SimEvent into in-memory log and MongoDB. + No embedding. The event_text_search Atlas Search index and the + $text index on events.facts.root_cause handle text retrieval. + """ self._event_log.append(event) - if len(self._event_log) % 100 == 0: # check periodically, not every call + if len(self._event_log) % 100 == 0: cutoff_day = (self._current_day or 0) - _EVENT_LOG_MAX_DAYS if cutoff_day > 0: self._event_log = [e for e in self._event_log if e.day >= cutoff_day] @@ -582,125 +473,136 @@ def log_event(self, event: SimEvent): doc = event.to_dict() doc["_id"] = event_id - doc["embedding"] = None # written immediately; filled in by embed worker self._events.update_one({"_id": event_id}, {"$set": doc}, upsert=True) - # Embed asynchronously if a worker is attached, synchronously otherwise - if event.type not in _SKIP_EMBED_TYPES: - if self._embed_worker is not None: - self._embed_worker.enqueue( - id=event_id, - type=event.type, - title=event.summary, - content=event.to_embed_text(), - day=event.day, - date=event.date, - timestamp=event.timestamp, - _target="events", # signals worker to update events, not artifacts - ) - else: - vector = self._embed( - event.to_embed_text(), - input_type="search_document", - caller="log_event", - doc_id=event_id, - doc_type=event.type, - ) - self._events.update_one( - {"_id": event_id}, {"$set": {"embedding": vector}} - ) - - @staticmethod - def _to_iso(as_of_time: Optional[Any]) -> Optional[str]: - """ - Normalise as_of_time to an ISO 8601 string for MongoDB comparisons. - Accepts datetime objects (from SimClock) or pre-formatted ISO strings - (legacy callers that already called .isoformat()). Returns None - unchanged so optional semantics are preserved throughout. - """ - if as_of_time is None: - return None - if isinstance(as_of_time, str): - return as_of_time - return as_of_time.isoformat() - - def recall( + def search_artifacts_text( self, query: str, n: int = 5, type_filter: Optional[str] = None, type_exclude: Optional[List[str]] = None, - day_range: Optional[tuple] = None, - since: Optional[Any] = None, + day_range: Optional[Tuple[int, int]] = None, as_of_time: Optional[Any] = None, + min_score: float = 1.0, ) -> List[Dict]: """ - Hybrid vector + metadata search over artifacts. + Alias-weighted BM25 text search over the artifacts collection using + the beliefs_search Atlas Search index. + + Scoring mirrors beliefsReader.searchText() [17]: + - canonical_name exact/fuzzy: boost 14 + - canonical_name phrase: boost 14 + - aliases fuzzy: boost 5 + - aliases shingle: boost 14 - since (datetime | str | None, SimClock-sourced): - Hard causal floor — only artifacts whose timestamp is >= this - value are eligible. + This score separation ensures a query for "auth token expiry" ranks + Jordan's persona skill record (aliases: ["auth", "identity", "sso"]) + well above unrelated artifacts, replacing cosine similarity entirely. - as_of_time (datetime | str | None, SimClock-sourced): - Hard causal ceiling — only artifacts whose timestamp is <= this - value are eligible. Enforced inside the $vectorSearch pre-filter - so the ANN candidate set is already bounded before scoring; no - post-retrieval pruning is needed or performed. + Args: + query: The search string. Noise-stripped by callers where + appropriate (context_for_prompt strips via the same + pattern as queryExpander.ts [19]). + n: Maximum results to return. + type_filter: Restrict to a single artifact type (e.g. "confluence"). + type_exclude: Exclude one or more artifact types. + day_range: (min_day, max_day) inclusive filter. + as_of_time: Causal ceiling -- only artifacts with timestamp <= + this value are eligible. + min_score: Minimum Atlas Search score threshold (default 1.0). - Both `since` and `as_of_time` accept a datetime (``clock.now(actor)``) - or a pre-formatted ISO string — both are normalised internally via _to_iso(). + Returns: + List of dicts with keys: id, title, type, day, timestamp, + metadata, canonical_name, aliases, score. """ - query_vec = self._embed( - query, - input_type="search_query", - caller="recall", - doc_id=query[:75], - doc_type="query", - ) + if not query or not query.strip(): + return [] - # Build the exact-match pre-filter for MongoDB Vector Search - filter_doc: Dict[str, Any] = {} if type_filter and type_exclude: raise ValueError( - "recall(): type_filter and type_exclude are mutually exclusive" + "search_artifacts_text(): type_filter and type_exclude " + "are mutually exclusive." ) + + fuzzy_opts = {"maxEdits": 1, "prefixLength": 2} + + filter_clauses: List[Dict] = [] + if type_filter: - filter_doc["type"] = {"$eq": type_filter} - if type_exclude: - filter_doc["type"] = {"$nin": type_exclude} - if day_range: - filter_doc["day"] = {"$gte": day_range[0], "$lte": day_range[1]} + filter_clauses.append({"equals": {"path": "type", "value": type_filter}}) + elif type_exclude: + for t in type_exclude: + filter_clauses.append({"equals": {"path": "type", "value": t}}) - # Handle time-based floor and ceiling - iso_floor = self._to_iso(since) iso_ceiling = self._to_iso(as_of_time) - if iso_floor is not None or iso_ceiling is not None: - timestamp_filter = {} - if iso_floor is not None: - timestamp_filter["$gte"] = iso_floor - if iso_ceiling is not None: - timestamp_filter["$lte"] = iso_ceiling - filter_doc["timestamp"] = timestamp_filter + if type_filter == "persona_skill": + _index_name = "persona_skill_search" + else: + _index_name = "confluence_search" - pipeline = [ - { - "$vectorSearch": { - "index": "vector_index", - "path": "embedding", - "queryVector": query_vec, - "numCandidates": max(n * 10, 100), # MongoDB ANN requirement - "limit": n, - } - } + search_stage: Dict[str, Any] = { + "index": _index_name, + "compound": { + "should": [ + { + "text": { + "query": query, + "path": "title", + "score": {"boost": {"value": 3}}, + } + }, + { + "text": { + "query": query, + "path": "aliases", + "score": {"boost": {"value": 2}}, + } + }, + { + "text": { + "query": query, + "path": "why_it_matters", + "score": {"boost": {"value": 1}}, + } + }, + ], + "minimumShouldMatch": 1, + }, + } + + if type_exclude: + search_stage["compound"]["mustNot"] = [ + {"equals": {"path": "type", "value": t}} for t in type_exclude + ] + filter_clauses = [ + fc + for fc in filter_clauses + if not ("equals" in fc and fc["equals"].get("path") == "type") + ] + + if filter_clauses: + search_stage["compound"]["filter"] = filter_clauses + + pipeline: List[Dict] = [ + {"$search": search_stage}, + {"$addFields": {"score": {"$meta": "searchScore"}}}, + {"$match": {"score": {"$gte": min_score}}}, ] - if filter_doc: - pipeline[0]["$vectorSearch"]["filter"] = filter_doc + match_stage: Dict[str, Any] = {} + if day_range: + match_stage["day"] = { + "$gte": day_range[0], + "$lte": day_range[1], + } + if iso_ceiling: + match_stage["timestamp"] = {"$lte": iso_ceiling} + if match_stage: + pipeline.append({"$match": match_stage}) - # Project output — include metadata so callers can read author, - # parent_id, etc. without a second round-trip. Raw embedding excluded. + pipeline.append({"$limit": n}) pipeline.append( { "$project": { @@ -708,196 +610,503 @@ def recall( "title": 1, "type": 1, "day": 1, - "date": 1, "timestamp": 1, "metadata": 1, - "score": {"$meta": "vectorSearchScore"}, + "aliases": 1, + "why_it_matters": 1, + "score": 1, } } ) + if type_filter == "persona_skill": + target_coll = self._persona_skills + elif type_filter == "confluence": + target_coll = self._confluence_pages + else: + # Default: query confluence_pages (the most common search target + # after persona_skill is excluded via type_exclude) + target_coll = self._confluence_pages + try: - results = list(self._artifacts.aggregate(pipeline)) - return results + return list(target_coll.aggregate(pipeline)) except Exception as e: - logger.error(f"[memory] recall failed: {e}") + logger.error(f"[memory] search_artifacts_text failed: {e}") return [] - def find_confluence_experts( + def search_persona_skills_text( self, - topic: str, - score_threshold: float = 0.75, + query: str, n: int = 5, - as_of_time: Optional[Any] = None, - ) -> List[Dict[str, str]]: + ) -> List[Dict]: + """ + Find the best-matched engineers for a topic by querying domain_registry + directly. """ - Vector search over Confluence artifacts to find subject-matter experts - for a given topic. Returns a list of {title, author, score, day} dicts - ordered by descending relevance score. + if not query or not query.strip(): + return [] - This is the semantic alternative to keyword matching in - NormalDayHandler._expertise_matched_participants(). Because every - Confluence page is embedded at write-time via embed_artifact(), this - call reuses existing vectors — no new embed calls are made for the - stored pages, only one embed call is made for the topic query string. + query_tokens = { + t.lower() + for t in re.split(r"[\s_\-/]+", query.lower()) + if len(t) >= 3 + } + if not query_tokens: + return [] - score_threshold filters out weak matches so only genuinely relevant - authors are injected into conversations. 0.75 is a reasonable default - for dotProduct similarity with 1024-dim vectors; tune down to ~0.65 for - shorter topics that produce noisier embeddings. + matched_domains = list( + self._db["domain_registry"].find( + {"system_tags": {"$in": list(query_tokens)}}, + { + "domain": 1, + "primary_owner": 1, + "known_by": 1, + "dept": 1, + "system_tags": 1, + "documentation_coverage": 1, + "_id": 0, + }, + ) + ) - Args: - topic: The subject of the Slack thread or design discussion. - score_threshold: Minimum similarity score to be considered an expert. - n: Max number of candidate pages to consider. - as_of_time: datetime or ISO string — only pages written before - this moment are eligible. Passed directly into - recall() so the cutoff is enforced in the MongoDB - $vectorSearch pre-filter, not post-retrieval. - Normalised via _to_iso() inside recall(). + if not matched_domains: + return [] - Returns: - List of dicts with keys: title, author, score, day. - Empty list if no pages exceed the threshold. - """ - # as_of_time is pushed into the $vectorSearch pre-filter inside - # recall() — the ANN candidate pool is already time-bounded, so no - # manual post-retrieval timestamp check is required here. - results = self.recall( - query=topic, - n=n, - type_filter="confluence", - as_of_time=as_of_time, - ) + candidate_scores: Dict[str, float] = {} + candidate_dept: Dict[str, str] = {} - experts: List[Dict[str, str]] = [] - for r in results: - if r.get("score", 0) < score_threshold: + for rec in matched_domains: + tags = set(rec.get("system_tags", [])) + overlap = len(query_tokens & tags) + if overlap == 0: continue - author = r.get("metadata", {}).get("author") - if author: - experts.append( - { - "title": r.get("title", ""), - "author": author, - "score": round(r.get("score", 0), 4), - "day": r.get("day", 0), - } + + dept = rec.get("dept", "") + coverage = rec.get("documentation_coverage", 0.0) + base_score = overlap * (1.0 + coverage) + + primary = rec.get("primary_owner") + if primary: + candidate_scores[primary] = ( + candidate_scores.get(primary, 0.0) + base_score * 1.5 ) + candidate_dept.setdefault(primary, dept) - return experts + for name in rec.get("known_by", []): + if name == primary: + continue + candidate_scores[name] = ( + candidate_scores.get(name, 0.0) + base_score + ) + candidate_dept.setdefault(name, dept) - def find_expert_by_skill(self, topic: str, n: int = 1) -> List[Dict]: - """ - DRY: Uses the existing recall() pattern to find the best persona match. - """ - results = self.recall(query=topic, n=n, type_filter="persona_skill") + if not candidate_scores: + return [] + + ranked = sorted(candidate_scores.items(), key=lambda x: x[1], reverse=True) return [ { - "name": r.get("metadata", {}).get("name"), - "dept": r.get("metadata", {}).get("dept"), - "score": r.get("score"), + "name": name, + "dept": candidate_dept.get(name, ""), + "score": round(score, 4), } - for r in results - if r.get("score", 0) > 0.65 + for name, score in ranked[:n] ] - def embed_persona_skills( - self, name: str, data: Dict[str, Any], dept: str, day: int, timestamp_iso: str - ): + def record_author_expertise_signals( + self, + author: str, + artifact_id: str, + artifact_type: str, + day: int, + timestamp_iso: str, + topics_in_doc: List[str], + topics_outside_my_expertise: Optional[List[str]] = [], + claims_approximated: Optional[List[str]] = [], + sections_left_thin: Optional[List[str]] = [], + ) -> None: """ - Standardizes employee expertise into searchable 'persona_skill' artifacts - using the simulation's current clock time. + Persist LLM-produced skill signals into the author_expertise collection. + + Args: + author: The engineer who authored the artifact. + artifact_id: ID of the source artifact (e.g. "CONF-ENG-017"). + artifact_type: Type of the source artifact (e.g. "confluence"). + day: Current simulation day. + timestamp_iso: ISO 8601 timestamp for the update. + topics_in_doc: All topics the LLM identified in the artifact. + topics_outside_my_expertise: Topics the LLM flagged as beyond the + author's known expertise. + claims_approximated: Statements the LLM produced with low + confidence. + sections_left_thin: Sections the LLM identified as + under-documented. """ - skill_text = ( - f"Employee: {name}. Dept: {dept}. " - f"Expertise: {', '.join(data.get('expertise', []))}. " - f"Role: {data.get('social_role', '')}. " - f"Style: {data.get('style', '')}" + if not topics_in_doc: + return + + doc: Dict[str, Any] = { + "author": author, + "artifact_id": artifact_id, + "artifact_type": artifact_type, + "day": day, + "timestamp": timestamp_iso, + "topics_in_doc": topics_in_doc, + "topics_outside_my_expertise": topics_outside_my_expertise, + "claims_approximated": claims_approximated, + "sections_left_thin": sections_left_thin, + } + + try: + self._db["author_expertise"].insert_one(doc) + except Exception as e: + logger.warning( + f"[memory] record_author_expertise_signals failed for " + f"author={author}, artifact_id={artifact_id}: {e}" + ) + return + + logger.debug( + f"[memory] record_author_expertise_signals: author={author}, " + f"artifact_type={artifact_type}, artifact_id={artifact_id}" ) - self.embed_artifact( - id=f"skill_{name.lower()}", - type="persona_skill", - title=f"Expertise Profile: {name}", - content=skill_text, - day=day, - date=timestamp_iso, - timestamp=timestamp_iso, - metadata={"name": name, "dept": dept}, + def find_expert_by_skill(self, topic: str, n: int = 1) -> List[Dict]: + """ + Backward-compatible alias for search_persona_skills_text(). + Called from flow.py _select_domain_expert() and org_lifecycle.py + scan_for_knowledge_gaps(). Returns [{name, dept, score}]. + """ + return self.search_persona_skills_text(query=topic, n=n) + + + def domain_context_for_topic( + self, + topic: str, + as_hint: bool = False, + ) -> Tuple[List[Dict], str]: + """ + Query domain_registry for all domains whose system_tags overlap the + topic string. Returns both the raw docs and a pre-formatted string + so callers never duplicate tokenisation or formatting logic. + """ + tokens = [ + t.lower() + for t in re.split(r"[\s_\-/]+", topic.lower()) + if len(t) >= 3 + ] + if not tokens: + return [], "" + + docs = list( + self._db["domain_registry"].find( + {"system_tags": {"$in": tokens}}, + { + "domain": 1, + "primary_owner": 1, + "known_by": 1, + "documentation_coverage": 1, + "_id": 0, + }, + ) + ) + if not docs: + return [], "" + + if as_hint: + lines = [ + "Note: the following domain knowledge exists and may be referenced naturally:" + ] + for rec in docs: + pct = int(rec.get("documentation_coverage", 0) * 100) + owner = rec.get("primary_owner") + known_by = rec.get("known_by", []) + if owner: + lines.append( + f" - '{rec['domain']}': owned by {owner} ({pct}% documented)" + ) + elif known_by: + lines.append( + f" - '{rec['domain']}': no primary owner, " + f"partial knowledge held by {', '.join(known_by)} ({pct}% documented)" + ) + else: + lines.append( + f" - '{rec['domain']}': orphaned domain ({pct}% documented, no known experts)" + ) + else: + lines = ["DOMAIN CONTEXT (from domain_registry):"] + for rec in docs: + pct = int(rec.get("documentation_coverage", 0) * 100) + owner = rec.get("primary_owner", "none") + lines.append( + f" - {rec['domain']}: owner={owner}, coverage={pct}%" + ) + + return docs, "\n".join(lines) + + def get_author_domain_tokens(self, author: str) -> set[str]: + """ + Returns the set of lowercase tokens representing an author's live + domain knowledge. Primary source is domain_registry (earned through + PRs, incidents, Confluence authorship). Falls back to PERSONAS config + on Day 0 before any registry signal exists. + + Mirrors TicketAssigner._skill_score's vocabulary strategy so both + assignment scoring and self-audit expertise comparison use identical + token sets. + """ + domain_docs = list( + self._db["domain_registry"].find( + {"$or": [{"known_by": author}, {"primary_owner": author}]}, + {"system_tags": 1, "_id": 0}, + ) + ) + + if domain_docs: + all_tags = [tag for doc in domain_docs for tag in doc.get("system_tags", [])] + return {t.lower() for t in all_tags} + + from config_loader import PERSONAS, DEFAULT_PERSONA + persona = PERSONAS.get(author, DEFAULT_PERSONA) + return {e.lower() for e in persona.get("expertise", [])} + + def get_author_domain_tokens(self, author: str) -> set[str]: + """ + Returns the set of lowercase tokens representing an author's live + domain knowledge. Primary source is domain_registry (earned through + PRs, incidents, Confluence authorship). Falls back to PERSONAS config + on Day 0 before any registry signal exists. + + Mirrors TicketAssigner._skill_score's vocabulary strategy so both + assignment scoring and self-audit expertise comparison use identical + token sets. + """ + domain_docs = list( + self._db["domain_registry"].find( + {"$or": [{"known_by": author}, {"primary_owner": author}]}, + {"system_tags": 1, "_id": 0}, + ) ) - def recall_events( + if domain_docs: + all_tags = [tag for doc in domain_docs for tag in doc.get("system_tags", [])] + return {t.lower() for t in all_tags} + + from config_loader import PERSONAS, DEFAULT_PERSONA + persona = PERSONAS.get(author, DEFAULT_PERSONA) + return {e.lower() for e in persona.get("expertise", [])} + + def context_for_prompt( self, query: str, - n: int = 3, + n: int = 4, as_of_time: Optional[Any] = None, - ) -> List[SimEvent]: + since: Optional[Any] = None, + ) -> str: """ - Returns the n most relevant SimEvents for the query using vector search. - Falls back to recency sort if the query embedding fails or returns empty. + Three-tier context block for LLM prompts, replacing vector RAG. + + Follows the contextBuilder.ts tiered pattern [18]: - as_of_time (datetime | str | None, SimClock-sourced): - Hard causal ceiling — only events whose timestamp is ≤ this value - are eligible. + Tier 1 -- Pinned / always-on (injected unconditionally): + Tech stack doc + any active knowledge_gap_detected events whose + domain tags overlap the query. These are structural facts the LLM + must always have regardless of query similarity. + + Tier 2 -- Text search (alias-weighted BM25): + search_artifacts_text() over artifacts, noise-stripped query, + excludes persona_skill type. The score separation from canonical + and alias boosting means the top results are genuinely relevant + rather than the semantically proximate blob that vector search + returns. + + Tier 3 -- Open questions (scoped by type, not search score): + Unresolved knowledge_gap_detected events fetched by type from + MongoDB. These are injected so the LLM is aware of documented + uncertainty in the current scope, matching the open_questions tier + in contextBuilder.ts [18]. + + as_of_time and since enforce causal ceilings/floors on Tier 2 results. """ - iso = self._to_iso(as_of_time) + stripped_query = self._strip_query_noise(query) + iso_ceiling = self._to_iso(as_of_time) + iso_floor = self._to_iso(since) + lines: List[str] = [] - # ── Attempt vector search ───────────────────────────────────────────── - query_vec = self._embed( - query, - input_type="search_query", - caller="recall_events", - doc_id=query[:75], - doc_type="query", - ) + tech = self.tech_stack_for_prompt() + if tech: + lines.append(tech) - if query_vec: - pipeline: List[Dict[str, Any]] = [ - { - "$vectorSearch": { - "index": "vector_index", - "path": "embedding", - "queryVector": query_vec, - "numCandidates": max(n * 10, 100), - "limit": n, - **({"filter": {"timestamp": {"$lte": iso}}} if iso else {}), - } - }, - { - "$project": { - "_id": 0, - "embedding": 0, - "tags": 0, - "artifacts": 0, - } - }, - ] - try: - results = list(self._events.aggregate(pipeline)) - if results: - return [SimEvent(**{k: v for k, v in e.items()}) for e in results] - except Exception as e: - logger.warning(f"[memory] recall_events vector search failed: {e}") + gap_filter: Dict[str, Any] = {"type": "knowledge_gap_detected"} + if iso_ceiling: + gap_filter["timestamp"] = {"$lte": iso_ceiling} - # ── Fallback: recency sort ──────────────────────────────────────────── - logger.debug("[memory] recall_events falling back to recency sort") - query_filter: Dict[str, Any] = {} - if iso: - query_filter["timestamp"] = {"$lte": iso} + active_gaps = list( + self._events.find(gap_filter, {"_id": 0}).sort("timestamp", -1).limit(3) + ) + if active_gaps: + query_lower = stripped_query.lower() + relevant_gaps = [ + g + for g in active_gaps + if any( + domain.lower() in query_lower + for domain in g.get("facts", {}).get("gap_areas", []) + + g.get("facts", {}).get("topics_beyond_author_expertise", []) + ) + ] + if relevant_gaps: + lines.append("=== ACTIVE KNOWLEDGE GAPS (awareness only) ===") + for g in relevant_gaps: + facts = g.get("facts", {}) + domains = facts.get("gap_areas") or facts.get( + "topics_beyond_author_expertise", [] + ) + lines.append( + f" Day {g.get('day', '?')} — gap in: " + f"{', '.join(domains)} " + f"(detection: {facts.get('detection_method', '?')})" + ) - results = ( - self._events.find( - query_filter, - {"_id": 0, "embedding": 0, "tags": 0, "artifacts": 0}, + if stripped_query: + day_range: Optional[Tuple[int, int]] = None + if iso_floor: + pass + + artifacts = self.search_artifacts_text( + query=stripped_query, + n=n, + type_exclude=["persona_skill"], + as_of_time=as_of_time, + min_score=1.0, ) - .sort("timestamp", -1) - .limit(n) + + if artifacts: + lines.append("=== RELEVANT ARTIFACTS ===") + for a in artifacts: + day_label = f"Day {a.get('day')}" if "day" in a else "?" + lines.append( + f" [{a.get('type', '').upper()}] {a.get('id')} -- " + f"{a.get('title')} ({day_label}, " + f"score {a.get('score', 0):.2f})" + ) + + open_q_filter: Dict[str, Any] = { + "type": "knowledge_gap_detected", + "facts.gap_classification": {"$in": ["likely", "possible"]}, + } + if iso_ceiling: + open_q_filter["timestamp"] = {"$lte": iso_ceiling} + + open_questions = list( + self._events.find(open_q_filter, {"_id": 0}).sort("timestamp", -1).limit(3) + ) + if open_questions: + lines.append("=== OPEN QUESTIONS / UNRESOLVED GAPS ===") + for q in open_questions: + facts = q.get("facts", {}) + topic = ( + facts.get("topic") + or facts.get("gap_domain") + or ", ".join(facts.get("gap_areas", [])) + or "unknown" + ) + classification = facts.get("gap_classification", "?") + lines.append(f" Day {q.get('day', '?')} — [{classification}] {topic}") + + if not lines: + return "No relevant prior context found." + + return "\n".join(lines) + + def recall_with_rewrite( + self, + raw_query: str, + n: int = 4, + as_of_time: Optional[Any] = None, + since: Optional[Any] = None, + ) -> str: + """ + Backward-compatible alias for context_for_prompt(). + + Previously performed HyDE query rewriting before embedding. That LLM + call and the embedding are now gone. The noise-stripping inside + context_for_prompt() handles the same cleanup that HyDE was compensating + for -- stripping code blocks, stack traces, and markdown before BM25 + search produces cleaner results than HyDE + cosine did. + + Called from confluence_writer.py write_design_doc() and + write_adhoc_page(). No callers need to change. + """ + return self.context_for_prompt( + query=raw_query, + n=n, + as_of_time=as_of_time, + since=since, ) - return [SimEvent(**{k: v for k, v in e.items()}) for e in results] - # ─── HELPERS ────────────────────────────── + @staticmethod + def _strip_query_noise(text: str) -> str: + """ + Remove noise from a query string before BM25 search. + + Mirrors the STRIP_PATTERNS pipeline from queryExpander.ts [19]: + - Fenced code blocks + - Inline code + - Stack trace lines (starting with "at ") + - URLs + - File paths + - Markdown headings and bold/italic markers + - HTML tags + - Long hex strings (commit hashes, UUIDs) + - Pure numbers + - Trailing punctuation + + Returns the cleaned string, or the original if cleaning produces + something too short to be useful (< 4 chars). + """ + import re + + patterns = [ + r"```[\s\S]*?```", + r"~~~[\s\S]*?~~~", + r"^\s+at\s+.+$", + r"https?://[^\s]+", + r"`[^`]+`", + r"^#{1,6}\s+", + r"\*{1,2}([^*]+)\*{1,2}", + r"_{1,2}([^_]+)_{1,2}", + r"<[^>]+>", + r"\b[0-9a-f]{8,}\b", + r"\b\d+\b", + r"[?!.,;:]+(?=\s|$)", + ] + + out = text + for pattern in patterns: + out = re.sub(pattern, " ", out, flags=re.MULTILINE | re.IGNORECASE) + + out = re.sub(r"\s{2,}", " ", out).strip() + + if len(out) < 4: + return text.strip() + + return out + + @staticmethod + def _to_iso(as_of_time: Optional[Any]) -> Optional[str]: + """ + Normalise as_of_time to an ISO 8601 string for MongoDB comparisons. + Accepts datetime objects or pre-formatted ISO strings. + Returns None unchanged so optional semantics are preserved. + """ + if as_of_time is None: + return None + if isinstance(as_of_time, str): + return as_of_time + return as_of_time.isoformat() _NOISY_PERSONAL_EVENTS = { "sprint_planned", @@ -912,11 +1121,6 @@ def recall_events( } def persona_history(self, name: str, n: int = 4) -> List[SimEvent]: - """ - Retrieves the n most recent events for a person, filtering out - macro-level or noisy events where the person's individual agency - is diluted (e.g., being tagged in a 24-ticket sprint planning summary). - """ relevant = [ e for e in self._event_log @@ -927,109 +1131,22 @@ def persona_history(self, name: str, n: int = 4) -> List[SimEvent]: def get_event_log( self, from_db: bool = False, as_of_time: Optional[str] = None ) -> List[SimEvent]: - """ - Returns the event log, optionally filtered by a specific timestamp - to prevent 'seeing into the future.' - """ - if from_db: - query = {} + query: Dict[str, Any] = {} if as_of_time: query["timestamp"] = {"$lte": as_of_time} - raw = self._events.find(query).sort("timestamp", 1) return [SimEvent.from_dict(r) for r in raw] log = self._event_log if as_of_time: log = [e for e in log if e.timestamp <= as_of_time] - return log def events_by_type(self, event_type: str) -> List[SimEvent]: return [e for e in self._event_log if e.type == event_type] - def context_for_prompt( - self, - query: str, - n: int = 4, - as_of_time: Optional[Any] = None, - since: Optional[Any] = None, - ) -> str: - """ - RAG context block for LLM prompts — embeds ``query``, retrieves the - most semantically similar artifacts and events, and formats them into - a text block that can be injected directly into a system or user prompt. - - as_of_time (datetime | str | None, SimClock-sourced): - Hard causal ceiling applied to *both* the artifact search and the - event search. Any artifact or event with a timestamp after this - value is invisible to the LLM. - - Accepts a datetime (``clock.now(actor)``) or a pre-formatted ISO - string — both are normalised internally via _to_iso(), so callers - that already hold an ISO string do not need to change. - """ - lines = [] - - # ── Artifact retrieval ───────────────────────────────────────────── - # as_of_time is pushed into the MongoDB $vectorSearch pre-filter inside - # recall(), so the ANN candidate pool is already time-bounded. - # We oversample (n * 3) to give the vector scorer enough candidates - # before the timestamp filter shrinks the eligible set. - artifact_count = self._artifacts.count_documents({}) - oversample = max(n * 3, min(artifact_count // 10, 200)) - - artifacts = self.recall( - query=query, - n=oversample, - as_of_time=as_of_time, - since=since, - type_exclude=["persona_skill"], - ) - # Trim to the top-n by vector score after the pre-filter is applied - artifacts = sorted(artifacts, key=lambda a: a.get("score", 0), reverse=True)[:n] - - if artifacts: - lines.append("=== RELEVANT ARTIFACTS ===") - for a in artifacts: - day_label = f"Day {a.get('day')}" if "day" in a else "?" - lines.append( - f" [{a.get('type', '').upper()}] {a.get('id')} — " - f"{a.get('title')} ({day_label}, " - f"relevance {a.get('score', 0):.3f})" - ) - - # ── Event retrieval ──────────────────────────────────────────────── - # recall_events() applies the same as_of_time ceiling against the - # events collection — same datetime type, same contract. - events = self.recall_events(query, n=3, as_of_time=as_of_time) - - tech = self.tech_stack_for_prompt() - if tech: - lines.append(tech) - - if events: - lines.append("=== RELEVANT EVENTS ===") - for e in events: - lines.append( - f" Day {e.day} ({e.date}) [{e.type}]: {e.summary} " - f"| actors={e.actors} | artifacts={e.artifact_ids} " - f"| facts={e.facts}" - ) - - if not lines: - return "No relevant prior context found." - - return "\n".join(lines) - def previous_day_context(self, current_day: int) -> str: - """ - Returns a tight CEO-oriented summary of the previous day for the org - theme prompt. Bounded output regardless of sim length — pulls the - day_summary health snapshot plus a small allowlist of high-signal - events only. No vector search. - """ if current_day == 1: return ( "This is the first observed day. The company has existing systems, " @@ -1039,9 +1156,6 @@ def previous_day_context(self, current_day: int) -> str: prev_day = current_day - 1 - # ── 1. Health header from day_summary facts ─────────────────────────── - # One structured document gives health/morale/incident counts without - # enumerating every individual event. summary_doc = self._events.find_one( {"type": "day_summary", "day": prev_day}, {"facts": 1, "date": 1, "_id": 0}, @@ -1059,12 +1173,6 @@ def previous_day_context(self, current_day: int) -> str: f"Health trend: {f.get('health_trend', 'unknown')}." ) - # ── 2. High-signal events only ──────────────────────────────────────── - # Explicit allowlist keeps output bounded as the sim grows. - # Operational noise (deep_work, watercooler, async_question, mentoring, - # 1on1, design_discussion, jira_ticket_created, confluence_created) is - # intentionally excluded — the CEO sets tone from pressure and risk, not - # from knowing who mentored who yesterday. _ALLOW = { "incident_opened", "incident_resolved", @@ -1095,7 +1203,6 @@ def previous_day_context(self, current_day: int) -> str: event_type = doc.get("type", "") facts = doc.get("facts", {}) artifact_ids = doc.get("artifact_ids", {}) - title = ( facts.get("root_cause") or facts.get("title") @@ -1104,7 +1211,6 @@ def previous_day_context(self, current_day: int) -> str: or artifact_ids.get("jira") or "" ) - label = event_type.replace("_", " ").title() line = f"- {label}" if title: @@ -1128,31 +1234,17 @@ def context_for_sprint_planning( sprint_theme: str = "", as_of_time: Optional[Any] = None, ) -> str: - """ - Purpose-built context for sprint planning — no embedding. - - Returns formatted text covering: - - Open tickets for the dept (from jira_tickets) - - Recent incidents in the last 14 days (from events) - - Last checkpoint's velocity and system health - - sprint_theme is optional; only included in the header when present - (not available yet during the theme-generation step). - - Use this instead of context_for_prompt() in _handle_sprint_planning. - """ iso = self._to_iso(as_of_time) lines: List[str] = [] header = f"=== SPRINT #{sprint_num} PLANNING CONTEXT" if sprint_theme: - header += f" — {sprint_theme}" + header += f" -- {sprint_theme}" if dept: header += f" ({dept})" header += " ===" lines.append(header) - # ── Open tickets for this dept ───────────────────────────────────────── ticket_filter: Dict[str, Any] = {"status": {"$ne": "Done"}} _SPRINT_TICKET_PROJECTION = { "_id": 0, @@ -1183,7 +1275,6 @@ def context_for_sprint_planning( else: lines.append(f" No open tickets for {dept or 'any dept'}.") - # ── Recent incidents (last 14 sim-days) ─────────────────────────────── incident_filter: Dict[str, Any] = {"type": "incident_detected"} if iso: incident_filter["timestamp"] = {"$lte": iso} @@ -1197,7 +1288,8 @@ def context_for_sprint_planning( for inc in recent_incidents: facts = inc.get("facts", {}) lines.append( - f" Day {inc.get('day', '?')} — {facts.get('title', facts.get('root_cause', 'Unknown'))}" + f" Day {inc.get('day', '?')} -- " + f"{facts.get('title', facts.get('root_cause', 'Unknown'))}" ) checkpoint = self._checkpoints.find_one(sort=[("day", -1)]) @@ -1217,15 +1309,12 @@ def context_for_sprint_planning( ) def get_domain_registry(self) -> dict: - """Returns {domain: record} for all registered domains.""" return {rec["domain"]: rec for rec in self._db["domain_registry"].find({})} def get_orphaned_domains(self) -> list: - """Returns all domains with no current primary owner.""" return list(self._db["domain_registry"].find({"primary_owner": None})) def update_domain_coverage(self, domain: str, delta: float, author: str, day: int): - """Called when a Confluence page is written covering this domain.""" key = domain.lower().replace(" ", "_") rec = self._db["domain_registry"].find_one({"_id": key}) if rec: @@ -1247,16 +1336,6 @@ def context_for_retrospective( since_iso: str, as_of_iso: str, ) -> str: - """ - Purpose-built context for sprint retrospectives — no embedding. - - Queries MongoDB directly for all tickets and notable events within the - sprint window (since_iso → as_of_iso). Returns formatted text covering: - - Completed tickets (Done status) with assignee - - Tickets that carried over (not Done) - - Incidents that fired during the sprint - - Retrospective-relevant events (deploys, postmortems, etc.) - """ lines: List[str] = [f"=== SPRINT #{sprint_num} RETROSPECTIVE CONTEXT ==="] done_tickets = list( @@ -1330,11 +1409,10 @@ def context_for_retrospective( actors = e.get("actors", []) actor_str = f" ({actors[0]})" if actors else "" lines.append( - f" Day {e.get('day', '?')} — {label}{actor_str}" + f" Day {e.get('day', '?')} -- {label}{actor_str}" + (f": {detail}" if detail else "") ) - # ── Velocity from the closing checkpoint ────────────────────────────── checkpoint = self._checkpoints.find_one(sort=[("day", -1)]) if checkpoint: state = checkpoint.get("state", {}) @@ -1359,21 +1437,9 @@ def context_for_incident( ticket_id: str, as_of_time: Optional[Any] = None, ) -> str: - """ - Purpose-built context for incident-related LLM calls — no embedding. - - Fetches: - - The incident ticket itself (jira_tickets) - - Any postmortem events linked to this ticket (events) - - The most recent prior incident for recurrence signal (events) - - Use this instead of context_for_prompt(inc.root_cause) in - _handle_external_contact and any other incident-scoped call sites. - """ iso = self._to_iso(as_of_time) lines: List[str] = [f"=== INCIDENT CONTEXT: {ticket_id} ==="] - # ── The ticket itself ───────────────────────────────────────────────── ticket = self._jira.find_one( {"id": ticket_id}, { @@ -1406,7 +1472,6 @@ def context_for_incident( else: lines.append(f" Ticket {ticket_id} not found in jira_tickets.") - # ── Linked postmortem events ────────────────────────────────────────── pm_filter: Dict[str, Any] = { "type": "postmortem_published", "artifact_ids.jira": ticket_id, @@ -1421,11 +1486,10 @@ def context_for_incident( for pm in postmortems: facts = pm.get("facts", {}) lines.append( - f" Day {pm.get('day', '?')} — " + f" Day {pm.get('day', '?')} -- " f"{facts.get('title', facts.get('summary', 'Postmortem'))}" ) - # ── Most recent prior incident for recurrence signal ────────────────── prior_filter: Dict[str, Any] = { "type": "incident_detected", "_id": {"$ne": ticket_id}, @@ -1440,7 +1504,7 @@ def context_for_incident( if prior: facts = prior.get("facts", {}) lines.append( - f" Prior incident: Day {prior.get('day', '?')} — " + f" Prior incident: Day {prior.get('day', '?')} -- " f"{facts.get('title', facts.get('root_cause', 'Unknown'))}" ) @@ -1452,20 +1516,9 @@ def context_for_person( as_of_time: Optional[Any] = None, n: int = 3, ) -> str: - """ - Purpose-built context for per-person calls (standup, 1-on-1, etc.) — no embedding. - - Returns formatted text covering: - - Open tickets assigned to this person (jira_tickets) - - Their n most recent events (events, by actor) - - Use this instead of context_for_prompt(f"{name} {expertise} recent tasks") - in _handle_standup and _handle_one_on_one. - """ iso = self._to_iso(as_of_time) lines: List[str] = [f"=== RECENT CONTEXT: {name} ==="] - # ── Open tickets for this person ────────────────────────────────────── ticket_filter: Dict[str, Any] = { "assignee": name, "status": {"$ne": "Done"}, @@ -1486,7 +1539,6 @@ def context_for_person( else: lines.append(" No open tickets assigned.") - # ── Recent events involving this person ─────────────────────────────── event_filter: Dict[str, Any] = {"actors": name} if iso: event_filter["timestamp"] = {"$lte": iso} @@ -1505,7 +1557,7 @@ def context_for_person( or "" ) lines.append( - f" Day {e.get('day', '?')} — {label}" + f" Day {e.get('day', '?')} -- {label}" + (f": {detail}" if detail else "") ) @@ -1515,391 +1567,27 @@ def context_for_person( else f"No recent context found for {name}." ) - def recall_with_rewrite( + def context_for_pr_review( self, - raw_query: str, - n: int = 4, + pr_id: str, + ticket_id: Optional[str] = None, as_of_time: Optional[Any] = None, - since: Optional[Any] = None, + n: int = 2, ) -> str: - """ - HyDE variant — rewrites the query before embedding. - - Use only when the query is dynamically generated (e.g. LLM-produced - topic strings in write_adhoc_page, write_design_doc). The rewrite step - generates a short hypothetical passage that captures what a relevant - document might say, then embeds that passage for better ANN recall. + lines = [] + iso = self._to_iso(as_of_time) - Higher latency than context_for_prompt() due to the extra LLM call. - Do NOT use in hot paths (standup loop, per-dept ticket generation, etc.). - - Args: - raw_query: The raw, LLM-generated or free-form query string. - n: Number of artifacts to retrieve. - as_of_time: Causal ceiling — passed through to recall(). - since: Causal floor — passed through to recall(). - - Returns: - Formatted context string identical in shape to context_for_prompt(). - """ - - rewritten = self._rewrite_query(raw_query) - return self.context_for_prompt( - rewritten, n=n, as_of_time=as_of_time, since=since - ) - - def _rewrite_query(self, raw_query) -> str: - """ - Generates a short hypothetical passage for HyDE-style query rewriting. - - The passage describes what a *relevant document* might say about the - topic — not an answer to it. This produces a richer embedding target - than the raw topic string alone. - """ - prompt = ( - f"Write a short passage (2-3 sentences) that a relevant internal " - f"company document might contain about the following topic. " - f"Do not answer the topic — describe what the document would cover.\n\n" - f"Topic: {raw_query}\n\nPassage:" - ) - try: - client = boto3.client("bedrock-runtime", region_name="us-east-1") - - body = json.dumps( - { - "prompt": prompt, - "max_gen_len": 256, - "temperature": 0.3, - } - ) - - response = client.invoke_model( - modelId="us.meta.llama3-3-70b-instruct-v1:0", - body=body, - contentType="application/json", - accept="application/json", - ) - - response_body = json.loads(response.get("body").read()) - rewritten = response_body.get("generation", "").strip() - - logger.debug( - f"[memory] query rewrite: '{raw_query[:60]}' → '{rewritten[:80]}'" - ) - return rewritten if rewritten else raw_query - except Exception as e: - logger.warning(f"[memory] _rewrite_query failed: {e} — using raw query") - return raw_query - - def facts_for_event_type(self, event_type: str) -> List[Dict]: - return [ - e.facts - | {"date": e.date, "actors": e.actors, "artifact_ids": e.artifact_ids} - for e in self.events_by_type(event_type) - ] - - def stats(self) -> Dict: - embedder_ok = getattr(self._embedder, "_ok", True) - return { - "artifact_count": self._artifacts.count_documents({}), - "event_count": self._events.count_documents({}), - "event_log_len": len(self._event_log), - "embed_provider": EMBED_PROVIDER, - "embed_model": EMBED_MODEL, - "embed_dims": self._embedder.dims, - "embedder_ok": embedder_ok, - "mongodb_ok": True, - } - - def reset(self, export_dir: Optional[str] = None): - """Drop and recreate the entire database. Optionally wipe export directory.""" - db_name = self._db.name - self._client.drop_database(db_name) - self._db = self._client[db_name] - - self._db.create_collection("artifacts") - self._db.create_collection("events") - - # Re-bind all collection references to the fresh database - self._artifacts = self._db["artifacts"] - self._events = self._db["events"] - self._jira = self._db["jira_tickets"] - self._prs = self._db["pull_requests"] - self._checkpoints = self._db["checkpoints"] - self._slack = self._db["slack_messages"] - self._plans = self._db["dept_plans"] - self._conversation_summaries = self._db["conversation_summaries"] - - self._event_log = [] - self._init_vector_indexes() - logger.info("[memory] 🗑️ Database reset.") - - if export_dir: - export_path = Path(export_dir) - if export_path.exists(): - shutil.rmtree(export_path) - export_path.mkdir(parents=True, exist_ok=True) - log_path = export_path / "simulation.log" - root_logger = logging.getLogger() - for handler in root_logger.handlers[:]: - if isinstance(handler, logging.FileHandler): - handler.close() - root_logger.removeHandler(handler) - new_handler = logging.FileHandler(log_path, mode="a") - new_handler.setFormatter( - logging.Formatter( - "%(asctime)s - %(levelname)s - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - ) - root_logger.addHandler(new_handler) - logger.info(f"[memory] 🗑️ Export directory cleared: {export_path}") - - def has_genesis_artifacts(self) -> bool: - """ - Returns True if Genesis artifacts already exist. - This prevents re-running expensive LLM Genesis calls. - """ - return self._events.count_documents({"tags": "genesis"}) > 0 - - def save_checkpoint( - self, - day: int, - state_vars: Dict, - stress: Dict, - cursors: Dict, - graph_data: Dict, - active_incidents: Optional[List[Dict]] = None, - sprint: Optional[Dict] = None, - resolved_incidents: Optional[List[str]] = None, - morale_history: Optional[List[float]] = None, - ): - """Saves daily volatile state to MongoDB.""" - self._checkpoints.update_one( - {"day": day}, - { - "$set": { - "day": day, - "state": state_vars, - "stress": stress, - "cursors": cursors, - "graph": graph_data, - "active_incidents": active_incidents or [], - "sprint": sprint or {}, - "resolved_incidents": resolved_incidents or [], - "morale_history": morale_history or [], - "timestamp": datetime.now(timezone.utc).isoformat(), - } - }, - upsert=True, - ) - - def load_latest_checkpoint(self) -> Optional[Dict]: - """Finds the most recent successful end-of-day snapshot.""" - return self._db["checkpoints"].find_one(sort=[("day", -1)]) - - def upsert_ticket(self, ticket: Dict): - self._jira.update_one({"id": ticket["id"]}, {"$set": ticket}, upsert=True) - - def get_ticket( - self, ticket_id: str, as_of_time: Optional[str] = None - ) -> Optional[Dict]: - query: dict[str, Union[str, dict[str, str]]] = {"id": ticket_id} - if as_of_time: - query["$or"] = [ - {"timestamp": {"$lte": as_of_time}}, - {"timestamp": {"$exists": False}}, - ] - - return self._jira.find_one(query, {"_id": 0}) - - def get_open_tickets_for_dept( - self, members: List[str], dept_name: str = "" - ) -> List[Dict]: - query: Dict[str, Any] = {"status": {"$ne": "Done"}} - if dept_name: - query["dept"] = dept_name - else: - # fallback for incident tickets which route by assignee - query["assignee"] = {"$in": members} - return list(self._jira.find(query, {"_id": 0})) - - def upsert_pr(self, pr: Dict): - self._prs.update_one({"pr_id": pr["pr_id"]}, {"$set": pr}, upsert=True) - - def get_reviewable_prs_for( - self, name: str, as_of_time: Optional[str] = None - ) -> List[Dict]: - query: dict[str, Union[str, dict[str, str]]] = { - "reviewers": name, - "status": "open", - } - if as_of_time: - query["timestamp"] = {"$lte": as_of_time} - - return list(self._prs.find(query, {"_id": 0})) - - def get_pr_by_ticket_id(self, ticket_id: str) -> Optional[Dict]: - return self._prs.find_one({"ticket_id": ticket_id}, {"_id": 0}) - - def log_slack_messages( - self, channel: str, messages: List[Dict], export_dir: Path - ) -> Tuple[str, str]: - """Batch saves Slack messages to JSON files and MongoDB.""" - if not messages: - return ("", "") - - date_str = messages[0].get("date") - thread_id = f"slack_{channel}_{messages[0].get('ts', datetime.now(timezone.utc).isoformat())}" - - for m in messages: - m["thread_id"] = thread_id - - channel_dir = export_dir / "slack" / "channels" / channel - channel_dir.mkdir(parents=True, exist_ok=True) - file_path = channel_dir / f"{date_str}.json" - - # Load-Append-Save Pattern - history = [] - if file_path.exists(): - with open(file_path, "r") as f: - try: - history = json.load(f) - except json.JSONDecodeError: - pass - - history.extend(messages) - with open(file_path, "w") as f: - json.dump(history, f, indent=2) - - # MongoDB batch sync - db_docs = [ - {**m, "channel": channel, "file_path": str(file_path)} for m in messages - ] - self._slack.insert_many(db_docs) - return (str(file_path), thread_id) - - def get_slack_history(self, channel: str, limit: int = 10) -> List[Dict]: - """Retrieve recent messages for a channel.""" - return list(self._slack.find({"channel": channel}).sort("ts", -1).limit(limit)) - - def get_recent_day_summaries(self, current_day: int, window: int = 7) -> List[dict]: - """ - Returns facts dicts from day_summary events in the last `window` days. - Used by PlanValidator for cooldown tracking. - Queries MongoDB directly — does not touch the in-memory _event_log. - """ - cutoff = max(1, current_day - window) - docs = self._events.find( - {"type": "day_summary", "day": {"$gte": cutoff}}, {"facts": 1, "_id": 0} - ).sort("day", 1) - return [d["facts"] for d in docs if "facts" in d] - - def log_dept_plan( - self, - day: int, - date: str, - dept: str, - lead: str, - theme: str, - engineer_plans: List[Dict], - proposed_events: List[Dict], - raw: dict, - ) -> None: - doc = { - "day": day, - "date": date, - "dept": dept, - "lead": lead, - "theme": theme, - "engineer_plans": engineer_plans, - "proposed_events": proposed_events, - "raw": raw, - "timestamp": datetime.now(timezone.utc).isoformat(), - } - try: - self._plans.insert_one(doc) - except Exception as e: - logger.warning(f"[memory] dept_plan insert failed: {e}") - - def _known_pressures_summary(self) -> str: - lines = [] - for doc in self._artifacts.find( - {"metadata.phase": "genesis"}, {"title": 1, "_id": 0} - ).limit(5): - lines.append(f" - Existing doc: {doc['title']}") - return "\n".join(lines) if lines else " - No prior artifacts found." - - def save_tech_stack(self, stack: dict) -> None: - self._db["sim_config"].update_one( - {"_id": "tech_stack"}, - { - "$set": { - "_id": "tech_stack", - "stack": stack, - "created_at": datetime.now(timezone.utc).isoformat(), - } - }, - upsert=True, - ) - - def get_tech_stack(self) -> Optional[dict]: - doc = self._db["sim_config"].find_one({"_id": "tech_stack"}) - return doc["stack"] if doc else None - - def tech_stack_for_prompt(self) -> str: - stack = self.get_tech_stack() - if not stack: - return "" - lines = [ - "CANONICAL TECH STACK — always reference these, never invent alternatives:" - ] - for key, value in stack.items(): - lines.append(f" {key}: {value}") - return "\n".join(lines) - - def save_inbound_email_sources(self, sources: list) -> None: - self._db["sim_config"].update_one( - {"_id": "inbound_email_sources"}, - { - "$set": { - "_id": "inbound_email_sources", - "sources": sources, - "created_at": datetime.now(timezone.utc).isoformat(), - } - }, - upsert=True, - ) - - def get_inbound_email_sources(self) -> Optional[list]: - doc = self._db["sim_config"].find_one({"_id": "inbound_email_sources"}) - return doc["sources"] if doc else None - - def context_for_pr_review( - self, - pr_id: str, - ticket_id: Optional[str] = None, - as_of_time: Optional[Any] = None, - n: int = 2, - ) -> str: - """ - Builds deterministic reviewer context from the PR and its linked ticket. - Combines: recent PR comments and author's ticket updates. - """ - lines = [] - iso = self._to_iso(as_of_time) - - pr = self._prs.find_one({"pr_id": pr_id}, {"_id": 0}) - if pr: - pr_comments = pr.get("comments", []) - if iso: - pr_comments = [c for c in pr_comments if c.get("timestamp", "") <= iso] - if pr_comments: - lines.append("=== PR REVIEW HISTORY ===") - for c in pr_comments[-3:]: - lines.append( - f" [{c['date']}] {c['author']} [{c.get('verdict', '?')}]: {c['text'][:200]}" - ) + pr = self._prs.find_one({"pr_id": pr_id}, {"_id": 0}) + if pr: + pr_comments = pr.get("comments", []) + if iso: + pr_comments = [c for c in pr_comments if c.get("timestamp", "") <= iso] + if pr_comments: + lines.append("=== PR REVIEW HISTORY ===") + for c in pr_comments[-3:]: + lines.append( + f" [{c['date']}] {c['author']} [{c.get('verdict', '?')}]: {c['text'][:200]}" + ) if ticket_id: ticket = self._jira.find_one({"id": ticket_id}, {"_id": 0, "comments": 1}) @@ -1925,29 +1613,9 @@ def context_for_ticket( ticket_id: str, as_of_time: Optional[Any] = None, ) -> str: - """ - Purpose-built context for ticket-scoped conversations — no embedding. - - Intended for _handle_async_question and _handle_design_discussion where - participants need to understand a ticket's current state and history but - are not necessarily the assignee and the ticket is not necessarily an - incident. - - Fetches: - - The ticket itself (title, status, assignee, description, last 3 comments) - - Recurrence signal if present (recurrence_of, gap_days, ancestor root cause) - - Recent async_question and design_discussion events linked to this ticket - or its participants — so the conversation doesn't rehash settled ground - - Any blocker events on this ticket - - Args: - ticket_id: The JIRA ticket ID the conversation is about. - as_of_time: Causal ceiling (datetime or ISO string). - """ iso = self._to_iso(as_of_time) lines: List[str] = [f"=== TICKET CONTEXT: {ticket_id} ==="] - # ── The ticket document ─────────────────────────────────────────────── ticket = self._jira.find_one( {"id": ticket_id}, { @@ -2000,9 +1668,6 @@ def context_for_ticket( text = c.get("text", "").strip().strip('"')[:150] lines.append(f" {author} ({date}): {text}") - # ── Recurrence signal — fetch ancestor root cause for full context ──── - # Gives participants the "we saw this before" signal without them - # needing to look it up themselves. recurrence_of = ticket.get("recurrence_of") if recurrence_of: gap = ticket.get("recurrence_gap_days", "?") @@ -2017,13 +1682,12 @@ def context_for_ticket( f" Recurrence: this is a repeat of {recurrence_of} " f"({gap} days ago)" + ( - f" — prior root cause: {ancestor_root_cause}" + f" -- prior root cause: {ancestor_root_cause}" if ancestor_root_cause else "" ) ) - # ── Blocker events on this ticket ───────────────────────────────────── blocker_filter: Dict[str, Any] = { "type": "blocker_flagged", "artifact_ids.jira": ticket_id, @@ -2043,204 +1707,479 @@ def context_for_ticket( for b in blockers: reason = b.get("facts", {}).get("blocker_reason", "")[:120] actor = b.get("actors", ["?"])[0] - lines.append(f" Day {b.get('day', '?')} — {actor}: {reason}") + lines.append(f" Day {b.get('day', '?')} -- {actor}: {reason}") + + discussion_filter: Dict[str, Any] = { + "type": {"$in": ["async_question", "design_discussion"]}, + "artifact_ids.jira": ticket_id, + } + if iso: + discussion_filter["timestamp"] = {"$lte": iso} + prior_discussions = list( + self._events.find( + discussion_filter, + { + "_id": 0, + "day": 1, + "type": 1, + "facts.topic": 1, + "facts.asker": 1, + "summary": 1, + }, + ) + .sort("timestamp", -1) + .limit(3) + ) + if prior_discussions: + lines.append(" Prior discussions (do not rehash these):") + for d in prior_discussions: + label = d.get("type", "").replace("_", " ").title() + topic = d.get("facts", {}).get("topic") or d.get("summary", "") + lines.append(f" Day {d.get('day', '?')} [{label}]: {topic[:120]}") + + return "\n".join(lines) + + def context_for_ticket_progress( + self, + ticket_id: str, + assignee: str, + as_of_time: Optional[Any] = None, + ) -> str: + iso = self._to_iso(as_of_time) + lines: List[str] = [f"=== TICKET CONTEXT: {ticket_id} ==="] + + ticket = self._jira.find_one({"id": ticket_id}, _TICKET_PROGRESS_PROJECTION) + if not ticket: + lines.append(f" Ticket {ticket_id} not found.") + return "\n".join(lines) + + lines.append( + f" [{ticket_id}] {ticket.get('title', '')} " + f"(status={ticket.get('status', '?')}, " + f"assignee={ticket.get('assignee', 'unassigned')}, " + f"points={ticket.get('story_points', '?')})" + ) + + description = ticket.get("description", "").strip() + if description: + lines.append(f" Description: {description[:200]}") + + if ticket.get("root_cause"): + lines.append(f" Root cause: {ticket['root_cause']}") + + if ticket.get("gap_areas"): + lines.append(f" Knowledge gap areas: {', '.join(ticket['gap_areas'])}") + + comments = ticket.get("comments", []) + if comments: + lines.append(" Recent comments:") + for c in comments: + author = c.get("author", "?") + date = c.get("date", "") + text = c.get("text", "").strip().strip('"')[:150] + lines.append(f" {author} ({date}): {text}") + + linked_prs = ticket.get("linked_prs", []) + if linked_prs: + lines.append(f" Linked PRs: {', '.join(linked_prs)}") + + blocker_filter: Dict[str, Any] = { + "type": "blocker_flagged", + "artifact_ids.jira": ticket_id, + } + if iso: + blocker_filter["timestamp"] = {"$lte": iso} + blockers = list( + self._events.find( + blocker_filter, + {"_id": 0, "day": 1, "actors": 1, "facts.blocker_reason": 1}, + ) + .sort("timestamp", -1) + .limit(2) + ) + if blockers: + lines.append(" Recent blockers:") + for b in blockers: + reason = b.get("facts", {}).get("blocker_reason", "")[:120] + actors = b.get("actors", []) + actor_str = actors[0] if actors else "?" + lines.append(f" Day {b.get('day', '?')} -- {actor_str}: {reason}") + + incident_filter: Dict[str, Any] = { + "type": "incident_opened", + "artifact_ids.jira": ticket_id, + } + if iso: + incident_filter["timestamp"] = {"$lte": iso} + incident_origin = self._events.find_one( + incident_filter, + {"_id": 0, "day": 1, "facts.root_cause": 1}, + ) + if incident_origin: + root_cause = incident_origin.get("facts", {}).get("root_cause", "") + lines.append( + f" Incident origin (Day {incident_origin.get('day', '?')}): {root_cause[:120]}" + ) + + progress_filter: Dict[str, Any] = { + "type": "ticket_progress", + "actors": assignee, + "artifact_ids.jira": ticket_id, + } + if iso: + progress_filter["timestamp"] = {"$lte": iso} + prior_progress = list( + self._events.find( + progress_filter, + {"_id": 0, "day": 1, "summary": 1}, + ) + .sort("timestamp", -1) + .limit(3) + ) + if prior_progress: + lines.append(" Prior progress:") + for p in prior_progress: + lines.append(f" Day {p.get('day', '?')} -- {p.get('summary', '')}") + + return "\n".join(lines) + + def context_for_person_conversations( + self, + name: str, + conv_type: Optional[str] = None, + as_of_time: Optional[Any] = None, + n: int = 3, + ) -> str: + iso = self._to_iso(as_of_time) + + query: Dict[str, Any] = {"participants": name} + if conv_type: + query["type"] = conv_type + if iso: + query["timestamp"] = {"$lte": iso} + + docs = list( + self._conversation_summaries.find( + query, + { + "_id": 0, + "type": 1, + "participants": 1, + "summary": 1, + "day": 1, + "slack_thread_id": 1, + }, + ) + .sort("day", -1) + .limit(n) + ) + + if not docs: + return "" + + lines = [f"=== PAST CONVERSATIONS: {name} ==="] + for d in docs: + other = next((p for p in d["participants"] if p != name), "?") + label = d.get("type", "conversation").replace("_", " ").title() + lines.append( + f" Day {d.get('day', '?')} [{label} with {other}]: {d.get('summary', '')}" + ) + + return "\n".join(lines) + + def design_discussions_for_ticket( + self, + ticket_id: str, + actors: List[str], + as_of_time: Optional[Any] = None, + n: int = 2, + ) -> List[Dict]: + iso = self._to_iso(as_of_time) - # ── Prior discussions on this ticket — dedup guard ──────────────────── - # Surfaces what has already been decided so participants don't rehash it. - discussion_filter: Dict[str, Any] = { - "type": {"$in": ["async_question", "design_discussion"]}, - "artifact_ids.jira": ticket_id, + query: Dict[str, Any] = { + "type": "design_discussion", + "$or": [ + {"facts.participants": {"$in": actors}}, + {"artifact_ids.jira": ticket_id}, + ], } if iso: - discussion_filter["timestamp"] = {"$lte": iso} - prior_discussions = list( + query["timestamp"] = {"$lte": iso} + + docs = list( self._events.find( - discussion_filter, + query, { "_id": 0, "day": 1, - "type": 1, "facts.topic": 1, - "facts.asker": 1, - "summary": 1, + "facts.participants": 1, + "artifact_ids.slack_thread": 1, + "artifact_ids.confluence": 1, }, ) .sort("timestamp", -1) - .limit(3) + .limit(n * 3) ) - if prior_discussions: - lines.append(" Prior discussions (do not rehash these):") - for d in prior_discussions: - label = d.get("type", "").replace("_", " ").title() - topic = d.get("facts", {}).get("topic") or d.get("summary", "") - lines.append(f" Day {d.get('day', '?')} [{label}]: {topic[:120]}") + + actor_set = set(actors) + scored = [] + for d in docs: + discussion_participants = set(d.get("facts", {}).get("participants", [])) + overlap = len(actor_set & discussion_participants) + if overlap >= 1: + scored.append((overlap, d)) + + scored.sort(key=lambda x: x[0], reverse=True) + results = [] + for _, d in scored[:n]: + results.append( + { + "day": d.get("day", "?"), + "topic": d.get("facts", {}).get("topic", ""), + "participants": d.get("facts", {}).get("participants", []), + "slack_thread_id": d.get("artifact_ids", {}).get( + "slack_thread", "" + ), + "confluence_id": d.get("artifact_ids", {}).get("confluence", ""), + } + ) + return results + + def format_design_discussions_hint(self, discussions: List[Dict]) -> str: + if not discussions: + return "" + + lines = [ + "Note: related design discussions already happened -- reference if relevant:" + ] + for d in discussions: + parts = [f" Day {d['day']}"] + if d["topic"]: + parts.append(f"topic: '{d['topic'][:80]}'") + if d["participants"]: + parts.append(f"participants: {', '.join(d['participants'])}") + if d["confluence_id"]: + parts.append(f"doc: {d['confluence_id']}") + lines.append(" -- ".join(parts)) return "\n".join(lines) - def context_for_ticket_progress( - self, - ticket_id: str, - assignee: str, - as_of_time: Optional[Any] = None, - ) -> str: - """ - Purpose-built context for _handle_ticket_progress — no embedding. + def stats(self) -> Dict: + return { + "confluence_page_count": self._confluence_pages.count_documents({}), + "event_count": self._events.count_documents({}), + "event_log_len": len(self._event_log), + "artifact_count": ( + self._confluence_pages.count_documents({}) + ), + "mongodb_ok": True, + } - Returns a tight context block covering: - - The ticket itself (title, status, description, last 3 comments) - - blocker_flagged events on this ticket (is it stuck?) - - incident_opened events that reference this ticket (incident origin?) - - ticket_progress events by this assignee on this ticket (recent momentum) + def reset(self, export_dir: Optional[str] = None): + db_name = self._db.name + self._client.drop_database(db_name) + self._db = self._client[db_name] - No artifacts are fetched — the ticket document already contains everything - the LLM needs to write a realistic JIRA comment. + self._db.create_collection("artifacts") + self._db.create_collection("events") - Args: - ticket_id: The JIRA ticket ID being worked on. - assignee: The engineer doing the work — used to scope event lookup. - as_of_time: Causal ceiling (datetime or ISO string). - """ - iso = self._to_iso(as_of_time) - lines: List[str] = [f"=== TICKET CONTEXT: {ticket_id} ==="] + self._artifacts = self._db["artifacts"] + self._events = self._db["events"] + self._jira = self._db["jira_tickets"] + self._prs = self._db["pull_requests"] + self._checkpoints = self._db["checkpoints"] + self._slack = self._db["slack_messages"] + self._plans = self._db["dept_plans"] + self._conversation_summaries = self._db["conversation_summaries"] + self._confluence_pages = self._db["confluence_pages"] + self._zoom_transcripts = self._db["zoom_transcripts"] - # ── The ticket document ─────────────────────────────────────────────────── - ticket = self._jira.find_one({"id": ticket_id}, _TICKET_PROGRESS_PROJECTION) - if not ticket: - lines.append(f" Ticket {ticket_id} not found.") - return "\n".join(lines) + self._event_log = [] + self._init_text_indexes() + logger.info("[memory] Database reset.") - lines.append( - f" [{ticket_id}] {ticket.get('title', '')} " - f"(status={ticket.get('status', '?')}, " - f"assignee={ticket.get('assignee', 'unassigned')}, " - f"points={ticket.get('story_points', '?')})" + if export_dir: + export_path = Path(export_dir) + if export_path.exists(): + shutil.rmtree(export_path) + export_path.mkdir(parents=True, exist_ok=True) + log_path = export_path / "simulation.log" + root_logger = logging.getLogger() + for handler in root_logger.handlers[:]: + if isinstance(handler, logging.FileHandler): + handler.close() + root_logger.removeHandler(handler) + new_handler = logging.FileHandler(log_path, mode="a") + new_handler.setFormatter( + logging.Formatter( + "%(asctime)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ) + root_logger.addHandler(new_handler) + logger.info(f"[memory] Export directory cleared: {export_path}") + + def has_genesis_artifacts(self) -> bool: + return self._events.count_documents({"tags": "genesis"}) > 0 + + def save_checkpoint( + self, + day: int, + state_vars: Dict, + stress: Dict, + cursors: Dict, + graph_data: Dict, + active_incidents: Optional[List[Dict]] = None, + sprint: Optional[Dict] = None, + resolved_incidents: Optional[List[str]] = None, + morale_history: Optional[List[float]] = None, + ): + self._checkpoints.update_one( + {"day": day}, + { + "$set": { + "day": day, + "state": state_vars, + "stress": stress, + "cursors": cursors, + "graph": graph_data, + "active_incidents": active_incidents or [], + "sprint": sprint or {}, + "resolved_incidents": resolved_incidents or [], + "morale_history": morale_history or [], + "timestamp": datetime.now(timezone.utc).isoformat(), + } + }, + upsert=True, ) - description = ticket.get("description", "").strip() - if description: - # Cap at 200 chars — enough for context, not enough to bloat the prompt - lines.append(f" Description: {description[:200]}") + def load_latest_checkpoint(self) -> Optional[Dict]: + return self._db["checkpoints"].find_one(sort=[("day", -1)]) - if ticket.get("root_cause"): - lines.append(f" Root cause: {ticket['root_cause']}") + def upsert_ticket(self, ticket: Dict): + self._jira.update_one({"id": ticket["id"]}, {"$set": ticket}, upsert=True) - if ticket.get("gap_areas"): - lines.append(f" Knowledge gap areas: {', '.join(ticket['gap_areas'])}") + def get_ticket( + self, ticket_id: str, as_of_time: Optional[str] = None + ) -> Optional[Dict]: + query: dict = {"id": ticket_id} + if as_of_time: + query["$or"] = [ + {"timestamp": {"$lte": as_of_time}}, + {"timestamp": {"$exists": False}}, + ] + return self._jira.find_one(query, {"_id": 0}) - # ── Prior comments (already sliced to last 3 by projection) ────────────── - comments = ticket.get("comments", []) - if comments: - lines.append(" Recent comments:") - for c in comments: - author = c.get("author", "?") - date = c.get("date", "") - text = c.get("text", "").strip().strip('"')[:150] - lines.append(f" {author} ({date}): {text}") + def get_open_tickets_for_dept( + self, members: List[str], dept_name: str = "" + ) -> List[Dict]: + query: Dict[str, Any] = {"status": {"$ne": "Done"}} + if dept_name: + query["dept"] = dept_name + else: + query["assignee"] = {"$in": members} + return list(self._jira.find(query, {"_id": 0})) - # ── Linked PRs ──────────────────────────────────────────────────────────── - linked_prs = ticket.get("linked_prs", []) - if linked_prs: - lines.append(f" Linked PRs: {', '.join(linked_prs)}") + def upsert_pr(self, pr: Dict): + self._prs.update_one({"pr_id": pr["pr_id"]}, {"$set": pr}, upsert=True) - # ── Blocker events on this ticket ───────────────────────────────────────── - blocker_filter: Dict = { - "type": "blocker_flagged", - "artifact_ids.jira": ticket_id, - } - if iso: - blocker_filter["timestamp"] = {"$lte": iso} + def get_reviewable_prs_for( + self, name: str, as_of_time: Optional[str] = None + ) -> List[Dict]: + query: dict = {"reviewers": name, "status": "open"} + if as_of_time: + query["timestamp"] = {"$lte": as_of_time} + return list(self._prs.find(query, {"_id": 0})) - blockers = list( - self._events.find( - blocker_filter, - {"_id": 0, "day": 1, "actors": 1, "facts.blocker_reason": 1}, - ) - .sort("timestamp", -1) - .limit(2) - ) - if blockers: - lines.append(" Recent blockers:") - for b in blockers: - reason = b.get("facts", {}).get("blocker_reason", "")[:120] - actors = b.get("actors", []) - actor_str = actors[0] if actors else "?" - lines.append(f" Day {b.get('day', '?')} — {actor_str}: {reason}") + def get_pr_by_ticket_id(self, ticket_id: str) -> Optional[Dict]: + return self._prs.find_one({"ticket_id": ticket_id}, {"_id": 0}) + + def log_slack_messages( + self, channel: str, messages: List[Dict], export_dir: Path + ) -> Tuple[str, str]: + if not messages: + return ("", "") + + date_str = messages[0].get("date") + thread_id = f"slack_{channel}_{messages[0].get('ts', datetime.now(timezone.utc).isoformat())}" + + for m in messages: + m["thread_id"] = thread_id + + channel_dir = export_dir / "slack" / "channels" / channel + channel_dir.mkdir(parents=True, exist_ok=True) + file_path = channel_dir / f"{date_str}.json" + + history = [] + if file_path.exists(): + with open(file_path, "r") as f: + try: + history = json.load(f) + except json.JSONDecodeError: + pass - # ── Incident origin — did an incident open this ticket? ─────────────────── - incident_filter: Dict = { - "type": "incident_opened", - "artifact_ids.jira": ticket_id, - } - if iso: - incident_filter["timestamp"] = {"$lte": iso} + history.extend(messages) + with open(file_path, "w") as f: + json.dump(history, f, indent=2) - incident_origin = self._events.find_one( - incident_filter, - {"_id": 0, "day": 1, "facts.root_cause": 1}, - ) - if incident_origin: - root_cause = incident_origin.get("facts", {}).get("root_cause", "") - lines.append( - f" Incident origin (Day {incident_origin.get('day', '?')}): {root_cause[:120]}" - ) + db_docs = [ + {**m, "channel": channel, "file_path": str(file_path)} for m in messages + ] + self._slack.insert_many(db_docs) + return (str(file_path), thread_id) - # ── Recent ticket_progress events by this assignee on this ticket ───────── - # Gives momentum signal — what was done on previous days? - progress_filter: Dict = { - "type": "ticket_progress", - "actors": assignee, - "artifact_ids.jira": ticket_id, - } - if iso: - progress_filter["timestamp"] = {"$lte": iso} + def get_slack_history(self, channel: str, limit: int = 10) -> List[Dict]: + return list(self._slack.find({"channel": channel}).sort("ts", -1).limit(limit)) - prior_progress = list( - self._events.find( - progress_filter, - {"_id": 0, "day": 1, "summary": 1}, - ) - .sort("timestamp", -1) - .limit(3) - ) - if prior_progress: - lines.append(" Prior progress:") - for p in prior_progress: - lines.append(f" Day {p.get('day', '?')} — {p.get('summary', '')}") + def get_recent_day_summaries(self, current_day: int, window: int = 7) -> List[dict]: + cutoff = max(1, current_day - window) + docs = self._events.find( + {"type": "day_summary", "day": {"$gte": cutoff}}, {"facts": 1, "_id": 0} + ).sort("day", 1) + return [d["facts"] for d in docs if "facts" in d] - return "\n".join(lines) + def log_dept_plan( + self, + day: int, + date: str, + dept: str, + lead: str, + theme: str, + engineer_plans: List[Dict], + proposed_events: List[Dict], + raw: dict, + ) -> None: + doc = { + "day": day, + "date": date, + "dept": dept, + "lead": lead, + "theme": theme, + "engineer_plans": engineer_plans, + "proposed_events": proposed_events, + "raw": raw, + "timestamp": datetime.now(timezone.utc).isoformat(), + } + try: + self._plans.insert_one(doc) + except Exception as e: + logger.warning(f"[memory] dept_plan insert failed: {e}") def save_conversation_summary( self, - conv_type: str, # "1on1" | "mentoring" - participants: List[str], # both names, order doesn't matter - summary: str, # extracted from last-turn JSON output + conv_type: str, + participants: List[str], + summary: str, day: int, date: str, timestamp: str, slack_thread_id: str = "", extra_facts: Optional[Dict] = None, ) -> None: - """ - Persist a conversation summary to the conversation_summaries collection. - - The summary text is produced by the LLM as part of the last speaker's - task output (see _last_turn_desc() helper in normal_day.py). This method - stores it so future context_for_person_conversations() calls can retrieve - it cheaply with a structured query — no embedding required. - - Args: - conv_type: "1on1" or "mentoring" - participants: Both participant names. - summary: The tightened summary extracted from the last turn JSON. - day: Simulation day. - date: ISO date string. - timestamp: ISO timestamp of the conversation. - slack_thread_id: Thread ID for cross-referencing. - extra_facts: Any additional fields to store (e.g. topic, ticket_id). - """ doc = { "type": conv_type, - "participants": sorted(participants), # sorted for consistent set-queries + "participants": sorted(participants), "summary": summary, "day": day, "date": date, @@ -2253,227 +2192,62 @@ def save_conversation_summary( except Exception as e: logger.warning(f"[memory] conversation_summary insert failed: {e}") - def context_for_person_conversations( - self, - name: str, - conv_type: Optional[str] = None, # None = both 1on1 and mentoring - as_of_time: Optional[Any] = None, - n: int = 3, - ) -> str: - """ - Retrieve recent conversation summaries involving a person. - Used by _handle_one_on_one and _handle_mentoring to give the LLM - continuity across sessions — "last time we talked about X". - - Args: - name: The participant to search for. - conv_type: Filter to "1on1" or "mentoring", or None for both. - as_of_time: Causal ceiling. - n: Max summaries to return. - """ - iso = self._to_iso(as_of_time) - - query: Dict = {"participants": name} - if conv_type: - query["type"] = conv_type - if iso: - query["timestamp"] = {"$lte": iso} - - docs = list( - self._conversation_summaries.find( - query, - { - "_id": 0, - "type": 1, - "participants": 1, - "summary": 1, - "day": 1, - "slack_thread_id": 1, - }, - ) - .sort("day", -1) - .limit(n) - ) - - if not docs: - return "" - - lines = [f"=== PAST CONVERSATIONS: {name} ==="] - for d in docs: - other = next((p for p in d["participants"] if p != name), "?") - label = d.get("type", "conversation").replace("_", " ").title() - lines.append( - f" Day {d.get('day', '?')} [{label} with {other}]: {d.get('summary', '')}" - ) - - return "\n".join(lines) - - def design_discussions_for_ticket( - self, - ticket_id: str, - actors: List[str], - as_of_time: Optional[Any] = None, - n: int = 2, - ) -> List[Dict]: - """ - Find design_discussion SimEvents that are likely related to a ticket, - identified by actor overlap. - - This avoids embedding entirely — it's a set intersection on participant - names. At least 2 of the ticket's actors must appear in the discussion's - participants for it to be returned. - - Returns a list of dicts with: day, topic, participants, slack_thread_id. - These can be injected into the async_question context so participants - know what design decisions have already been made. - - Args: - ticket_id: Used to check if the ticket was directly referenced in - the discussion's facts (belt-and-suspenders). - actors: The participants in the current async thread — used for - actor overlap matching. - as_of_time: Causal ceiling. - n: Max discussions to return. - """ - iso = self._to_iso(as_of_time) - - # Match: design_discussion events where at least one actor overlaps - # AND the event happened before as_of_time. - # MongoDB $in on an array field checks if any element matches. - query: Dict = { - "type": "design_discussion", - "$or": [ - {"facts.participants": {"$in": actors}}, # actor overlap - {"artifact_ids.jira": ticket_id}, # direct ticket ref - ], - } - if iso: - query["timestamp"] = {"$lte": iso} - - docs = list( - self._events.find( - query, - { - "_id": 0, - "day": 1, - "facts.topic": 1, - "facts.participants": 1, - "artifact_ids.slack_thread": 1, - "artifact_ids.confluence": 1, - }, - ) - .sort("timestamp", -1) - .limit(n * 3) # oversample before actor-overlap scoring - ) + def facts_for_event_type(self, event_type: str) -> List[Dict]: + return [ + e.facts + | {"date": e.date, "actors": e.actors, "artifact_ids": e.artifact_ids} + for e in self.events_by_type(event_type) + ] - # Score by actor overlap — prefer discussions with more shared participants - scored = [] - actor_set = set(actors) - for d in docs: - discussion_participants = set(d.get("facts", {}).get("participants", [])) - overlap = len(actor_set & discussion_participants) - if overlap >= 1: - scored.append((overlap, d)) + def _known_pressures_summary(self) -> str: + lines = [] + for doc in self._confluence_pages.find( + {"metadata.phase": "genesis"}, {"title": 1, "_id": 0} + ).limit(5): + lines.append(f" - Existing doc: {doc['title']}") + return "\n".join(lines) if lines else " - No prior artifacts found." - # Sort descending by overlap, take top n - scored.sort(key=lambda x: x[0], reverse=True) - results = [] - for _, d in scored[:n]: - results.append( - { - "day": d.get("day", "?"), - "topic": d.get("facts", {}).get("topic", ""), - "participants": d.get("facts", {}).get("participants", []), - "slack_thread_id": d.get("artifact_ids", {}).get( - "slack_thread", "" - ), - "confluence_id": d.get("artifact_ids", {}).get("confluence", ""), + def save_tech_stack(self, stack: dict) -> None: + self._db["sim_config"].update_one( + {"_id": "tech_stack"}, + { + "$set": { + "_id": "tech_stack", + "stack": stack, + "created_at": datetime.now(timezone.utc).isoformat(), } - ) - return results - - def format_design_discussions_hint(self, discussions: List[Dict]) -> str: - """ - Formats the output of design_discussions_for_ticket() into a prompt-ready - string for injection into _handle_async_question. + }, + upsert=True, + ) - Call this as a module-level helper (not a method) since it operates on - already-fetched data and has no memory dependency. + def get_tech_stack(self) -> Optional[dict]: + doc = self._db["sim_config"].find_one({"_id": "tech_stack"}) + return doc["stack"] if doc else None - Usage in normal_day.py: - from memory import format_design_discussions_hint - discussions = self._mem.design_discussions_for_ticket( - ticket_id, all_actors, meeting_time_iso - ) - design_hint = format_design_discussions_hint(discussions) - # inject design_hint into the first task description only - """ - if not discussions: + def tech_stack_for_prompt(self) -> str: + stack = self.get_tech_stack() + if not stack: return "" - lines = [ - "Note: related design discussions already happened — reference if relevant:" + "CANONICAL TECH STACK -- always reference these, never invent alternatives:" ] - for d in discussions: - parts = [f" Day {d['day']}"] - if d["topic"]: - parts.append(f"topic: '{d['topic'][:80]}'") - if d["participants"]: - parts.append(f"participants: {', '.join(d['participants'])}") - if d["confluence_id"]: - parts.append(f"doc: {d['confluence_id']}") - lines.append(" — ".join(parts)) - + for key, value in stack.items(): + lines.append(f" {key}: {value}") return "\n".join(lines) - def search_events( - self, - query: str, - event_types: Optional[List[str]] = None, - n: int = 10, - as_of_day: Optional[int] = None, - ) -> List[Tuple["SimEvent", float]]: - """ - Vector search over the events collection. - Returns (SimEvent, score) pairs sorted by descending relevance. - """ - query_vector = self._embedder.embed(query) - if not query_vector: - return [] - - pipeline_filter: Dict[str, Any] = {} - if event_types: - pipeline_filter["type"] = {"$in": event_types} - if as_of_day is not None: - pipeline_filter["day"] = {"$lte": as_of_day} - - pipeline: List[Dict] = [ + def save_inbound_email_sources(self, sources: list) -> None: + self._db["sim_config"].update_one( + {"_id": "inbound_email_sources"}, { - "$vectorSearch": { - "index": "vector_index", - "path": "embedding", - "queryVector": query_vector, - "numCandidates": n * 10, - "limit": n, + "$set": { + "_id": "inbound_email_sources", + "sources": sources, + "created_at": datetime.now(timezone.utc).isoformat(), } }, - {"$addFields": {"vector_score": {"$meta": "vectorSearchScore"}}}, - {"$project": {"_id": 0, "embedding": 0}}, - ] - - if pipeline_filter: - pipeline[0]["$vectorSearch"]["filter"] = pipeline_filter - - try: - results = list(self._events.aggregate(pipeline)) - return [ - (SimEvent.from_dict(r), round(r.get("vector_score", 0.0), 4)) - for r in results - ] - except Exception as e: - logger.warning(f"[memory] search_events failed: {e}") - return [] + upsert=True, + ) - def set_embed_worker(self, worker) -> None: - """Attach the background embed worker. Called from OrgForgeSimulation.__init__.""" - self._embed_worker = worker + def get_inbound_email_sources(self) -> Optional[list]: + doc = self._db["sim_config"].find_one({"_id": "inbound_email_sources"}) + return doc["sources"] if doc else None diff --git a/src/normal_day.py b/src/normal_day.py index 87cc148..c70b381 100644 --- a/src/normal_day.py +++ b/src/normal_day.py @@ -274,8 +274,6 @@ def _handle_ticket_progress( backstory = persona_utils.get_voice_card(assignee, "async", self._gd, self._mem) - # Surface any orphaned domains this ticket touches so the engineer's - # comment naturally reflects uncertainty about legacy systems orphaned_domain_hint = "" if self._lifecycle: all_domains = list( @@ -612,6 +610,14 @@ def _handle_ticket_progress( if completion_id: generated_artifacts.append(completion_id) + if self._lifecycle: + self._credit_domain_knowledge( + actor=assignee, + text=f"{ticket.get('title', '')} {ticket.get('description', '')} {comment_text}", + day=self._state.day, + pathway="pr_authorship", + ) + return generated_artifacts def _try_force_merge_stale_pr( @@ -738,6 +744,13 @@ def _complete_non_eng_ticket( ) if completion_id: chain.append(completion_id) + if self._lifecycle: + self._credit_domain_knowledge( + actor=assignee, + text=ticket.get("title", ""), + day=self._state.day, + pathway="confluence_authorship", + ) logger.info( f" [dim]📄 {assignee} completed [{ticket.get('id')}] → {completion_id}[/dim]" ) @@ -869,7 +882,6 @@ def _handle_pr_review( ) current_actor_time = artifact_time.isoformat() - ctx = self._mem.context_for_prompt(pr_title, n=2, as_of_time=current_actor_time) backstory = persona_utils.get_voice_card(reviewer, "async", self._gd, self._mem) p = self._config.get("personas", {}).get(reviewer, {}) @@ -894,7 +906,7 @@ def _handle_pr_review( if prior_reviews: rounds = "\n".join( f" - {c['author']} ({c['date']}): [{c.get('verdict', '?')}] {c['text'][:120]}" - for c in prior_reviews[-6:] # last 6 comments max + for c in prior_reviews[-6:] ) review_history = f"\n--- PRIOR REVIEW ROUNDS ---\n{rounds}\n\n" @@ -959,6 +971,7 @@ def _handle_pr_review( description=( f"You are {reviewer}. You are reviewing this PR by {author}: {pr_title}\n\n" f"{review_history}" + + (f"RECURRENCE NOTE: {recurrence_hint}\n\n" if recurrence_hint else "") + ( f"DOMAIN CONTEXT:{orphaned_domain_context}\n\n" if orphaned_domain_context @@ -971,41 +984,11 @@ def _handle_pr_review( f"Write 1-3 sentences consistent with your verdict. If approved, acknowledge " f"what looks good. If changes_requested, name the specific issue only.\n" f"Your tone must reflect your current stress level (see your backstory).\n\n" - f"STEP 3 — AUTHOR KNOWLEDGE AUDIT (answer objectively, NOT in character):\n" - f"Your expertise as reviewer: [{reviewer_expertise_str}]\n" - f"Your department: {reviewer_dept}\n" - f"{author}'s expertise on record: [{expertise_str}]\n" - f"{author}'s department: {author_dept}\n" - f"Based on the PR content and your expertise, assess whether {author} " - f"demonstrates a knowledge gap in the domains this PR touches.\n" - f"- In 'topics_beyond_author_expertise', list any technical areas where " - f"{author}'s implementation, approach, or omissions suggest unfamiliarity.\n" - f"- In 'hedged_claims', list specific decisions or statements in the PR " - f"that appear incorrect, naive, or underconfident given the problem domain.\n" - f"- If {author} deferred or left incomplete any section you know should " - f"exist, list it in 'deferred_or_incomplete'.\n\n" - f"Use these criteria:\n" - f" author_domain_fit:\n" - f" 'high' — PR demonstrates fluency: correct abstractions, aware of edge cases, idiomatic\n" - f" 'medium' — PR is functional but shows shallow understanding or minor missteps\n" - f" 'low' — PR shows clear unfamiliarity: wrong patterns, missing fundamentals, or over-reliance on guesswork\n\n" - f" gap_classification:\n" - f" 'none' — {author}'s expertise aligns with all domains touched by this PR\n" - f" 'possible' — PR touches 1-2 domains outside {author}'s expertise but implementation looks adequate\n" - f" 'likely' — PR touches domains outside {author}'s expertise AND the implementation shows it\n\n" f"Respond ONLY with valid JSON. No preamble, no markdown fences.\n" f"{{\n" f' "comment": "your review comment here",\n' - f' "verdict": "approved" or "changes_requested",\n' - f' "metadata": {{\n' - f' "author_domain_fit": "low | medium | high",\n' - f' "confidence": "low | medium | high",\n' - f' "gap_classification": "none | possible | likely",\n' - f' "topics_beyond_author_expertise": ["string"],\n' - f' "hedged_claims": ["string"],\n' - f' "deferred_or_incomplete": ["string"]\n' - f" }}\n" - f"}}" + f' "verdict": "approved" or "changes_requested"\n' + f"}}\n" ), expected_output="Valid JSON only. No preamble, no markdown fences.", agent=agent, @@ -1022,11 +1005,9 @@ def _handle_pr_review( verdict = parsed_review.get("verdict", "approved") if verdict not in ("approved", "changes_requested"): verdict = "approved" - review_metadata = parsed_review.get("metadata", {}) except (json.JSONDecodeError, AttributeError): review_text = raw_review verdict = "approved" - review_metadata = {} pr_comment = { "author": reviewer, @@ -1059,6 +1040,13 @@ def _handle_pr_review( f" [green]✅ {pr.get('pr_id', pr_id)} merged — " f"{linked_ticket_id} → Done[/green]" ) + if self._lifecycle: + self._credit_domain_knowledge( + actor=author, + text=f"{pr_title} {review_text}", + day=self._state.day, + pathway="pr_authorship", + ) else: pr["changes_requested"] = True if linked_ticket and linked_ticket.get("status") == "In Review": @@ -1173,55 +1161,21 @@ def _handle_pr_review( date_str=date_str, state=self._state, timestamp=current_actor_time, + author=reviewer, ) - # Fire a structured gap event from reviewer audit metadata — distinct - # from the embedding scan above so the two signal sources are traceable - if review_metadata: - domain_fit = review_metadata.get("author_domain_fit", "high") - gap_class = review_metadata.get("gap_classification", "none") - beyond = review_metadata.get("topics_beyond_author_expertise", []) - hedged = review_metadata.get("hedged_claims", []) - deferred = review_metadata.get("deferred_or_incomplete", []) - - gap_detected = ( - domain_fit == "low" - or gap_class == "likely" - or (gap_class == "possible" and len(beyond) > 0) + if self._lifecycle: + pr_text = f"{pr_title} {review_text}" + self._lifecycle.scan_for_knowledge_gaps( + text=pr_text, + triggered_by=pr.get("pr_id", pr_id or ""), + day=self._state.day, + date_str=date_str, + state=self._state, + timestamp=current_actor_time, + author=author, # Note: author of the PR, not the reviewer ) - if gap_detected: - self._mem.log_event( - SimEvent( - type="knowledge_gap_detected", - timestamp=current_actor_time, - day=self._state.day, - date=date_str, - actors=[author], - artifact_ids={"pr": pr.get("pr_id", pr_id or "")}, - facts={ - "detection_method": "reviewer_audit", - "reviewer": reviewer, - "author": author, - "pr_title": pr_title, - "author_domain_fit": domain_fit, - "author_expertise": expertise_list, - "reviewer_expertise": reviewer_expertise_list, - "gap_classification": gap_class, - "topics_beyond_author_expertise": beyond, - "hedged_claims": hedged, - "deferred_or_incomplete": deferred, - }, - summary=( - f"Knowledge gap detected via reviewer audit: " - f"{author} (expertise: {expertise_str}) submitted PR '{pr_title}' " - f"with fit={domain_fit}, gap={gap_class}. " - f"Reviewed by {reviewer}." - ), - tags=["knowledge_gap", "pr_review", "reviewer_audit"], - ) - ) - logger.info( f" [dim]🔍 {reviewer} reviewed {pr.get('pr_id', 'PR')} [{verdict}][/dim]" ) @@ -1275,7 +1229,12 @@ def _handle_pr_review_for_incident( f"Prior root cause: {ancestor_root_cause[:120]}" ) - ctx = self._mem.context_for_prompt(pr_title, n=2, as_of_time=current_actor_time) + ctx = self._mem.context_for_pr_review( + pr_id=pr_id, + ticket_id=linked_ticket_id or "", + as_of_time=current_actor_time, + n=2, + ) backstory = persona_utils.get_voice_card(reviewer, "async", self._gd, self._mem) p = self._config.get("personas", {}).get(reviewer, {}) @@ -1610,19 +1569,7 @@ def _handle_async_question( ticket_id=ticket_id, as_of_time=meeting_time_iso ) - relevant_experts = self._mem.find_confluence_experts( - topic=ticket_title, - score_threshold=0.75, - n=3, - as_of_time=meeting_time_iso, - ) - doc_hint = ( - "Note: the following internal documentation exists and may be " - "referenced naturally in this conversation:\n" - + "\n".join(f" - '{e['title']}' day {e['day']})" for e in relevant_experts) - if relevant_experts - else "" - ) + _, doc_hint = self._mem.domain_context_for_topic(ticket_title, as_hint=True) discussions = self._mem.design_discussions_for_ticket( ticket_id=ticket_id or "", @@ -2147,7 +2094,6 @@ def _handle_collision_event(self, event: ProposedEvent, date_str: str): participants = event.actors tension = event.facts_hint.get("tension_level", "medium") - # Sync all participants to a shared start time chat_duration_mins = random.randint(5, 30) thread_start, _ = self._clock.sync_and_advance( participants, hours=chat_duration_mins / 60.0 @@ -2201,11 +2147,11 @@ def _handle_collision_event(self, event: ProposedEvent, date_str: str): f"Situation: {event.rationale}\n" f"Tension level: {tension}\n\n" f"{tension_guidance}\n\n" - f"Participants (voice cards — each person's style, mood, and pet peeves):\n{voice_cards}\n\n" + f"Participants (voice cards: each person's style, mood, and pet peeves):\n{voice_cards}\n\n" f"Context: {ctx}\n\n" f"Turn order: {speaker_sequence}\n\n" f"Rules:\n" - f"- Each message must sound distinctly like that person — use their typing quirks\n" + f"- Each message must sound distinctly like that person. Use their typing quirks\n" f"- The conversation must have a natural arc: opening → escalation or negotiation → some resolution or stalemate\n" f"- Each message 1-2 sentences\n" f"- Do not add narration or stage directions\n\n" @@ -2973,7 +2919,19 @@ def _maybe_adhoc_confluence(self) -> None: # using daily_active_actors and persona expertise — do not pick randomly here. # daily_theme is passed so the topic agent can skew toward operational docs # on incident days and strategic docs on calm ones. - self._confluence.write_adhoc_page() + result = self._confluence.write_adhoc_page() + + if isinstance(result, tuple) and len(result) >= 3: + conf_id, adhoc_author, adhoc_topic = result[0], result[1], result[2] + else: + return + if conf_id and adhoc_author and self._lifecycle: + self._credit_domain_knowledge( + actor=adhoc_author, + text=adhoc_topic, + day=self._state.day, + pathway="confluence_authorship", + ) def _trigger_watercooler_chat( self, target_actor: str, date_str: str, penalty_hours: float @@ -3214,6 +3172,7 @@ def _assess_async_thread_gap( date_str=date_str, state=self._state, timestamp=timestamp, + author=asker, ) self._mem.log_event( @@ -3893,42 +3852,24 @@ def _expertise_matched_participants( """ Given a topic string and a seed participant list, return an augmented list that pulls in people whose persona expertise overlaps the topic. - - Priority order: - 1. Anyone in seed_participants stays. - 2. Authors of semantically similar Confluence pages already in MongoDB - are injected as subject-matter experts. This uses vector similarity - via Memory.find_confluence_experts() -- no new embed calls are made - for stored pages, only one embed call for the topic query string. - Causal ordering is enforced by the as_of_time cutoff so a page - being written right now cannot be referenced before it is saved. - 3. Up to max_extras additional people whose persona expertise tags - appear in the topic string, weighted by social-graph proximity to - the seed so the conversation stays socially plausible. - - People with zero expertise overlap are never added -- primary eval guard - against off-domain participants joining technical threads. """ topic_lower = topic.lower() participants: List[str] = list(seed_participants) - # 1. Semantic expert injection via MongoDB vector search. - # find_confluence_experts() reuses already-stored embeddings, so the - # only new embed call is for the topic query string itself. - # as_of_time enforces causal ordering at sub-day precision. - experts = self._mem.find_confluence_experts( - topic=topic, - score_threshold=0.75, - n=5, - as_of_time=as_of_time, - ) - for e in experts: - author = e.get("author") - if author and author in self._all_names and author not in participants: - participants.append(author) + domain_docs, _ = self._mem.domain_context_for_topic(topic) + for rec in domain_docs: + primary = rec.get("primary_owner") + if primary and primary in self._all_names and primary not in participants: + participants.append(primary) + for name in rec.get("known_by", []): + if ( + name in self._all_names + and name not in participants + and len(participants) < len(seed_participants) + max_extras + ): + participants.append(name) + - # 2. Expertise-tag fallback for engineers with no Confluence history yet - # (new hires, or topics that haven't been documented before). if len(participants) >= len(seed_participants) + max_extras: return participants @@ -3985,6 +3926,146 @@ def _turn_count(self, participants: List[str], default_range: tuple) -> int: else: return random.randint(*default_range) # full range + def _credit_domain_knowledge( + self, + actor: str, + text: str, + day: int, + pathway: str, # "incident_resolution" | "pr_authorship" | "confluence_authorship" + ownership_coverage_threshold: float = 0.70, + pr_ownership_hit_threshold: int = 3, + ) -> None: + """ + Award domain knowledge to `actor` based on demonstrated work in `text`. + + For every domain_registry entry whose system_tags appear in `text`: + + 1. Add `actor` to `known_by` (always, all pathways). + + 2. Promote `actor` to `primary_owner` when: + - pathway == "confluence_authorship" and + documentation_coverage >= ownership_coverage_threshold (default 70%) + - pathway == "pr_authorship" and the actor has now touched this + domain in >= pr_ownership_hit_threshold PRs (default 3) + - pathway == "incident_resolution" and the domain is orphaned + (primary_owner is None) — incident responders who step up for + an orphaned system are the most natural owners. + + """ + if not self._lifecycle or not self._mem: + return + + text_lower = text.lower() + all_domains = list(self._mem._db["domain_registry"].find({})) + + for rec in all_domains: + tags = rec.get("system_tags", []) + if not any(tag in text_lower for tag in tags): + continue + + domain_id = rec["_id"] + domain_name = rec["domain"] + current_owner = rec.get("primary_owner") + current_coverage = rec.get("documentation_coverage", 0.0) + known_by = rec.get("known_by", []) + + # ── Always: add to known_by ─────────────────────────────────────── + if actor not in known_by: + self._mem._db["domain_registry"].update_one( + {"_id": domain_id}, + {"$addToSet": {"known_by": actor}}, + ) + logger.info( + f" [dim]→ Domain '{domain_name}': {actor} added to known_by " + f"via {pathway}.[/dim]" + ) + + if current_owner is not None and current_owner != actor: + if pathway != "confluence_authorship": + continue + continue + + promote = False + promotion_reason = "" + + if pathway == "incident_resolution": + if current_owner is None: + promote = True + promotion_reason = ( + f"resolved incident touching orphaned domain " + f"(coverage={int(current_coverage * 100)}%)" + ) + + elif pathway == "pr_authorship": + pr_touches = rec.get("pr_touches", {}) + actor_touches = pr_touches.get(actor, 0) + 1 + + self._mem._db["domain_registry"].update_one( + {"_id": domain_id}, + {"$set": {f"pr_touches.{actor}": actor_touches}}, + ) + + if ( + current_owner is None + and actor_touches >= pr_ownership_hit_threshold + ): + promote = True + promotion_reason = ( + f"authored {actor_touches} PRs touching this domain " + f"(threshold={pr_ownership_hit_threshold})" + ) + + elif pathway == "confluence_authorship": + if ( + current_owner is None + and current_coverage >= ownership_coverage_threshold + ): + promote = True + promotion_reason = ( + f"documentation coverage reached " + f"{int(current_coverage * 100)}% " + f"(threshold={int(ownership_coverage_threshold * 100)}%)" + ) + + if promote: + self._mem._db["domain_registry"].update_one( + {"_id": domain_id}, + { + "$set": { + "primary_owner": actor, + "last_updated_day": day, + }, + "$addToSet": {"known_by": actor}, + }, + ) + logger.info( + f" [cyan]🏷 Domain '{domain_name}':[/cyan] " + f"{actor} promoted to primary_owner — {promotion_reason}." + ) + + self._mem.log_event( + SimEvent( + type="domain_ownership_claimed", + timestamp=self._clock.now(actor).isoformat(), + day=day, + date=str(self._state.current_date.date()), + actors=[actor], + artifact_ids={}, + facts={ + "domain": domain_name, + "new_owner": actor, + "pathway": pathway, + "documentation_coverage": round(current_coverage, 3), + "promotion_reason": promotion_reason, + }, + summary=( + f"{actor} became primary owner of '{domain_name}' " + f"via {pathway.replace('_', ' ')}. {promotion_reason}." + ), + tags=["domain_registry", "knowledge_recovery", pathway], + ) + ) + def dept_of_name(name: str, org_chart: Dict[str, List[str]]) -> str: for dept, members in org_chart.items(): diff --git a/src/org_lifecycle.py b/src/org_lifecycle.py index 677bfe1..bd0f105 100644 --- a/src/org_lifecycle.py +++ b/src/org_lifecycle.py @@ -100,9 +100,9 @@ def __init__( config: dict, graph_dynamics: GraphDynamics, mem: Memory, - org_chart: Dict[str, List[str]], # mutable — mutated in place - personas: Dict[str, dict], # mutable — mutated in place - all_names: List[str], # mutable — mutated in place + org_chart: Dict[str, List[str]], + personas: Dict[str, dict], + all_names: List[str], leads: Dict[str, str], worker_llm=None, base_export_dir: str = "", @@ -193,7 +193,9 @@ def process_departures( attrition_cfg = { "name": candidate, "reason": "voluntary", - "knowledge_domains": [], + "knowledge_domains": self._personas.get(candidate, {}).get( + "expertise", [] + ), "documented_pct": 0.5, } record = self._execute_departure( @@ -231,48 +233,86 @@ def scan_for_knowledge_gaps( date_str: str, state, timestamp: str, - similarity_threshold: float = 0.65, + author: Optional[str] = None, ) -> List[KnowledgeGapEvent]: """ - Detect knowledge gaps using semantic similarity against departed employee - expertise profiles, then cross-reference the DomainRegistry for live - documentation coverage and orphan status. - - Two-pass detection: - Pass 1 — Embedding similarity: embed the trigger text and compare - against departed employees' persona_skill vectors. Catches - semantic drift (e.g. "auth timeout" → "identity management"). - Pass 2 — DomainRegistry cross-reference: for each matched domain, - pull the current documentation_coverage from the registry so - the SimEvent carries an accurate, mutable coverage score rather - than the static documented_pct frozen at departure. + Detect knowledge gaps deterministically using BM25 text search against + departed employee expertise profiles, then cross-reference the + DomainRegistry for live documentation coverage and orphan status. + + Two-pass detection (fully deterministic given the same index state): + + Pass 1 -- BM25 text search: run the trigger text through + find_expert_by_skill() which calls search_persona_skills_text() + -> search_artifacts_text() with type_filter="persona_skill". + Atlas Search scores using alias-weighted BM25: + title boost 3x, aliases boost 2x, why_it_matters boost 1x. + This catches vocabulary overlap (e.g. "auth timeout" matches + aliases ["auth", "identity", "sso"] on a departed employee's + persona_skill record). + + Pass 2 -- DomainRegistry cross-reference: for each matched departed + employee, check whether their orphaned domains have a live + owner or whether the current author is in known_by. Pull + documentation_coverage from the registry so the SimEvent + carries an accurate, mutable coverage score rather than the + static documented_pct frozen at departure. + + Classification uses hardcoded thresholds on documentation_coverage: + - coverage < 0.3 -> "likely" + - coverage < 0.6 -> "possible" + - else -> "none" + + No LLM involvement. Fully reproducible given the same Atlas Search + index state and domain_registry contents. Args: - text: The incident root cause or description text. - triggered_by: The artifact ID (e.g., JIRA ticket) that surfaced this. - day: Current simulation day. - date_str: Current date as ISO string. - state: Simulation state object. - timestamp: ISO timestamp of the triggering event. - similarity_threshold: Minimum vector similarity score (0–1) to consider - a departed employee's expertise a match. Default 0.65 - is tuned for dotProduct with 1024-dim vectors. + text: The content text to check for gap overlap. + triggered_by: The artifact ID (e.g., CONF page) that surfaced this. + day: Current simulation day. + date_str: Current date as ISO string. + state: Simulation state object. + timestamp: ISO timestamp of the triggering event. + author: Optional author name; if provided, gaps where the + author is in known_by for all affected domains are + suppressed (they already have coverage). """ found: List[KnowledgeGapEvent] = [] if not self._departed: return found - # ── Pass 1: embedding similarity ────────────────────────────────────── - expert_matches = self._mem.find_expert_by_skill(text, n=20) + registry = self._mem.get_domain_registry() + orphaned_by_former_owner: Dict[str, List[dict]] = {} + for rec in registry.values(): + if rec.get("primary_owner") is not None: + continue + former_owners = set(rec.get("former_owners", [rec.get("former_owner", "")])) + for fo in former_owners: + orphaned_by_former_owner.setdefault(fo, []).append(rec) + + expert_matches = self._mem.find_expert_by_skill(text, n=3) match_scores: Dict[str, float] = {} for match in expert_matches: name = match.get("name") score = match.get("score", 0.0) - if name and score >= similarity_threshold: - if name not in match_scores or score > match_scores[name]: - match_scores[name] = score + if not name: + continue + + if name not in orphaned_by_former_owner: + continue + + if author: + owned_domains = orphaned_by_former_owner[name] + all_covered = all( + author in rec.get("known_by", []) for rec in owned_domains + ) + if all_covered: + continue + + if name not in match_scores or score > match_scores[name]: + match_scores[name] = score for record in self._departed: if record.name not in match_scores: @@ -291,12 +331,8 @@ def scan_for_knowledge_gaps( domain_label = ", ".join(gap_domains) - # ── Pass 2: DomainRegistry cross-reference ───────────────────── - # Look up live coverage for each matched domain. If the registry - # has been updated by Confluence writes since genesis, this will - # reflect partial recovery rather than the static departure value. registry_hits: List[dict] = [] - live_coverage: float = record.documented_pct # fallback to departure value + live_coverage: float = record.documented_pct for domain in gap_domains: key = domain.lower().replace(" ", "_") @@ -305,8 +341,6 @@ def scan_for_knowledge_gaps( registry_hits.append(reg_doc) if registry_hits: - # Use the lowest coverage across all matched domains — most - # conservative estimate, most useful for gap severity scoring. live_coverage = min(r["documentation_coverage"] for r in registry_hits) known_by = list( {name for r in registry_hits for name in r.get("known_by", [])} @@ -316,7 +350,7 @@ def scan_for_knowledge_gaps( ] else: known_by = [] - orphaned_domains = gap_domains # assume orphaned if not in registry + orphaned_domains = gap_domains gap_event = KnowledgeGapEvent( departed_name=record.name, @@ -328,7 +362,6 @@ def scan_for_knowledge_gaps( self._gap_events.append(gap_event) found.append(gap_event) - # Severity classification — used downstream by gap_detected logic if live_coverage < 0.3: gap_classification = "likely" elif live_coverage < 0.6: @@ -348,18 +381,15 @@ def scan_for_knowledge_gaps( "departed_employee": record.name, "gap_areas": gap_domains, "triggered_by": triggered_by, - "documented_pct": record.documented_pct, # at departure - "live_documentation_coverage": round( - live_coverage, 3 - ), # current + "documented_pct": record.documented_pct, + "live_documentation_coverage": round(live_coverage, 3), "days_since_departure": day - record.day, "escalation_harder": True, "semantic_score": round(score, 4), - "detection_method": "embedding_similarity", + "detection_method": "bm25_text_search", "gap_classification": gap_classification, "orphaned_domains": orphaned_domains, "known_by": known_by, - # Fields aligned with PR reviewer audit schema "topics_beyond_author_expertise": gap_domains, "author_domain_fit": ( "low" @@ -371,8 +401,8 @@ def scan_for_knowledge_gaps( }, summary=( f"Knowledge gap: {domain_label} (owned by ex-{record.name}, " - f"similarity={score:.3f}) surfaced in {triggered_by}. " - f"Coverage at departure: {int(record.documented_pct * 100)}% → " + f"score={score:.3f}) surfaced in {triggered_by}. " + f"Coverage at departure: {int(record.documented_pct * 100)}% -> " f"live: {int(live_coverage * 100)}%." + ( f" Orphaned: {orphaned_domains}." @@ -410,7 +440,6 @@ def _load_departed_from_log(self, events: List[SimEvent]) -> None: name = next(iter(e.actors), None) if not name: continue - # Avoid duplicates if called multiple times if any(r.name == name for r in self._departed): continue self._departed.append( @@ -445,7 +474,6 @@ def get_roster_context(self, include_all_open_gaps: bool = False) -> str: else self._departed[-3:] ) for d in departed_to_show: - # Pull live coverage from registry rather than the static departure value live_coverages: List[str] = [] orphaned: List[str] = [] for domain in d.knowledge_domains: @@ -526,8 +554,6 @@ def find_departure(self, name: str) -> Optional[DepartureRecord]: def find_hire(self, name: str) -> Optional[HireRecord]: return next((h for h in self._hired if h.name == name), None) - # ─── DEPARTURE ENGINE ───────────────────────────────────────────────────── - def _execute_departure( self, dep_cfg: dict, day: int, date_str: str, state, scheduled: bool, clock ) -> Optional[DepartureRecord]: @@ -547,7 +573,6 @@ def _execute_departure( or f"{dept} Employee" ) - # Snapshot before any mutation edge_snapshot = {nb: G[name][nb].get("weight", 1.0) for nb in G.neighbors(name)} peak_stress = self._gd._stress.get(name, 30) centrality_before = dict(self._gd._get_centrality()) @@ -571,7 +596,6 @@ def _execute_departure( ) timestamp_iso = departure_time.isoformat() - # Side-effects run in this exact order so each can still reference live graph: # 1. Incident handoff — needs Dijkstra path through departing node # 2. JIRA reassignment — reads ticket assignees from state # 3. Remove node — graph mutation @@ -592,7 +616,6 @@ def _execute_departure( centrality_before, name, day, date_str, timestamp_iso ) - # Mutate org-level collections if dept in self._org_chart and name in self._org_chart[dept]: self._org_chart[dept].remove(name) if name in self._all_names: @@ -664,10 +687,8 @@ def _execute_departure( day=day, ) - # ── Orphan any DomainRegistry records this engineer owned ───────────── - # Mid-sim departures may leave primary_owner set. Null it out so - # scan_for_knowledge_gaps correctly treats those domains as orphaned. self._orphan_domains_on_departure(name, record, day) + self._register_domains_on_departure(name, record, day) logger.info( f" [red]👋 Departure:[/red] {name} ({dept}) [{record.reason}]. " @@ -680,7 +701,42 @@ def _execute_departure( ) return record - # ── Side-effect 1: Incident handoff ────────────────────────────────────── + def _register_domains_on_departure( + self, name: str, record: DepartureRecord, day: int + ) -> None: + """ + Insert domain_registry entries for any knowledge domains this engineer + owned that don't already have a record. Mirrors what seed_knowledge_gaps + does for pre-sim (genesis) gaps so mid-sim departures are treated + identically by scan_for_knowledge_gaps and get_roster_context. + """ + from genesis import _domain_key, _build_system_tags + + for domain in record.knowledge_domains: + key = _domain_key(domain) + if self._mem._db["domain_registry"].find_one({"_id": key}): + continue + + system_tags = _build_system_tags(domain, record.knowledge_domains) + doc = { + "_id": key, + "domain": domain, + "primary_owner": None, + "former_owner": name, + "former_owners": [name], + "documentation_coverage": record.documented_pct, + "coverage_at_last_departure": record.documented_pct, + "last_updated_day": day, + "known_by": [], + "system_tags": system_tags, + "dept": record.dept, + "is_genesis_gap": False, + } + self._mem._db["domain_registry"].insert_one(doc) + logger.info( + f" [dim]→ Domain registry: '{domain}' registered on departure of {name}. " + f"Coverage={int(record.documented_pct * 100)}%, orphaned.[/dim]" + ) def _handoff_active_incidents( self, @@ -703,14 +759,12 @@ def _handoff_active_incidents( if not jira or jira.get("assignee") != name: continue - # Build chain while departing node is still in the graph chain = self._gd.build_escalation_chain( first_responder=name, domain_keywords=record.knowledge_domains or None, ) new_owner = next((n for n, _ in chain.chain if n != name), dept_lead) - # Deterministic mutation — engine owns this, not the LLM jira["assignee"] = new_owner self._mem.upsert_ticket(jira) @@ -749,8 +803,6 @@ def _handoff_active_incidents( f"(stage={inc.stage}) {name} → {new_owner}" ) - # ── Side-effect 2: JIRA ticket reassignment ─────────────────────────────── - def _reassign_jira_tickets( self, name: str, @@ -781,9 +833,7 @@ def _reassign_jira_tickets( for ticket in open_tickets: if ticket.get("assignee") != name or ticket.get("status") == "Done": continue - # Skip tickets that were already handed off via incident handoff above if ticket.get("id") in record.incident_handoffs: - # Assignee already updated — just log and continue record.reassigned_tickets.append(ticket["id"]) continue @@ -830,8 +880,6 @@ def _reassign_jira_tickets( f"{', '.join(record.reassigned_tickets)} → {dept_lead}" ) - # ── Side-effect 3: Centrality vacuum ───────────────────────────────────── - def _apply_centrality_vacuum( self, centrality_before: Dict[str, float], @@ -881,7 +929,7 @@ def _apply_centrality_vacuum( self._mem.log_event( SimEvent( - type="knowledge_gap_detected", # closest existing type for RAG eval + type="knowledge_gap_detected", timestamp=clock, day=day, date=date_str, @@ -907,8 +955,6 @@ def _apply_centrality_vacuum( f"Stress absorbed by: {summary_str}" ) - # ── Domain registry maintenance ────────────────────────────────────────── - def _orphan_domains_on_departure( self, name: str, record: DepartureRecord, day: int ) -> None: @@ -926,7 +972,7 @@ def _orphan_domains_on_departure( { "$set": { "primary_owner": None, - "coverage_at_last_departure": None, # will be set per-doc below + "coverage_at_last_departure": None, }, "$addToSet": {"former_owners": name}, }, @@ -934,7 +980,6 @@ def _orphan_domains_on_departure( if result.modified_count == 0: return - # Per-doc update to capture individual coverage values at departure orphaned = self._mem._db["domain_registry"].find( {"former_owners": name, "primary_owner": None} ) @@ -969,7 +1014,6 @@ def _claim_domains_on_hire(self, name: str, expertise: List[str], day: int) -> N {"_id": key, "primary_owner": None} ) if not doc: - # Also try system_tags match for inexact expertise names doc = self._mem._db["domain_registry"].find_one( {"system_tags": key, "primary_owner": None} ) @@ -989,8 +1033,6 @@ def _claim_domains_on_hire(self, name: str, expertise: List[str], day: int) -> N f"Coverage: {int(doc.get('documentation_coverage', 0) * 100)}%.[/dim]" ) - # ─── HIRE ENGINE ────────────────────────────────────────────────────────── - def _execute_hire( self, hire_cfg: dict, day: int, date_str: str, state, clock ) -> Optional[HireRecord]: @@ -1023,7 +1065,6 @@ def _execute_hire( timestamp_iso=clock.now("system").isoformat(), ) - # Claim any orphaned DomainRegistry domains that match this hire's expertise self._claim_domains_on_hire(name, expertise, day) if name in G: @@ -1040,8 +1081,6 @@ def _execute_hire( self._gd._stress[name] = 20 self._gd._centrality_dirty = True - # Same-dept peers get 2× floor; cross-dept gets floor. - # Both are below warmup_threshold so the planner naturally proposes 1on1s. for other in list(G.nodes()): if other == name: continue @@ -1211,7 +1250,6 @@ def _generate_backfill_persona( Crew(agents=[agent], tasks=[task], verbose=False).kickoff() ).strip() - # Strip markdown fences if the LLM included them anyway raw = ( raw.strip() .removeprefix("```json") diff --git a/src/plan_validator.py b/src/plan_validator.py index 6c8bbe4..27d1835 100644 --- a/src/plan_validator.py +++ b/src/plan_validator.py @@ -100,7 +100,6 @@ def external_contact_names(self, names: List[str]): Called daily by the DayPlannerOrchestrator. """ self._external_names = set(names) - # Recalculate the union set used by _validate_one self._valid_actors = self._internal_names | self._external_names def validate_plan( diff --git a/src/planner_models.py b/src/planner_models.py index 58090ca..20c657d 100644 --- a/src/planner_models.py +++ b/src/planner_models.py @@ -140,8 +140,6 @@ class SprintContext: in_review: List[str] sprint_theme: str = "" ticket_domain_tags: Dict[str, List[str]] = field(default_factory=dict) - # e.g. {"TICKET-42": ["billing-legacy"], "TICKET-43": ["kafka", "infra"]} - # Populated by TicketAssigner via domain registry cross-reference. @dataclass @@ -176,6 +174,7 @@ class DepartmentDayPlan: day: int date: str sprint_context: Optional["SprintContext"] = None + avg_stress: int = 0 @dataclass diff --git a/src/post_sim_artifacts.py b/src/post_sim_artifacts.py index 38b0645..01f7fbc 100644 --- a/src/post_sim_artifacts.py +++ b/src/post_sim_artifacts.py @@ -744,7 +744,6 @@ def build_alerts( resolve_ts = inc.get("resolve_ts") alert = { - # Datadog Events API schema fields "id": iid, "title": f"[P1] {monitor_name}", "text": ( diff --git a/src/ticket_assigner.py b/src/ticket_assigner.py index fc73d88..712ed83 100644 --- a/src/ticket_assigner.py +++ b/src/ticket_assigner.py @@ -5,10 +5,11 @@ Graph-weighted assignment Scores every (engineer, ticket) pair using: - • skill match via embedding cosine similarity (ticket title ↔ engineer expertise) - • inverse stress (burnt-out engineers get lighter loads) - • betweenness centrality penalty (key players shouldn't hoard tickets) - • recency bonus (engineer already touched this ticket in a prior sprint) + - skill match via BM25 term overlap (ticket title tokens vs engineer + expertise tags from domain_registry) + - inverse stress (burnt-out engineers get lighter loads) + - betweenness centrality penalty (key players shouldn't hoard tickets) + - recency bonus (engineer already touched this ticket in a prior sprint) Uses scipy linear_sum_assignment (Hungarian algorithm) for globally optimal matching. Falls back to greedy round-robin if scipy is absent. @@ -16,39 +17,30 @@ Pass 1 (this module): builds a fully valid SprintContext with locked assignments before any LLM call. Pass 2 (DepartmentPlanner): receives SprintContext and only writes - narrative — it cannot affect who owns what. - -The result is a SprintContext injected into every DepartmentPlanner prompt. -The LLM sees only its legal menu; ownership conflicts become structurally -impossible rather than validated-away after the fact. + narrative -- it cannot affect who owns what. Skill scoring ------------- - Engineer expertise strings (joined from persona["expertise"]) and ticket - titles are both embedded at runtime using the same embedder already wired - into Memory. Cosine similarity replaces the old hardcoded _SKILL_KEYWORDS - dict, so the scorer generalises to any domain or industry defined in - config.yaml without code changes. - - Engineer vectors are computed once in __init__ and cached in memory. - Ticket title vectors are cached in a dedicated MongoDB collection - ("ticket_skill_embeddings") keyed by ticket_id, so each title is only - embedded once across the full simulation. + Engineer expertise tags (from domain_registry) are matched against + ticket title tokens via a normalized term overlap score. The overlap is + rescaled to [0.5, 1.5] -- the same range used by the old cosine path -- + so the Hungarian matrix is numerically identical. Public API ---------- assigner = TicketAssigner(config, graph_dynamics, mem) sprint_ctx = assigner.build(state, dept_members) - # → SprintContext with owned_tickets, available_tickets, capacity_by_member """ from __future__ import annotations import logging -from typing import Dict, List -import json as _json +import os +import re +from typing import Dict, List, Set import numpy as np +import json as _json from graph_dynamics import GraphDynamics from memory import Memory @@ -57,13 +49,57 @@ logger = logging.getLogger("orgforge.ticket_assigner") -def _cosine(a: List[float], b: List[float]) -> float: - """Safe cosine similarity — returns 0.0 if either vector is empty/zero.""" - if not a or not b: - return 0.0 - va, vb = np.asarray(a, dtype=np.float32), np.asarray(b, dtype=np.float32) - denom = np.linalg.norm(va) * np.linalg.norm(vb) - return float(np.dot(va, vb) / denom) if denom > 1e-9 else 0.0 +_STOP_WORDS: Set[str] = { + "a", + "an", + "the", + "and", + "or", + "in", + "of", + "to", + "for", + "with", + "on", + "at", + "by", + "up", + "is", + "are", + "was", + "be", + "as", + "from", + "into", + "that", + "this", + "it", + "its", + "our", + "we", + "i", + "my", + "their", + "not", + "no", + "new", + "add", + "fix", + "update", + "improve", + "support", + "using", + "via", +} + + +def _tokenize(text: str) -> Set[str]: + """ + Lowercase, split on non-alphanumeric boundaries, strip stop words. + Returns a set of meaningful tokens. + """ + tokens = re.findall(r"[a-z0-9]+", text.lower()) + return {t for t in tokens if len(t) > 1 and t not in _STOP_WORDS} class TicketAssigner: @@ -74,7 +110,7 @@ class TicketAssigner: ---------- config : the full OrgForge config dict graph_dynamics : live GraphDynamics instance (owns stress + betweenness) - mem : shared Memory instance (embedder + MongoDB collections) + mem : shared Memory instance (MongoDB collections) """ def __init__(self, config: dict, graph_dynamics: GraphDynamics, mem: Memory): @@ -83,23 +119,17 @@ def __init__(self, config: dict, graph_dynamics: GraphDynamics, mem: Memory): self._mem = mem self._base = config["simulation"].get("output_dir", "./export") - self._skill_embed_cache = mem._db["ticket_skill_embeddings"] - self._skill_embed_cache.create_index([("ticket_id", 1)], unique=True) - - self._engineer_vectors: Dict[str, List[float]] = {} - self._precompute_engineer_vectors() - def build( self, state, dept_members: List[str], dept_name: str = "", on_call: str = "" ) -> SprintContext: """ - Main entry point. Call once per department, before DepartmentPlanner.plan(). + Main entry point. Call once per department, before DepartmentPlanner.plan(). Returns a SprintContext with: - • owned_tickets — final {ticket_id: engineer} mapping - • available_tickets — unowned ticket IDs (for the LLM to reference) - • in_progress_ids — tickets already "In Progress" - • capacity_by_member — {name: available_hrs} for every dept member + - owned_tickets -- final {ticket_id: engineer} mapping + - available_tickets -- unowned ticket IDs (for the LLM to reference) + - in_progress_ids -- tickets already "In Progress" + - capacity_by_member -- {name: available_hrs} for every dept member """ capacity = self._compute_capacity(dept_members, state, on_call=on_call) @@ -129,6 +159,7 @@ def build( ticket["assignee"] = owner if self._base: path = f"{self._base}/jira/{ticket['id']}.json" + os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as f: _json.dump(ticket, f, indent=2) self._mem.upsert_ticket(ticket) @@ -205,8 +236,8 @@ def _hungarian_assign( """ Globally optimal assignment via scipy's Hungarian algorithm. - Cost matrix [engineers × tickets] - Each cell = -(skill_score × stress_score × centrality_factor) + Cost matrix [engineers x tickets] + Each cell = -(skill_score x stress_score x centrality_factor x recency) Negative because linear_sum_assignment minimises cost. """ from scipy.optimize import linear_sum_assignment @@ -217,6 +248,7 @@ def _hungarian_assign( n_eng = len(members) n_tkt = len(tickets) cost = np.zeros((n_eng, n_tkt)) + assignment_scores = [] for i, eng in enumerate(members): stress = self._gd._stress.get(eng, 30) @@ -234,12 +266,12 @@ def _hungarian_assign( { "day": state.day, "engineer": eng, - "ticket_id": tickets[j]["id"], - "skill_score": self._skill_score(eng, tickets[j]), - "stress_score": 1.0 - (self._gd._stress.get(eng, 30) / 100), - "centrality_factor": 1.0 - (centrality.get(eng, 0.0) * 0.3), - "composite_score": -cost[i][j], - "was_assigned": False, # update after linear_sum_assignment + "ticket_id": ticket["id"], + "skill_score": skill, + "stress_score": stress_score, + "centrality_factor": cent_factor, + "composite_score": score, + "was_assigned": False, } ) @@ -260,7 +292,10 @@ def _hungarian_assign( else: logger.debug(f"[assigner] {eng} over capacity, skipping {tkt['id']}") - self._mem._db["assignment_scores"].insert_many(assignment_scores) + try: + self._mem._db["assignment_scores"].insert_many(assignment_scores) + except Exception as exc: + logger.warning(f"[assigner] assignment_scores insert failed: {exc}") return result @@ -290,184 +325,39 @@ def _greedy_assign( return result def _skill_score(self, engineer: str, ticket: dict) -> float: - """ - Returns a [0.5, 1.5] score representing how well the engineer's - expertise matches the ticket title. - - Method - ------ - 1. Engineer expertise is embedded once at init (or lazily on first use - for mid-sim hires) and stored in self._engineer_vectors. - 2. Ticket title is embedded on first encounter and cached in MongoDB - ("ticket_skill_embeddings"), so re-scoring the same ticket in a - later sprint costs zero embed calls. - 3. Cosine similarity ∈ [-1, 1] is linearly rescaled to [0.5, 1.5] - so it's a drop-in replacement for the old keyword ratio. - - Neutral / empty-expertise engineers default to 1.0 (no preference). - """ - eng_vec = self._expertise_vector(engineer) - if not eng_vec: + title_tokens = _tokenize(f"{ticket.get('title', '')} {ticket.get('description', '')}") + if not title_tokens: return 1.0 - tkt_vec = self._ticket_title_vector(ticket) - if not tkt_vec: + engineer_tokens = self._mem.get_author_domain_tokens(engineer) + if not engineer_tokens: return 1.0 - similarity = _cosine(eng_vec, tkt_vec) - - return 0.5 + (similarity + 1.0) / 2.0 - - def _precompute_engineer_vectors(self) -> None: - """ - Embed every known persona's expertise at startup. - Called once in __init__; new-hire personas picked up lazily via - _expertise_vector() during the sim. - """ - from config_loader import PERSONAS - - for name in PERSONAS: - doc = self._mem._artifacts.find_one( - {"_id": name, "type": "persona_skills"}, {"_id": 0, "embedding": 1} - ) - if doc and doc.get("embedding"): - self._engineer_vectors[name] = doc["embedding"] - - def _expertise_vector(self, engineer: str) -> List[float]: - """ - Return (and lazily cache) the expertise embedding for an engineer. - Handles mid-sim hires whose persona wasn't present at __init__ time. - """ - if engineer not in self._engineer_vectors: - from config_loader import PERSONAS, DEFAULT_PERSONA - - persona = PERSONAS.get(engineer, DEFAULT_PERSONA) - self._engineer_vectors[engineer] = self._build_expertise_vector( - engineer, persona - ) - return self._engineer_vectors[engineer] - - def _build_expertise_vector(self, name: str, persona: dict) -> List[float]: - """ - Produce a single embedding for an engineer by joining their expertise - list and style into a short descriptive string. - - Example input → "backend infra distributed-systems | methodical architect" - This gives the embedder enough semantic context to differentiate - a backend specialist from a mobile or design engineer. - """ - expertise: List[str] = [e.lower() for e in persona.get("expertise", [])] - style: str = persona.get("style", "").lower() - - if not expertise and not style: - return [] - - text_parts = [] - if expertise: - text_parts.append(" ".join(expertise)) - if style: - text_parts.append(style) - - text = " | ".join(text_parts) - try: - return self._mem._embed( - text, - input_type="search_document", - caller="ticket_assigner.expertise", - doc_id=name, - doc_type="engineer_expertise", - ) - except Exception as exc: - logger.warning(f"[assigner] expertise embed failed for {name}: {exc}") - return [] - - def _ticket_title_vector(self, ticket: dict) -> List[float]: - """ - Return the embedding for a ticket title, using MongoDB as a - write-through cache to avoid re-embedding across sprints. - - Cache document schema: - { ticket_id: str, title: str, embedding: List[float] } - """ - ticket_id: str = ticket.get("id", "") - title: str = ticket.get("title", "").strip() - - if not title: - return [] - - cached = self._skill_embed_cache.find_one( - {"ticket_id": ticket_id}, {"embedding": 1, "_id": 0} - ) - if cached and cached.get("embedding"): - return cached["embedding"] - - try: - vector = self._mem._embed( - title, - input_type="search_query", - caller="ticket_assigner.ticket_title", - doc_id=ticket_id, - doc_type="ticket_title", - ) - except Exception as exc: - logger.warning( - f"[assigner] ticket title embed failed for {ticket_id!r}: {exc}" - ) - return [] - - if vector: - try: - self._skill_embed_cache.update_one( - {"ticket_id": ticket_id}, - { - "$set": { - "ticket_id": ticket_id, - "title": title, - "embedding": vector, - } - }, - upsert=True, - ) - except Exception as exc: - logger.warning( - f"[assigner] ticket embed cache write failed for {ticket_id!r}: {exc}" - ) - - return vector + overlap_count = len(engineer_tokens & title_tokens) + raw_overlap = min(1.0, overlap_count / len(title_tokens)) + return 0.5 + raw_overlap def evict_engineer(self, name: str) -> None: """ - Remove a departed engineer's vector from the cache so they no longer - influence cost matrix scoring in _hungarian_assign. Called by OrgLifecycleManager after _execute_departure completes. + No-op in the BM25 pipeline since there are no cached vectors to evict. + Retained for interface compatibility with OrgLifecycleManager. """ - self._engineer_vectors.pop(name, None) - logger.debug(f"[assigner] Evicted vector for departed engineer: {name}") + logger.debug(f"[assigner] Eviction noted for departed engineer: {name}") def register_hire(self, name: str) -> None: """ - Pre-warm the expertise vector for a new hire so their first sprint - assignment scores correctly rather than defaulting to the neutral 1.0. Called by OrgLifecycleManager after _execute_hire completes. - The persona must already be written to PERSONAS before this is called. + No pre-warming needed -- expertise comes from the live persona config + which is already populated before this is called. + Retained for interface compatibility with OrgLifecycleManager. """ - vec = self._expertise_vector(name) - if vec: - logger.debug( - f"[assigner] Pre-warmed expertise vector for new hire: {name} " - f"({len(vec)}-dim)" - ) - else: - logger.debug( - f"[assigner] No expertise vector for new hire {name} " - f"(empty expertise — will score neutral)" - ) + logger.debug(f"[assigner] New hire registered: {name} (no pre-warm needed)") def _ticket_history(self, state) -> Dict[str, set]: """ Returns {engineer: {ticket_ids they've touched in prior days}}. - Derived from ticket_actors_today which flow.py accumulates over the sim. - Also checks jira_tickets assignee history for continuity. + Derived from jira_tickets assignee history for continuity across sprints. """ history: Dict[str, set] = {} for ticket in self._mem._jira.find( diff --git a/src/utils/persona_utils.py b/src/utils/persona_utils.py index 01dc30f..8681528 100644 --- a/src/utils/persona_utils.py +++ b/src/utils/persona_utils.py @@ -37,6 +37,7 @@ def get_voice_card( graph_dynamics=None, mem=None, internal=True, + include_expertise: bool = True, ) -> str: """ Unified persona generator for all OrgForge LLM prompts. @@ -60,6 +61,7 @@ def get_voice_card( tenure = p.get("tenure", "mid") dept = dept_of_name(name) + expertise = ", ".join( str(e) for e in p.get("expertise", [])[:3] ) or ", ".join( @@ -67,12 +69,13 @@ def get_voice_card( dept, ["cross-functional communication"] ) ) + if mem and include_expertise: + live_tokens = mem.get_author_domain_tokens(name) + expertise = ", ".join(sorted(live_tokens)[:5]) if live_tokens else expertise social_role = p.get("social_role", "Contributor") interests = ( - ", ".join( - str(i) for i in (p.get("interests") or p.get("expertise", []))[:3] - ) + ", ".join(str(i) for i in p.get("interests", [])[:3]) or "general topics" ) style = p.get("style", "") @@ -155,7 +158,8 @@ def get_voice_card( lines = [header, f" Typing style: {quirks}", f" Current mood: {mood}"] - lines.insert(2, f" Expertise: {expertise}") + if include_expertise: + lines.insert(2, f" Expertise: {expertise}") if context == "watercooler": lines.insert(2, f" Personal interests: {interests}") diff --git a/tests/conftest.py b/tests/conftest.py index 397dcbf..f4cde1a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,11 +22,44 @@ def smart_open(filename, mode="r", *args, **kwargs): return real_open(filename, mode, *args, **kwargs) +@pytest.fixture(autouse=True) +def patch_mongomock_search_indexes(): + """ + mongomock doesn't implement Atlas Search index methods + (list_search_indexes, create_search_index, drop_search_index). + This patches them to no-ops so Memory._init_text_indexes() + runs without error during tests. + """ + def _noop_list_search_indexes(self): + return [] + + def _noop_create_search_index(self, model=None): + return "mock-index" + def _noop_drop_search_index(self, name): + pass + + with patch.object( + mongomock.Collection, "list_search_indexes", _noop_list_search_indexes, create=True + ), patch.object( + mongomock.Collection, "create_search_index", _noop_create_search_index, create=True + ), patch.object( + mongomock.Collection, "drop_search_index", _noop_drop_search_index, create=True + ): + yield + +@pytest.fixture(autouse=True) +def patch_search_artifacts_text(): + """ + mongomock doesn't support $search (Atlas Search aggregation stage). + Patch search_artifacts_text to return empty results during tests. + """ + with patch.object(Memory, "search_artifacts_text", return_value=[]): + yield + @pytest.fixture def make_test_memory(): - with patch("memory.Memory._init_vector_indexes"): - mem = Memory(mongo_client=mongomock.MongoClient()) + mem = Memory(mongo_client=mongomock.MongoClient()) mem.log_event = MagicMock(wraps=mem.log_event) return mem @@ -35,7 +68,7 @@ def make_test_memory(): def mock_config_and_db(): """ Prevents tests from actually trying to load local files or - connect to MongoDB/Ollama during initialization. + connect to MongoDB during initialization. """ mock_cfg = { "simulation": { @@ -77,16 +110,10 @@ def mock_config_and_db(): "external_contacts": [], } - mock_embedder = MagicMock() - mock_embedder.embed.return_value = [0.1] * 1024 - mock_embedder.dims = 1024 - with ( patch("builtins.open", side_effect=smart_open), patch("yaml.safe_load", return_value=mock_cfg), patch("memory.MongoClient"), - patch("memory.build_embedder", return_value=mock_embedder), - patch("memory.Memory._init_vector_indexes"), patch("agent_factory.Agent"), patch("normal_day.Task"), patch("normal_day.Crew") as mock_crew_cls, diff --git a/tests/test_causal_chain_handler.py b/tests/test_causal_chain_handler.py index 677cdcd..53f6290 100644 --- a/tests/test_causal_chain_handler.py +++ b/tests/test_causal_chain_handler.py @@ -45,84 +45,3 @@ def _mock_sim_event(jira_id: str, day: int) -> SimEvent: facts={"root_cause": "test"}, summary="", ) - - -@patch.object(RecurrenceDetector, "_text_search") -@patch.object(RecurrenceDetector, "_vector_search") -def test_recurrence_detector_rejects_below_thresholds( - mock_vec, mock_text, detector, make_test_memory -): - """ - If the best match falls below BOTH the text threshold (0.40) and the - vector threshold (0.72), it must be rejected to prevent false positives. - """ - - mock_text.return_value = [{"score": 2.0}] # Normalises to ~0.35 (below 0.40) - mock_vec.return_value = [(_mock_sim_event("ENG-10", 1), 0.65)] # Below 0.72 - - match = detector.find_prior_incident( - "weak issue", current_day=5, current_ticket_id="ENG-20" - ) - - assert match is None - - store = make_test_memory._db[RecurrenceMatchStore.COLLECTION] - record = store.find_one({"current_ticket_id": "ENG-20"}) - assert record is not None - assert record["matched"] is False - assert record["confidence"] == "rejected" - - -@patch.object(RecurrenceDetector, "_text_search") -@patch.object(RecurrenceDetector, "_vector_search") -def test_recurrence_detector_accepts_strong_vector_only_match( - mock_vec, mock_text, detector, make_test_memory -): - """ - If the text search yields nothing (e.g. completely paraphrased root cause), - but the vector similarity is very high, it must confidently match. - """ - mock_text.return_value = [] - expected_match = _mock_sim_event("ENG-11", 2) - mock_vec.return_value = [(expected_match, 0.85)] # Well above 0.72 - - match = detector.find_prior_incident( - "paraphrased issue", current_day=5, current_ticket_id="ENG-21" - ) - - assert match is not None - assert match.artifact_ids["jira"] == "ENG-11" - - record = make_test_memory._db[RecurrenceMatchStore.COLLECTION].find_one( - {"current_ticket_id": "ENG-21"} - ) - assert record["fusion_strategy"] == "vector_only" - assert record["matched"] is True - - -@patch.object(RecurrenceDetector, "_text_search") -@patch.object(RecurrenceDetector, "_vector_search") -def test_rrf_fusion_favors_earliest_incident_on_tie(mock_vec, mock_text, detector): - """ - If multiple incidents breach the threshold with identical scores (e.g., a recurring - issue that has happened 3 times), the detector MUST return the earliest one - to anchor the recurrence depth properly and avoid daisy-chaining. - """ - - match_day_1 = _mock_sim_event("ENG-01", 1) - match_day_3 = _mock_sim_event("ENG-03", 3) - match_day_5 = _mock_sim_event("ENG-05", 5) - - mock_text.return_value = [] - - mock_vec.return_value = [ - (match_day_5, 0.90), - (match_day_3, 0.90), - (match_day_1, 0.90), - ] - - match = detector.find_prior_incident( - "recurring issue", current_day=10, current_ticket_id="ENG-99" - ) - - assert match.artifact_ids["jira"] == "ENG-01" diff --git a/tests/test_flow.py b/tests/test_flow.py index 718b699..ca8cf17 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -32,21 +32,6 @@ def mock_flow(): return sim -def test_embed_and_count_recursion_fix(mock_flow): - """ - Verifies that _embed_and_count enqueues to the embed worker - rather than calling embed_artifact synchronously (which would - previously cause recursion if wired incorrectly). - """ - mock_flow._embed_worker.enqueue = MagicMock() - - mock_flow._embed_and_count( - id="test", type="doc", title="T", content="C", day=1, date="2026-01-01" - ) - - assert mock_flow._embed_worker.enqueue.called - assert mock_flow.state.daily_artifacts_created == 1 - @patch("flow.Crew") @patch("flow.Task") @@ -81,7 +66,6 @@ def test_social_graph_burnout_propagation(mock_flow): """Tests if stress correctly propagates through the network.""" person = list(mock_flow.social_graph.nodes)[0] - # Dynamically grab the configured threshold and push them over it burnout_threshold = mock_flow.graph_dynamics.cfg["burnout_threshold"] mock_flow.graph_dynamics._stress[person] = burnout_threshold + 10 @@ -91,34 +75,6 @@ def test_social_graph_burnout_propagation(mock_flow): assert len(result.stress_snapshot) > 0 -def test_memory_context_retrieval(mock_flow): - """Tests semantic context window construction.""" - from memory import Memory - - mem = Memory() # Safe: MongoClient, build_embedder, and _init_vector_indexes are all mocked by autouse fixture - - # Wire up the instance so context_for_prompt has something to work with - mem._embedder.embed = MagicMock(return_value=[0.1] * 1024) - mem._artifacts.count_documents = MagicMock(return_value=10) - mem._artifacts.aggregate = MagicMock(return_value=[]) - mem.recall_events = MagicMock( - return_value=[ - SimEvent( - type="test", - day=1, - date="2026-01-01", - actors=[], - artifact_ids={}, - facts={}, - summary="Test Event", - timestamp="2026-03-05T13:33:51.027Z", - ) - ] - ) - - context = mem.context_for_prompt("server crash") - assert "RELEVANT EVENTS" in context - def test_edge_weight_decay(mock_flow): """Verifies that social edges decay over time without interaction.""" @@ -127,7 +83,6 @@ def test_edge_weight_decay(mock_flow): floor = mock_flow.graph_dynamics.cfg["edge_weight_floor"] decay_rate = mock_flow.graph_dynamics.cfg["edge_decay_rate"] - # Manually boost the weight safely above the floor initial_weight = floor + 5.0 mock_flow.social_graph[u][v]["weight"] = initial_weight @@ -135,7 +90,6 @@ def test_edge_weight_decay(mock_flow): new_weight = mock_flow.social_graph[u][v]["weight"] - # Dynamically calculate the exact expected result expected_weight = round(max(floor, initial_weight * decay_rate), 4) assert new_weight == expected_weight @@ -147,12 +101,10 @@ def test_escalation_path_logic(mock_flow): bestie = "Bob" lead = "Charlie" - # Register the nodes mock_flow.social_graph.add_node(responder) mock_flow.social_graph.add_node(bestie) mock_flow.social_graph.add_node(lead) - # Explicitly make Charlie a lead so the algorithm targets him mock_flow.graph_dynamics._leads = {"Engineering": lead} mock_flow.social_graph.add_edge(responder, bestie, weight=10.0) @@ -164,42 +116,14 @@ def test_escalation_path_logic(mock_flow): assert "Bob" in [node for node, role in chain.chain] -def test_temporal_memory_isolation(mock_flow): - """Ensures context_for_prompt respects the day limit.""" - from memory import Memory - - mem = Memory() # Safe: mocked by autouse fixture - mem._embedder.embed = MagicMock(return_value=[0.1] * 1024) - mem._artifacts.count_documents = MagicMock(return_value=10) - mem._artifacts.aggregate = MagicMock(return_value=[]) - # recall_events chains .find().sort().limit() — mock the full cursor chain - mock_cursor = MagicMock() - mock_cursor.sort.return_value = mock_cursor - mock_cursor.limit.return_value = iter([]) - mem._events.find = MagicMock(return_value=mock_cursor) - - mem.context_for_prompt("incident", as_of_time="2026-03-05T13:33:51.027Z") - - args, kwargs = mem._artifacts.aggregate.call_args - pipeline = args[0] - - vector_search_stage = next(s for s in pipeline if "$vectorSearch" in s) - assert "filter" in vector_search_stage["$vectorSearch"] - assert "timestamp" in vector_search_stage["$vectorSearch"]["filter"] - assert vector_search_stage["$vectorSearch"]["filter"]["timestamp"] == { - "$lte": "2026-03-05T13:33:51.027Z" - } - def test_graph_interaction_boost(mock_flow): """Verifies that Slack interactions boost edge weights between participants.""" u, v = "Alice", "Bob" - # Ensure nodes exist and set a baseline weight mock_flow.social_graph.add_node(u) mock_flow.social_graph.add_node(v) mock_flow.social_graph.add_edge(u, v, weight=1.0) - # Simulate a Slack thread between them mock_flow.graph_dynamics.record_slack_interaction([u, v]) new_weight = mock_flow.social_graph[u][v]["weight"] @@ -283,8 +207,7 @@ def test_memory_log_event(): from memory import Memory from unittest.mock import MagicMock - mem = Memory() # Safe: mocked by autouse fixture - mem._embedder.embed = MagicMock(return_value=[0.1] * 1024) + mem = Memory() mem._events.update_one = MagicMock() event = SimEvent( @@ -300,15 +223,12 @@ def test_memory_log_event(): mem.log_event(event) - # Verify in-memory state assert len(mem._event_log) == 1 assert mem._event_log[0].type == "test_event" - # Verify DB call assert mem._events.update_one.called args, kwargs = mem._events.update_one.call_args - # The ID generator logic is EVT-{day}-{type}-{index} assert args[0] == {"_id": "EVT-1-test_event-1"} @@ -406,10 +326,8 @@ def test_incident_sync_to_system_advances_on_call_cursor( system_after = mock_flow._clock.now("system") on_call_after = mock_flow._clock.now(on_call) - # The system clock must have advanced (tick_system was called) assert system_after > system_before, "tick_system did not advance system cursor" - # The on-call engineer must be at or after the incident start time assert on_call_after >= system_after, ( f"on-call cursor {on_call_after} is before system clock {system_after} " "after sync_to_system — sync had no effect" @@ -422,12 +340,12 @@ def test_incident_sync_to_system_advances_on_call_cursor( @patch("flow.Task") @patch("agent_factory.Agent") def test_postmortem_artifact_timestamp_within_actor_work_block( - mock_agent_class, # Maps to @patch("agent_factory.Agent") - mock_task_class, # Maps to @patch("flow.Task") - mock_crew_class, # Maps to @patch("flow.Crew") - mock_cw_crew, # Maps to @patch("confluence_writer.Crew") - mock_cw_task, # Maps to @patch("confluence_writer.Task") - mock_flow, # FIXTURE: Must be the absolute last argument + mock_agent_class, + mock_task_class, + mock_crew_class, + mock_cw_crew, + mock_cw_task, + mock_flow, ): """ _write_postmortem uses advance_actor to compute the artifact timestamp. @@ -439,7 +357,6 @@ def test_postmortem_artifact_timestamp_within_actor_work_block( mock_crew_instance = MagicMock() mock_crew_instance.kickoff.return_value = "## Postmortem\n\nRoot cause: OOM." - # Assign the mock instance to BOTH Crew mocks mock_crew_class.return_value = mock_crew_instance mock_cw_crew.return_value = mock_crew_instance @@ -448,6 +365,8 @@ def test_postmortem_artifact_timestamp_within_actor_work_block( cursor_before = mock_flow._clock.now(writer) inc = MagicMock() + inc.actors = ["Jax", "Deepa", "Liam"] + writer = inc.actors[1] inc.ticket_id = "ORG-999" inc.title = "Test Incident" inc.root_cause = "OOM on worker node" @@ -461,7 +380,6 @@ def test_postmortem_artifact_timestamp_within_actor_work_block( cursor_after = mock_flow._clock.now(writer) - # Grab the confluence_created SimEvent with the postmortem tag to inspect its timestamp all_calls = mock_flow._mem.log_event.call_args_list pm_evt = next( c.args[0] @@ -719,17 +637,3 @@ def test_advance_incidents_state_machine(mock_flow): mock_flow._advance_incidents() assert inc.stage == "resolved" assert "INC-1" in mock_flow.state.resolved_incidents - - -def test_build_llm_ollama_branch(): - with ( - patch("flow._PROVIDER", "ollama"), - patch("flow._PRESET", {"planner": "mock-planner"}), - patch("flow.OllamaLLM") as MockOllama, - ): - from flow import build_llm - - build_llm("planner") - MockOllama.assert_called_with( - model="mock-planner", base_url="http://localhost:11434" - ) diff --git a/tests/test_integration.py b/tests/test_integration.py index 1822dcd..1890d28 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1237,16 +1237,18 @@ def test_incident_causal_chain_preserves_full_lifecycle( assert "slack" in chain_snap[1] assert "slack" in chain_snap[2] - sim._advance_incidents() - sim._advance_incidents() + with patch("flow.random.random", return_value=0.0): + sim._advance_incidents() + sim._advance_incidents() chain_snap = inc.causal_chain.snapshot() assert len(chain_snap) == 4 assert "PR-" in chain_snap[3] pr_id = chain_snap[3] - sim._advance_incidents() - sim._advance_incidents() + with patch("flow.random.random", return_value=0.0): + sim._advance_incidents() + sim._advance_incidents() final_ticket = sim._mem.get_ticket(ticket_id) final_chain = final_ticket.get("causal_chain", []) diff --git a/tests/test_memory.py b/tests/test_memory.py index 2edb5fa..49b086a 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -2,106 +2,6 @@ import pytest -def test_embedder_fallback_mechanism(): - """Ensures the embedder fallback generates a deterministic vector of the correct dimension.""" - from memory import BaseEmbedder - - class DummyEmbedder(BaseEmbedder): - def embed(self, text): - return self._fallback(text) - - dims = 1024 - embedder = DummyEmbedder(dims=dims) - - vec1 = embedder.embed("The server is on fire") - vec2 = embedder.embed("The server is on fire") - vec3 = embedder.embed("Everything is fine") - - assert len(vec1) == dims - assert vec1 == vec2 - assert vec1 != vec3 - - -def test_memory_recall_pipeline_filters(): - """Verifies Memory.recall builds the correct MongoDB aggregation pipeline with filters.""" - from memory import Memory - - mem = Memory() - mem._artifacts.aggregate = MagicMock(return_value=[]) - - mem.recall( - query="database crash", - n=5, - type_filter="jira", - day_range=(2, 8), - ) - - args, kwargs = mem._artifacts.aggregate.call_args - pipeline = args[0] - - vector_search_stage = pipeline[0]["$vectorSearch"] - search_filter = vector_search_stage.get("filter", {}) - - assert search_filter["type"]["$eq"] == "jira" - assert search_filter["day"]["$gte"] == 2 - assert search_filter["day"]["$lte"] == 8 - assert vector_search_stage["limit"] == 5 - - -def test_memory_recall_pipeline_as_of_time_datetime(): - """ - recall() must translate a datetime as_of_time into a $lte timestamp - filter inside $vectorSearch. - """ - from datetime import datetime - from memory import Memory - - mem = Memory() - mem._artifacts.aggregate = MagicMock(return_value=[]) - - cutoff = datetime(2026, 1, 5, 14, 30, 0) - mem.recall(query="auth failure", n=3, as_of_time=cutoff) - - pipeline = mem._artifacts.aggregate.call_args[0][0] - search_filter = pipeline[0]["$vectorSearch"].get("filter", {}) - - assert search_filter["timestamp"]["$lte"] == cutoff.isoformat() - - -def test_memory_recall_pipeline_as_of_time_iso_string(): - """ - recall() must also accept a pre-formatted ISO string for as_of_time. - """ - from memory import Memory - - mem = Memory() - mem._artifacts.aggregate = MagicMock(return_value=[]) - - iso = "2026-01-05T14:30:00" - mem.recall(query="auth failure", n=3, as_of_time=iso) - - pipeline = mem._artifacts.aggregate.call_args[0][0] - search_filter = pipeline[0]["$vectorSearch"].get("filter", {}) - - assert search_filter["timestamp"]["$lte"] == iso - - -def test_memory_recall_pipeline_no_as_of_time(): - """ - When as_of_time is None no timestamp filter must appear in the pipeline. - """ - from memory import Memory - - mem = Memory() - mem._artifacts.aggregate = MagicMock(return_value=[]) - - mem.recall(query="anything", n=3) - - pipeline = mem._artifacts.aggregate.call_args[0][0] - search_filter = pipeline[0]["$vectorSearch"].get("filter", {}) - - assert "timestamp" not in search_filter - def test_log_dept_plan_serializes_nested_dataclasses(make_test_memory): """ @@ -176,222 +76,6 @@ def test_simevent_serialization(): assert restored_event.facts["duration_days"] == 2 assert "p1" in restored_event.tags - -class TestOllamaEmbedderInputType: - """ - Guards the fix for the silent input_type drop in OllamaEmbedder.embed(). - The Ollama /api/embed payload must include the correct instruction prefix - so stella / mxbai use asymmetric query vs. document embeddings. - """ - - def _make_embedder(self, mock_post): - """Return an OllamaEmbedder whose HTTP layer is fully mocked.""" - from memory import OllamaEmbedder - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.json.return_value = { - "embeddings": [[0.1] * 1024], - "prompt_eval_count": 10, - } - mock_post.return_value = mock_response - - embedder = OllamaEmbedder(model="stella_en_1.5b_v5", dims=1024) - embedder._ok = True - return embedder - - @patch("requests.post") - def test_search_query_prefix_sent_to_ollama(self, mock_post): - """ - When input_type='search_query', the payload sent to Ollama must contain - the query instruction prefix — not the bare text. - """ - embedder = self._make_embedder(mock_post) - embedder.embed("firmware upgrade 503 error", input_type="search_query") - - payload = mock_post.call_args[1]["json"] - assert "search_query" in payload["input"].lower() or payload[ - "input" - ].startswith("Represent this query"), ( - "OllamaEmbedder.embed() is silently dropping input_type='search_query'. " - "The Ollama payload must include the instruction prefix." - ) - - @patch("requests.post") - def test_search_document_prefix_sent_to_ollama(self, mock_post): - """ - When input_type='search_document', the payload must contain the document - instruction prefix so stored vectors are in the correct embedding space. - """ - embedder = self._make_embedder(mock_post) - embedder.embed("iOS mobile crash stack trace", input_type="search_document") - - payload = mock_post.call_args[1]["json"] - assert "search_document" in payload["input"].lower() or payload[ - "input" - ].startswith("Represent this sentence"), ( - "OllamaEmbedder.embed() is silently dropping input_type='search_document'. " - "The Ollama payload must include the instruction prefix." - ) - - @patch("requests.post") - def test_query_and_document_payloads_differ(self, mock_post): - """ - The same raw text must produce different payloads for 'search_query' - vs 'search_document' — if they're identical, the asymmetric instruction - tuning is not being applied and routing scores will compress. - """ - embedder = self._make_embedder(mock_post) - - embedder.embed("rate limit config", input_type="search_query") - query_payload = mock_post.call_args[1]["json"]["input"] - - embedder.embed("rate limit config", input_type="search_document") - doc_payload = mock_post.call_args[1]["json"]["input"] - - assert query_payload != doc_payload, ( - "search_query and search_document produced identical Ollama payloads. " - "input_type is being ignored — this kills asymmetric retrieval quality." - ) - - -def test_recall_raises_on_type_filter_and_type_exclude_together(): - """ - Passing both type_filter and type_exclude is a programmer error. - recall() must raise ValueError rather than silently applying one and ignoring - the other, since that would produce misleading search results. - """ - from memory import Memory - - mem = Memory() - mem._artifacts.aggregate = MagicMock(return_value=[]) - - with pytest.raises(ValueError, match="mutually exclusive"): - mem.recall( - query="test", - type_filter="jira", - type_exclude=["persona_skill"], - ) - - -def test_recall_type_exclude_builds_nin_filter(): - """ - type_exclude must translate to a $nin filter in the vectorSearch stage, - not a $eq. context_for_prompt() relies on this to hide persona_skill - artifacts from general RAG retrieval. - """ - from memory import Memory - - mem = Memory() - mem._artifacts.aggregate = MagicMock(return_value=[]) - - mem.recall(query="incident response", n=3, type_exclude=["persona_skill"]) - - pipeline = mem._artifacts.aggregate.call_args[0][0] - search_filter = pipeline[0]["$vectorSearch"].get("filter", {}) - - assert "$nin" in search_filter.get("type", {}), ( - "type_exclude must produce a {'type': {'$nin': [...]}} filter. " - "If it's missing, persona_skill artifacts pollute general RAG results." - ) - assert "persona_skill" in search_filter["type"]["$nin"] - - -def test_recall_since_builds_gte_timestamp_filter(): - """ - `since` (causal floor) must produce a $gte timestamp filter alongside - as_of_time's $lte. When both are provided, the pipeline must carry both - bounds in the same timestamp filter dict. - """ - from memory import Memory - - mem = Memory() - mem._artifacts.aggregate = MagicMock(return_value=[]) - - mem.recall( - query="sprint planning", - n=3, - since="2026-03-01T00:00:00", - as_of_time="2026-03-10T23:59:59", - ) - - pipeline = mem._artifacts.aggregate.call_args[0][0] - search_filter = pipeline[0]["$vectorSearch"].get("filter", {}) - ts = search_filter.get("timestamp", {}) - - assert ts.get("$gte") == "2026-03-01T00:00:00" - assert ts.get("$lte") == "2026-03-10T23:59:59" - - -# ── log_event — embedding skip list ────────────────────────────────────────── - - -def test_log_event_skips_embed_for_noisy_types(make_test_memory): - """ - High-volume event types in _SKIP_EMBED_TYPES must be stored with - embedding=None. Embedding every slack_message_sent or jira_ticket_created - would waste tokens and inflate the vector index with low-signal noise. - """ - from memory import SimEvent - - mem = make_test_memory - mem._embed = MagicMock(return_value=[0.1] * 1024) - - skip_event = SimEvent( - type="slack_message_sent", - day=1, - date="2026-03-02", - timestamp="2026-03-02T10:00:00", - actors=["Jax"], - artifact_ids={}, - facts={}, - summary="Jax posted in #backend", - tags=["slack"], - ) - mem.log_event(skip_event) - - assert not mem._embed.called, ( - "log_event() called _embed() for 'slack_message_sent', which is in " - "_SKIP_EMBED_TYPES. These events must be stored with embedding=None." - ) - - stored = mem._events.find_one({"type": "slack_message_sent"}) - assert stored is not None - assert stored.get("embedding") is None - - -def test_log_event_embeds_high_signal_types(make_test_memory): - """ - Events NOT in _SKIP_EMBED_TYPES (e.g. incident_resolved) must be - embedded so they appear in vector search results. - """ - from memory import SimEvent - - mem = make_test_memory - fake_vec = [0.42] * 1024 - mem._embed = MagicMock(return_value=fake_vec) - - event = SimEvent( - type="incident_resolved", - day=3, - date="2026-03-04", - timestamp="2026-03-04T15:00:00", - actors=["Deepa", "Jax"], - artifact_ids={"jira": "INC-001"}, - facts={"root_cause": "db connection pool exhausted"}, - summary="Deepa resolved the connection pool incident", - tags=["incident"], - ) - mem.log_event(event) - - assert mem._embed.called, ( - "log_event() did not call _embed() for 'incident_resolved'. " - "High-signal events must be embedded for vector retrieval." - ) - stored = mem._events.find_one({"type": "incident_resolved"}) - assert stored["embedding"] == fake_vec - - def test_log_event_appends_to_in_memory_log(make_test_memory): """ Every logged event, regardless of type, must be appended to _event_log. @@ -420,64 +104,6 @@ def test_log_event_appends_to_in_memory_log(make_test_memory): assert len(mem._event_log) == initial_len + 3 - -def test_embed_artifact_upserts_not_duplicates(make_test_memory): - """ - Calling embed_artifact() twice with the same id must update the existing - document, not insert a second one. The artifact store is an upsert cache — - duplicate IDs would corrupt vector search recall. - """ - mem = make_test_memory - mem._embed = MagicMock(return_value=[0.1] * 1024) - - kwargs = dict( - id="conf_001", - type="confluence", - title="TitanDB Overview", - content="TitanDB is the legacy sports tracking database.", - day=1, - date="2026-03-02", - timestamp="2026-03-02T08:00:00", - ) - mem.embed_artifact(**kwargs) - mem.embed_artifact(**{**kwargs, "content": "Updated content after review."}) - - count = mem._artifacts.count_documents({"_id": "conf_001"}) - assert count == 1, ( - "embed_artifact() inserted duplicate documents for the same id. " - "It must upsert so re-embedding an artifact doesn't inflate the index." - ) - - stored = mem._artifacts.find_one({"_id": "conf_001"}) - assert "Updated" in stored["content"] - - -def test_embed_artifact_uses_search_document_input_type(make_test_memory): - """ - embed_artifact() must call _embed() with input_type='search_document'. - Using 'search_query' for stored documents would flip the asymmetric - instruction tuning and make all retrieval scores meaningless. - """ - mem = make_test_memory - mem._embed = MagicMock(return_value=[0.1] * 1024) - - mem.embed_artifact( - id="conf_002", - type="confluence", - title="Auth Service Runbook", - content="Steps for rotating the OAuth2 client secret.", - day=2, - date="2026-03-03", - timestamp="2026-03-03T09:00:00", - ) - - call_kwargs = mem._embed.call_args[1] - assert call_kwargs.get("input_type") == "search_document", ( - "embed_artifact() must pass input_type='search_document' to _embed(). " - "Using 'search_query' inverts the asymmetric embedding space." - ) - - class TestToIso: """ _to_iso() is the normalisation layer that lets every causal-ceiling argument @@ -923,7 +549,7 @@ def test_stats_reflects_artifact_and_event_counts(make_test_memory): from memory import SimEvent mem = make_test_memory - mem._embed = MagicMock(return_value=[0.1] * 1024) + before = mem.stats() @@ -964,7 +590,7 @@ def test_reset_clears_all_collections_and_event_log(make_test_memory): from memory import SimEvent mem = make_test_memory - mem._embed = MagicMock(return_value=[0.1] * 1024) + mem.embed_artifact( id="pre_reset", diff --git a/uv.lock b/uv.lock index f8406e5..b365c64 100644 --- a/uv.lock +++ b/uv.lock @@ -1,28 +1,26 @@ version = 1 revision = 3 -requires-python = ">=3.11" -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' 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'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", +requires-python = ">=3.13" + +[manifest] +overrides = [{ name = "json-repair", specifier = ">=0.58.6" }] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, ] [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, ] [[package]] @@ -40,40 +38,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, - { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, - { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, - { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, - { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, @@ -133,7 +97,6 @@ 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 = [ @@ -152,22 +115,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, ] -[[package]] -name = "altair" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "narwhals" }, - { name = "packaging" }, - { name = "typing-extensions", marker = "python_full_version < '3.15'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/c0/184a89bd5feba14ff3c41cfaf1dd8a82c05f5ceedbc92145e17042eb08a4/altair-6.0.0.tar.gz", hash = "sha256:614bf5ecbe2337347b590afb111929aa9c16c9527c4887d96c9bc7f6640756b4", size = 763834, upload-time = "2025-11-12T08:59:11.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/33/ef2f2409450ef6daa61459d5de5c08128e7d3edb773fefd0a324d1310238/altair-6.0.0-py3-none-any.whl", hash = "sha256:09ae95b53d5fe5b16987dccc785a7af8588f2dca50de1e7a156efa8a461515f8", size = 795410, upload-time = "2025-11-12T08:59:09.804Z" }, -] - [[package]] name = "annotated-doc" version = "0.0.4" @@ -192,7 +139,6 @@ version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } wheels = [ @@ -208,24 +154,6 @@ 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 = "appnope" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, -] - -[[package]] -name = "asttokens" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, -] - [[package]] name = "attrs" version = "26.1.0" @@ -308,79 +236,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, - { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, - { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, ] [[package]] -name = "blinker" -version = "1.9.0" +name = "beautifulsoup4" +version = "4.13.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, ] [[package]] name = "boto3" -version = "1.40.76" +version = "1.42.63" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/04/8cf6cf7e6390c71b9c958f3bfedc45d1182b51a35f7789354bf7b2ff4e8c/boto3-1.40.76.tar.gz", hash = "sha256:16f4cf97f8dd8e0aae015f4dc66219bd7716a91a40d1e2daa0dafa241a4761c5", size = 111598, upload-time = "2025-11-18T20:23:10.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/2a/33d5d4b16fd97dfd629421ebed2456392eae1553cc401d9f86010c18065e/boto3-1.42.63.tar.gz", hash = "sha256:cd008cfd0d7ea30f1c5e22daf0998c55b7c6c68cb68eea05110e33fe641173d5", size = 112778, upload-time = "2026-03-06T22:47:55.96Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/8e/966263696eb441e8d1c4daa5fdfb3b4be10a96a23c418cc74c80b0b03d4e/boto3-1.40.76-py3-none-any.whl", hash = "sha256:8df6df755727be40ad9e309cfda07f9a12c147e17b639430c55d4e4feee8a167", size = 139359, upload-time = "2025-11-18T20:23:08.75Z" }, + { url = "https://files.pythonhosted.org/packages/f5/19/f1d8d2b24871d3d0ccb2cbd0b0cb64a3396d439384bd9643d2c25c641b84/boto3-1.42.63-py3-none-any.whl", hash = "sha256:d502a89a0acc701692ae020d15981f2a82e9eb3485acc651cfd0cf1a3afe79ee", size = 140554, upload-time = "2026-03-06T22:47:53.463Z" }, ] [[package]] name = "botocore" -version = "1.40.76" +version = "1.42.97" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/eb/50e2d280589a3c20c3b649bb66262d2b53a25c03262e4cc492048ac7540a/botocore-1.40.76.tar.gz", hash = "sha256:2b16024d68b29b973005adfb5039adfe9099ebe772d40a90ca89f2e165c495dc", size = 14494001, upload-time = "2025-11-18T20:22:59.131Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/95/c37edb602948fad2253ffd1bb3dba5b938645bd1845ee4160350136a0f41/botocore-1.42.97.tar.gz", hash = "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", size = 15269348, upload-time = "2026-04-27T20:39:05.625Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/6c/522e05388aa6fc66cf8ea46c6b29809a1a6f527ea864998b01ffb368ca36/botocore-1.40.76-py3-none-any.whl", hash = "sha256:fe425d386e48ac64c81cbb4a7181688d813df2e2b4c78b95ebe833c9e868c6f4", size = 14161738, upload-time = "2025-11-18T20:22:55.332Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d2/8e025ba1a4e257879af72d06913272311af79673d82fa2581a351b924317/botocore-1.42.97-py3-none-any.whl", hash = "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc", size = 14950367, upload-time = "2026-04-27T20:39:01.261Z" }, ] [[package]] name = "build" -version = "1.4.2" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, { name = "packaging" }, { name = "pyproject-hooks" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/1d/ab15c8ac57f4ee8778d7633bc6685f808ab414437b8644f555389cdc875e/build-1.4.2.tar.gz", hash = "sha256:35b14e1ee329c186d3f08466003521ed7685ec15ecffc07e68d706090bf161d1", size = 83433, upload-time = "2026-03-25T14:20:27.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/57/3b7d4dd193ade4641c865bc2b93aeeb71162e81fc348b8dad020215601ed/build-1.4.2-py3-none-any.whl", hash = "sha256:7a4d8651ea877cb2a89458b1b198f2e69f536c95e89129dbf5d448045d60db88", size = 24643, upload-time = "2026-03-25T14:20:26.568Z" }, -] - -[[package]] -name = "cachetools" -version = "7.0.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, ] [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.5.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, ] [[package]] @@ -392,31 +311,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, @@ -459,38 +353,6 @@ version = "3.4.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, @@ -605,24 +467,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "comm" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, -] - [[package]] name = "crewai" -version = "1.13.0" +version = "1.14.5" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "aiofiles" }, { name = "aiosqlite" }, { name = "appdirs" }, { name = "chromadb" }, { name = "click" }, + { name = "crewai-cli" }, + { name = "crewai-core" }, { name = "httpx" }, { name = "instructor" }, { name = "json-repair" }, @@ -643,113 +499,144 @@ dependencies = [ { name = "python-dotenv" }, { name = "pyyaml" }, { name = "regex" }, - { name = "textual" }, { name = "tokenizers" }, { name = "tomli" }, { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/12/b974c1b36af1d4a5928825f6ac7f7d34fc88b12e4715dcb43e119157663d/crewai-1.14.5.tar.gz", hash = "sha256:e64dfa4f309f94bbaacab0a4ca5d4f8e02b18c528b2876a390cd396135d46631", size = 7681243, upload-time = "2026-05-18T19:20:43.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/5c/c274c1657d164ef170b70ee725e1b6164062440b8b47a96a104d12f5245c/crewai-1.14.5-py3-none-any.whl", hash = "sha256:be6580aab9d5ee2c73de5ed26cdd29c7d04d826e0d14e0a5ddd2e1e0742fa9f8", size = 976841, upload-time = "2026-05-18T19:20:41.437Z" }, +] + +[[package]] +name = "crewai-cli" +version = "1.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "certifi" }, + { name = "click" }, + { name = "crewai-core" }, + { name = "cryptography" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "textual" }, + { name = "tomli" }, + { name = "tomli-w" }, { name = "uv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/4a/ea4470501934a9cfc228bcbb572c48cffd5d573634e8d6e97577ff9a1e25/crewai-1.13.0.tar.gz", hash = "sha256:a2d105d00f65a9a306d1aa57c167f77b543b66983c1729753bf9a1c900a0bef9", size = 7773234, upload-time = "2026-04-02T23:17:04.775Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/e2/5fec7c86f561be3cf6a1766939e4e6dc8592213fd1f3a3c2228f53cbf644/crewai_cli-1.14.5.tar.gz", hash = "sha256:faf05a8ce8ba655f49a1fc61473efc439a9ad70ce0329a768de265243726e0f0", size = 107149, upload-time = "2026-05-18T19:20:46.521Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/db/8b6a5026bb038e538c59b643ecd940e731ed3e4c3769fdc54d803ce6b5a1/crewai-1.13.0-py3-none-any.whl", hash = "sha256:6de1241960c18f5ae984005763b0311a1e11224d59830e42020d12ebf0665b5f", size = 1021512, upload-time = "2026-04-02T23:17:02.666Z" }, + { url = "https://files.pythonhosted.org/packages/07/3e/a8e66982d3b5a9e98ccd66cd4516c184a37f94ccb2320e7ae1d3969ed9ac/crewai_cli-1.14.5-py3-none-any.whl", hash = "sha256:0e4811a093a95a9c7a177abfdac6cb90af806cf0850efddc42072739e0bdcac9", size = 108319, upload-time = "2026-05-18T19:20:45.436Z" }, ] -[package.optional-dependencies] -bedrock = [ - { name = "boto3" }, +[[package]] +name = "crewai-core" +version = "1.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "cryptography" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "portalocker" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "rich" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/1a/b3ce01f4d4b7773dbd2aa21bdda9d4d3d11778dde3e6b6f4a690f5cd89f8/crewai_core-1.14.5.tar.gz", hash = "sha256:b72c0cfb953bf934b252fa826cb71c80127ccc42f9a39feb2a7a97ee8272be82", size = 19168, upload-time = "2026-05-18T19:20:48.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/52/05da585bdc287c7d9b368e6f58bf23d01a2bf20ba953370119e8cdc2d3ea/crewai_core-1.14.5-py3-none-any.whl", hash = "sha256:f2349160d10345e9096660c78ed6c5a6a9922033cd4c5e1b18dcbf8ea72a48b3", size = 27385, upload-time = "2026-05-18T19:20:47.325Z" }, +] + +[[package]] +name = "crewai-tools" +version = "1.14.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "crewai" }, + { name = "pymupdf" }, + { name = "python-docx" }, + { name = "pytube" }, + { name = "requests" }, + { name = "tiktoken" }, + { name = "youtube-transcript-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/be/a1d068a8cd416b0be3cb649fb9c960b7bb8501a17e903614ce35949e8a53/crewai_tools-1.14.5.tar.gz", hash = "sha256:eacf3e719eadf91607c3103dbd42ff5dcf149e64801f97c50c6904f356c7efd2", size = 900866, upload-time = "2026-05-18T19:20:53.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/08/6d838e6f9476b18f5db61c20dff7054dcb1fa97aadbf43c6bc88bb3f8903/crewai_tools-1.14.5-py3-none-any.whl", hash = "sha256:e17b0bbc2dbc3b0e98b87d797964c603ae13293a8c7a936faf40a9e117563eb1", size = 814053, upload-time = "2026-05-18T19:20:51.95Z" }, ] [[package]] name = "cryptography" -version = "46.0.6" +version = "48.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, - { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, - { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, - { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, - { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, - { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, - { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, - { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, - { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, - { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, - { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, - { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, - { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, - { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, - { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, - { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, - { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, - { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, - { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" }, - { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" }, -] - -[[package]] -name = "debugpy" -version = "1.8.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, - { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, - { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, - { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, - { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, - { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, - { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, - { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, - { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, - { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, - { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, - { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, -] - -[[package]] -name = "decorator" -version = "5.2.1" +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] @@ -773,13 +660,22 @@ 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 = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + [[package]] name = "docstring-parser" -version = "0.17.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +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/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, + { 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]] @@ -800,22 +696,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] -[[package]] -name = "executing" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, -] - [[package]] name = "filelock" -version = "3.25.2" +version = "3.29.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] [[package]] @@ -826,62 +713,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, ] -[[package]] -name = "flow" -source = { editable = "." } -dependencies = [ - { name = "crewai", extra = ["bedrock"] }, - { name = "ipykernel" }, - { name = "pandas" }, - { name = "streamlit" }, -] - -[package.metadata] -requires-dist = [ - { name = "crewai", extras = ["bedrock"], specifier = ">=1.13.0" }, - { name = "ipykernel", specifier = ">=7.2.0" }, - { name = "pandas", specifier = ">=3.0.2" }, - { name = "streamlit", specifier = ">=1.56.0" }, -] - [[package]] name = "frozenlist" version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, - { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, - { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, - { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, - { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, - { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, @@ -951,47 +788,23 @@ wheels = [ [[package]] name = "fsspec" -version = "2026.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, -] - -[[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.46" +version = "2026.4.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +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/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.74.0" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [[package]] @@ -1003,26 +816,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/db/1d56e5f5823257b291962d6c0ce106146c6447f405b60b234c4f222a7cde/grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a", size = 6055009, upload-time = "2026-03-30T08:46:46.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/18/c83f3cad64c5ca63bca7e91e5e46b0d026afc5af9d0a9972472ceba294b3/grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060", size = 12035295, upload-time = "2026-03-30T08:46:49.099Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8e/e14966b435be2dda99fbe89db9525ea436edc79780431a1c2875a3582644/grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2", size = 6610297, upload-time = "2026-03-30T08:46:52.123Z" }, - { url = "https://files.pythonhosted.org/packages/cc/26/d5eb38f42ce0e3fdc8174ea4d52036ef8d58cc4426cb800f2610f625dd75/grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21", size = 7300208, upload-time = "2026-03-30T08:46:54.859Z" }, - { url = "https://files.pythonhosted.org/packages/25/51/bd267c989f85a17a5b3eea65a6feb4ff672af41ca614e5a0279cc0ea381c/grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab", size = 6813442, upload-time = "2026-03-30T08:46:57.056Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d9/d80eef735b19e9169e30164bbf889b46f9df9127598a83d174eb13a48b26/grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1", size = 7414743, upload-time = "2026-03-30T08:46:59.682Z" }, - { url = "https://files.pythonhosted.org/packages/de/f2/567f5bd5054398ed6b0509b9a30900376dcf2786bd936812098808b49d8d/grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106", size = 8426046, upload-time = "2026-03-30T08:47:02.474Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/73ef0141b4732ff5eacd68430ff2512a65c004696997f70476a83e548e7e/grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6", size = 7851641, upload-time = "2026-03-30T08:47:05.462Z" }, - { url = "https://files.pythonhosted.org/packages/46/69/abbfa360eb229a8623bab5f5a4f8105e445bd38ce81a89514ba55d281ad0/grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440", size = 4154368, upload-time = "2026-03-30T08:47:08.027Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d4/ae92206d01183b08613e846076115f5ac5991bae358d2a749fa864da5699/grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9", size = 4894235, upload-time = "2026-03-30T08:47:10.839Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, - { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, - { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, - { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, - { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, - { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, @@ -1056,34 +849,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload-time = "2026-03-31T22:39:49.618Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" }, - { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" }, - { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" }, - { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" }, - { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" }, - { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, - { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, - { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, ] [[package]] @@ -1105,20 +898,6 @@ version = "0.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, - { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, - { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, - { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, - { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, @@ -1161,7 +940,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.9.0" +version = "1.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -1174,18 +953,18 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/bb/62c7aa86f63a05e2f9b96642fdef9b94526a23979820b09f5455deff4983/huggingface_hub-1.9.0.tar.gz", hash = "sha256:0ea5be7a56135c91797cae6ad726e38eaeb6eb4b77cefff5c9d38ba0ecf874f7", size = 750326, upload-time = "2026-04-03T08:35:55.888Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/0f/ed994dbade67a54407c28cab96ef845e0e6d25500be56aca6394f8bfc9dd/huggingface_hub-1.16.1.tar.gz", hash = "sha256:7f1dc4c5ec21aed69be630ad0c3378616be16f3de1a47b141c0e812965d9c832", size = 792534, upload-time = "2026-05-21T18:40:00.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/37/0d15d16150e1829f3e90962c99f28257f6de9e526a680b4c6f5acdb54fd2/huggingface_hub-1.9.0-py3-none-any.whl", hash = "sha256:2999328c058d39fd19ab748dd09bd4da2fbaa4f4c1ddea823eab103051e14a1f", size = 637355, upload-time = "2026-04-03T08:35:53.897Z" }, + { url = "https://files.pythonhosted.org/packages/49/79/621a7dbb80c70974f73a597275351ebe03ce5bc65cb5f8f4acb5859252bc/huggingface_hub-1.16.1-py3-none-any.whl", hash = "sha256:64340de934b9ce37857ef85a82de72f5629e8a270f9119eabb12bf495eb53c22", size = 668176, upload-time = "2026-05-21T18:39:58.596Z" }, ] [[package]] name = "idna" -version = "3.11" +version = "3.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, ] [[package]] @@ -1202,11 +981,20 @@ wheels = [ [[package]] name = "importlib-resources" -version = "6.5.2" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +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/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, + { 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]] @@ -1231,114 +1019,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/c8/36c5d9b80aaf40ba9a7084a8fc18c967db6bf248a4cc8d0f0816b14284be/instructor-1.15.1-py3-none-any.whl", hash = "sha256:be81d17ba2b154a04ab4720808f24f9d6b598f80992f82eaf9cc79006099cf6c", size = 178156, upload-time = "2026-04-03T01:51:23.098Z" }, ] -[[package]] -name = "ipykernel" -version = "7.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "appnope", marker = "sys_platform == 'darwin'" }, - { name = "comm" }, - { name = "debugpy" }, - { name = "ipython", version = "9.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "ipython", version = "9.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "matplotlib-inline" }, - { name = "nest-asyncio" }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, -] - -[[package]] -name = "ipython" -version = "9.10.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.12'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version < '3.12'" }, - { name = "jedi", marker = "python_full_version < '3.12'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.12'" }, - { name = "pexpect", marker = "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.12'" }, - { name = "pygments", marker = "python_full_version < '3.12'" }, - { name = "stack-data", marker = "python_full_version < '3.12'" }, - { name = "traitlets", marker = "python_full_version < '3.12'" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/25/daae0e764047b0a2480c7bbb25d48f4f509b5818636562eeac145d06dfee/ipython-9.10.1.tar.gz", hash = "sha256:e170e9b2a44312484415bdb750492699bf329233b03f2557a9692cce6466ada4", size = 4426663, upload-time = "2026-03-27T09:53:26.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/09/ba70f8d662d5671687da55ad2cc0064cf795b15e1eea70907532202e7c97/ipython-9.10.1-py3-none-any.whl", hash = "sha256:82d18ae9fb9164ded080c71ef92a182ee35ee7db2395f67616034bebb020a232", size = 622827, upload-time = "2026-03-27T09:53:24.566Z" }, -] - -[[package]] -name = "ipython" -version = "9.12.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' 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'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.12'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, - { name = "jedi", marker = "python_full_version >= '3.12'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, - { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "stack-data", marker = "python_full_version >= '3.12'" }, - { name = "traitlets", marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, -] - -[[package]] -name = "ipython-pygments-lexers" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, -] - -[[package]] -name = "jedi" -version = "0.19.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "parso" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -1357,32 +1037,6 @@ version = "0.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, - { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, - { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, - { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, - { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, @@ -1426,14 +1080,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, - { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] [[package]] @@ -1447,11 +1093,11 @@ wheels = [ [[package]] name = "json-repair" -version = "0.25.3" +version = "0.59.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/60/484ee009c1867ddc5ffe0ff2131b82e80bbf13fdb59f3d93834f98e56a9f/json_repair-0.25.3.tar.gz", hash = "sha256:4ee970581a05b0b258b749eb8bcac21de380edda97c3717a4edfafc519ec21a4", size = 20619, upload-time = "2024-07-10T13:42:18.977Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/7c/e95bb03068572146eba37e8175c760f470ea0a6097310e16bbf2bc6e6457/json_repair-0.59.10.tar.gz", hash = "sha256:2e4b85537c752d8a513ea28fdad891e5ede32c83de745366b97f648b8c34ede7", size = 49133, upload-time = "2026-05-14T06:41:51.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/9e/2ab68cc0ff030e1ef78329d7b933473d3ad2c7d0e66aede6a7c87f74753c/json_repair-0.25.3-py3-none-any.whl", hash = "sha256:f00b510dd21b31ebe72581bdb07e66381df2883d6f640c89605e482882c12b17", size = 12812, upload-time = "2024-07-10T13:42:16.918Z" }, + { url = "https://files.pythonhosted.org/packages/ee/87/49b20c6b81493d55c311f711ed87319d0fbad8bd0bbfbe36e52103af36bd/json_repair-0.59.10-py3-none-any.whl", hash = "sha256:5468fa3eaadcc9b4a5646776bc4176e2fe5f374b5848a15f468cce3b60e3db0e", size = 47742, upload-time = "2026-05-14T06:41:49.812Z" }, ] [[package]] @@ -1499,40 +1145,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] -[[package]] -name = "jupyter-client" -version = "8.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jupyter-core" }, - { name = "python-dateutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, -] - -[[package]] -name = "jupyter-core" -version = "5.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "platformdirs" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, -] - [[package]] name = "kubernetes" -version = "35.0.0" +version = "36.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "aiohttp" }, { name = "certifi" }, { name = "durationpy" }, { name = "python-dateutil" }, @@ -1543,26 +1161,26 @@ dependencies = [ { name = "urllib3" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/59/dc635e4e9afb3884bc5c57f14fe23783e4c04601aa20b835ac75c41d1625/kubernetes-36.0.0.tar.gz", hash = "sha256:027b606bb8032e6c6464a53236bdd9bd9a94c237e1063bc45a303c25b304ced9", size = 2346728, upload-time = "2026-05-20T20:44:24.28Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/6f99ca9c7eb961dfdd45b9643101399a8ee20922c662c362c91e9cc7e832/kubernetes-36.0.0-py2.py3-none-any.whl", hash = "sha256:a766433357ec9f90db7565cccf52e28e7fca40b0ef366c80a6022adbc0ac0425", size = 4660469, upload-time = "2026-05-20T20:44:20.893Z" }, ] [[package]] name = "lance-namespace" -version = "0.6.1" +version = "0.7.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lance-namespace-urllib3-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/9f/7906ba4117df8d965510285eaf07264a77de2fd283b9d44ec7fc63a4a57a/lance_namespace-0.6.1.tar.gz", hash = "sha256:f0deea442bd3f1056a8e2fed056ae2778e3356517ec2e680db049058b824d131", size = 10666, upload-time = "2026-03-17T17:55:44.977Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/5c/9822af615fc1bd3ee1073994696c739aecde377be32435ec3303aed1bc5d/lance_namespace-0.7.7.tar.gz", hash = "sha256:d00b525f2e26993a6c61668e798bca6c808605ab8a79f29f86a1a1af92d91ae2", size = 10754, upload-time = "2026-05-20T17:32:59.45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/91/aee1c0a04d17f2810173bd304bd444eb78332045df1b0c1b07cebd01f530/lance_namespace-0.6.1-py3-none-any.whl", hash = "sha256:9699c9e3f12236e5e08ea979cc4e036a8e3c67ed2f37ae6f25c5353ab908e1be", size = 12498, upload-time = "2026-03-17T17:55:44.062Z" }, + { url = "https://files.pythonhosted.org/packages/11/43/186acc1156da20c351db196e2b6241b2453b16dc1b4cc8e0a626667ca471/lance_namespace-0.7.7-py3-none-any.whl", hash = "sha256:477a7ca6b5e1f673a2c9ba52f42d6e8e3ff7c27a601392a21eb90fba98d0309b", size = 12581, upload-time = "2026-05-20T17:32:57.389Z" }, ] [[package]] name = "lance-namespace-urllib3-client" -version = "0.6.1" +version = "0.7.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, @@ -1570,9 +1188,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/a1/8706a2be25bd184acccc411e48f1a42a4cbf3b6556cba15b9fcf4c15cfcc/lance_namespace_urllib3_client-0.6.1.tar.gz", hash = "sha256:31fbd058ce1ea0bf49045cdeaa756360ece0bc61e9e10276f41af6d217debe87", size = 182567, upload-time = "2026-03-17T17:55:46.87Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/95/38ab81ccc1e09beeecd8ddfc61b8bc73831dc5053db1e3f9021f64a4896b/lance_namespace_urllib3_client-0.7.7.tar.gz", hash = "sha256:4d8c066628c17c6a10cf643b51a7f7ae1bfb8a614d9cc54a5af38a4ba2b4b102", size = 202930, upload-time = "2026-05-20T17:32:58.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/c7/cb9580602dec25f0fdd6005c1c9ba1d4c8c0c3dc8d543107e5a9f248bba8/lance_namespace_urllib3_client-0.6.1-py3-none-any.whl", hash = "sha256:b9c103e1377ad46d2bd70eec894bfec0b1e2133dae0964d7e4de543c6e16293b", size = 317111, upload-time = "2026-03-17T17:55:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/35/96/5483e48e40433b1d078183c15a92c99e59a156041b0260e7f18ee34e7c08/lance_namespace_urllib3_client-0.7.7-py3-none-any.whl", hash = "sha256:9221c3e00fd89f0c811953d94b32d2ea527765280460a174f5872dc8a74c0ed6", size = 334767, upload-time = "2026-05-20T17:32:55.883Z" }, ] [[package]] @@ -1583,7 +1201,6 @@ dependencies = [ { name = "deprecation" }, { name = "lance-namespace" }, { name = "numpy" }, - { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "pyarrow" }, { name = "pydantic" }, @@ -1610,16 +1227,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] +[[package]] +name = "lxml" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, + { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, + { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, + { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, + { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, + { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, + { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, + { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, + { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, + { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, + { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, + { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, + { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, + { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, + { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, + { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, + { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, +] + [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { 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.optional-dependencies] @@ -1633,28 +1312,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, @@ -1701,18 +1358,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "matplotlib-inline" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, -] - [[package]] name = "mcp" version = "1.26.0" @@ -1740,14 +1385,14 @@ wheels = [ [[package]] name = "mdit-py-plugins" -version = "0.5.0" +version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, ] [[package]] @@ -1765,38 +1410,6 @@ version = "5.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/d7/3312a59df3c1cdd783f4cf0c4ee8e9decff9c5466937182e4cc7dbbfe6c5/mmh3-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450", size = 56082, upload-time = "2026-03-05T15:53:59.702Z" }, - { url = "https://files.pythonhosted.org/packages/61/96/6f617baa098ca0d2989bfec6d28b5719532cd8d8848782662f5b755f657f/mmh3-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0", size = 40458, upload-time = "2026-03-05T15:54:01.548Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b4/9cd284bd6062d711e13d26c04d4778ab3f690c1c38a4563e3c767ec8802e/mmh3-5.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082", size = 40079, upload-time = "2026-03-05T15:54:02.743Z" }, - { url = "https://files.pythonhosted.org/packages/f6/09/a806334ce1d3d50bf782b95fcee8b3648e1e170327d4bb7b4bad2ad7d956/mmh3-5.2.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997", size = 97242, upload-time = "2026-03-05T15:54:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/ee/93/723e317dd9e041c4dc4566a2eb53b01ad94de31750e0b834f1643905e97c/mmh3-5.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d", size = 103082, upload-time = "2026-03-05T15:54:06.387Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/f96121e69cc48696075071531cf574f112e1ffd08059f4bffb41210e6fc5/mmh3-5.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e", size = 106054, upload-time = "2026-03-05T15:54:07.506Z" }, - { url = "https://files.pythonhosted.org/packages/82/49/192b987ec48d0b2aecf8ac285a9b11fbc00030f6b9c694664ae923458dde/mmh3-5.2.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d", size = 112910, upload-time = "2026-03-05T15:54:09.403Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a1/03e91fd334ed0144b83343a76eb11f17434cd08f746401488cfeafb2d241/mmh3-5.2.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4", size = 120551, upload-time = "2026-03-05T15:54:10.587Z" }, - { url = "https://files.pythonhosted.org/packages/93/b9/b89a71d2ff35c3a764d1c066c7313fc62c7cc48fa48a4b3b0304a4a0146f/mmh3-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15", size = 99096, upload-time = "2026-03-05T15:54:11.76Z" }, - { url = "https://files.pythonhosted.org/packages/36/b5/613772c1c6ed5f7b63df55eb131e887cc43720fec392777b95a79d34e640/mmh3-5.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503", size = 98524, upload-time = "2026-03-05T15:54:13.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0e/1524566fe8eaf871e4f7bc44095929fcd2620488f402822d848df19d679c/mmh3-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2", size = 106239, upload-time = "2026-03-05T15:54:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/04/94/21adfa7d90a7a697137ad6de33eeff6445420ca55e433a5d4919c79bc3b5/mmh3-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1", size = 109797, upload-time = "2026-03-05T15:54:15.819Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e6/1aacc3a219e1aa62fa65669995d4a3562b35be5200ec03680c7e4bec9676/mmh3-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38", size = 97228, upload-time = "2026-03-05T15:54:16.992Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b9/5e4cca8dcccf298add0a27f3c357bc8cf8baf821d35cdc6165e4bd5a48b0/mmh3-5.2.1-cp311-cp311-win32.whl", hash = "sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728", size = 40751, upload-time = "2026-03-05T15:54:18.714Z" }, - { url = "https://files.pythonhosted.org/packages/72/fc/5b11d49247f499bcda591171e9cf3b6ee422b19e70aa2cef2e0ae65ca3b9/mmh3-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a", size = 41517, upload-time = "2026-03-05T15:54:19.764Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/2a511ee8a1c2a527c77726d5231685b72312c5a1a1b7639ad66a9652aa84/mmh3-5.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f", size = 39287, upload-time = "2026-03-05T15:54:20.904Z" }, - { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, - { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" }, - { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, - { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, - { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, - { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, - { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, - { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" }, - { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" }, { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832, upload-time = "2026-03-05T15:54:43.212Z" }, { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964, upload-time = "2026-03-05T15:54:44.204Z" }, { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" }, @@ -1858,12 +1471,17 @@ wheels = [ ] [[package]] -name = "mpmath" -version = "1.3.0" +name = "mongomock" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +dependencies = [ + { name = "packaging" }, + { name = "pytz" }, + { name = "sentinels" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/a4/4a560a9f2a0bec43d5f63104f55bc48666d619ca74825c8ae156b08547cf/mongomock-4.3.0.tar.gz", hash = "sha256:32667b79066fabc12d4f17f16a8fd7361b5f4435208b3ba32c226e52212a8c30", size = 135862, upload-time = "2024-11-16T11:23:25.957Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, + { url = "https://files.pythonhosted.org/packages/94/4d/8bea712978e3aff017a2ab50f262c620e9239cc36f348aae45e48d6a4786/mongomock-4.3.0-py2.py3-none-any.whl", hash = "sha256:5ef86bd12fc8806c6e7af32f21266c61b6c4ba96096f85129852d1c4fec1327e", size = 64891, upload-time = "2024-11-16T11:23:24.748Z" }, ] [[package]] @@ -1872,42 +1490,6 @@ version = "6.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, - { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, - { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, - { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, - { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, - { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, - { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, - { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, @@ -1984,100 +1566,62 @@ wheels = [ ] [[package]] -name = "narwhals" -version = "2.18.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/96/45218c2fdec4c9f22178f905086e85ef1a6d63862dcc3cd68eb60f1867f5/narwhals-2.18.1.tar.gz", hash = "sha256:652a1fcc9d432bbf114846688884c215f17eb118aa640b7419295d2f910d2a8b", size = 620578, upload-time = "2026-03-24T15:11:25.456Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/c3/06490e98393dcb4d6ce2bf331a39335375c300afaef526897881fbeae6ab/narwhals-2.18.1-py3-none-any.whl", hash = "sha256:a0a8bb80205323851338888ba3a12b4f65d352362c8a94be591244faf36504ad", size = 444952, upload-time = "2026-03-24T15:11:23.801Z" }, -] - -[[package]] -name = "nest-asyncio" -version = "1.6.0" +name = "networkx" +version = "3.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/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/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, + { url = "https://files.pythonhosted.org/packages/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.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, - { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, - { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, - { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, - { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, - { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, - { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, - { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, - { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, - { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, - { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, - { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, - { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, - { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, - { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, - { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, - { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, - { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, - { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, - { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, - { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, - { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, - { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, - { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, - { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, - { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, - { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, - { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, - { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, - { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, - { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, - { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, - { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, - { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, - { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, - { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, - { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, - { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, - { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, - { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, - { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, - { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, ] [[package]] @@ -2091,45 +1635,34 @@ wheels = [ [[package]] name = "onnxruntime" -version = "1.24.4" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flatbuffers" }, { name = "numpy" }, { name = "packaging" }, { name = "protobuf" }, - { name = "sympy" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/69/6c40720201012c6af9aa7d4ecdd620e521bd806dc6269d636fdd5c5aeebe/onnxruntime-1.24.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2", size = 17332131, upload-time = "2026-03-17T22:05:49.005Z" }, - { url = "https://files.pythonhosted.org/packages/38/e9/8c901c150ce0c368da38638f44152fb411059c0c7364b497c9e5c957321a/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:046ff290045a387676941a02a8ae5c3ebec6b4f551ae228711968c4a69d8f6b7", size = 15152472, upload-time = "2026-03-17T22:03:26.176Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b6/7a4df417cdd01e8f067a509e123ac8b31af450a719fa7ed81787dd6057ec/onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e54ad52e61d2d4618dcff8fa1480ac66b24ee2eab73331322db1049f11ccf330", size = 17222993, upload-time = "2026-03-17T22:04:34.485Z" }, - { url = "https://files.pythonhosted.org/packages/dd/59/8febe015f391aa1757fa5ba82c759ea4b6c14ef970132efb5e316665ba61/onnxruntime-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b43b63eb24a2bc8fc77a09be67587a570967a412cccb837b6245ccb546691153", size = 12594863, upload-time = "2026-03-17T22:05:38.749Z" }, - { url = "https://files.pythonhosted.org/packages/32/84/4155fcd362e8873eb6ce305acfeeadacd9e0e59415adac474bea3d9281bb/onnxruntime-1.24.4-cp311-cp311-win_arm64.whl", hash = "sha256:e26478356dba25631fb3f20112e345f8e8bf62c499bb497e8a559f7d69cf7e7b", size = 12259895, upload-time = "2026-03-17T22:05:28.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/38/31db1b232b4ba960065a90c1506ad7a56995cd8482033184e97fadca17cc/onnxruntime-1.24.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78", size = 17341875, upload-time = "2026-03-17T22:05:51.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/60/c4d1c8043eb42f8a9aa9e931c8c293d289c48ff463267130eca97d13357f/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5", size = 15172485, upload-time = "2026-03-17T22:03:32.182Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ab/5b68110e0460d73fad814d5bd11c7b1ddcce5c37b10177eb264d6a36e331/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c", size = 17244912, upload-time = "2026-03-17T22:04:37.251Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f4/6b89e297b93704345f0f3f8c62229bee323ef25682a3f9b4f89a39324950/onnxruntime-1.24.4-cp312-cp312-win_amd64.whl", hash = "sha256:535b29475ca42b593c45fbb2152fbf1cdf3f287315bf650e6a724a0a1d065cdb", size = 12596856, upload-time = "2026-03-17T22:05:41.224Z" }, - { url = "https://files.pythonhosted.org/packages/43/06/8b8ec6e9e6a474fcd5d772453f627ad4549dfe3ab8c0bf70af5afcde551b/onnxruntime-1.24.4-cp312-cp312-win_arm64.whl", hash = "sha256:e6214096e14b7b52e3bee1903dc12dc7ca09cb65e26664668a4620cc5e6f9a90", size = 12270275, upload-time = "2026-03-17T22:05:31.132Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f0/8a21ec0a97e40abb7d8da1e8b20fb9e1af509cc6d191f6faa75f73622fb2/onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0", size = 17341922, upload-time = "2026-03-17T22:03:56.364Z" }, - { url = "https://files.pythonhosted.org/packages/8b/25/d7908de8e08cee9abfa15b8aa82349b79733ae5865162a3609c11598805d/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13", size = 15172290, upload-time = "2026-03-17T22:03:37.124Z" }, - { url = "https://files.pythonhosted.org/packages/7f/72/105ec27a78c5aa0154a7c0cd8c41c19a97799c3b12fc30392928997e3be3/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f", size = 17244738, upload-time = "2026-03-17T22:04:40.625Z" }, - { url = "https://files.pythonhosted.org/packages/05/fb/a592736d968c2f58e12de4d52088dda8e0e724b26ad5c0487263adb45875/onnxruntime-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93", size = 12597435, upload-time = "2026-03-17T22:05:43.826Z" }, - { url = "https://files.pythonhosted.org/packages/ad/04/ae2479e9841b64bd2eb44f8a64756c62593f896514369a11243b1b86ca5c/onnxruntime-1.24.4-cp313-cp313-win_arm64.whl", hash = "sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19", size = 12269852, upload-time = "2026-03-17T22:05:33.353Z" }, - { url = "https://files.pythonhosted.org/packages/b4/af/a479a536c4398ffaf49fbbe755f45d5b8726bdb4335ab31b537f3d7149b8/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee", size = 15176861, upload-time = "2026-03-17T22:03:40.143Z" }, - { url = "https://files.pythonhosted.org/packages/be/13/19f5da70c346a76037da2c2851ecbf1266e61d7f0dcdb887c667210d4608/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36", size = 17247454, upload-time = "2026-03-17T22:04:46.643Z" }, - { url = "https://files.pythonhosted.org/packages/89/db/b30dbbd6037847b205ab75d962bc349bf1e46d02a65b30d7047a6893ffd6/onnxruntime-1.24.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbff2a248940e3398ae78374c5a839e49a2f39079b488bc64439fa0ec327a3e4", size = 17343300, upload-time = "2026-03-17T22:03:59.223Z" }, - { url = "https://files.pythonhosted.org/packages/61/88/1746c0e7959961475b84c776d35601a21d445f463c93b1433a409ec3e188/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b7969e72d8cb53ffc88ab6d49dd5e75c1c663bda7be7eb0ece192f127343d1", size = 15175936, upload-time = "2026-03-17T22:03:43.671Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ba/4699cde04a52cece66cbebc85bd8335a0d3b9ad485abc9a2e15946a1349d/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14ed1f197fab812b695a5eaddb536c635e58a2fbbe50a517c78f082cc6ce9177", size = 17246432, upload-time = "2026-03-17T22:04:49.58Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/4590910841bb28bd3b4b388a9efbedf4e2d2cca99ddf0c863642b4e87814/onnxruntime-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:311e309f573bf3c12aa5723e23823077f83d5e412a18499d4485c7eb41040858", size = 12903276, upload-time = "2026-03-17T22:05:46.349Z" }, - { url = "https://files.pythonhosted.org/packages/7f/6f/60e2c0acea1e1ac09b3e794b5a19c166eebf91c0b860b3e6db8e74983fda/onnxruntime-1.24.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f0b910e86b759a4732663ec61fd57ac42ee1b0066f68299de164220b660546d", size = 12594365, upload-time = "2026-03-17T22:05:35.795Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/0c05d10f8f6c40fe0912ebec0d5a33884aaa2af2053507e864dab0883208/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa12ddc54c9c4594073abcaa265cd9681e95fb89dae982a6f508a794ca42e661", size = 15176889, upload-time = "2026-03-17T22:03:48.021Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1d/1666dc64e78d8587d168fec4e3b7922b92eb286a2ddeebcf6acb55c7dc82/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731", size = 17247021, upload-time = "2026-03-17T22:04:52.377Z" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/a2/c801242685e0ce48a4ca51dfafbb588765e0446397e123be53ba5598f3f5/onnxruntime-1.26.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccce19c5f771b8268902f77d9fed9e88f9499465d6780808faa6611a789d33f0", size = 18016563, upload-time = "2026-05-08T19:07:28.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/64/0492c0b1db04e29b2630c87cfa36f9d6872b1ca8614b90c5cad58fac7d76/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdbed8cf3b672b66acb032f33a253bc27f42bce6ece48ae3fab4fa483a5e96e0", size = 16052634, upload-time = "2026-05-08T19:07:16.885Z" }, + { url = "https://files.pythonhosted.org/packages/3d/26/4d09ddc755a84fc8d5e192991626b0e0680e8f6c5d58f4f1d05c42bc48cf/onnxruntime-1.26.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07af6fc6d5557835f2b6ee7a96d8b3235d0c57a8e230efdedaee106a8a3cbc6", size = 18185632, upload-time = "2026-05-08T19:07:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/3e52249aa08fa301e217ecba07b5246a8338fa2b401e109326e3fc5be0f9/onnxruntime-1.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:61bec80655efa460591c2bc655392d57d2650ce85533a6b9b3b7a790d7ea7916", size = 13026751, upload-time = "2026-05-08T19:08:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/c1c8782b14af6797c303de132d6eef26a9fb80dfacd3750ce57911d11c6b/onnxruntime-1.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a6677545ff451e3539a02746d2f207d8c5baa4a0a818886bb9d6a6eb9511ee89", size = 12796807, upload-time = "2026-05-08T19:07:54.879Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f5/47b0676408abec652c14b84d7173e389837832d850c24f87184277313e8d/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e016edc15d3c19f36807e1c6b10be5b27807688c32720f91b5ae480a95215d0", size = 16057265, upload-time = "2026-05-08T19:07:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/3b/45/33ab6deeef010ca844c877dd618cebc079590bbe52d2a3678e7223b1b908/onnxruntime-1.26.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5fc48a91a046a6a5c9b147f83fb41d65d24d24923373b222cdd248f0f4f4aac", size = 18197590, upload-time = "2026-05-08T19:07:41.422Z" }, + { url = "https://files.pythonhosted.org/packages/40/89/17546c1c20f6bfc3ae41c22152378a26edfea918af3129e2139dcd7c99f3/onnxruntime-1.26.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:33a791f31432a3af1a96db5e54818b37aba5e5eefc2e6af5794c10a9118a9993", size = 18019724, upload-time = "2026-05-08T19:07:30.723Z" }, + { url = "https://files.pythonhosted.org/packages/bb/24/89457a35f6af29538a76647f2c18c3a28277e6c19234c847e7b4b7c19860/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e90c00732c4553618103149d93f688e8c3063017938f8983e21a71d9f3b6d22e", size = 16054821, upload-time = "2026-05-08T19:07:22.348Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/15b2e1815cf570d238e0135529f80d2dce64e8e8818a1489cae83823c5c6/onnxruntime-1.26.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01498e80ba8988428d08c2d51b1338f89e3de2a93e6ffe555f79c68f26a5c06b", size = 18185815, upload-time = "2026-05-08T19:07:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/2e11055faf015e4b07f45b513fa49b391baf2e19d92d77d73ebee13c1004/onnxruntime-1.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:7ead61450d8405167c87dd3a31d8da1d576b490a57dab1aa8b82a7da6825f5aa", size = 13349887, upload-time = "2026-05-08T19:08:08.671Z" }, + { url = "https://files.pythonhosted.org/packages/19/e4/0f9d1a5718b1781c610c1e354765a3820597081754277a6a9a2b50705702/onnxruntime-1.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:31d71a53490e46910877d0902b5ad99c69a5955e5c7ea6c82863519410e1ba7c", size = 13140121, upload-time = "2026-05-08T19:07:57.804Z" }, + { url = "https://files.pythonhosted.org/packages/1c/42/3b8e635f067d06d9f45bede470b8d539d101a4166c272213158dfd08b6ce/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b6d258fb78fdfcf049795bcfaa74dcb90ae7baa277afd21e6fd28b83f2c496", size = 16057240, upload-time = "2026-05-08T19:07:25.163Z" }, + { url = "https://files.pythonhosted.org/packages/93/99/f2be40a31b908d96b861ae0ce98582fa376c18a7f816b9d5eb4cd6aa0a4c/onnxruntime-1.26.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4eefd386a45202aefb7a5132b94f32df9d506c9edcc7faf2fc60d65183f4b183", size = 18197382, upload-time = "2026-05-08T19:07:46.965Z" }, ] [[package]] name = "openai" -version = "2.30.0" +version = "2.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2141,9 +1674,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3", size = 772764, upload-time = "2026-05-21T21:23:42.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c", size = 1344910, upload-time = "2026-05-21T21:23:39.636Z" }, ] [[package]] @@ -2258,72 +1791,90 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/89/267b0af1b1d0ba828f0e60642b6a5116ac1fd917cde7fc02821627029bd1/opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed", size = 196223, upload-time = "2025-06-10T08:55:17.638Z" }, ] +[[package]] +name = "orgforge" +source = { editable = "." } +dependencies = [ + { name = "boto3" }, + { name = "crewai" }, + { name = "crewai-tools" }, + { name = "json-repair" }, + { name = "networkx" }, + { name = "pydantic" }, + { name = "pymongo" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, + { name = "scipy" }, + { name = "vadersentiment" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mongomock" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "boto3", specifier = "==1.42.63" }, + { name = "crewai", specifier = ">=1.14.5" }, + { name = "crewai-tools", specifier = ">=1.14.5" }, + { name = "json-repair", specifier = ">=0.58.6" }, + { name = "networkx", specifier = ">=3.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pymongo", specifier = ">=4.6.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "requests", specifier = ">=2.31.0" }, + { name = "rich", specifier = ">=13.0.0" }, + { name = "scipy", specifier = ">=1.17.1" }, + { name = "vadersentiment", specifier = ">=3.3.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mongomock", specifier = ">=4.3.0" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "ruff", specifier = ">=0.4.0" }, +] + [[package]] name = "orjson" -version = "3.11.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, - { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, - { url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, - { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, - { url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" }, - { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" }, - { url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" }, - { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, - { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, - { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, - { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, - { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, - { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, - { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, - { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, - { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, - { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, - { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, - { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, - { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, - { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, - { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, - { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, - { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, - { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, - { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, - { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, - { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, - { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, - { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, - { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, - { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, +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/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" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, ] [[package]] @@ -2337,80 +1888,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +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/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, -] - -[[package]] -name = "pandas" -version = "3.0.2" -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/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, - { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, - { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, - { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, - { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, - { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, - { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, - { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, - { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, - { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, - { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, - { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, - { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, - { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, - { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, - { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, - { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, - { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, - { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, - { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, - { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, - { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, - { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, - { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, - { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, - { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, - { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, - { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, - { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, -] - -[[package]] -name = "parso" -version = "0.8.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, + { 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]] @@ -2440,46 +1922,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/c8/cdbc975f5b634e249cfa6597e37c50f3078412474f21c015e508bfbfe3c3/pdfplumber-0.11.9-py3-none-any.whl", hash = "sha256:33ec5580959ba524e9100138746e090879504c42955df1b8a997604dd326c443", size = 60045, upload-time = "2026-01-05T08:10:27.512Z" }, ] -[[package]] -name = "pexpect" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, -] - [[package]] name = "pillow" version = "12.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, @@ -2530,22 +1978,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +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/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, + { 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]] @@ -2576,115 +2026,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/98/e480cab9a08d1c09b1c59a93dade92c1bb7544826684ff2acbfd10fcfbd4/posthog-5.4.0-py3-none-any.whl", hash = "sha256:284dfa302f64353484420b52d4ad81ff5c2c2d1d607c4e2db602ac72761831bd", size = 105364, upload-time = "2025-06-20T23:19:22.001Z" }, ] -[[package]] -name = "prompt-toolkit" -version = "3.0.52" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, -] - [[package]] name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +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/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/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { 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]] @@ -2701,100 +2117,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, ] -[[package]] -name = "psutil" -version = "7.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, - { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, - { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, - { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, - { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, - { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, - { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, - { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, - { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, - { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, - { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, - { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, - { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, - { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, - { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, -] - -[[package]] -name = "pure-eval" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, -] - [[package]] name = "pyarrow" -version = "23.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230, upload-time = "2026-02-16T10:09:03.859Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050, upload-time = "2026-02-16T10:09:11.877Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918, upload-time = "2026-02-16T10:09:18.144Z" }, - { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811, upload-time = "2026-02-16T10:09:25.792Z" }, - { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766, upload-time = "2026-02-16T10:09:34.645Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669, upload-time = "2026-02-16T10:09:44.153Z" }, - { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698, upload-time = "2026-02-16T10:09:50.263Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, - { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, - { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, - { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, - { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, - { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, - { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, - { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, - { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, - { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, - { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, - { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, - { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, - { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, - { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, - { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, - { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, - { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, - { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, - { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, - { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, - { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, - { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, - { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, - { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, ] [[package]] @@ -2803,44 +2159,6 @@ version = "1.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/63/21e981e9d3f1f123e0b0ee2130112b1956cad9752309f574862c7ae77c08/pybase64-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70b0d4a4d54e216ce42c2655315378b8903933ecfa32fced453989a92b4317b2", size = 38237, upload-time = "2025-12-06T13:22:52.159Z" }, - { url = "https://files.pythonhosted.org/packages/92/fb/3f448e139516404d2a3963915cc10dc9dde7d3a67de4edba2f827adfef17/pybase64-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8127f110cdee7a70e576c5c9c1d4e17e92e76c191869085efbc50419f4ae3c72", size = 31673, upload-time = "2025-12-06T13:22:53.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/bb06a5b9885e7d853ac1e801c4d8abfdb4c8506deee33e53d55aa6690e67/pybase64-1.4.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f9ef0388878bc15a084bd9bf73ec1b2b4ee513d11009b1506375e10a7aae5032", size = 68331, upload-time = "2025-12-06T13:22:54.197Z" }, - { url = "https://files.pythonhosted.org/packages/64/15/8d60b9ec5e658185fc2ee3333e01a6e30d717cf677b24f47cbb3a859d13c/pybase64-1.4.3-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95a57cccf106352a72ed8bc8198f6820b16cc7d55aa3867a16dea7011ae7c218", size = 71370, upload-time = "2025-12-06T13:22:55.517Z" }, - { url = "https://files.pythonhosted.org/packages/ac/29/a3e5c1667cc8c38d025a4636855de0fc117fc62e2afeb033a3c6f12c6a22/pybase64-1.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cd1c47dfceb9c7bd3de210fb4e65904053ed2d7c9dce6d107f041ff6fbd7e21", size = 59834, upload-time = "2025-12-06T13:22:56.682Z" }, - { url = "https://files.pythonhosted.org/packages/a9/00/8ffcf9810bd23f3984698be161cf7edba656fd639b818039a7be1d6405d4/pybase64-1.4.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9fe9922698f3e2f72874b26890d53a051c431d942701bb3a37aae94da0b12107", size = 56652, upload-time = "2025-12-06T13:22:57.724Z" }, - { url = "https://files.pythonhosted.org/packages/81/62/379e347797cdea4ab686375945bc77ad8d039c688c0d4d0cfb09d247beb9/pybase64-1.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:af5f4bd29c86b59bb4375e0491d16ec8a67548fa99c54763aaedaf0b4b5a6632", size = 59382, upload-time = "2025-12-06T13:22:58.758Z" }, - { url = "https://files.pythonhosted.org/packages/c6/f2/9338ffe2f487086f26a2c8ca175acb3baa86fce0a756ff5670a0822bb877/pybase64-1.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c302f6ca7465262908131411226e02100f488f531bb5e64cb901aa3f439bccd9", size = 59990, upload-time = "2025-12-06T13:23:01.007Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a4/85a6142b65b4df8625b337727aa81dc199642de3d09677804141df6ee312/pybase64-1.4.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2f3f439fa4d7fde164ebbbb41968db7d66b064450ab6017c6c95cef0afa2b349", size = 54923, upload-time = "2025-12-06T13:23:02.369Z" }, - { url = "https://files.pythonhosted.org/packages/ac/00/e40215d25624012bf5b7416ca37f168cb75f6dd15acdb91ea1f2ea4dc4e7/pybase64-1.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a23c6866551043f8b681a5e1e0d59469148b2920a3b4fc42b1275f25ea4217a", size = 58664, upload-time = "2025-12-06T13:23:03.378Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/d7e19a63e795c13837f2356268d95dc79d1180e756f57ced742a1e52fdeb/pybase64-1.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:56e6526f8565642abc5f84338cc131ce298a8ccab696b19bdf76fa6d7dc592ef", size = 52338, upload-time = "2025-12-06T13:23:04.458Z" }, - { url = "https://files.pythonhosted.org/packages/f2/32/3c746d7a310b69bdd9df77ffc85c41b80bce00a774717596f869b0d4a20e/pybase64-1.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6a792a8b9d866ffa413c9687d9b611553203753987a3a582d68cbc51cf23da45", size = 68993, upload-time = "2025-12-06T13:23:05.526Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b3/63cec68f9d6f6e4c0b438d14e5f1ef536a5fe63ce14b70733ac5e31d7ab8/pybase64-1.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62ad29a5026bb22cfcd1ca484ec34b0a5ced56ddba38ceecd9359b2818c9c4f9", size = 58055, upload-time = "2025-12-06T13:23:06.931Z" }, - { url = "https://files.pythonhosted.org/packages/d5/cb/7acf7c3c06f9692093c07f109668725dc37fb9a3df0fa912b50add645195/pybase64-1.4.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11b9d1d2d32ec358c02214363b8fc3651f6be7dd84d880ecd597a6206a80e121", size = 54430, upload-time = "2025-12-06T13:23:07.936Z" }, - { url = "https://files.pythonhosted.org/packages/33/39/4eb33ff35d173bfff4002e184ce8907f5d0a42d958d61cd9058ef3570179/pybase64-1.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0aebaa7f238caa0a0d373616016e2040c6c879ebce3ba7ab3c59029920f13640", size = 56272, upload-time = "2025-12-06T13:23:09.253Z" }, - { url = "https://files.pythonhosted.org/packages/19/97/a76d65c375a254e65b730c6f56bf528feca91305da32eceab8bcc08591e6/pybase64-1.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e504682b20c63c2b0c000e5f98a80ea867f8d97642e042a5a39818e44ba4d599", size = 70904, upload-time = "2025-12-06T13:23:10.336Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2c/8338b6d3da3c265002839e92af0a80d6db88385c313c73f103dfb800c857/pybase64-1.4.3-cp311-cp311-win32.whl", hash = "sha256:e9a8b81984e3c6fb1db9e1614341b0a2d98c0033d693d90c726677db1ffa3a4c", size = 33639, upload-time = "2025-12-06T13:23:11.9Z" }, - { url = "https://files.pythonhosted.org/packages/39/dc/32efdf2f5927e5449cc341c266a1bbc5fecd5319a8807d9c5405f76e6d02/pybase64-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:a90a8fa16a901fabf20de824d7acce07586e6127dc2333f1de05f73b1f848319", size = 35797, upload-time = "2025-12-06T13:23:13.174Z" }, - { url = "https://files.pythonhosted.org/packages/da/59/eda4f9cb0cbce5a45f0cd06131e710674f8123a4d570772c5b9694f88559/pybase64-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:61d87de5bc94d143622e94390ec3e11b9c1d4644fe9be3a81068ab0f91056f59", size = 31160, upload-time = "2025-12-06T13:23:15.696Z" }, - { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" }, - { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" }, - { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, - { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, - { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, - { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, - { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, - { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, - { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, - { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, - { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, - { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, - { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, - { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, - { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901, upload-time = "2025-12-06T13:23:40.951Z" }, { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807, upload-time = "2025-12-06T13:23:42.006Z" }, { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932, upload-time = "2025-12-06T13:23:43.348Z" }, @@ -2927,22 +2245,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/86/a236ecfc5b494e1e922da149689f690abc84248c7c1358f5605b8c9fdd60/pybase64-1.4.3-cp314-cp314t-win32.whl", hash = "sha256:343b1901103cc72362fd1f842524e3bb24978e31aea7ff11e033af7f373f66ab", size = 34513, upload-time = "2025-12-06T13:25:24.592Z" }, { url = "https://files.pythonhosted.org/packages/56/ce/ca8675f8d1352e245eb012bfc75429ee9cf1f21c3256b98d9a329d44bf0f/pybase64-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:57aff6f7f9dea6705afac9d706432049642de5b01080d3718acc23af87c5af76", size = 36702, upload-time = "2025-12-06T13:25:25.72Z" }, { url = "https://files.pythonhosted.org/packages/3b/30/4a675864877397179b09b720ee5fcb1cf772cf7bebc831989aff0a5f79c1/pybase64-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e906aa08d4331e799400829e0f5e4177e76a3281e8a4bc82ba114c6b30e405c9", size = 31904, upload-time = "2025-12-06T13:25:26.826Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/545fd4935a0e1ddd7147f557bf8157c73eecec9cffd523382fa7af2557de/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_10_9_x86_64.whl", hash = "sha256:d27c1dfdb0c59a5e758e7a98bd78eaca5983c22f4a811a36f4f980d245df4611", size = 38393, upload-time = "2025-12-06T13:26:19.535Z" }, - { url = "https://files.pythonhosted.org/packages/c3/ca/ae7a96be9ddc96030d4e9dffc43635d4e136b12058b387fd47eb8301b60f/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0f1a0c51d6f159511e3431b73c25db31095ee36c394e26a4349e067c62f434e5", size = 32109, upload-time = "2025-12-06T13:26:20.72Z" }, - { url = "https://files.pythonhosted.org/packages/bf/44/d4b7adc7bf4fd5b52d8d099121760c450a52c390223806b873f0b6a2d551/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a492518f3078a4e3faaef310697d21df9c6bc71908cebc8c2f6fbfa16d7d6b1f", size = 43227, upload-time = "2025-12-06T13:26:21.845Z" }, - { url = "https://files.pythonhosted.org/packages/08/86/2ba2d8734ef7939debeb52cf9952e457ba7aa226cae5c0e6dd631f9b851f/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae1a0f47784fd16df90d8acc32011c8d5fcdd9ab392c9ec49543e5f6a9c43a4", size = 35804, upload-time = "2025-12-06T13:26:23.149Z" }, - { url = "https://files.pythonhosted.org/packages/4f/5b/19c725dc3aaa6281f2ce3ea4c1628d154a40dd99657d1381995f8096768b/pybase64-1.4.3-graalpy311-graalpy242_311_native-win_amd64.whl", hash = "sha256:03cea70676ffbd39a1ab7930a2d24c625b416cacc9d401599b1d29415a43ab6a", size = 35880, upload-time = "2025-12-06T13:26:24.663Z" }, - { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" }, - { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" }, - { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/160dded493c00d3376d4ad0f38a2119c5345de4a6693419ad39c3565959b/pybase64-1.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:277de6e03cc9090fb359365c686a2a3036d23aee6cd20d45d22b8c89d1247f17", size = 37939, upload-time = "2025-12-06T13:26:41.014Z" }, - { url = "https://files.pythonhosted.org/packages/b7/b8/a0f10be8d648d6f8f26e560d6e6955efa7df0ff1e009155717454d76f601/pybase64-1.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab1dd8b1ed2d1d750260ed58ab40defaa5ba83f76a30e18b9ebd5646f6247ae5", size = 31466, upload-time = "2025-12-06T13:26:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/d3/22/832a2f9e76cdf39b52e01e40d8feeb6a04cf105494f2c3e3126d0149717f/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:bd4d2293de9fd212e294c136cec85892460b17d24e8c18a6ba18750928037750", size = 40681, upload-time = "2025-12-06T13:26:43.782Z" }, - { url = "https://files.pythonhosted.org/packages/12/d7/6610f34a8972415fab3bb4704c174a1cc477bffbc3c36e526428d0f3957d/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af6d0d3a691911cc4c9a625f3ddcd3af720738c21be3d5c72de05629139d393", size = 41294, upload-time = "2025-12-06T13:26:44.936Z" }, - { url = "https://files.pythonhosted.org/packages/64/25/ed24400948a6c974ab1374a233cb7e8af0a5373cea0dd8a944627d17c34a/pybase64-1.4.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfc8c49a28322d82242088378f8542ce97459866ba73150b062a7073e82629d", size = 35447, upload-time = "2025-12-06T13:26:46.098Z" }, - { url = "https://files.pythonhosted.org/packages/ee/2b/e18ee7c5ee508a82897f021c1981533eca2940b5f072fc6ed0906c03a7a7/pybase64-1.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:debf737e09b8bf832ba86f5ecc3d3dbd0e3021d6cd86ba4abe962d6a5a77adb3", size = 36134, upload-time = "2025-12-06T13:26:47.35Z" }, ] [[package]] @@ -2956,7 +2258,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.10" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2964,74 +2266,62 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/54/ecab642b3bed45f7d5f59b38443dcb36ef50f85af192e6ece103dbfe9587/pydantic-2.11.10.tar.gz", hash = "sha256:dc280f0982fbda6c38fada4e476dc0a4f3aeaf9c6ad4c28df68a666ec3c61423", size = 788494, upload-time = "2025-10-04T10:40:41.338Z" } +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/bd/1f/73c53fcbfb0b5a78f91176df41945ca466e71e9d9d836e5c522abda39ee7/pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a", size = 444823, upload-time = "2025-10-04T10:40:39.055Z" }, + { 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.33.2" +version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/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/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/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, ] [[package]] @@ -3048,19 +2338,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, ] -[[package]] -name = "pydeck" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240, upload-time = "2024-05-10T15:36:21.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403, upload-time = "2024-05-10T15:36:17.36Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -3072,11 +2349,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.12.1" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] [package.optional-dependencies] @@ -3084,33 +2361,89 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pymongo" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/64/50be6fbac9c79fe2e4c17401a467da2d8764d82833d83cec325afe5cab32/pymongo-4.17.0.tar.gz", hash = "sha256:70ffa08ba641468cc068cf46c06b34f01a8ce3489f6411309fcb5ceabe6b2fc0", size = 2523370, upload-time = "2026-04-20T16:39:53.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/6b/c1206879708b94e82fcd8b9653440ec271f79a3674d122192df383047f5a/pymongo-4.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:809ec74de3b9148ae43fa8df9faf53470f511c8d384f13b99d6f671f2a379f15", size = 985829, upload-time = "2026-04-20T16:38:21.031Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cf/bb044ed85160e5c40f568c7c4f4e8ea16f40764ff5d302e5befbe8f6f814/pymongo-4.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a431b737816bf4cddd4fa0fcef04e424ad36b7692734a64150f872fb8f3208be", size = 985899, upload-time = "2026-04-20T16:38:23.409Z" }, + { url = "https://files.pythonhosted.org/packages/74/0a/f6dfd5ea3901e5d6888da8de8ba728971a1d447debab681cfc56f90d1208/pymongo-4.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4fab10f8403169ce92f3cea921609d9ee81107306caae06c08f592d4b8ad2b5", size = 2028569, upload-time = "2026-04-20T16:38:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c5/081f59a1c02ae8c0dc73ae58e563838c44eec81aeafa7d0b93a637841c9b/pymongo-4.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20323b0b1c1d33770ad1fc68d429c757734ce9ad3594421c3d6618f10572b1b9", size = 2072916, upload-time = "2026-04-20T16:38:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/31/42/6e41d434297ffe8b30d9c3717916591a4a7be9075a0dcc2fafdfaaaa62ed/pymongo-4.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5a5de048e6da5c18e27cc2437e8c15b3b0cdc8385c15b41178b0caa3322a09c2", size = 2173234, upload-time = "2026-04-20T16:38:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/3d/cf/1e4a7db352ef9485831c7268dfe8402f0117b32a9ad54b16e810699e3617/pymongo-4.17.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dff3de1294fbbc1db0ba6b511f77b8e540601d092538a31312e99c8a91a78b1e", size = 2156784, upload-time = "2026-04-20T16:38:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/12/10/6195be29962a61ebb5f4bd9e4c7519890b172f7968a0a0d880398c6ddb02/pymongo-4.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faf03e4c2aafd6de626dbd30ba246d369ae33f47f10629d1bbe40f72115027a6", size = 2074446, upload-time = "2026-04-20T16:38:34.004Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/33410b8819837ed370c738587306bdf060b59cef11823be212f4a07703c5/pymongo-4.17.0-cp313-cp313-win32.whl", hash = "sha256:c9786665926a09630c5d420c79762cfadbff35a9438bcbc4c81a9fb5ab9228b7", size = 948435, upload-time = "2026-04-20T16:38:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/6f/77/c0ed522f798a286b99acaa7914ed8d9c80ab091f97f57c59ffed72906e5e/pymongo-4.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:5960519b4d7168f1ecdd3ea10c81b2aedeb9423651aca953cfbc8e76705d3b38", size = 972847, upload-time = "2026-04-20T16:38:37.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/f0/c39480a2db385fde23861d0c8acda41cdaf1d43e46579db72c5c013a2e81/pymongo-4.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:0ff6bd2f735ab5356541e3e57d5b7dbfbc3f2ee1ccb10b6b0f82d58af69d1d8e", size = 951575, upload-time = "2026-04-20T16:38:40.544Z" }, + { url = "https://files.pythonhosted.org/packages/da/49/2b0250762a89737ed6f9cea238331baca061b89a8ddd10dd17fee52c3970/pymongo-4.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff5aa3f1c7e3f08eb0e7a016c91ba468b1850ccfd63d9b1f12f56350f4974cef", size = 1040945, upload-time = "2026-04-20T16:38:42.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/1c/7a9b5447a08be20e84b6e5b17330917e8d6d9507daa3cd099a9309f11ad7/pymongo-4.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e816db649ba5d7de0568cf3a9f287a9dc9aad21cf0ca667ab156a7ef47fca0b0", size = 1041187, upload-time = "2026-04-20T16:38:45.358Z" }, + { url = "https://files.pythonhosted.org/packages/78/a1/71704f61632dfc90407a5834fe5f6132854937c4a3648f6c05c351d85a45/pymongo-4.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c4fded3a9f1d6a687e36ebd384ac6d00b9b00de1969aa74048e7051ec2a713", size = 2294806, upload-time = "2026-04-20T16:38:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b9/aff42be75108b96c2469b1d9329b912c15108f3e7ef32fdc86da8423c330/pymongo-4.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2db66aa8dd253a0fc1fad3b0d23d5b3993f7ebde02fbbd7727128debf2853675", size = 2348231, upload-time = "2026-04-20T16:38:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/f2/30/44c115b8ba1479942c15fd9480eb29a7da0ba68acd56983423ba0deb4a94/pymongo-4.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3987e96e7c7be4083d42e8ac2cc6c0d5b78db9973c90fce42ae800b616ca6b20", size = 2467614, upload-time = "2026-04-20T16:38:52.665Z" }, + { url = "https://files.pythonhosted.org/packages/d2/84/21ee95c8bf0ca7acae7ec7eb365d740bf8fc0156c194baf2c3bdfcb85ec0/pymongo-4.17.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cee36b3c0d0354f880fa7a7fdcdaf2bb5e542c2281e25c1bfadf8cfe21eba7d2", size = 2445970, upload-time = "2026-04-20T16:38:55.175Z" }, + { url = "https://files.pythonhosted.org/packages/06/89/081d7f1809d5ca09d1e47e49f2111b245f5694de3a7af32cd3a353a6f43f/pymongo-4.17.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:320b34457b20bbcc79997801f95d25ce00472915ca5241167242b42c4359e027", size = 2348605, upload-time = "2026-04-20T16:38:57.557Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c3/0d949f9d3f2a341c1f635c398c16615e96f89f51ff424ed81e914cf1a4de/pymongo-4.17.0-cp314-cp314-win32.whl", hash = "sha256:df4a644af9ae132d4bfdb2e9516ea51a615fd881caddfbfbd071cf1354844479", size = 1004119, upload-time = "2026-04-20T16:39:00.309Z" }, + { url = "https://files.pythonhosted.org/packages/f7/55/5c3a3db1048054c695c75c5964cc8bedc2247fdb5a75ef6fab4ec8bb013e/pymongo-4.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:c797f8a80957134f6dd9690367a0f8f5906d672119af2c6aa55f0c527b656bed", size = 1032314, upload-time = "2026-04-20T16:39:02.665Z" }, + { url = "https://files.pythonhosted.org/packages/e0/19/e235f39906134cb0ffd5574c5a59c355ef5380f0499644ab94994afbb109/pymongo-4.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:68fca71e05ee5da23a8d73cee8379dfb3d26e609a377cae731d742771ed96946", size = 1007627, upload-time = "2026-04-20T16:39:04.678Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e0/c4c1a86791415b14c684fa0908f9da96de91594a3fd1fa1b8dc689fbb800/pymongo-4.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b4384700cffc3f1dd98e088bc0072dedf6d7d68a230bb4b972665cf69c071c1e", size = 1099151, upload-time = "2026-04-20T16:39:06.969Z" }, + { url = "https://files.pythonhosted.org/packages/81/4b/69c67f3e23fd9b23b9bedc7ebd23754881cc9d5c5d5b2a9811e96b07f475/pymongo-4.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:93641192644fa1ee0f34030e774fd31022a27ad11ba22cb1716142231524f8bd", size = 1099346, upload-time = "2026-04-20T16:39:08.996Z" }, + { url = "https://files.pythonhosted.org/packages/a2/19/a5208f62f9508a26d73acc69bd3821b8c8adae253679a3c26d2f9652f0d5/pymongo-4.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75bc3aa5b94fdb7138d357ec6ca61cd97e0c79f4f7f0bd3efe9639b15cc50942", size = 2619034, upload-time = "2026-04-20T16:39:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/77/27/426cba1ec5973082a56d4150798529bfdf4151c31391ed1fbbecb23ef2ac/pymongo-4.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e8f8e23c6df7c6d6929f5e734980b227706e73ee847517c9ba5af90f7fc466", size = 2689939, upload-time = "2026-04-20T16:39:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2e/f70993d1255e33f6ee59a4ec4371cc65bff7a7e3fda7d55c3386f25287e8/pymongo-4.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:15d3f3d732aecac1f8d481bde4029755615639bd3076f258a2147210aec8515a", size = 2824994, upload-time = "2026-04-20T16:39:16.057Z" }, + { url = "https://files.pythonhosted.org/packages/b3/eb/87b0e988ba889e1fcc3430c2cfc166b251872c813e92b43174298bee17ff/pymongo-4.17.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5f62862d0f87be481fa1fe8cb811994486773c94a2b61e509285e3f2890763", size = 2801745, upload-time = "2026-04-20T16:39:18.476Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/3f83412d086f682d4d468761d66ddc49cf161e786ea74073045eb4491c60/pymongo-4.17.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64837adbbd72073301af51bb0fc80e3d7707fe5527cea1033ba0320f0b2f881b", size = 2684636, upload-time = "2026-04-20T16:39:20.878Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/b75f6f4ab6c8beb50b0270a4f1e2530b5774f5e116563440e1677ca1820f/pymongo-4.17.0-cp314-cp314t-win32.whl", hash = "sha256:b93b22eedc62598cf5ee9d8c8007a8e9121c50fd88137012d8985500e9dc3151", size = 1056356, upload-time = "2026-04-20T16:39:22.996Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5e/648c8a238eef18a25ed8a169ea6542d4a860bbec3e95b3d9badac2935c71/pymongo-4.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3689ea34f6b647c7d1e7bdc60fcfb214b2789ed1359a7fb96569c69f50e5f18f", size = 1090964, upload-time = "2026-04-20T16:39:24.989Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cb/d9780b66939c4fc1f024bcc7be23a2abcfe06a9745ca8fa76dc73395482e/pymongo-4.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9543d8f84c2e5608565c08ac679774811e6730770d8a645439b073422a4276fb", size = 1058526, upload-time = "2026-04-20T16:39:27.924Z" }, +] + +[[package]] +name = "pymupdf" +version = "1.26.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/d6/09b28f027b510838559f7748807192149c419b30cb90e6d5f0cf916dc9dc/pymupdf-1.26.7.tar.gz", hash = "sha256:71add8bdc8eb1aaa207c69a13400693f06ad9b927bea976f5d5ab9df0bb489c3", size = 84327033, upload-time = "2025-12-11T21:48:50.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/35/cd74cea1787b2247702ef8522186bdef32e9cb30a099e6bb864627ef6045/pymupdf-1.26.7-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:07085718dfdae5ab83b05eb5eb397f863bcc538fe05135318a01ea353e7a1353", size = 23179369, upload-time = "2025-12-11T21:47:21.587Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/448b6172927c829c6a3fba80078d7b0a016ebbe2c9ee528821f5ea21677a/pymupdf-1.26.7-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:31aa9c8377ea1eea02934b92f4dcf79fb2abba0bf41f8a46d64c3e31546a3c02", size = 22470101, upload-time = "2025-12-11T21:47:37.105Z" }, + { url = "https://files.pythonhosted.org/packages/65/e7/47af26f3ac76be7ac3dd4d6cc7ee105948a8355d774e5ca39857bf91c11c/pymupdf-1.26.7-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e419b609996434a14a80fa060adec72c434a1cca6a511ec54db9841bc5d51b3c", size = 23502486, upload-time = "2025-12-12T09:51:25.824Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/3de1714d734ff949be1e90a22375d0598d3540b22ae73eb85c2d7d1f36a9/pymupdf-1.26.7-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:69dfc78f206a96e5b3ac22741263ebab945fdf51f0dbe7c5757c3511b23d9d72", size = 24115727, upload-time = "2025-12-11T21:47:51.274Z" }, + { url = "https://files.pythonhosted.org/packages/62/9b/f86224847949577a523be2207315ae0fd3155b5d909cd66c274d095349a3/pymupdf-1.26.7-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1d5106f46e1ca0d64d46bd51892372a4f82076bdc14a9678d33d630702abca36", size = 24324386, upload-time = "2025-12-12T14:58:45.483Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/a117d39092ca645fde8b903f4a941d9aa75b370a67b4f1f435f56393dc5a/pymupdf-1.26.7-cp310-abi3-win32.whl", hash = "sha256:7c9645b6f5452629c747690190350213d3e5bbdb6b2eca227d82702b327f6eee", size = 17203888, upload-time = "2025-12-12T13:59:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c3/d0047678146c294469c33bae167c8ace337deafb736b0bf97b9bc481aa65/pymupdf-1.26.7-cp310-abi3-win_amd64.whl", hash = "sha256:425b1befe40d41b72eb0fe211711c7ae334db5eb60307e9dd09066ed060cceba", size = 18405952, upload-time = "2025-12-11T21:48:02.947Z" }, +] + [[package]] name = "pypdfium2" -version = "5.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/01/be763b9081c7eb823196e7d13d9c145bf75ac43f3c1466de81c21c24b381/pypdfium2-5.6.0.tar.gz", hash = "sha256:bcb9368acfe3547054698abbdae68ba0cbd2d3bda8e8ee437e061deef061976d", size = 270714, upload-time = "2026-03-08T01:05:06.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/b1/129ed0177521a93a892f8a6a215dd3260093e30e77ef7035004bb8af7b6c/pypdfium2-5.6.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:fb7858c9707708555b4a719b5548a6e7f5d26bc82aef55ae4eb085d7a2190b11", size = 3346059, upload-time = "2026-03-08T01:04:21.37Z" }, - { url = "https://files.pythonhosted.org/packages/86/34/cbdece6886012180a7f2c7b2c360c415cf5e1f83f1973d2c9201dae3506a/pypdfium2-5.6.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:6a7e1f4597317786f994bfb947eef480e53933f804a990193ab89eef8243f805", size = 2804418, upload-time = "2026-03-08T01:04:23.384Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f6/9f9e190fe0e5a6b86b82f83bd8b5d3490348766062381140ca5cad8e00b1/pypdfium2-5.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e468c38997573f0e86f03273c2c1fbdea999de52ba43fee96acaa2f6b2ad35f7", size = 3412541, upload-time = "2026-03-08T01:04:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8d/e57492cb2228ba56ed57de1ff044c8ac114b46905f8b1445c33299ba0488/pypdfium2-5.6.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:ad3abddc5805424f962e383253ccad6a0d1d2ebd86afa9a9e1b9ca659773cd0d", size = 3592320, upload-time = "2026-03-08T01:04:27.509Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8a/8ab82e33e9c551494cbe1526ea250ca8cc4e9e98d6a4fc6b6f8d959aa1d1/pypdfium2-5.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b5eb9eae5c45076395454522ca26add72ba8bd1fe473e1e4721aa58521470c", size = 3596450, upload-time = "2026-03-08T01:04:29.183Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b5/602a792282312ccb158cc63849528079d94b0a11efdc61f2a359edfb41e9/pypdfium2-5.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:258624da8ef45cdc426e11b33e9d83f9fb723c1c201c6e0f4ab5a85966c6b876", size = 3325442, upload-time = "2026-03-08T01:04:30.886Z" }, - { url = "https://files.pythonhosted.org/packages/81/1f/9e48ec05ed8d19d736c2d1f23c1bd0f20673f02ef846a2576c69e237f15d/pypdfium2-5.6.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9367451c8a00931d6612db0822525a18c06f649d562cd323a719e46ac19c9bb", size = 3727434, upload-time = "2026-03-08T01:04:33.619Z" }, - { url = "https://files.pythonhosted.org/packages/33/90/0efd020928b4edbd65f4f3c2af0c84e20b43a3ada8fa6d04f999a97afe7a/pypdfium2-5.6.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a757869f891eac1cc1372e38a4aa01adac8abc8fe2a8a4e2ebf50595e3bf5937", size = 4139029, upload-time = "2026-03-08T01:04:36.08Z" }, - { url = "https://files.pythonhosted.org/packages/ff/49/a640b288a48dab1752281dd9b72c0679fccea107874e80a65a606b00efa9/pypdfium2-5.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:515be355222cc57ae9e62cd5c7c350b8e0c863efc539f80c7d75e2811ba45cb6", size = 3646387, upload-time = "2026-03-08T01:04:38.151Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/a344c19c01021eeb5d830c102e4fc9b1602f19c04aa7d11abbe2d188fd8e/pypdfium2-5.6.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c4753c7caf7d004211d7f57a21f10d127f5e0e5510a14d24bc073e7220a3ea", size = 3097212, upload-time = "2026-03-08T01:04:40.776Z" }, - { url = "https://files.pythonhosted.org/packages/50/96/e48e13789ace22aeb9b7510904a1b1493ec588196e11bbacc122da330b3d/pypdfium2-5.6.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c49729090281fdd85775fb8912c10bd19e99178efaa98f145ab06e7ce68554d2", size = 2965026, upload-time = "2026-03-08T01:04:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/cb/06/3100e44d4935f73af8f5d633d3bd40f0d36d606027085a0ef1f0566a6320/pypdfium2-5.6.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a4a1749a8d4afd62924a8d95cfa4f2e26fc32957ce34ac3b674be6f127ed252e", size = 4131431, upload-time = "2026-03-08T01:04:44.982Z" }, - { url = "https://files.pythonhosted.org/packages/64/ef/d8df63569ce9a66c8496057782eb8af78e0d28667922d62ec958434e3d4b/pypdfium2-5.6.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:36469ebd0fdffb7130ce45ed9c44f8232d91571c89eb851bd1633c64b6f6114f", size = 3747469, upload-time = "2026-03-08T01:04:46.702Z" }, - { url = "https://files.pythonhosted.org/packages/a6/47/fd2c6a67a49fade1acd719fbd11f7c375e7219912923ef2de0ea0ac1544e/pypdfium2-5.6.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da900df09be3cf546b637a127a7b6428fb22d705951d731269e25fd3adef457", size = 4337578, upload-time = "2026-03-08T01:04:49.007Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f5/836c83e54b01e09478c4d6bf4912651d6053c932250fcee953f5c72d8e4a/pypdfium2-5.6.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:45fccd5622233c5ec91a885770ae7dd4004d4320ac05a4ad8fa03a66dea40244", size = 4376104, upload-time = "2026-03-08T01:04:51.04Z" }, - { url = "https://files.pythonhosted.org/packages/6e/7f/b940b6a1664daf8f9bad87c6c99b84effa3611615b8708d10392dc33036c/pypdfium2-5.6.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:282dc030e767cd61bd0299f9d581052b91188e2b87561489057a8e7963e7e0cb", size = 3929824, upload-time = "2026-03-08T01:04:53.544Z" }, - { url = "https://files.pythonhosted.org/packages/88/79/00267d92a6a58c229e364d474f5698efe446e0c7f4f152f58d0138715e99/pypdfium2-5.6.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:a1c1dfe950382c76a7bba1ba160ec5e40df8dd26b04a1124ae268fda55bc4cbe", size = 4270201, upload-time = "2026-03-08T01:04:55.81Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ab/b127f38aba41746bdf9ace15ba08411d7ef6ecba1326d529ba414eb1ed50/pypdfium2-5.6.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:43b0341ca6feb6c92e4b7a9eb4813e5466f5f5e8b6baeb14df0a94d5f312c00b", size = 4180793, upload-time = "2026-03-08T01:04:57.961Z" }, - { url = "https://files.pythonhosted.org/packages/0e/8c/a01c8e4302448b614d25a85c08298b0d3e9dfbdac5bd1b2f32c9b02e83d9/pypdfium2-5.6.0-py3-none-win32.whl", hash = "sha256:9dfcd4ff49a2b9260d00e38539ab28190d59e785e83030b30ffaf7a29c42155d", size = 3596753, upload-time = "2026-03-08T01:05:00.566Z" }, - { url = "https://files.pythonhosted.org/packages/9b/5f/2d871adf46761bb002a62686545da6348afe838d19af03df65d1ece786a2/pypdfium2-5.6.0-py3-none-win_amd64.whl", hash = "sha256:c6bc8dd63d0568f4b592f3e03de756afafc0e44aa1fe8878cc4aba1b11ae7374", size = 3716526, upload-time = "2026-03-08T01:05:02.433Z" }, - { url = "https://files.pythonhosted.org/packages/3a/80/0d9b162098597fbe3ac2b269b1682c0c3e8db9ba87679603fdd9b19afaa6/pypdfium2-5.6.0-py3-none-win_arm64.whl", hash = "sha256:5538417b199bdcb3207370c88df61f2ba3dac7a3253f82e1aa2708e6376b6f90", size = 3515049, upload-time = "2026-03-08T01:05:04.587Z" }, +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/3d/dc934d3b606c51c3ecc95b6731d84b7dd7ab8e513a50b0e98a4da6c8a719/pypdfium2-5.8.0.tar.gz", hash = "sha256:049397c647e50f83115ee951c49394dab9e9ba52ebdd5a11ab1109390eb3d34e", size = 271934, upload-time = "2026-05-04T17:39:43.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/8c/6b75b923cb81368fa3ea7c48a0616b839620a3aeff899885bd930449b89e/pypdfium2-5.8.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:f67b6c74b716d9ac725ad1af49ae786ad813ac20823d45606d59f1fc06caa8af", size = 3374554, upload-time = "2026-05-04T17:39:05.552Z" }, + { url = "https://files.pythonhosted.org/packages/ef/61/a885c7f36efba89ec98e3d1fe95c83b48c2d6dea321e9194ac6460e7a834/pypdfium2-5.8.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:53e82bf3e6a2da170b1bda83f93b7eec57cb6efe3cacd05cba78823879a85203", size = 2831667, upload-time = "2026-05-04T17:39:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/86/1f/04b5627f6dba312d3e707e5b019c9f24d8b03b5aa366866a9e02ec00f8d4/pypdfium2-5.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:085e633dcc89b65ff4035a4787e98ce7ae636836eb39c83dd0db26113d9774bc", size = 3450815, upload-time = "2026-05-04T17:39:09.551Z" }, + { url = "https://files.pythonhosted.org/packages/a9/77/8e3a2aba2bc4aef5abe1b1306d05b00588dc0bf7f5c850d1adf6164c786b/pypdfium2-5.8.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:bc84b7c6efede88fcfb9467f81daf416f26b973a54fc1cf4d3410d622fda6d7a", size = 3634395, upload-time = "2026-05-04T17:39:11.225Z" }, + { url = "https://files.pythonhosted.org/packages/93/11/6f2b1847d9fa457b3b7251afc2bba2706d104a0c6f01431dfae5d679a839/pypdfium2-5.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63bf09b2e13ba8545c930d243f0650c664a1b51314daa3b5f38df6d1a17b4bc", size = 3617413, upload-time = "2026-05-04T17:39:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fd/99ce639de5ca06d21743c740dd988cd209dda623bc763ae10b8a162022e1/pypdfium2-5.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:937881c1698456749ed203a58db1895baa5eb7178cdb837ef84867790638da28", size = 3347639, upload-time = "2026-05-04T17:39:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/fa/47/82864cc6e26dd8969d5594c168635acb16458d35cf5fed65d6b2e32abb42/pypdfium2-5.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be9dc2b84a8694ad7e626bab133244e8241014d5ed1930d865a9bdf90df1e24", size = 3746404, upload-time = "2026-05-04T17:39:17.094Z" }, + { url = "https://files.pythonhosted.org/packages/82/58/e41e49bba951f61921bac7289e67fe02af5ac57192d0bbfb5f459dc3691d/pypdfium2-5.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f27bd82891ae302dd02d736b14809661f6d1220ee1e96dbed9b23e2811922a3", size = 4177893, upload-time = "2026-05-04T17:39:18.729Z" }, + { url = "https://files.pythonhosted.org/packages/b4/15/fa7031010d5cf6853dadb4864680a0bfb7782c5bb6a1a401e0c25c4fca87/pypdfium2-5.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26c1089cdbbdc7fe1248f6d17fe3f30214be4f287dd0196b31aaee18a1564240", size = 3665152, upload-time = "2026-05-04T17:39:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/de/6a/5a3520a8b0cfa8d7fdc3f03a07ad9d6146c28ffd519330706f64fd8939a8/pypdfium2-5.8.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c038a9290864aaa4862dd32e591993d82551ca4d152b4e8ce6d43ba37dc04a8", size = 3095365, upload-time = "2026-05-04T17:39:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/32/d3/845bae4de3cfa36865959046156edb5bf9baea400ccdecdd84fdd911b0f5/pypdfium2-5.8.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f104bc1a6d8bfc1ff088aa50db13b9729cfdb3722b44975c3c457e9a7b9c7318", size = 2961801, upload-time = "2026-05-04T17:39:23.817Z" }, + { url = "https://files.pythonhosted.org/packages/99/76/cf54eabee4a172241dfcfe63533bd1e11e2162114a983453a5a40bfec114/pypdfium2-5.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:04ca7c57a553facf8d46c6ea8ba6fa557e698670cfa4a58e0e01fdae2f6be87d", size = 4133067, upload-time = "2026-05-04T17:39:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/77/66/dcf871d19187ca04ea184a99801a6e7e556d8347aa49540fee33cda6dfc5/pypdfium2-5.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ad42b9c22477b32dbedcbc8232833f385d92fd0cf92822547b02383cf9a476d7", size = 3749100, upload-time = "2026-05-04T17:39:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/32/67/0d456c79660959ca45ad307b4d67161d29f9ed4083ee1e8fe8c6925b7c82/pypdfium2-5.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:388e3119cf5ca0979b7d5f6d40b7fcd5ab49e17ed4e6de6af89ba116061acfda", size = 4339212, upload-time = "2026-05-04T17:39:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/76/89/e5b0e0f7936be341c91c0f45cd70d693878894ed62aed93a6ee32e9c43c4/pypdfium2-5.8.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:aa05bbfa485ce7916217aa78d856c9f9cd86b08b20846c650392a67975ee72e9", size = 4383943, upload-time = "2026-05-04T17:39:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/21/4502ed255f082f579cd3537c2971cf1a57778d43703a08bcd1a92253189f/pypdfium2-5.8.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:f0813a16bb39d5ebd173ea5484430bb67a89b4b181db0a636c73b64ad063c3ea", size = 3925680, upload-time = "2026-05-04T17:39:33.241Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/2e59723e7a07779439bd885c1b4960079c9710603308888d29ac926ae69a/pypdfium2-5.8.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:a3c78f7d20dd821bec6c072efdb21a1370b9efe10fdeeb68c969e67608e25385", size = 4269560, upload-time = "2026-05-04T17:39:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/34/4e/7b6b1bde3788c8b880d4b8131d95d9d339cebafb3ad9102d82e234bb65be/pypdfium2-5.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:86d302e207c138c827b885a72784f7b306d840646ebeae07e8efdbc39321c629", size = 4182434, upload-time = "2026-05-04T17:39:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/11/7b/6ed4782e0d7a5278330598ce8c4b2df7255f4585a0b3d04520fa580d6507/pypdfium2-5.8.0-py3-none-win32.whl", hash = "sha256:3f25fd436920a907291462b41bdc0ab9f8235c3944b4c9c15398da595ffd1fed", size = 3636680, upload-time = "2026-05-04T17:39:38.49Z" }, + { url = "https://files.pythonhosted.org/packages/19/55/da7223d4202b2461f4f889b0baf10dddec3db7f88e6fd8c52db4a516eecd/pypdfium2-5.8.0-py3-none-win_amd64.whl", hash = "sha256:55592af0bddd2d62bed18e0053c546c9b72041430c5115e54870f7f6163125b0", size = 3754962, upload-time = "2026-05-04T17:39:40.13Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7a/f3dcefe6ee7389aad3ca1488c177e8fbf978206de21c7a99ccf487ea38ab/pypdfium2-5.8.0-py3-none-win_arm64.whl", hash = "sha256:3f17ed97ae8a5a1705301ca93af256a5b02f9009dee4e99c5e175831d46ebd7c", size = 3548362, upload-time = "2026-05-04T17:39:42.304Z" }, ] [[package]] @@ -3131,6 +2464,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] +[[package]] +name = "pytest" +version = "9.1.1" +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/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -3143,22 +2492,53 @@ 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-docx" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, +] + [[package]] name = "python-dotenv" -version = "1.1.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/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/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.29" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, +] + +[[package]] +name = "pytube" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/e7/16fec46c8d255c4bbc4b185d89c91dc92cdb802836570d8004d0db169c91/pytube-15.0.0.tar.gz", hash = "sha256:076052efe76f390dfa24b1194ff821d4e86c17d41cb5562f3a276a8bcbfc9d1d", size = 67229, upload-time = "2023-05-07T19:39:01.903Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/51/64/bcf8632ed2b7a36bbf84a0544885ffa1d0b4bcf25cc0903dba66ec5fdad9/pytube-15.0.0-py3-none-any.whl", hash = "sha256:07b9904749e213485780d7eb606e5e5b8e4341aa4dccf699160876da00e12d78", size = 57594, upload-time = "2023-05-07T19:38:59.191Z" }, +] + +[[package]] +name = "pytz" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, ] [[package]] @@ -3166,12 +2546,6 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, @@ -3186,25 +2560,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, @@ -3235,64 +2590,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "pyzmq" -version = "27.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, - { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, - { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, - { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, - { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, - { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, - { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, - { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, - { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, - { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, - { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, - { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, - { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, - { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, - { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, - { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, - { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, - { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, - { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, - { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, -] - [[package]] name = "referencing" version = "0.37.0" @@ -3300,7 +2597,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ @@ -3313,38 +2609,6 @@ version = "2026.1.15" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, - { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, - { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, - { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, - { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, - { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, - { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, - { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, - { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, - { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, - { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, - { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, - { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, - { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, - { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, - { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, - { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, - { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, - { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, @@ -3413,7 +2677,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -3421,9 +2685,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +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/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { 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]] @@ -3441,15 +2705,15 @@ wheels = [ [[package]] name = "rich" -version = "14.3.3" +version = "14.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +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/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { 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]] @@ -3458,36 +2722,6 @@ version = "0.30.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, - { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, - { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, @@ -3546,30 +2780,103 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, - { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, - { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, - { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, - { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" }, + { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" }, + { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" }, + { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" }, + { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" }, ] [[package]] name = "s3transfer" -version = "0.14.0" +version = "0.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/29/af14f4ef3c11a50435308660e2cc68761c9a7742475e0585cd4396b91777/s3transfer-0.16.1.tar.gz", hash = "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524", size = 154801, upload-time = "2026-04-22T20:36:06.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/19/90d7d4ed51932c022d53f1d02d564b62d10e272692a1f9b76425c1ad2a02/s3transfer-0.16.1-py3-none-any.whl", hash = "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", size = 86825, upload-time = "2026-04-22T20:36:04.992Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "sentinels" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/9b/07195878aa25fe6ed209ec74bc55ae3e3d263b60a489c6e73fdca3c8fe05/sentinels-1.1.1.tar.gz", hash = "sha256:3c2f64f754187c19e0a1a029b148b74cf58dd12ec27b4e19c0e5d6e22b5a9a86", size = 4393, upload-time = "2025-08-12T07:57:50.26Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/49/65/dea992c6a97074f6d8ff9eab34741298cac2ce23e2b6c74fb7d08afdf85c/sentinels-1.1.1-py3-none-any.whl", hash = "sha256:835d3b28f3b47f5284afa4bf2db6e00f2dc5f80f9923d4b7e7aeeeccf6146a11", size = 3744, upload-time = "2025-08-12T07:57:48.858Z" }, ] [[package]] @@ -3590,15 +2897,6 @@ 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" @@ -3609,84 +2907,37 @@ wheels = [ ] [[package]] -name = "sse-starlette" -version = "3.3.4" +name = "soupsieve" +version = "2.8.3" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] -name = "stack-data" -version = "0.6.3" +name = "sse-starlette" +version = "3.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asttokens" }, - { name = "executing" }, - { name = "pure-eval" }, + { name = "anyio" }, + { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/2b/58abc2d1fd397e7dde08e947e05c884d8ef2f78d5e2588c17a12d42d6994/sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0", size = 31819, upload-time = "2026-05-12T17:37:17.019Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/805710444ea8cc75fbf70b920ed431a560c4bf9c57f7d5a3117213189399/sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973", size = 16514, upload-time = "2026-05-12T17:37:15.601Z" }, ] [[package]] name = "starlette" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/66/4d20cdf39a8d6a51e663b7038e3b828ff211d3891a43a713fe7e4643f3a8/starlette-1.1.0.tar.gz", hash = "sha256:e83c7fe0ddecd8719c5b840080325aec0260acec86e9832899e377b91d65e90f", size = 2660060, upload-time = "2026-05-23T16:55:41.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, -] - -[[package]] -name = "streamlit" -version = "1.56.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "altair" }, - { name = "blinker" }, - { name = "cachetools" }, - { name = "click" }, - { name = "gitpython" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pandas" }, - { name = "pillow" }, - { name = "protobuf" }, - { name = "pyarrow" }, - { name = "pydeck" }, - { name = "requests" }, - { name = "tenacity" }, - { name = "toml" }, - { name = "tornado" }, - { name = "typing-extensions" }, - { name = "watchdog", marker = "sys_platform != 'darwin'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/03/85/7c669b3a1336d34ef39fa9760fbd343185f3b15db2ad0838fd78423d1c7f/streamlit-1.56.0.tar.gz", hash = "sha256:1176acfa89ae1318b79078e8efe689a9d02e8d58e325c00fc0e55fa2f3fe8d6a", size = 8559239, upload-time = "2026-03-31T22:29:38.59Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/91/cb6f13a89e376ef179309d74f37a70ea0041d5e4b5ba5c4836dbf6e020ad/streamlit-1.56.0-py3-none-any.whl", hash = "sha256:8677a335734a30a51bc57ad0ec910e365d95f7c456fc02c60032927cd0729dc5", size = 9052089, upload-time = "2026-03-31T22:29:36.342Z" }, -] - -[[package]] -name = "sympy" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mpmath" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/93/79/920b8e0a8b20f793e8d64855095cb8febabf6175b8550b6f7a547d813891/starlette-1.1.0-py3-none-any.whl", hash = "sha256:7f0dfd38e428aad5cb6f9f667f0ca1d2d8ca3f3385dccac8305f79ec98458382", size = 72899, upload-time = "2026-05-23T16:55:39.201Z" }, ] [[package]] @@ -3700,7 +2951,7 @@ wheels = [ [[package]] name = "textual" -version = "8.2.2" +version = "8.2.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", extra = ["linkify"] }, @@ -3710,44 +2961,76 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/b0/a9aedf13af1bfb1bf01cbc645ea5d5a4151b5d77ac1748b85c4f0d777d7d/textual-8.2.2.tar.gz", hash = "sha256:94e85267650cf679ac16ade5ac929055e836dc00798a0e6e3925926a5beee303", size = 1848623, upload-time = "2026-04-03T13:19:06.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/7a/c519db0aba5024f86e71e9631810bfdd6866ed2c8695bd7fa34b90e7ef59/textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105", size = 1859249, upload-time = "2026-05-19T10:52:49.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/18/4d59eb3f2241db6d346a90f2452fc47a19d61090a38b9cf331afe23e8431/textual-8.2.2-py3-none-any.whl", hash = "sha256:35a8f439875dc6e5b4dc7ee72dc9698a40bd13091c2de5bd5b2d4318522af8df", size = 724078, upload-time = "2026-04-03T13:19:08.115Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f5/c1e18bc0707300a0e90204343abbf7d7acd6fb7ebe03a6d4893b99a234b8/textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73", size = 731129, upload-time = "2026-05-19T10:52:51.773Z" }, ] [[package]] -name = "tokenizers" -version = "0.22.2" +name = "tiktoken" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "huggingface-hub" }, + { name = "regex" }, + { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, - { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, - { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, - { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, - { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, - { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, - { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, - { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, ] [[package]] -name = "toml" -version = "0.10.2" +name = "tokenizers" +version = "0.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, ] [[package]] @@ -3768,23 +3051,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440, upload-time = "2024-10-08T11:13:27.897Z" }, ] -[[package]] -name = "tornado" -version = "6.5.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, - { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, - { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, - { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, - { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, - { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, - { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, -] - [[package]] name = "tqdm" version = "4.67.3" @@ -3797,15 +3063,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] -[[package]] -name = "traitlets" -version = "5.14.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, -] - [[package]] name = "typer" version = "0.23.1" @@ -3842,15 +3099,6 @@ 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.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, -] - [[package]] name = "uc-micro-py" version = "2.0.0" @@ -3862,50 +3110,50 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +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/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { 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 = "uv" -version = "0.9.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/a0/63cea38fe839fb89592728b91928ee6d15705f1376a7940fee5bbc77fea0/uv-0.9.30.tar.gz", hash = "sha256:03ebd4b22769e0a8d825fa09d038e31cbab5d3d48edf755971cb0cec7920ab95", size = 3846526, upload-time = "2026-02-04T21:45:37.58Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/3c/71be72f125f0035348b415468559cc3b335ec219376d17a3d242d2bd9b23/uv-0.9.30-py3-none-linux_armv6l.whl", hash = "sha256:a5467dddae1cd5f4e093f433c0f0d9a0df679b92696273485ec91bbb5a8620e6", size = 21927585, upload-time = "2026-02-04T21:46:14.935Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fd/8070b5423a77d4058d14e48a970aa075762bbff4c812dda3bb3171543e44/uv-0.9.30-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ec38ae29aa83a37c6e50331707eac8ecc90cf2b356d60ea6382a94de14973be", size = 21050392, upload-time = "2026-02-04T21:45:55.649Z" }, - { url = "https://files.pythonhosted.org/packages/42/5f/3ccc9415ef62969ed01829572338ea7bdf4c5cf1ffb9edc1f8cb91b571f3/uv-0.9.30-py3-none-macosx_11_0_arm64.whl", hash = "sha256:777ecd117cf1d8d6bb07de8c9b7f6c5f3e802415b926cf059d3423699732eb8c", size = 19817085, upload-time = "2026-02-04T21:45:40.881Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3f/76b44e2a224f4c4a8816fc92686ef6d4c2656bc5fc9d4f673816162c994d/uv-0.9.30-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:93049ba3c41fa2cc38b467cb78ef61b2ddedca34b6be924a5481d7750c8111c6", size = 21620537, upload-time = "2026-02-04T21:45:47.846Z" }, - { url = "https://files.pythonhosted.org/packages/60/2a/50f7e8c6d532af8dd327f77bdc75ce4652322ac34f5e29f79a8e04ea3cc8/uv-0.9.30-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:f295604fee71224ebe2685a0f1f4ff7a45c77211a60bd57133a4a02056d7c775", size = 21550855, upload-time = "2026-02-04T21:46:26.269Z" }, - { url = "https://files.pythonhosted.org/packages/0e/10/f823d4af1125fae559194b356757dc7d4a8ac79d10d11db32c2d4c9e2f63/uv-0.9.30-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2faf84e1f3b6fc347a34c07f1291d11acf000b0dd537a61d541020f22b17ccd9", size = 21516576, upload-time = "2026-02-04T21:46:03.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/f3/64b02db11f38226ed34458c7fbdb6f16b6d4fd951de24c3e51acf02b30f8/uv-0.9.30-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b3b3700ecf64a09a07fd04d10ec35f0973ec15595d38bbafaa0318252f7e31f", size = 22718097, upload-time = "2026-02-04T21:45:51.875Z" }, - { url = "https://files.pythonhosted.org/packages/28/21/a48d1872260f04a68bb5177b0f62ddef62ab892d544ed1922f2d19fd2b00/uv-0.9.30-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b176fc2937937dd81820445cb7e7e2e3cd1009a003c512f55fa0ae10064c8a38", size = 24107844, upload-time = "2026-02-04T21:46:19.032Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c6/d7e5559bfe1ab7a215a7ad49c58c8a5701728f2473f7f436ef00b4664e88/uv-0.9.30-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:180e8070b8c438b9a3fb3fde8a37b365f85c3c06e17090f555dc68fdebd73333", size = 23685378, upload-time = "2026-02-04T21:46:07.166Z" }, - { url = "https://files.pythonhosted.org/packages/a8/bf/b937bbd50d14c6286e353fd4c7bdc09b75f6b3a26bd4e2f3357e99891f28/uv-0.9.30-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4125a9aa2a751e1589728f6365cfe204d1be41499148ead44b6180b7df576f27", size = 22848471, upload-time = "2026-02-04T21:45:18.728Z" }, - { url = "https://files.pythonhosted.org/packages/6a/57/12a67c569e69b71508ad669adad266221f0b1d374be88eaf60109f551354/uv-0.9.30-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4366dd740ac9ad3ec50a58868a955b032493bb7d7e6ed368289e6ced8bbc70f3", size = 22774258, upload-time = "2026-02-04T21:46:10.798Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b8/a26cc64685dddb9fb13f14c3dc1b12009f800083405f854f84eb8c86b494/uv-0.9.30-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:33e50f208e01a0c20b3c5f87d453356a5cbcfd68f19e47a28b274cd45618881c", size = 21699573, upload-time = "2026-02-04T21:45:44.365Z" }, - { url = "https://files.pythonhosted.org/packages/c8/59/995af0c5f0740f8acb30468e720269e720352df1d204e82c2d52d9a8c586/uv-0.9.30-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5e7a6fa7a3549ce893cf91fe4b06629e3e594fc1dca0a6050aba2ea08722e964", size = 22460799, upload-time = "2026-02-04T21:45:26.658Z" }, - { url = "https://files.pythonhosted.org/packages/bb/0b/6affe815ecbaebf38b35d6230fbed2f44708c67d5dd5720f81f2ec8f96ff/uv-0.9.30-py3-none-musllinux_1_1_i686.whl", hash = "sha256:62d7e408d41e392b55ffa4cf9b07f7bbd8b04e0929258a42e19716c221ac0590", size = 22001777, upload-time = "2026-02-04T21:45:34.656Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b6/47a515171c891b0d29f8e90c8a1c0e233e4813c95a011799605cfe04c74c/uv-0.9.30-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6dc65c24f5b9cdc78300fa6631368d3106e260bbffa66fb1e831a318374da2df", size = 22968416, upload-time = "2026-02-04T21:45:22.863Z" }, - { url = "https://files.pythonhosted.org/packages/3d/3a/c1df8615385138bb7c43342586431ca32b77466c5fb086ac0ed14ab6ca28/uv-0.9.30-py3-none-win32.whl", hash = "sha256:74e94c65d578657db94a753d41763d0364e5468ec0d368fb9ac8ddab0fb6e21f", size = 20889232, upload-time = "2026-02-04T21:46:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a8/e8761c8414a880d70223723946576069e042765475f73b4436d78b865dba/uv-0.9.30-py3-none-win_amd64.whl", hash = "sha256:88a2190810684830a1ba4bb1cf8fb06b0308988a1589559404259d295260891c", size = 23432208, upload-time = "2026-02-04T21:45:30.85Z" }, - { url = "https://files.pythonhosted.org/packages/49/e8/6f2ebab941ec559f97110bbbae1279cd0333d6bc352b55f6fa3fefb020d9/uv-0.9.30-py3-none-win_arm64.whl", hash = "sha256:7fde83a5b5ea027315223c33c30a1ab2f2186910b933d091a1b7652da879e230", size = 21887273, upload-time = "2026-02-04T21:45:59.787Z" }, +version = "0.11.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/99/025154611a4bd97a23851574c15d73bb71ada09d35f092d6972f9ac87f70/uv-0.11.16.tar.gz", hash = "sha256:4b435fcb0af8f34833dcc1903a8a223856437efd0d515c2160a2871def221238", size = 4177038, upload-time = "2026-05-21T22:10:01.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e3/8b8cfc802bc476c67e31a39725538193265cf3a19585b4a60c232659f919/uv-0.11.16-py3-none-linux_armv6l.whl", hash = "sha256:c9e9d9cb73ee8cd2ad696dbf1bc3232abaac363270557684b6b85a2bdb8eb276", size = 23508087, upload-time = "2026-05-21T22:10:06.227Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/d5ca91c636ac88e902b6b3ff31ad32d2d02663232d844aff871467a323d2/uv-0.11.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:01172238a75e42a5a55d12555cd9ec98bee24249f3645b98a4b32eb5f1ff5e43", size = 23028989, upload-time = "2026-05-21T22:09:50.127Z" }, + { url = "https://files.pythonhosted.org/packages/c7/26/c84580dfec5a87c36fb1218eac17c5194fa3e58e2a9232cf085d69eb6bed/uv-0.11.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75f9b5bac49b97131973910c220feac60fe47b10a333941b237ff0ae4b36721", size = 21572023, upload-time = "2026-05-21T22:09:58.703Z" }, + { url = "https://files.pythonhosted.org/packages/84/68/ba2bdc64fea96ef8c9796a991f244541b65bb9d31c661b322cc724857a4e/uv-0.11.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a801484f4507b6c2133e557350f3143b61b8f8b61dddb01ff7b84a74cdfab1fb", size = 23289936, upload-time = "2026-05-21T22:10:15.423Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/74922f693d5804a77d009338ca8dc709eff871fb60d9f2c263dede8d77d1/uv-0.11.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:eb538069e768b042cf870be700a210518ce628e36d99d9a83b85acaf484d7f6a", size = 23020906, upload-time = "2026-05-21T22:10:24.242Z" }, + { url = "https://files.pythonhosted.org/packages/60/81/cda8886f5df4dd28854a9b97bcc3ee6a7d1b5b5b23aaaccfbf1ed3e5e2bf/uv-0.11.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7cdb23457a4d1bc76bf1016638ea1d1ada0e8e032f656168e933d4d17c47e72", size = 23004220, upload-time = "2026-05-21T22:10:32.847Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/65837e07de23f0a40ab860bc6601f7c022d4bcf4b97ca79b6c35a2e72e65/uv-0.11.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:451327388d59ac3041cbda474296f3ceeafac5b1f645476198e7b95f504fcfd5", size = 24319651, upload-time = "2026-05-21T22:10:21.492Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/9d364542bf118433b60ed71422e47d2c8c470aca7d3aef0df9449a5f726a/uv-0.11.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7992b8276149b3ffaf35ce9434702d3e16bae6ec393e99df209b870a7e19eb0", size = 25359517, upload-time = "2026-05-21T22:09:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/99/b4/650896e8cff5a3289cee860c41fd9876da83ca628c5871f9a61d5fc75c72/uv-0.11.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a8db9b3314d900e7a240105afce43f806c9e04c59ea10a40bdbdca84c6d0c5", size = 24563421, upload-time = "2026-05-21T22:10:35.82Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7d/184711a8c02466e1486d57efdc9394ce09cbf43ee2c5794da70bd25db3fb/uv-0.11.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b10086165189c39c53142a0e2f34e0b8889ef681886f589ed17be45a1a774c7", size = 24676607, upload-time = "2026-05-21T22:10:39.784Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3f/5b338df6505f77f73c20eae38cb29f57d14dba56dac835386e3dc6e2a5d6/uv-0.11.16-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:cfe1f06fb8f135a735a961065d5ee90f99cccf41749fb1f964edb5b3c3dae19b", size = 23401615, upload-time = "2026-05-21T22:10:30.124Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f9/54bbcbc77443dc76468f09a49cc9f4f92ca49b4159a011c6010d223de4ea/uv-0.11.16-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:2454f80d8b548fb2e246151578809b14ad4395b3f357d738bae1af11918e91af", size = 24104468, upload-time = "2026-05-21T22:09:53.323Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0a/b5f105514fddea5110fe3947cd18a9f199ff93dbad78e5e5a08e1b5d0ea2/uv-0.11.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:4249d57a563165d368050680deeb722f9c0053a0dbf3244b11cca3e6d85a3c7d", size = 24164861, upload-time = "2026-05-21T22:10:09.458Z" }, + { url = "https://files.pythonhosted.org/packages/f6/01/15d4ca2be7257862b077a9077ac31ce81c419f35ef7994e76356a317716b/uv-0.11.16-py3-none-musllinux_1_1_i686.whl", hash = "sha256:374c30126483ce95675c5de49e54c2454ddedb01c17b8321417fe4eb9da83406", size = 23644919, upload-time = "2026-05-21T22:10:03.129Z" }, + { url = "https://files.pythonhosted.org/packages/49/bf/9de3e262e6ff93aec2e0a4c238857293fd2c616dd79f25bb440f126bf32c/uv-0.11.16-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:746edfc9d1d8cd03dd58739989f634d3580648048d09f81a9c68da74c4eb9d62", size = 24973746, upload-time = "2026-05-21T22:10:18.413Z" }, + { url = "https://files.pythonhosted.org/packages/f6/7d/f4126dce104f1b5d0b451ce3ca41c4db69b963c2e78c3465fcda6440de31/uv-0.11.16-py3-none-win32.whl", hash = "sha256:50299b20aab2d28c05ff27d781ce2af3f5af2102bc304dc07a4ad54b05e2af8a", size = 22400991, upload-time = "2026-05-21T22:10:27.119Z" }, + { url = "https://files.pythonhosted.org/packages/8f/38/99627cb995a03389b227ce4b12b08e770565d0aa7850cd0420973194a638/uv-0.11.16-py3-none-win_amd64.whl", hash = "sha256:e901aafa5007beffafe57bfa44e5e248d99fb5d97036a3718fd65cf9723c5cd3", size = 25067163, upload-time = "2026-05-21T22:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/b6/68/3ed1c0bdfb4bec501e5cde73419b4f39c8a125ef905a85fc0f239f19eb9b/uv-0.11.16-py3-none-win_arm64.whl", hash = "sha256:d777cb29661cdfa7f90dae77406c85fb5b729bf8bc13941dc237958a1ea1ba00", size = 23502015, upload-time = "2026-05-21T22:09:56.014Z" }, ] [[package]] name = "uvicorn" -version = "0.43.0" +version = "0.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/f2/368268300fb8af33743508d738ef7bb4d56afdb46c6d9c0fa3dd515df171/uvicorn-0.43.0.tar.gz", hash = "sha256:ab1652d2fb23abf124f36ccc399828558880def222c3cb3d98d24021520dc6e8", size = 85686, upload-time = "2026-04-03T18:37:48.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/df/0cf5b0c451602748fdc7a702d4667f6e209bf96aa6e3160d754234445f2a/uvicorn-0.43.0-py3-none-any.whl", hash = "sha256:46fac64f487fd968cd999e5e49efbbe64bd231b5bd8b4a0b482a23ebce499620", size = 68591, upload-time = "2026-04-03T18:37:47.64Z" }, + { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, ] [package.optional-dependencies] @@ -3925,18 +3173,6 @@ version = "0.22.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, - { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, - { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, - { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, - { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, - { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, - { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, - { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, - { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, @@ -3958,117 +3194,87 @@ wheels = [ ] [[package]] -name = "watchdog" -version = "6.0.0" +name = "vadersentiment" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/8c/4a48c10a50f750ae565e341e697d74a38075a3e43ff0df6f1ab72e186902/vaderSentiment-3.3.2.tar.gz", hash = "sha256:5d7c06e027fc8b99238edb0d53d970cf97066ef97654009890b83703849632f9", size = 2466783, upload-time = "2020-05-22T15:06:32.81Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, + { url = "https://files.pythonhosted.org/packages/76/fc/310e16254683c1ed35eeb97386986d6c00bc29df17ce280aed64d55537e9/vaderSentiment-3.3.2-py2.py3-none-any.whl", hash = "sha256:3bf1d243b98b1afad575b9f22bc2cb1e212b94ff89ca74f8a23a588d024ea311", size = 125950, upload-time = "2020-05-22T15:07:00.052Z" }, ] [[package]] name = "watchfiles" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, - { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, - { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, - { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, -] - -[[package]] -name = "wcwidth" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, ] [[package]] @@ -4086,24 +3292,6 @@ version = "16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, @@ -4131,141 +3319,92 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] [[package]] name = "yarl" -version = "1.23.0" +version = "1.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, - { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, - { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, - { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, - { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, - { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, - { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, - { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, - { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, - { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +] + +[[package]] +name = "youtube-transcript-api" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/43/4104185a2eaa839daa693b30e15c37e7e58795e8e09ec414f22b3db54bec/youtube_transcript_api-1.2.4.tar.gz", hash = "sha256:b72d0e96a335df599d67cee51d49e143cff4f45b84bcafc202ff51291603ddcd", size = 469839, upload-time = "2026-01-29T09:09:17.088Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/95/129ea37efd6cd6ed00f62baae6543345c677810b8a3bf0026756e1d3cf3c/youtube_transcript_api-1.2.4-py3-none-any.whl", hash = "sha256:03878759356da5caf5edac77431780b91448fb3d8c21d4496015bdc8a7bc43ff", size = 485227, upload-time = "2026-01-29T09:09:15.427Z" }, ] [[package]] name = "zipp" -version = "3.23.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ]