From fcd920f09d6071484bd2519270bdaa2c1559729a Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 24 Jun 2026 04:03:32 -0400 Subject: [PATCH] chore: remove bundled preprocessor surfaces --- .github/workflows/ci.yml | 6 +- README.md | 36 - docs/DesignPhilosophy.md | 2 +- docs/README.md | 6 - docs/architecture.md | 8 +- docs/llm-preprocessor.md | 115 - examples/README.md | 2 +- examples/integrations/README.md | 58 - examples/integrations/litellm/README.md | 210 -- examples/integrations/litellm/basic.py | 349 --- .../integrations/litellm/response_format.py | 214 -- .../integrations/litellm/with_preprocessor.py | 477 ---- examples/integrations/litellm_proxy/README.md | 123 - .../litellm_proxy/config.example.yaml | 19 - .../context_compiler_precall_hook.py | 135 - ...compiler_precall_hook_with_preprocessor.py | 280 -- .../ollama_structured_output/README.md | 58 - .../ollama_structured_output/example.py | 169 -- examples/integrations/openwebui/README.md | 151 -- .../integrations/openwebui/open_webui_pipe.py | 693 ----- .../open_webui_pipe_with_preprocessor.py | 984 ------- experimental/__init__.py | 1 - experimental/preprocessor/README.md | 146 - experimental/preprocessor/__init__.py | 28 - experimental/preprocessor/constants.py | 29 - .../preprocessor/heuristic_preprocessor.py | 276 -- .../preprocessor/output_validation.py | 297 -- experimental/preprocessor/prompt_utils.py | 57 - experimental/preprocessor/prompts/default.txt | 129 - experimental/preprocessor/prompts/llama.txt | 114 - host_support/__init__.py | 2 - host_support/confirmation.py | 109 - pyproject.toml | 12 +- src/context_compiler/observability.py | 199 -- src/context_compiler/repl.py | 56 +- tests/fixtures/README.md | 39 +- .../engine-regression/structured/README.md | 4 +- .../admin-alias-remove-policies-unknown.json | 8 - .../admin-alias-reset-policy-unknown.json | 8 - .../ambiguous-directive-adjacent.json | 8 - .../canonical-directive-bracket-wrapper.json | 8 - ...-directive-case-normalized-use-docker.json | 8 - ...anonical-directive-clear-state-period.json | 8 - .../canonical-directive-clear-state.json | 8 - .../canonical-directive-paren-wrapper.json | 8 - .../canonical-directive-prohibit-peanuts.json | 8 - ...l-directive-quoted-payload-use-docker.json | 8 - ...nonical-directive-reset-policies-bang.json | 8 - ...ctive-whitespace-collapsed-use-docker.json | 8 - .../fenced-code-block-directive-unknown.json | 8 - .../inline-prose-code-directive-unknown.json | 8 - .../list-prefix-directive-unknown.json | 8 - ...ed-replacement-instead-docker-unknown.json | 8 - .../meta-prefix-directive-unknown.json | 8 - ...ix-the-command-is-clear-state-unknown.json | 8 - .../mixed-directive-task-unknown.json | 8 - ...ent-clear-state-then-continue-unknown.json | 8 - .../preprocessor/mixed-intent-unknown.json | 8 - ...ocker-because-repo-has-docker-unknown.json | 8 - ...hange-premise-concise-replies-unknown.json | 8 - .../modal-please-clear-state-unknown.json | 8 - ...et-premise-to-concise-replies-unknown.json | 8 - ...-use-docker-actually-prohibit-unknown.json | 8 - .../multiline-multi-directive-unknown.json | 8 - .../natural-language-do-not-use-unknown.json | 8 - .../natural-language-dont-use-unknown.json | 8 - ...ge-premise-missing-to-keyword-unknown.json | 8 - ...iss-change-premise-missing-to-unknown.json | 8 - ...-miss-change-premise-to-empty-unknown.json | 8 - .../near-miss-prohibit-empty-unknown.json | 8 - ...near-miss-remove-policy-empty-unknown.json | 8 - .../near-miss-set-premise-empty-unknown.json | 8 - ...ss-set-premise-to-formal-tone-unknown.json | 8 - .../near-miss-set-premise-to-unknown.json | 8 - .../near-miss-use-empty-unknown.json | 8 - ...e-instead-of-missing-new-item-unknown.json | 8 - ...e-instead-of-missing-old-item-unknown.json | 8 - .../nested-wrapper-clear-state-unknown.json | 8 - .../preprocessor/ordinary-non-directive.json | 8 - ...-canonical-directive-prohibit-peanuts.json | 6 - .../parse-malformed-near-miss-rejected.json | 6 - .../parse-multiple-directives-rejected.json | 6 - .../parse-no-directive-sentinel.json | 6 - ...rse-source-input-fenced-code-rejected.json | 7 - ...urce-input-inline-prose-code-rejected.json | 7 - ...e-input-mixed-directive-task-rejected.json | 7 - ...parse-source-input-multiline-rejected.json | 7 - ...e-source-input-question-form-rejected.json | 7 - ...se-source-input-quoted-payload-locked.json | 7 - ...source-input-reported-speech-rejected.json | 7 - ...ource-input-safe-canonical-use-docker.json | 7 - ...nput-safe-canonicalization-use-docker.json | 7 - ...ructured-json-directive-self-accepted.json | 7 - ...parse-unknown-directive-like-rejected.json | 6 - .../fixtures/preprocessor/public-api-v1.json | 17 - .../question-can-you-use-docker-unknown.json | 8 - ...question-help-with-lunch-no-directive.json | 8 - .../question-use-docker-unknown.json | 8 - ...ted-exact-use-docker-backtick-unknown.json | 8 - ...uoted-exact-use-docker-single-unknown.json | 8 - .../quoted-exact-use-docker-unknown.json | 8 - .../preprocessor/quoted-reported-unknown.json | 8 - .../reported-speech-docs-say-unknown.json | 8 - .../sentence-adjacent-directive-unknown.json | 8 - .../unsupported-alias-unknown.json | 8 - .../validator-invalid-json-shape-unknown.json | 11 - ...validator-malformed-json-text-unknown.json | 9 - .../validator-malformed-sentinel-unknown.json | 9 - .../validator-malformed-text-unknown.json | 9 - ...tor-multi-candidate-directive-unknown.json | 9 - .../validator-sentinel-no-directive.json | 9 - ...tor-source-input-allow-safe-directive.json | 10 - ...ce-input-block-change-premise-rewrite.json | 10 - ...ource-input-block-set-premise-rewrite.json | 10 - .../validator-structured-json-directive.json | 9 - tests/test_example_integrations_imports.py | 124 - tests/test_host_confirmation.py | 308 --- tests/test_host_observability.py | 381 --- tests/test_litellm_checkpoint_integration.py | 776 ------ tests/test_litellm_integration_error_paths.py | 283 -- .../test_litellm_preprocessor_model_config.py | 232 -- tests/test_litellm_proxy_hooks.py | 317 --- tests/test_litellm_response_format_example.py | 112 - .../test_ollama_structured_output_example.py | 60 - tests/test_openwebui_pipe.py | 1394 ---------- tests/test_openwebui_preprocessor_pipe.py | 2390 ----------------- tests/test_precompiler_prompt_utils.py | 67 - .../test_preprocessor_api_contract_fixture.py | 29 - tests/test_preprocessor_conformance.py | 143 - tests/test_preprocessor_heuristic.py | 320 --- .../test_preprocessor_heuristic_properties.py | 161 -- tests/test_preprocessor_output_validation.py | 214 -- .../test_preprocessor_validator_properties.py | 185 -- tests/test_repl.py | 155 +- uv.lock | 876 +----- 135 files changed, 26 insertions(+), 14722 deletions(-) delete mode 100644 docs/llm-preprocessor.md delete mode 100644 examples/integrations/README.md delete mode 100644 examples/integrations/litellm/README.md delete mode 100644 examples/integrations/litellm/basic.py delete mode 100644 examples/integrations/litellm/response_format.py delete mode 100644 examples/integrations/litellm/with_preprocessor.py delete mode 100644 examples/integrations/litellm_proxy/README.md delete mode 100644 examples/integrations/litellm_proxy/config.example.yaml delete mode 100644 examples/integrations/litellm_proxy/context_compiler_precall_hook.py delete mode 100644 examples/integrations/litellm_proxy/context_compiler_precall_hook_with_preprocessor.py delete mode 100644 examples/integrations/ollama_structured_output/README.md delete mode 100644 examples/integrations/ollama_structured_output/example.py delete mode 100644 examples/integrations/openwebui/README.md delete mode 100644 examples/integrations/openwebui/open_webui_pipe.py delete mode 100644 examples/integrations/openwebui/open_webui_pipe_with_preprocessor.py delete mode 100644 experimental/__init__.py delete mode 100644 experimental/preprocessor/README.md delete mode 100644 experimental/preprocessor/__init__.py delete mode 100644 experimental/preprocessor/constants.py delete mode 100644 experimental/preprocessor/heuristic_preprocessor.py delete mode 100644 experimental/preprocessor/output_validation.py delete mode 100644 experimental/preprocessor/prompt_utils.py delete mode 100644 experimental/preprocessor/prompts/default.txt delete mode 100644 experimental/preprocessor/prompts/llama.txt delete mode 100644 host_support/confirmation.py delete mode 100644 src/context_compiler/observability.py delete mode 100644 tests/fixtures/preprocessor/admin-alias-remove-policies-unknown.json delete mode 100644 tests/fixtures/preprocessor/admin-alias-reset-policy-unknown.json delete mode 100644 tests/fixtures/preprocessor/ambiguous-directive-adjacent.json delete mode 100644 tests/fixtures/preprocessor/canonical-directive-bracket-wrapper.json delete mode 100644 tests/fixtures/preprocessor/canonical-directive-case-normalized-use-docker.json delete mode 100644 tests/fixtures/preprocessor/canonical-directive-clear-state-period.json delete mode 100644 tests/fixtures/preprocessor/canonical-directive-clear-state.json delete mode 100644 tests/fixtures/preprocessor/canonical-directive-paren-wrapper.json delete mode 100644 tests/fixtures/preprocessor/canonical-directive-prohibit-peanuts.json delete mode 100644 tests/fixtures/preprocessor/canonical-directive-quoted-payload-use-docker.json delete mode 100644 tests/fixtures/preprocessor/canonical-directive-reset-policies-bang.json delete mode 100644 tests/fixtures/preprocessor/canonical-directive-whitespace-collapsed-use-docker.json delete mode 100644 tests/fixtures/preprocessor/fenced-code-block-directive-unknown.json delete mode 100644 tests/fixtures/preprocessor/inline-prose-code-directive-unknown.json delete mode 100644 tests/fixtures/preprocessor/list-prefix-directive-unknown.json delete mode 100644 tests/fixtures/preprocessor/malformed-replacement-instead-docker-unknown.json delete mode 100644 tests/fixtures/preprocessor/meta-prefix-directive-unknown.json delete mode 100644 tests/fixtures/preprocessor/meta-prefix-the-command-is-clear-state-unknown.json delete mode 100644 tests/fixtures/preprocessor/mixed-directive-task-unknown.json delete mode 100644 tests/fixtures/preprocessor/mixed-intent-clear-state-then-continue-unknown.json delete mode 100644 tests/fixtures/preprocessor/mixed-intent-unknown.json delete mode 100644 tests/fixtures/preprocessor/mixed-intent-use-docker-because-repo-has-docker-unknown.json delete mode 100644 tests/fixtures/preprocessor/modal-please-change-premise-concise-replies-unknown.json delete mode 100644 tests/fixtures/preprocessor/modal-please-clear-state-unknown.json delete mode 100644 tests/fixtures/preprocessor/modal-please-set-premise-to-concise-replies-unknown.json delete mode 100644 tests/fixtures/preprocessor/multi-instruction-use-docker-actually-prohibit-unknown.json delete mode 100644 tests/fixtures/preprocessor/multiline-multi-directive-unknown.json delete mode 100644 tests/fixtures/preprocessor/natural-language-do-not-use-unknown.json delete mode 100644 tests/fixtures/preprocessor/natural-language-dont-use-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-change-premise-missing-to-keyword-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-change-premise-missing-to-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-change-premise-to-empty-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-prohibit-empty-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-remove-policy-empty-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-set-premise-empty-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-set-premise-to-formal-tone-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-set-premise-to-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-use-empty-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-use-instead-of-missing-new-item-unknown.json delete mode 100644 tests/fixtures/preprocessor/near-miss-use-instead-of-missing-old-item-unknown.json delete mode 100644 tests/fixtures/preprocessor/nested-wrapper-clear-state-unknown.json delete mode 100644 tests/fixtures/preprocessor/ordinary-non-directive.json delete mode 100644 tests/fixtures/preprocessor/parse-canonical-directive-prohibit-peanuts.json delete mode 100644 tests/fixtures/preprocessor/parse-malformed-near-miss-rejected.json delete mode 100644 tests/fixtures/preprocessor/parse-multiple-directives-rejected.json delete mode 100644 tests/fixtures/preprocessor/parse-no-directive-sentinel.json delete mode 100644 tests/fixtures/preprocessor/parse-source-input-fenced-code-rejected.json delete mode 100644 tests/fixtures/preprocessor/parse-source-input-inline-prose-code-rejected.json delete mode 100644 tests/fixtures/preprocessor/parse-source-input-mixed-directive-task-rejected.json delete mode 100644 tests/fixtures/preprocessor/parse-source-input-multiline-rejected.json delete mode 100644 tests/fixtures/preprocessor/parse-source-input-question-form-rejected.json delete mode 100644 tests/fixtures/preprocessor/parse-source-input-quoted-payload-locked.json delete mode 100644 tests/fixtures/preprocessor/parse-source-input-reported-speech-rejected.json delete mode 100644 tests/fixtures/preprocessor/parse-source-input-safe-canonical-use-docker.json delete mode 100644 tests/fixtures/preprocessor/parse-source-input-safe-canonicalization-use-docker.json delete mode 100644 tests/fixtures/preprocessor/parse-source-input-structured-json-directive-self-accepted.json delete mode 100644 tests/fixtures/preprocessor/parse-unknown-directive-like-rejected.json delete mode 100644 tests/fixtures/preprocessor/public-api-v1.json delete mode 100644 tests/fixtures/preprocessor/question-can-you-use-docker-unknown.json delete mode 100644 tests/fixtures/preprocessor/question-help-with-lunch-no-directive.json delete mode 100644 tests/fixtures/preprocessor/question-use-docker-unknown.json delete mode 100644 tests/fixtures/preprocessor/quoted-exact-use-docker-backtick-unknown.json delete mode 100644 tests/fixtures/preprocessor/quoted-exact-use-docker-single-unknown.json delete mode 100644 tests/fixtures/preprocessor/quoted-exact-use-docker-unknown.json delete mode 100644 tests/fixtures/preprocessor/quoted-reported-unknown.json delete mode 100644 tests/fixtures/preprocessor/reported-speech-docs-say-unknown.json delete mode 100644 tests/fixtures/preprocessor/sentence-adjacent-directive-unknown.json delete mode 100644 tests/fixtures/preprocessor/unsupported-alias-unknown.json delete mode 100644 tests/fixtures/preprocessor/validator-invalid-json-shape-unknown.json delete mode 100644 tests/fixtures/preprocessor/validator-malformed-json-text-unknown.json delete mode 100644 tests/fixtures/preprocessor/validator-malformed-sentinel-unknown.json delete mode 100644 tests/fixtures/preprocessor/validator-malformed-text-unknown.json delete mode 100644 tests/fixtures/preprocessor/validator-multi-candidate-directive-unknown.json delete mode 100644 tests/fixtures/preprocessor/validator-sentinel-no-directive.json delete mode 100644 tests/fixtures/preprocessor/validator-source-input-allow-safe-directive.json delete mode 100644 tests/fixtures/preprocessor/validator-source-input-block-change-premise-rewrite.json delete mode 100644 tests/fixtures/preprocessor/validator-source-input-block-set-premise-rewrite.json delete mode 100644 tests/fixtures/preprocessor/validator-structured-json-directive.json delete mode 100644 tests/test_example_integrations_imports.py delete mode 100644 tests/test_host_confirmation.py delete mode 100644 tests/test_host_observability.py delete mode 100644 tests/test_litellm_checkpoint_integration.py delete mode 100644 tests/test_litellm_integration_error_paths.py delete mode 100644 tests/test_litellm_preprocessor_model_config.py delete mode 100644 tests/test_litellm_proxy_hooks.py delete mode 100644 tests/test_litellm_response_format_example.py delete mode 100644 tests/test_ollama_structured_output_example.py delete mode 100644 tests/test_openwebui_pipe.py delete mode 100644 tests/test_openwebui_preprocessor_pipe.py delete mode 100644 tests/test_precompiler_prompt_utils.py delete mode 100644 tests/test_preprocessor_api_contract_fixture.py delete mode 100644 tests/test_preprocessor_conformance.py delete mode 100644 tests/test_preprocessor_heuristic.py delete mode 100644 tests/test_preprocessor_heuristic_properties.py delete mode 100644 tests/test_preprocessor_output_validation.py delete mode 100644 tests/test_preprocessor_validator_properties.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 349f69c..01e8cb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,11 +34,11 @@ jobs: - name: Ruff (format) run: uv run ruff format --check . - - name: Install mypy integration dependencies - run: uv pip install --python .venv/bin/python litellm pydantic + - name: Install mypy demo dependencies + run: uv pip install --python .venv/bin/python litellm - name: Mypy - run: uv run mypy src examples evals/swe-bench demos + run: uv run mypy src examples demos - name: Install Hypothesis for tests run: uv pip install --python .venv/bin/python hypothesis diff --git a/README.md b/README.md index 6d6e701..1bca40a 100644 --- a/README.md +++ b/README.md @@ -180,16 +180,10 @@ test host-side state handling. ```bash pip install context-compiler context-compiler -context-compiler --with-preprocessor ``` `context-compiler` launches the interactive REPL. -`--with-preprocessor` enables the experimental preprocessor before each input -(simple rule-based checks plus conservative validation). For near-miss inputs, -the preprocessor does not rewrite the text. It passes the input to the engine, -and the engine can return `clarify`. - Preload options keep saved rules separate from in-progress confirmation state: - `--initial-state-json` / `--initial-state-file` load saved state (via exported state JSON). @@ -215,9 +209,6 @@ context-compiler --json < input.txt `--json` enables machine-readable NDJSON output for non-interactive usage (one complete JSON object per processed input line). -You can combine `--with-preprocessor` with `--json` when you want the same -preprocessing path in non-interactive usage. - Preload options keep saved rules separate from in-progress confirmation state: - `--initial-state-json` / `--initial-state-file` load saved state (via exported state JSON). @@ -237,10 +228,6 @@ pip install context-compiler Packaging notes: - Base install includes core engine modules and `examples/` artifacts. - LLM demos require: `pip install "context-compiler[demos]"`. -- Optional preprocessor support: `pip install "context-compiler[experimental]"`. -- Integration-oriented dependency support: `pip install "context-compiler[integrations]"`. -- LiteLLM Proxy example dependency bundle: `pip install "context-compiler[litellm_proxy]"`. -- Host runtimes (for example, Open WebUI) are not installed by `integrations`. ### Development @@ -541,11 +528,6 @@ For full directive grammar and edge-case behavior, see [DirectiveGrammarSpec.md] - [examples](examples/) — minimal usage patterns and core integration primitives - [demos](demos/) — concrete scenarios showing how behavior differs with and without the compiler -- [integrations](examples/integrations/) — production-style host integrations (OpenWebUI, LiteLLM, etc.) - -Integration note: current OpenWebUI example pipes return deterministic local -acknowledgements for directive-only `update` decisions instead of forwarding -those turns to the downstream LLM. --- @@ -560,24 +542,6 @@ those turns to the downstream LLM. These invariants are verified through behavioral tests and Hypothesis-based property tests. -### Optional: LLM Preprocessor (Experimental) - -An optional host-side preprocessor can conservatively convert some natural-language instructions -into canonical directives before compilation. - -It is designed to be conservative and must be used with validation: - -- reject-first; directive-adjacent unsafe forms abstain instead of rewriting -- all outputs must be validated with `parse_preprocessor_output(...)` -- no directive grammar expansion -- raw outputs must not be passed directly to the compiler - -If `engine.has_pending_clarification()` is true, bypass preprocessing and pass raw input directly to `engine.step(...)`. -Boundary policy is false-negative-preferred: abstain rather than risk unsafe state mutation. - -See [LLM preprocessor](docs/llm-preprocessor.md) and -[`experimental/preprocessor/`](experimental/preprocessor/) for details. - ### Multiple engines - [Multiple engines](docs/multi-engine.md) diff --git a/docs/DesignPhilosophy.md b/docs/DesignPhilosophy.md index 2aaded7..2f3fb0b 100644 --- a/docs/DesignPhilosophy.md +++ b/docs/DesignPhilosophy.md @@ -29,7 +29,7 @@ Context Compiler applies explicit state management to the modern LLM context, wi - The LLM handles what it does well: language understanding, reasoning, generation, and ambiguity in user intent - The deterministic engine handles what probabilistic systems handle poorly: keep explicit state across turns, enforce constraints, and make corrections replace prior state instead of competing with it -The preprocessor layer bridges the two: it uses the LLM's language understanding to translate natural language directive intent into canonical form, which the deterministic engine can then process reliably. Fuzzy where it needs to be fuzzy, deterministic where determinism matters. +If a host adds acquisition-layer drafting, that belongs outside the core authority package. Core stays deterministic; host-owned drafting stays non-authoritative. This is not a workaround for LLM limitations. It is an appropriate allocation of responsibilities based on what each component is actually suited for. diff --git a/docs/README.md b/docs/README.md index 874f8b0..0019fc0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,12 +7,6 @@ - [Architecture boundaries](architecture.md) - [Directive Grammar (exact command forms the engine accepts)](DirectiveGrammarSpec.md) -## Integrations -- [Open WebUI integration](../examples/integrations/openwebui/README.md) - -## Preprocessor -- [LLM preprocessor](llm-preprocessor.md) - ## Evaluation & Evidence - [Demo results matrix](demos-results.md) - [Demo outputs / scored scenarios](../demos/README.md) diff --git a/docs/architecture.md b/docs/architecture.md index b810f64..a10132a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,7 +12,7 @@ Responsibilities: - draft candidate directives without becoming a second authority Examples: -- the optional heuristic/LLM preprocessor, including a future extracted directive drafter package +- external directive-drafter or host-owned drafting packages - host-side input shaping before `engine.step(...)` Out of scope: @@ -85,9 +85,9 @@ Out of scope: - current Context Compiler core behavior - built-in coordination behavior -The preprocessor belongs to the Acquisition Layer. It is optional, -conservative, and never the source of truth. Context Compiler core belongs to -the Authority Layer. Host applications own Application Layer behavior. +Acquisition-layer drafting belongs outside Context Compiler core. Context +Compiler core belongs to the Authority Layer. Host applications own +Application Layer behavior. Composition remains exploratory. It is a future possibility, not a planned 0.8 core change. diff --git a/docs/llm-preprocessor.md b/docs/llm-preprocessor.md deleted file mode 100644 index dbf891d..0000000 --- a/docs/llm-preprocessor.md +++ /dev/null @@ -1,115 +0,0 @@ -# LLM Preprocessor (Optional, Experimental) - -The experimental preprocessor is an optional app-side layer that can convert -natural-language messages into canonical Context Compiler directives before -compilation. - -The compiler keeps state rules fixed. The preprocessor does not replace core -parsing or state rules. - -Install path for integrations using this layer: -`pip install "context-compiler[experimental]"`. - -Integration runtimes must use installed-package imports/resources for this -layer. Do not rely on repo-relative preprocessor paths. - -## Architectural framing - -The preprocessor helps your app, but it does not own state changes. - -Model/tool-description translation can help with simple direct cases, but -integrations should not rely on model intent translation alone to decide when -state changes. - -In simpler hosts without an embedded model, this preprocessor provides a -conservative translation path. - -In model-assisted hosts, the app still validates outputs before applying them. - -Both paths send canonical directives to the same deterministic engine. The -engine controls state updates. - -In MCP/tool-calling environments, over-eager tool calling on conversational or -ambiguous input is a known failure mode. Conservative preprocessing and -validation help reduce unintended mutation. - -## Required flow - -Recommended flow: - -1. heuristic preprocessing -2. validate candidate output -3. LLM fallback preprocessing (only when needed) -4. validate candidate output -5. If a valid directive is produced, pass it to the compiler. - Otherwise pass the original input unchanged. - -All preprocessor outputs, including heuristic outputs, must be validated with -`parse_preprocessor_output(...)` before being applied. - -Raw heuristic/LLM outputs must not be passed directly to the compiler. -Raw model output must never directly change state. - -Pending clarification rule: - -- If the engine has pending clarification state, bypass preprocessing and pass - raw user input directly to `engine.step(...)`. -- This keeps confirmation flows correct: while confirmation is pending, only - confirmation-style input should resolve it. - -Host handling notes: - -- `passthrough`: no directive was applied; handle as ordinary user input. -- `clarify`: mutation is blocked; surface `prompt_to_user` and do not treat - state as updated. -- `update`: the app applied a validated directive; use updated state as the - source of truth. - -## Limits - -The preprocessor is best-effort and intentionally conservative. Ambiguous, -reported, quoted, or mixed-intent inputs may still require abstention or host -clarification behavior. - -Boundary policy (explicit): - -- Only process the full message, not pieces of it. -- At most one canonical directive may be emitted; otherwise abstain. -- Do not extract directives from surrounding prose, questions, or reporting. -- Do not split sentences or mine multi-line batches for commands. -- Do not extract from markdown/code blocks or quoted/reported text. -- Do not perform broad semantic rewrites. -- Preserve quoted payload tokens in canonical directives; do not silently strip - payload quotes (for example `use "docker"` remains quoted). -- Prefer false negatives over false positive state mutation. - -If you want natural-language proposals for state, handle that in an explicit -host flow. Do not treat implicit preprocessing as state mutation. - -## Future direction (planning note) - -This section is architectural direction, not committed implementation. - -Future preprocessing may evolve beyond direct natural-language to directive -conversion. - -- Policy preprocessing and premise-like facts have different risk profiles. -- Premise-like facts (for example, `I am vegetarian`) may be useful persistent - context, but should not be auto-persisted without confirmation. -- Likely direction: - - keep conservative directive preprocessing separate - - add a possible suggestion layer that is inspectable, non-mutating, and - previewable - - require explicit host/user confirmation before any mutation - -This aligns with the post-0.7 / 0.8 direction: inspectable, previewable, -non-mutating suggestions that users review first and confirm before saving, -while the engine keeps state rules explicit and repeatable. - -## Status - -This preprocessor surface is experimental and may evolve independently of the -core engine. - -For concrete module usage, prompt guidance, and integration details, see: -[`experimental/preprocessor/README.md`](../experimental/preprocessor/README.md). diff --git a/examples/README.md b/examples/README.md index a604eac..32746d0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # Examples -This directory contains small integration examples showing typical app-side usage of Context Compiler. +This directory contains small authority-layer examples showing typical app-side usage of Context Compiler. These files are included in the base package install (`pip install context-compiler`). Non-integration example files in this directory are standalone scripts and can be run directly. diff --git a/examples/integrations/README.md b/examples/integrations/README.md deleted file mode 100644 index b1834e8..0000000 --- a/examples/integrations/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Integrations - -These examples show how to use Context Compiler inside external app runtimes. - -## LiteLLM (SDK) - -Minimal examples showing how to run Context Compiler before sending a request to the LLM with LiteLLM. - -Files: -- Examples (basic + preprocessor): [litellm/README.md](litellm/README.md) - -### Requirements - -```shell -pip install "context-compiler[integrations]" -export OPENAI_API_KEY=... -export PROVIDER=openai -``` - -Checkpoint continuation in these integration examples requires `context-compiler>=0.7.0`. - -### Run - -See the LiteLLM examples README for setup and usage: -[litellm/README.md](litellm/README.md) - -### Behavior - -- Context Compiler runs before each LLM call. -- If result is `clarify`, show the question and do not call the LLM. -- If result is `passthrough`, send normal user input. -- If result is `update`, use updated state and call the model with saved state in the prompt. -- `response_format.py` shows a different boundary: saved compiler state changes the LiteLLM request shape instead of being reinjected into prompt text. - -## LiteLLM Proxy - -Gateway-level integration using a LiteLLM pre-call hook. - -See: [LiteLLM Proxy README](litellm_proxy/README.md) - -## Ollama structured output (host-side schema selection) - -Minimal example showing host-side JSON Schema selection for Ollama `format` based on -Context Compiler policy state. - -See: [ollama_structured_output/README.md](ollama_structured_output/README.md) - -## Open WebUI Pipe Function - -Tested target: Open WebUI `v0.8.12`. - -Open WebUI is a separate runtime and must be installed/configured separately. - -Files: -- Basic example: [open_webui_pipe.py](openwebui/open_webui_pipe.py) -- With preprocessor: [open_webui_pipe_with_preprocessor.py](openwebui/open_webui_pipe_with_preprocessor.py) - -Details: [Open WebUI Pipe Integration README](openwebui/README.md) diff --git a/examples/integrations/litellm/README.md b/examples/integrations/litellm/README.md deleted file mode 100644 index c2ad3dd..0000000 --- a/examples/integrations/litellm/README.md +++ /dev/null @@ -1,210 +0,0 @@ -# LiteLLM examples - -This directory contains three small Context Compiler + LiteLLM integration examples: - -- `basic.py`: compiler-only flow (no preprocessor) -- `response_format.py`: host-side LiteLLM `response_format` selection from saved compiler state -- `with_preprocessor.py`: heuristic-first preprocessor with optional LLM fallback before `engine.step(...)` - -## Requirements - -```shell -pip install "context-compiler[integrations]" -export OPENAI_API_KEY=... -``` - -Checkpoint continuation in these examples requires `context-compiler>=0.7.0`. - -For `with_preprocessor.py`: - -```shell -pip install "context-compiler[experimental]" -``` - -## Quickstart (copy/paste) - -```shell -pip install "context-compiler[integrations]" -export OPENAI_API_KEY=... -export MODEL=openai/gpt-4o-mini -python - <<'PY' -from context_compiler import create_engine -from examples.integrations.litellm.basic import handle_turn -engine = create_engine() -print(handle_turn("set premise concise replies", engine)) -PY -``` - -For preprocessor behavior: - -```shell -pip install "context-compiler[experimental]" -export OPENAI_API_KEY=... -export MODEL=openai/gpt-4o-mini -python - <<'PY' -from context_compiler import create_engine -from examples.integrations.litellm.with_preprocessor import handle_turn -engine = create_engine() -print(handle_turn("set premise to concise replies", engine)) -PY -``` - -This near-miss input should return `clarify` instead of being rewritten. - -For host-side response shape selection: - -```shell -pip install "context-compiler[integrations]" -export OPENAI_API_KEY=... -export MODEL=openai/gpt-4o-mini -python - <<'PY' -from context_compiler import create_engine -from examples.integrations.litellm.response_format import plan_turn -engine = create_engine() -engine.step("use compact_summary") -print(plan_turn("Summarize the release notes.", engine)) -PY -``` - -## Environment configuration - -Required (normal `openai` mode): - -```shell -export OPENAI_API_KEY=... -``` - -Optional: - -```shell -export PROVIDER=openai -export MODEL=openai/gpt-4o-mini -export PREPROCESSOR_MODEL=openai/gpt-4o-mini -export OPENAI_BASE_URL=... -export PREPROCESSOR_PROMPT_PROFILE=default -``` - -Provider mode contract (`PROVIDER`) is strict: - -- `openai` -- `ollama` -- `openai_compatible` - -Unknown values hard fail with a validation error. - -Resolution precedence: - -1. `OPENAI_BASE_URL` override -2. `PROVIDER` -3. default (`openai`) - -Operational behavior by mode: - -- `openai` - - default `base_url`: `https://api.openai.com/v1` - - requires `OPENAI_API_KEY` -- `ollama` - - default `base_url`: `http://localhost:11434` - - API key optional -- `openai_compatible` - - requires `OPENAI_BASE_URL` when explicitly selected with `PROVIDER` - - API key requirement depends on endpoint - -Startup emits one concise config line showing resolved `mode`, `base_url`, `model`, -and resolution `source` (`default`, `PROVIDER`, or `OPENAI_BASE_URL override`). - -`MODEL` and `PREPROCESSOR_MODEL` use LiteLLM format: `/`. -`PREPROCESSOR_MODEL` is optional and defaults to `MODEL`. - -For heuristic-first usage, keep `PREPROCESSOR_PROMPT_PROFILE=default`. -Use `llama` only for LLM-only preprocessing with Llama-family models. - -## Usage pattern - -You can import these files as integration references in host applications. - -- Import `handle_turn(...)` from either `basic.py` or `with_preprocessor.py`. -- Create and retain an engine instance in host/session state. -- Pass each user input through `handle_turn(user_input, engine)`. -- Optional checkpointing: pass `session_key=...`. - The example restores checkpoint data before the first `engine.step(...)` and - saves checkpoint data after `update`/`clarify`. -- In this example, checkpoint/session storage is in-memory only. - State lasts only for the current process. To survive restarts, store - checkpoints in external storage (DB/Redis/etc.). -- Display the returned assistant text. - -Note: In these LiteLLM examples, `update` is rendered locally and does not call -the downstream LLM. This makes state changes explicit. Production apps may -choose different rendering behavior. - -## Response format example boundary - -`response_format.py` shows a different integration boundary from prompt reinjection: - -- Context Compiler owns authoritative state. -- The host reads saved policy state and selects a LiteLLM `response_format` or omits it. -- LiteLLM owns model invocation and provider behavior. -- Context Compiler does not call LiteLLM on its own. -- Context Compiler does not validate model output. -- Context Compiler does not generate schemas dynamically. -- This is application-layer use of authoritative state, not compiler semantics. - -## Troubleshooting - -- `litellm is required`: install `context-compiler[integrations]` (or `context-compiler[experimental]` for preprocessor). -- `OPENAI_API_KEY is required in openai mode`: export a key or use `ollama` / explicit endpoint override. -- `Invalid PROVIDER value ...`: set `PROVIDER` to one of `openai`, `ollama`, `openai_compatible`. -- `OPENAI_BASE_URL is required when PROVIDER=openai_compatible`: set an explicit endpoint URL. -- model/provider errors (`Model not found`, provider auth errors): confirm `MODEL` uses LiteLLM format and provider credentials are valid. - -## Basic vs preprocessor behavior - -- Basic: passes raw user input to `engine.step(...)`. -- With preprocessor: runs heuristic preprocessor first. - - If heuristic returns a directive, that directive is passed to `engine.step(...)`. - - If heuristic does not produce a directive (`no_directive` or `unknown`), LLM fallback prompt conversion runs. - - If fallback yields nothing usable or errors, behavior safely remains equivalent to basic. - - If `engine.has_pending_clarification()` is true, bypass preprocessing and pass raw input directly to `engine.step(...)`. - - Behavior is reject-first and does not broaden the directive grammar. - -Decision flow in both examples: -- `passthrough`: call the model with normal input. -- `clarify`: show `prompt_to_user`; do not treat state as changed. -- `update`: state changed; use updated state for the next model call. - -Decision flow in `response_format.py`: -- `passthrough`: let the host decide whether to send `response_format`. -- `clarify`: show `prompt_to_user`; do not call LiteLLM. -- `update`: state changed; the next host request may use a different `response_format`. - -## Example checks - -- Near-miss passthrough (`with_preprocessor.py`): - - `set premise to concise replies` is not rewritten by the preprocessor and is passed through unchanged. - - Engine returns clarify (`Did you mean 'set premise concise replies'?`). -- Lifecycle enforcement (both): - - `change premise to formal tone` with no premise -> clarify (`set premise ...` first). -- Conflict behavior (both): - - `use docker` then `prohibit docker` -> conflict clarify. -- Replacement precondition (both): - - `use podman instead of docker` without prior `use docker` -> replacement clarify. -- Directive-adjacent abstain (`with_preprocessor.py`): - - `change premise concise replies` is classified as `unknown`, not rewritten, and handled by engine clarify. -- Host-side request shaping (`response_format.py`): - - `use compact_summary` -> host selects compact-summary `response_format`. - - `use action_plan` -> host selects action-plan `response_format`. - - `prohibit compact_summary` -> host omits that `response_format`. - -## Optional smoke run for `response_format.py` - -```shell -export RUN_LITELLM_SMOKE=1 -export PROVIDER=ollama -export MODEL=ollama/qwen2.5:1.5b-instruct -uv run python examples/integrations/litellm/response_format.py -``` - -For local Ollama smoke runs in this repo, `PROVIDER=ollama` is required. A -`MODEL=ollama/...` value by itself still follows the default OpenAI provider -path. diff --git a/examples/integrations/litellm/basic.py b/examples/integrations/litellm/basic.py deleted file mode 100644 index 49da0f9..0000000 --- a/examples/integrations/litellm/basic.py +++ /dev/null @@ -1,349 +0,0 @@ -"""Minimal LiteLLM integration with Context Compiler. - -Flow: -1. Call engine.step(user_input) -2. clarify -> return prompt_to_user (no model call) -3. update -> return deterministic acknowledgment text (no model call) -4. passthrough -> call LiteLLM with compiled state + user input - -Intended host usage: -- collect user input -- call handle_turn(user_input, engine) -- display returned assistant text -""" - -import logging -import re -from collections.abc import Callable, Mapping, Sequence -from importlib import import_module -from typing import TypedDict, cast - -from context_compiler import ( - DECISION_CLARIFY, - DECISION_PASSTHROUGH, - DECISION_UPDATE, - POLICY_PROHIBIT, - POLICY_USE, - State, - get_clarify_prompt, - get_decision_state, - get_policy_items, - get_premise_value, - is_clarify, - is_passthrough, - is_update, -) -from context_compiler.engine import Engine -from context_compiler.observability import build_trace - -try: - from host_support import is_confirmation_text -except ImportError: - import host_support.confirmation as _confirmation - - is_confirmation_text = _confirmation.is_confirmation_text - -try: - from host_support.confirmation import summarize_confirmation_update_from_checkpoint -except ImportError: - from host_support.confirmation import ( - summarize_confirmation_update as _summarize_confirmation_update_from_pending, - ) - - def summarize_confirmation_update_from_checkpoint(user_input: str, checkpoint: object) -> str: - pending = checkpoint.get("pending") if isinstance(checkpoint, dict) else None - return _summarize_confirmation_update_from_pending(user_input, pending) - - -try: - from host_support import print_startup_config, resolve_provider_config -except ImportError: - from host_support.provider_mode import print_startup_config, resolve_provider_config - -logger = logging.getLogger(__name__) -# Example-only in-memory checkpoint store. -# This keeps continuation state only for the current process lifetime. -# Real deployments should persist checkpoints externally (DB/Redis/etc.), -# or restart continuity for pending flows will be lost. -_CHECKPOINTS_BY_SESSION_KEY: dict[str, str] = {} -_RESTORED_ENGINE_BY_SESSION_KEY: dict[str, int] = {} -_NEGATIVE_CONFIRMATION_TOKENS = {"no", "nope", "no thanks"} -_TRAILING_CONFIRM_PUNCT_RE = re.compile(r"[.,!?]+$") -SHOW_CONTEXT_COMPILER_TRACE = False - - -class _LiteLLMCallKwargs(TypedDict, total=False): - model: str - messages: list[dict[str, str]] - api_key: str - temperature: float - api_base: str - - -def _extract_response_content(response: object) -> str | None: - if isinstance(response, Mapping): - choices = response.get("choices") - if isinstance(choices, Sequence) and choices: - first = choices[0] - if isinstance(first, Mapping): - message = first.get("message") - if isinstance(message, Mapping): - content = message.get("content") - if isinstance(content, str): - return content - - choices_attr = getattr(response, "choices", None) - if isinstance(choices_attr, Sequence) and choices_attr: - first = choices_attr[0] - message_attr = getattr(first, "message", None) - content_attr = getattr(message_attr, "content", None) - if isinstance(content_attr, str): - return content_attr - - return None - - -def _render_compiled_state_contract(compiled_state: State) -> str: - premise = get_premise_value(compiled_state) - use_items = sorted(get_policy_items(compiled_state, POLICY_USE)) - prohibit_items = sorted(get_policy_items(compiled_state, POLICY_PROHIBIT)) - - lines: list[str] = ["The following constraints are authoritative."] - if premise: - lines.append(f"Current premise: {premise}.") - if use_items: - lines.append("Items marked use: " + ", ".join(use_items) + ".") - if prohibit_items: - lines.append("Items marked prohibit: " + ", ".join(prohibit_items) + ".") - lines.append("If user text conflicts with constraints, follow constraints exactly.") - - return "Host policy contract:\n" + "\n".join(f"- {line}" for line in lines) - - -def _build_messages(user_input: str, compiled_state: State) -> list[dict[str, str]]: - return [ - { - "role": "system", - "content": "You are a helpful assistant.\n" - + _render_compiled_state_contract(compiled_state), - }, - {"role": "user", "content": user_input}, - ] - - -def _call_litellm(messages: list[dict[str, str]]) -> str: - try: - litellm_module = import_module("litellm") - except ModuleNotFoundError as exc: - raise RuntimeError("litellm is required. Install with: pip install litellm") from exc - completion_fn = cast(Callable[..., object], litellm_module.completion) - - config = resolve_provider_config(default_model="openai/gpt-4o-mini") - print_startup_config(config, logger=logger) - - kwargs: _LiteLLMCallKwargs = { - "model": config.model, - "messages": messages, - "temperature": 0, - "api_base": config.base_url, - } - if config.api_key: - kwargs["api_key"] = config.api_key - - response = completion_fn(**kwargs) - content = _extract_response_content(response) - if content is None: - raise RuntimeError("LiteLLM response missing choices[0].message.content") - return content - - -def _restore_session_checkpoint_if_needed(engine: Engine, session_key: str | None) -> None: - if session_key is None: - return - engine_id = id(engine) - if _RESTORED_ENGINE_BY_SESSION_KEY.get(session_key) == engine_id: - return - - checkpoint = _CHECKPOINTS_BY_SESSION_KEY.get(session_key) - if checkpoint is not None: - engine.import_checkpoint_json(checkpoint) - _RESTORED_ENGINE_BY_SESSION_KEY[session_key] = engine_id - - -def _persist_session_checkpoint_if_needed( - engine: Engine, kind: str, session_key: str | None -) -> None: - if session_key is None: - return - if kind not in {DECISION_UPDATE, DECISION_CLARIFY}: - return - _CHECKPOINTS_BY_SESSION_KEY[session_key] = engine.export_checkpoint_json() - - -def _normalize_confirmation_for_summary(value: str) -> str: - normalized = value.strip().lower() - normalized = re.sub(r"\s+", " ", normalized) - normalized = _TRAILING_CONFIRM_PUNCT_RE.sub("", normalized).strip() - return re.sub(r"\s+", " ", normalized) - - -def _render_item_label(value: str) -> str: - return re.sub(r"\s+", " ", value).strip().lower() - - -def _near_miss_directive_clarify(value: str) -> str | None: - normalized = re.sub(r"\s+", " ", value.strip()) - lower = normalized.lower() - - if lower in {"reset premise", "reset premises", "clear premises"}: - return "Unknown directive.\nUse 'clear premise' or 'reset policies'." - if lower.startswith("set premise to "): - return "Invalid premise syntax.\nUse 'set premise '." - if lower.startswith("change premise ") and not lower.startswith("change premise to "): - return "Invalid premise syntax.\nUse 'change premise to '." - return None - - -def _summarize_confirmation_update(user_input: str, checkpoint: object) -> str: - summarize_fn: Callable[[str, object], str] = summarize_confirmation_update_from_checkpoint - return summarize_fn(user_input, checkpoint) - - -def _summarize_update_from_input(user_input: str) -> str: - normalized = re.sub(r"\s+", " ", user_input.strip()) - lower = normalized.lower() - - if lower == "clear state": - return "State cleared." - if lower == "clear premise": - return "Premise cleared." - if lower == "reset policies": - return "Policies reset." - - replacement_match = re.match( - r"^use\s+(.+?)\s+instead\s+of\s+(.+)$", normalized, flags=re.IGNORECASE - ) - if replacement_match is not None: - item = _render_item_label(replacement_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Use {item}." - - use_match = re.match(r"^use\s+(.+)$", normalized, flags=re.IGNORECASE) - if use_match is not None: - item = _render_item_label(use_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Use {item}." - - prohibit_match = re.match(r"^prohibit\s+(.+)$", normalized, flags=re.IGNORECASE) - if prohibit_match is not None: - item = _render_item_label(prohibit_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Prohibit {item}." - - remove_policy_match = re.match(r"^remove\s+policy\s+(.+)$", normalized, flags=re.IGNORECASE) - if remove_policy_match is not None: - item = _render_item_label(remove_policy_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Removed policy {item}." - - return "State updated." - - -def _append_trace( - response_text: str, - *, - original_input: str, - compiler_input: str, - decision: object, - state_before: object, - state_after: object, - llm_called: bool, -) -> str: - if not SHOW_CONTEXT_COMPILER_TRACE: - return response_text - trace_text = build_trace( - original_input=original_input, - compiler_input=compiler_input, - decision=decision, - state_before=state_before, - state_after=state_after, - llm_called=llm_called, - ) - return f"{response_text}\n\n{trace_text}" - - -def handle_turn(user_input: str, engine: Engine, *, session_key: str | None = None) -> str: - _restore_session_checkpoint_if_needed(engine, session_key) - state_before = engine.state - has_pending_before = engine.has_pending_clarification() - checkpoint_before = engine.export_checkpoint() if has_pending_before else None - logger.debug("litellm_basic: engine_input=%s", f"user_input len={len(user_input)}") - decision = engine.step(user_input) - if is_clarify(decision): - kind = DECISION_CLARIFY - elif is_update(decision): - kind = DECISION_UPDATE - else: - kind = DECISION_PASSTHROUGH - logger.debug("litellm_basic: decision=%s", kind) - near_miss_prompt = _near_miss_directive_clarify(user_input) - - if is_clarify(decision): - _persist_session_checkpoint_if_needed(engine, kind, session_key) - response_text = near_miss_prompt or get_clarify_prompt(decision) or "" - return _append_trace( - response_text, - original_input=user_input, - compiler_input=user_input, - decision=decision, - state_before=state_before, - state_after=engine.state, - llm_called=False, - ) - if near_miss_prompt is not None and is_passthrough(decision): - return _append_trace( - near_miss_prompt, - original_input=user_input, - compiler_input=user_input, - decision={"kind": DECISION_CLARIFY, "prompt_to_user": near_miss_prompt}, - state_before=state_before, - state_after=engine.state, - llm_called=False, - ) - _persist_session_checkpoint_if_needed(engine, kind, session_key) - if is_update(decision) and is_confirmation_text(user_input) and checkpoint_before is not None: - response_text = _summarize_confirmation_update(user_input, checkpoint_before) - return _append_trace( - response_text, - original_input=user_input, - compiler_input=user_input, - decision=decision, - state_before=state_before, - state_after=engine.state, - llm_called=False, - ) - if is_update(decision): - response_text = _summarize_update_from_input(user_input) - return _append_trace( - response_text, - original_input=user_input, - compiler_input=user_input, - decision=decision, - state_before=state_before, - state_after=engine.state, - llm_called=False, - ) - - decision_state = get_decision_state(decision) - compiled_state = decision_state if decision_state is not None else engine.state - messages = _build_messages(user_input, compiled_state) - response_text = _call_litellm(messages) - return _append_trace( - response_text, - original_input=user_input, - compiler_input=user_input, - decision=decision, - state_before=state_before, - state_after=compiled_state, - llm_called=True, - ) diff --git a/examples/integrations/litellm/response_format.py b/examples/integrations/litellm/response_format.py deleted file mode 100644 index f70ce59..0000000 --- a/examples/integrations/litellm/response_format.py +++ /dev/null @@ -1,214 +0,0 @@ -"""Minimal LiteLLM response_format selection from authoritative state. - -Flow: -Context Compiler state -> host response_format decision -> LiteLLM model call. - -This example keeps model execution optional so tests can validate behavior -without a live provider. -""" - -import os -from collections.abc import Callable, Mapping -from importlib import import_module -from typing import Any, TypedDict, cast - -from context_compiler import ( - POLICY_PROHIBIT, - POLICY_USE, - State, - create_engine, - get_clarify_prompt, - get_decision_state, - get_policy_items, - is_clarify, -) -from context_compiler.engine import Engine - -try: - from host_support import print_startup_config, resolve_provider_config -except ImportError: - from host_support.provider_mode import print_startup_config, resolve_provider_config - -COMPACT_SUMMARY_RESPONSE_FORMAT: dict[str, Any] = { - "type": "json_schema", - "json_schema": { - "name": "compact_summary", - "schema": { - "type": "object", - "properties": { - "summary": { - "type": "string", - "description": "A compact summary of the answer.", - } - }, - "required": ["summary"], - "additionalProperties": False, - }, - }, -} - -ACTION_PLAN_RESPONSE_FORMAT: dict[str, Any] = { - "type": "json_schema", - "json_schema": { - "name": "action_plan", - "schema": { - "type": "object", - "properties": { - "steps": { - "type": "array", - "items": {"type": "string"}, - "description": "Ordered next steps for the user.", - } - }, - "required": ["steps"], - "additionalProperties": False, - }, - }, -} - -_RESPONSE_FORMAT_BY_ITEM: dict[str, dict[str, Any]] = { - "compact_summary": COMPACT_SUMMARY_RESPONSE_FORMAT, - "action_plan": ACTION_PLAN_RESPONSE_FORMAT, -} - - -class TurnPlan(TypedDict): - decision_kind: str - clarify_prompt: str | None - selected_response_format_item: str | None - response_format: dict[str, Any] | None - - -class _LiteLLMCallKwargs(TypedDict, total=False): - model: str - messages: list[dict[str, str]] - temperature: float - api_base: str - api_key: str - response_format: dict[str, Any] - - -def select_litellm_response_format(state: State) -> tuple[str | None, dict[str, Any] | None]: - """Return (policy_item, response_format) or (None, None) when no safe match exists.""" - - use_items = set(get_policy_items(state, POLICY_USE)) - prohibit_items = set(get_policy_items(state, POLICY_PROHIBIT)) - - for item, response_format in _RESPONSE_FORMAT_BY_ITEM.items(): - if item in use_items and item not in prohibit_items: - return item, response_format - - return None, None - - -def plan_turn(user_input: str, engine: Engine) -> TurnPlan: - """Run compiler step and decide whether to request LiteLLM structured output.""" - - decision = engine.step(user_input) - if is_clarify(decision): - return { - "decision_kind": "clarify", - "clarify_prompt": get_clarify_prompt(decision), - "selected_response_format_item": None, - "response_format": None, - } - - decision_state = get_decision_state(decision) - compiled_state = decision_state if decision_state is not None else engine.state - selected_item, response_format = select_litellm_response_format(compiled_state) - - return { - "decision_kind": str(decision["kind"]), - "clarify_prompt": None, - "selected_response_format_item": selected_item, - "response_format": response_format, - } - - -def _get_litellm_completion() -> Callable[..., object]: - litellm_module = import_module("litellm") - return cast(Callable[..., object], litellm_module.completion) - - -def _extract_response_content(response: object) -> str | None: - if isinstance(response, Mapping): - choices = response.get("choices") - if isinstance(choices, list) and choices: - first = choices[0] - if isinstance(first, Mapping): - message = first.get("message") - if isinstance(message, Mapping): - content = message.get("content") - if isinstance(content, str): - return content - - choices_attr = getattr(response, "choices", None) - if isinstance(choices_attr, list) and choices_attr: - first = choices_attr[0] - message_attr = getattr(first, "message", None) - content_attr = getattr(message_attr, "content", None) - if isinstance(content_attr, str): - return content_attr - - return None - - -def optional_litellm_call( - *, - user_input: str, - response_format: Mapping[str, Any] | None, -) -> str: - """Optional smoke call to LiteLLM. - - If `response_format` is provided, it is passed through unchanged. - """ - - try: - completion = _get_litellm_completion() - except ModuleNotFoundError as exc: - raise RuntimeError("litellm is required. Install with: pip install litellm") from exc - - config = resolve_provider_config(default_model="openai/gpt-4o-mini") - print_startup_config(config) - - kwargs: _LiteLLMCallKwargs = { - "model": config.model, - "messages": [{"role": "user", "content": user_input}], - "temperature": 0, - "api_base": config.base_url, - } - if config.api_key: - kwargs["api_key"] = config.api_key - if response_format is not None: - kwargs["response_format"] = dict(response_format) - - response = completion(**kwargs) - content = _extract_response_content(response) - if content is None: - raise RuntimeError("LiteLLM response missing choices[0].message.content") - return content - - -def main() -> None: - engine = create_engine() - - # Demonstration setup. - engine.step("use compact_summary") - engine.step("prohibit action_plan") - - plan = plan_turn("Summarize what changed in this project.", engine) - print("decision_kind:", plan["decision_kind"]) - print("selected_response_format_item:", plan["selected_response_format_item"]) - print("response_format_selected:", plan["response_format"] is not None) - - # Optional model execution path; disabled by default. - if os.getenv("RUN_LITELLM_SMOKE") == "1": - response = optional_litellm_call( - user_input="Summarize what changed in this project.", - response_format=plan["response_format"], - ) - print("litellm_response:", response) - - -if __name__ == "__main__": - main() diff --git a/examples/integrations/litellm/with_preprocessor.py b/examples/integrations/litellm/with_preprocessor.py deleted file mode 100644 index 01c449b..0000000 --- a/examples/integrations/litellm/with_preprocessor.py +++ /dev/null @@ -1,477 +0,0 @@ -"""LiteLLM integration with optional preprocessor before Context Compiler. - -Flow: -1. Extract user input -2. Run heuristic preprocessor -3. If no directive, run LLM fallback preprocessor using prompt files -4. Pass directive (or original input) to engine.step(...) -5. clarify -> return prompt_to_user (no model call) -6. update -> return deterministic acknowledgment text (no model call) -7. passthrough -> call LiteLLM with compiled state + user input - -Intended host usage: -- collect user input -- call handle_turn(user_input, engine) -- display returned assistant text -""" - -import logging -import os -import re -from collections.abc import Callable, Mapping, Sequence -from importlib import import_module -from importlib.resources import as_file, files -from importlib.resources.abc import Traversable -from typing import TypedDict, cast - -from context_compiler import ( - DECISION_CLARIFY, - DECISION_PASSTHROUGH, - DECISION_UPDATE, - POLICY_PROHIBIT, - POLICY_USE, - State, - get_clarify_prompt, - get_decision_state, - get_policy_items, - get_premise_value, - is_clarify, - is_passthrough, - is_update, -) -from context_compiler.engine import Engine -from context_compiler.observability import build_trace -from experimental.preprocessor import ( - PREPROCESS_OUTCOME_DIRECTIVE, - parse_preprocessor_output, - preprocess_heuristic, - render_prompt, -) - -try: - from host_support import is_confirmation_text -except ImportError: - import host_support.confirmation as _confirmation - - is_confirmation_text = _confirmation.is_confirmation_text - -try: - from host_support.confirmation import summarize_confirmation_update_from_checkpoint -except ImportError: - from host_support.confirmation import ( - summarize_confirmation_update as _summarize_confirmation_update_from_pending, - ) - - def summarize_confirmation_update_from_checkpoint(user_input: str, checkpoint: object) -> str: - pending = checkpoint.get("pending") if isinstance(checkpoint, dict) else None - return _summarize_confirmation_update_from_pending(user_input, pending) - - -try: - from host_support import print_startup_config, resolve_provider_config -except ImportError: - from host_support.provider_mode import print_startup_config, resolve_provider_config - -logger = logging.getLogger(__name__) - -_PROMPTS_DIR = files("experimental.preprocessor").joinpath("prompts") -# Example-only in-memory checkpoint store. -# This keeps continuation state only for the current process lifetime. -# Real deployments should persist checkpoints externally (DB/Redis/etc.), -# or restart continuity for pending flows will be lost. -_CHECKPOINTS_BY_SESSION_KEY: dict[str, str] = {} -_RESTORED_ENGINE_BY_SESSION_KEY: dict[str, int] = {} -_NEGATIVE_CONFIRMATION_TOKENS = {"no", "nope", "no thanks"} -_TRAILING_CONFIRM_PUNCT_RE = re.compile(r"[.,!?]+$") -SHOW_CONTEXT_COMPILER_TRACE = False - - -def _is_directive_shaped_input(message: str) -> bool: - normalized = re.sub(r"\s+", " ", message.strip()).lower() - return ( - normalized.startswith("use") - or normalized.startswith("prohibit") - or normalized.startswith("remove policy") - or normalized.startswith("set premise") - or normalized.startswith("change premise") - or normalized.startswith("clear") - or normalized.startswith("reset") - ) - - -class _LiteLLMCallKwargs(TypedDict, total=False): - model: str - messages: list[dict[str, str]] - api_key: str - temperature: float - api_base: str - - -def _extract_response_content(response: object) -> str | None: - if isinstance(response, Mapping): - choices = response.get("choices") - if isinstance(choices, Sequence) and choices: - first = choices[0] - if isinstance(first, Mapping): - message = first.get("message") - if isinstance(message, Mapping): - content = message.get("content") - if isinstance(content, str): - return content - - choices_attr = getattr(response, "choices", None) - if isinstance(choices_attr, Sequence) and choices_attr: - first = choices_attr[0] - message_attr = getattr(first, "message", None) - content_attr = getattr(message_attr, "content", None) - if isinstance(content_attr, str): - return content_attr - - return None - - -def _get_litellm_completion() -> Callable[..., object]: - litellm_module = import_module("litellm") - return cast(Callable[..., object], litellm_module.completion) - - -def _render_compiled_state_contract(compiled_state: State) -> str: - premise = get_premise_value(compiled_state) - use_items = sorted(get_policy_items(compiled_state, POLICY_USE)) - prohibit_items = sorted(get_policy_items(compiled_state, POLICY_PROHIBIT)) - - lines: list[str] = ["The following constraints are authoritative."] - if premise: - lines.append(f"Current premise: {premise}.") - if use_items: - lines.append("Items marked use: " + ", ".join(use_items) + ".") - if prohibit_items: - lines.append("Items marked prohibit: " + ", ".join(prohibit_items) + ".") - lines.append("If user text conflicts with constraints, follow constraints exactly.") - - return "Host policy contract:\n" + "\n".join(f"- {line}" for line in lines) - - -def _build_messages(user_input: str, compiled_state: State) -> list[dict[str, str]]: - return [ - { - "role": "system", - "content": "You are a helpful assistant.\n" - + _render_compiled_state_contract(compiled_state), - }, - {"role": "user", "content": user_input}, - ] - - -def _call_litellm(messages: list[dict[str, str]]) -> str: - try: - completion = _get_litellm_completion() - except ModuleNotFoundError as exc: - raise RuntimeError("litellm is required. Install with: pip install litellm") from exc - - config = resolve_provider_config(default_model="openai/gpt-4o-mini") - print_startup_config(config, logger=logger) - - kwargs: _LiteLLMCallKwargs = { - "model": config.model, - "messages": messages, - "temperature": 0, - "api_base": config.base_url, - } - if config.api_key: - kwargs["api_key"] = config.api_key - - response = completion(**kwargs) - content = _extract_response_content(response) - if content is None: - raise RuntimeError("LiteLLM response missing choices[0].message.content") - return content - - -def _prompt_file_path() -> Traversable: - profile = os.getenv("PREPROCESSOR_PROMPT_PROFILE", "default").strip().lower() - if profile == "llama": - return _PROMPTS_DIR.joinpath("llama.txt") - return _PROMPTS_DIR.joinpath("default.txt") - - -def _llm_fallback_preprocess(message: str, state: State) -> str | None: - with as_file(_prompt_file_path()) as prompt_path: - prompt = render_prompt(prompt_path, state) - if prompt is None: - return None - - try: - completion = _get_litellm_completion() - except ModuleNotFoundError: - return None - - try: - config = resolve_provider_config(default_model="openai/gpt-4o-mini") - except RuntimeError: - return None - if config.mode == "openai" and not config.api_key: - return None - preprocessor_model = os.getenv("PREPROCESSOR_MODEL", "").strip() - if not preprocessor_model: - preprocessor_model = os.getenv("MODEL", "openai/gpt-4o-mini") - - kwargs: _LiteLLMCallKwargs = { - "model": preprocessor_model, - "messages": [ - {"role": "system", "content": prompt}, - {"role": "user", "content": message}, - ], - "temperature": 0, - "api_base": config.base_url, - } - if config.api_key: - kwargs["api_key"] = config.api_key - - try: - response = completion(**kwargs) - raw_output = _extract_response_content(response) - except Exception: - return None - - parsed = parse_preprocessor_output(raw_output, source_input=message) - if parsed is None: - return None - return parsed - - -def _preprocess_user_input(message: str, state: State) -> str | None: - # Heuristic first (fast + high precision), then optional LLM fallback. - try: - heuristic_result = preprocess_heuristic(message) - logger.debug("preprocessor: heuristic_outcome=%s", heuristic_result["outcome"]) - if ( - heuristic_result["outcome"] == PREPROCESS_OUTCOME_DIRECTIVE - and heuristic_result["directive"] - ): - parsed = parse_preprocessor_output(heuristic_result["directive"]) - logger.debug("preprocessor: heuristic_directive=%r", heuristic_result["directive"]) - if parsed is not None: - return parsed - except Exception: - logger.debug("preprocessor: heuristic_exception", exc_info=True) - - if _is_directive_shaped_input(message): - return None - - try: - fallback_directive = _llm_fallback_preprocess(message, state) - logger.debug("preprocessor: fallback_directive=%r", fallback_directive) - return fallback_directive - except Exception: - # Safe no-op fallback: if preprocessor path fails, preserve basic behavior. - return None - - -def _restore_session_checkpoint_if_needed(engine: Engine, session_key: str | None) -> None: - if session_key is None: - return - engine_id = id(engine) - if _RESTORED_ENGINE_BY_SESSION_KEY.get(session_key) == engine_id: - return - - checkpoint = _CHECKPOINTS_BY_SESSION_KEY.get(session_key) - if checkpoint is not None: - engine.import_checkpoint_json(checkpoint) - _RESTORED_ENGINE_BY_SESSION_KEY[session_key] = engine_id - - -def _persist_session_checkpoint_if_needed( - engine: Engine, kind: str, session_key: str | None -) -> None: - if session_key is None: - return - if kind not in {DECISION_UPDATE, DECISION_CLARIFY}: - return - _CHECKPOINTS_BY_SESSION_KEY[session_key] = engine.export_checkpoint_json() - - -def _normalize_confirmation_for_summary(value: str) -> str: - normalized = value.strip().lower() - normalized = re.sub(r"\s+", " ", normalized) - normalized = _TRAILING_CONFIRM_PUNCT_RE.sub("", normalized).strip() - return re.sub(r"\s+", " ", normalized) - - -def _render_item_label(value: str) -> str: - return re.sub(r"\s+", " ", value).strip().lower() - - -def _near_miss_directive_clarify(value: str) -> str | None: - normalized = re.sub(r"\s+", " ", value.strip()) - lower = normalized.lower() - - if lower in {"reset premise", "reset premises", "clear premises"}: - return "Unknown directive.\nUse 'clear premise' or 'reset policies'." - if lower.startswith("set premise to "): - return "Invalid premise syntax.\nUse 'set premise '." - if lower.startswith("change premise ") and not lower.startswith("change premise to "): - return "Invalid premise syntax.\nUse 'change premise to '." - return None - - -def _summarize_confirmation_update(user_input: str, checkpoint: object) -> str: - summarize_fn: Callable[[str, object], str] = summarize_confirmation_update_from_checkpoint - return summarize_fn(user_input, checkpoint) - - -def _summarize_update_from_input(user_input: str) -> str: - normalized = re.sub(r"\s+", " ", user_input.strip()) - lower = normalized.lower() - - if lower == "clear state": - return "State cleared." - if lower == "clear premise": - return "Premise cleared." - if lower == "reset policies": - return "Policies reset." - - replacement_match = re.match( - r"^use\s+(.+?)\s+instead\s+of\s+(.+)$", normalized, flags=re.IGNORECASE - ) - if replacement_match is not None: - item = _render_item_label(replacement_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Use {item}." - - use_match = re.match(r"^use\s+(.+)$", normalized, flags=re.IGNORECASE) - if use_match is not None: - item = _render_item_label(use_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Use {item}." - - prohibit_match = re.match(r"^prohibit\s+(.+)$", normalized, flags=re.IGNORECASE) - if prohibit_match is not None: - item = _render_item_label(prohibit_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Prohibit {item}." - - remove_policy_match = re.match(r"^remove\s+policy\s+(.+)$", normalized, flags=re.IGNORECASE) - if remove_policy_match is not None: - item = _render_item_label(remove_policy_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Removed policy {item}." - - return "State updated." - - -def _append_trace( - response_text: str, - *, - original_input: str, - compiler_input: str, - preprocessor_output: str | None, - decision: object, - state_before: object, - state_after: object, - llm_called: bool, -) -> str: - if not SHOW_CONTEXT_COMPILER_TRACE: - return response_text - trace_text = build_trace( - original_input=original_input, - compiler_input=compiler_input, - preprocessor_output=preprocessor_output, - decision=decision, - state_before=state_before, - state_after=state_after, - llm_called=llm_called, - ) - return f"{response_text}\n\n{trace_text}" - - -def handle_turn(user_input: str, engine: Engine, *, session_key: str | None = None) -> str: - _restore_session_checkpoint_if_needed(engine, session_key) - state_before = engine.state - has_pending_before = engine.has_pending_clarification() - checkpoint_before = engine.export_checkpoint() if has_pending_before else None - preprocessd: str | None = None - if engine.has_pending_clarification(): - compile_input = user_input - else: - preprocessd = _preprocess_user_input(user_input, engine.state) - compile_input = preprocessd if preprocessd else user_input - logger.debug( - "preprocessor: engine_input=%s", - "directive" if preprocessd else f"user_input len={len(user_input)}", - ) - - decision = engine.step(compile_input) - if is_clarify(decision): - kind = DECISION_CLARIFY - elif is_update(decision): - kind = DECISION_UPDATE - else: - kind = DECISION_PASSTHROUGH - logger.debug("preprocessor: decision=%s", kind) - near_miss_prompt = _near_miss_directive_clarify(user_input) - - if is_clarify(decision): - _persist_session_checkpoint_if_needed(engine, kind, session_key) - response_text = near_miss_prompt or get_clarify_prompt(decision) or "" - return _append_trace( - response_text, - original_input=user_input, - compiler_input=compile_input, - preprocessor_output=preprocessd, - decision=decision, - state_before=state_before, - state_after=engine.state, - llm_called=False, - ) - if near_miss_prompt is not None and is_passthrough(decision): - return _append_trace( - near_miss_prompt, - original_input=user_input, - compiler_input=compile_input, - preprocessor_output=preprocessd, - decision={"kind": DECISION_CLARIFY, "prompt_to_user": near_miss_prompt}, - state_before=state_before, - state_after=engine.state, - llm_called=False, - ) - _persist_session_checkpoint_if_needed(engine, kind, session_key) - if is_update(decision) and is_confirmation_text(user_input) and checkpoint_before is not None: - response_text = _summarize_confirmation_update(user_input, checkpoint_before) - return _append_trace( - response_text, - original_input=user_input, - compiler_input=compile_input, - preprocessor_output=preprocessd, - decision=decision, - state_before=state_before, - state_after=engine.state, - llm_called=False, - ) - if is_update(decision): - response_text = _summarize_update_from_input(compile_input) - return _append_trace( - response_text, - original_input=user_input, - compiler_input=compile_input, - preprocessor_output=preprocessd, - decision=decision, - state_before=state_before, - state_after=engine.state, - llm_called=False, - ) - - decision_state = get_decision_state(decision) - compiled_state = decision_state if decision_state is not None else engine.state - messages = _build_messages(user_input, compiled_state) - response_text = _call_litellm(messages) - return _append_trace( - response_text, - original_input=user_input, - compiler_input=compile_input, - preprocessor_output=preprocessd, - decision=decision, - state_before=state_before, - state_after=compiled_state, - llm_called=True, - ) diff --git a/examples/integrations/litellm_proxy/README.md b/examples/integrations/litellm_proxy/README.md deleted file mode 100644 index 11df998..0000000 --- a/examples/integrations/litellm_proxy/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# LiteLLM Proxy (pre-call hook) - -This example shows how to run Context Compiler inside a LiteLLM proxy pre-call hook. -The hook applies fixed state rules before any upstream model call. - -Available hook files: - -- Basic replay-only hook: `context_compiler_precall_hook.py` -- Preprocessor-enabled hook: `context_compiler_precall_hook_with_preprocessor.py` - -## Requirements - -```shell -pip install "context-compiler[litellm_proxy]" -export OPENAI_API_KEY=... -``` - -`litellm_proxy` is intentionally separate from the general `integrations` -extra because this path targets proxy/gateway runtime use. - -For `context_compiler_precall_hook_with_preprocessor.py`: - -```shell -pip install "context-compiler[experimental]" -``` - -## Quickstart (copy/paste) - -From the repo root: - -```shell -pip install "context-compiler[litellm_proxy]" -export OPENAI_API_KEY=... -litellm --config examples/integrations/litellm_proxy/config.example.yaml -``` - -`config.example.yaml` includes both OpenAI and Ollama model definitions. -Use the Ollama model entry for local testing without API credentials. - -## Run proxy - -Typical startup command (environment-sensitive): - -```shell -litellm --config config.example.yaml -``` - -Hook behavior and proxy startup were re-validated end-to-end with -`litellm==1.88.2`. - -Validated behaviors: - -- passthrough: upstream model called normally -- update: compiler state injected before upstream model call -- clarify: request blocked before upstream model call and surfaced as HTTP 400 - -The proxy runs on `http://localhost:4000` by default. -By default, `config.example.yaml` points to the basic replay-only hook. -To use the preprocessor variant, switch the callback path in the config. - -## Make a request - -```python -from openai import OpenAI - -client = OpenAI( - api_key="anything", - base_url="http://localhost:4000", -) - -response = client.chat.completions.create( - model="gpt-4o-mini", - messages=[{"role": "user", "content": "prohibit peanuts"}], -) -``` - -Or with curl: - -```shell -curl http://localhost:4000/v1/chat/completions \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer anything" \ - -d '{ - "model": "gpt-4o-mini", - "messages": [{"role": "user", "content": "prohibit peanuts"}] - }' -``` - -## Behavior - -- User messages are replayed through Context Compiler before the model call. -- If result is `clarify`, the proxy does not call the model and LiteLLM surfaces the clarification as an HTTP 400 response. -- If result is `passthrough`, the proxy forwards the request normally. -- If result is `update`, the proxy injects compiler state as a system message and then calls the model. - -Preprocessor-enabled variant behavior: - -- Only the latest user transcript message is preprocessed for compiler replay input. -- Heuristic runs first; if no directive is found, LLM fallback is attempted. -- If `engine.has_pending_clarification()` is true, bypass preprocessing and pass raw input directly to `engine.step(...)`. -- Forwarded upstream request messages are not rewritten (except injected compiler system message). - -Optional env vars for preprocessor fallback: - -```shell -export PREPROCESSOR_MODEL=openai/gpt-4o-mini -export PREPROCESSOR_PROMPT_PROFILE=default -``` - -`PREPROCESSOR_MODEL` is optional and defaults to `MODEL`. - -For heuristic-first usage, keep `PREPROCESSOR_PROMPT_PROFILE=default`. -Use `llama` only for LLM-only preprocessing with Llama-family models. - -## Note - -- The callback path in `config.example.yaml` must be importable by LiteLLM. - -## Troubleshooting - -- Callback import failures: verify the callback path configured in `config.example.yaml` is importable in the current LiteLLM environment. -- proxy starts but upstream calls fail: check `OPENAI_API_KEY` and upstream model/provider config in `config.example.yaml`. -- preprocessor fallback issues: `PREPROCESSOR_MODEL` defaults to `MODEL`; set it explicitly only when using a separate fallback model. diff --git a/examples/integrations/litellm_proxy/config.example.yaml b/examples/integrations/litellm_proxy/config.example.yaml deleted file mode 100644 index 3c970f6..0000000 --- a/examples/integrations/litellm_proxy/config.example.yaml +++ /dev/null @@ -1,19 +0,0 @@ -model_list: - # `model_name` is the client-facing alias sent to the proxy. - # `litellm_params.model` is the upstream provider/model LiteLLM calls. - - model_name: gpt-4o-mini - litellm_params: - model: openai/gpt-4o-mini - api_key: os.environ/OPENAI_API_KEY - - - model_name: llama3.1 - litellm_params: - model: ollama/llama3.1:8b - api_base: http://localhost:11434 - -litellm_settings: - callbacks: - # Basic replay-only hook: - - context_compiler_precall_hook.proxy_handler_instance - # Preprocessor-enabled replay hook (use this instead of the basic hook): - # - context_compiler_precall_hook_with_preprocessor.proxy_handler_instance diff --git a/examples/integrations/litellm_proxy/context_compiler_precall_hook.py b/examples/integrations/litellm_proxy/context_compiler_precall_hook.py deleted file mode 100644 index 1206c1d..0000000 --- a/examples/integrations/litellm_proxy/context_compiler_precall_hook.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Minimal LiteLLM Proxy pre-call hook example. - -Architecture: -- Replay user transcript through Context Compiler before any model call. -- If clarification is required, block upstream model call. -- Otherwise inject compiled state guidance into a system message. -""" - -import logging -from typing import Any - -try: - from litellm.integrations.custom_logger import CustomLogger -except ModuleNotFoundError: - # Keep this import path optional: CI/tests run without integration extras. - # A tiny fallback base class keeps module imports deterministic so coverage - # validates behavior instead of failing or silently skipping on missing litellm. - class CustomLogger: # type: ignore[no-redef] - pass - - -from context_compiler import ( - POLICY_PROHIBIT, - State, - Transcript, - compile_transcript, - get_policy_items, - get_premise_value, -) - -logger = logging.getLogger(__name__) - -_SUPPORTED_CALL_TYPES = { - "completion", - "acompletion", - "chat_completion", - "achat_completion", -} - - -def _render_compiled_state_contract(compiled_state: State) -> str: - prohibited = get_policy_items(compiled_state, POLICY_PROHIBIT) - premise = get_premise_value(compiled_state) - - lines: list[str] = ["The following constraints are authoritative."] - if prohibited: - items = ", ".join(prohibited) - lines.append(f"Never recommend or use prohibited items: {items}.") - if premise: - lines.append( - "When the answer depends on user preference/style, " - f"treat the current premise as: {premise}." - ) - lines.append("If the user message conflicts with these constraints, follow them exactly.") - - return "Host policy contract:\n" + "\n".join(f"- {line}" for line in lines) - - -def _extract_request_messages(data: dict[str, object]) -> list[dict[str, object]]: - raw_messages = data.get("messages") - if not isinstance(raw_messages, list): - return [] - return [msg for msg in raw_messages if isinstance(msg, dict)] - - -def _extract_text_content(content: object) -> str | None: - if isinstance(content, str): - return content - if isinstance(content, list): - text_parts: list[str] = [] - for item in content: - if not isinstance(item, dict): - continue - if item.get("type") != "text": - continue - text = item.get("text") - if isinstance(text, str): - text_parts.append(text) - if text_parts: - return " ".join(text_parts) - return None - - -def _extract_user_transcript(messages: list[dict[str, object]]) -> Transcript: - transcript: Transcript = [] - for message in messages: - role = message.get("role") - content = message.get("content") - text_content = _extract_text_content(content) - if role == "user" and text_content is not None: - transcript.append({"role": "user", "content": text_content}) - return transcript - - -class ContextCompilerPreCallHook(CustomLogger): - async def async_pre_call_hook( - self, - user_api_key_dict: Any, - cache: Any, - data: dict[str, object], - call_type: str, - ) -> dict[str, object] | str: - del user_api_key_dict, cache - logger.debug("litellm_proxy: call_type=%s", call_type) - if call_type not in _SUPPORTED_CALL_TYPES: - return data - - request_messages = _extract_request_messages(data) - logger.debug("litellm_proxy: message_count=%d", len(request_messages)) - user_transcript = _extract_user_transcript(request_messages) - logger.debug("litellm_proxy: transcript_len=%d", len(user_transcript)) - replay_result = compile_transcript(user_transcript) - logger.debug("litellm_proxy: replay_kind=%s", replay_result["kind"]) - - if replay_result["kind"] == "confirm": - # Returning a string from this pre-call hook blocks the upstream - # LiteLLM model call under LiteLLM callback semantics. - logger.debug("litellm_proxy: blocking_on_confirm=true") - return replay_result["prompt_to_user"] or "Confirmation required." - - compiled_state = replay_result["state"] - # For long-running conversations, you can optionally compact transcripts by removing user inputs that were compiled into state. See Demo 6. # noqa: E501 - system_message: dict[str, object] = { - "role": "system", - "content": "You are a helpful assistant.\n" - + _render_compiled_state_contract(compiled_state), - } - # Prepend one compiler contract system message, then forward the original - # request messages unchanged. Existing system messages are preserved. - logger.debug("litellm_proxy: inject_system_message=true") - data["messages"] = [system_message, *request_messages] - return data - - -proxy_handler_instance = ContextCompilerPreCallHook() diff --git a/examples/integrations/litellm_proxy/context_compiler_precall_hook_with_preprocessor.py b/examples/integrations/litellm_proxy/context_compiler_precall_hook_with_preprocessor.py deleted file mode 100644 index b5bc6af..0000000 --- a/examples/integrations/litellm_proxy/context_compiler_precall_hook_with_preprocessor.py +++ /dev/null @@ -1,280 +0,0 @@ -"""LiteLLM Proxy pre-call hook with optional preprocessor on latest user message. - -Architecture: -- Replay user transcript through Context Compiler before any model call. -- Preprocess only the latest user message for compiler replay input. -- If clarification is required, block upstream model call. -- Otherwise inject compiled state guidance into a system message. -""" - -import logging -import os -from collections.abc import Callable, Mapping, Sequence -from importlib import import_module -from importlib.resources import as_file, files -from importlib.resources.abc import Traversable -from typing import Any, cast - -try: - from litellm.integrations.custom_logger import CustomLogger -except ModuleNotFoundError: - # Keep this import path optional: CI/tests run without integration extras. - # A tiny fallback base class keeps module imports deterministic so coverage - # validates behavior instead of failing or silently skipping on missing litellm. - class CustomLogger: # type: ignore[no-redef] - pass - - -from context_compiler import ( - POLICY_PROHIBIT, - State, - Transcript, - compile_transcript, - get_policy_items, - get_premise_value, -) -from experimental.preprocessor import ( - PREPROCESS_OUTCOME_DIRECTIVE, - parse_preprocessor_output, - preprocess_heuristic, - render_prompt, -) - -logger = logging.getLogger(__name__) - -_SUPPORTED_CALL_TYPES = { - "completion", - "acompletion", - "chat_completion", - "achat_completion", -} - -_PROMPTS_DIR = files("experimental.preprocessor").joinpath("prompts") - - -def _render_compiled_state_contract(compiled_state: State) -> str: - prohibited = get_policy_items(compiled_state, POLICY_PROHIBIT) - premise = get_premise_value(compiled_state) - - lines: list[str] = ["The following constraints are authoritative."] - if prohibited: - items = ", ".join(prohibited) - lines.append(f"Never recommend or use prohibited items: {items}.") - if premise: - lines.append( - "When the answer depends on user preference/style, " - f"treat the current premise as: {premise}." - ) - lines.append("If the user message conflicts with these constraints, follow them exactly.") - - return "Host policy contract:\n" + "\n".join(f"- {line}" for line in lines) - - -def _extract_request_messages(data: dict[str, object]) -> list[dict[str, object]]: - raw_messages = data.get("messages") - if not isinstance(raw_messages, list): - return [] - return [msg for msg in raw_messages if isinstance(msg, dict)] - - -def _extract_text_content(content: object) -> str | None: - if isinstance(content, str): - return content - if isinstance(content, list): - text_parts: list[str] = [] - for item in content: - if not isinstance(item, dict): - continue - if item.get("type") != "text": - continue - text = item.get("text") - if isinstance(text, str): - text_parts.append(text) - if text_parts: - return " ".join(text_parts) - return None - - -def _extract_user_transcript(messages: list[dict[str, object]]) -> Transcript: - transcript: Transcript = [] - for message in messages: - role = message.get("role") - content = message.get("content") - text_content = _extract_text_content(content) - if role == "user" and text_content is not None: - transcript.append({"role": "user", "content": text_content}) - return transcript - - -def _extract_response_content(response: object) -> str | None: - if isinstance(response, Mapping): - choices = response.get("choices") - if isinstance(choices, Sequence) and choices: - first = choices[0] - if isinstance(first, Mapping): - message = first.get("message") - if isinstance(message, Mapping): - content = message.get("content") - if isinstance(content, str): - return content - - choices_attr = getattr(response, "choices", None) - if isinstance(choices_attr, Sequence) and choices_attr: - first = choices_attr[0] - message_attr = getattr(first, "message", None) - content_attr = getattr(message_attr, "content", None) - if isinstance(content_attr, str): - return content_attr - - return None - - -def _prompt_file_path() -> Traversable: - profile = os.getenv("PREPROCESSOR_PROMPT_PROFILE", "default").strip().lower() - if profile == "llama": - return _PROMPTS_DIR.joinpath("llama.txt") - return _PROMPTS_DIR.joinpath("default.txt") - - -def _get_litellm_completion() -> Callable[..., object]: - litellm_module = import_module("litellm") - return cast(Callable[..., object], litellm_module.completion) - - -def _llm_fallback_preprocess(message: str, state: State) -> str | None: - with as_file(_prompt_file_path()) as prompt_path: - prompt = render_prompt(prompt_path, state) - if prompt is None: - return None - - preprocessor_model = os.getenv("PREPROCESSOR_MODEL", "").strip() - if not preprocessor_model: - preprocessor_model = os.getenv("MODEL", "").strip() - if not preprocessor_model: - return None - - api_key = os.getenv("OPENAI_API_KEY") - if not api_key: - return None - - try: - completion = _get_litellm_completion() - except ModuleNotFoundError: - return None - - kwargs: dict[str, object] = { - "model": preprocessor_model, - "messages": [ - {"role": "system", "content": prompt}, - {"role": "user", "content": message}, - ], - "api_key": api_key, - "temperature": 0, - } - api_base = os.getenv("OPENAI_BASE_URL") - if api_base: - kwargs["api_base"] = api_base - - try: - response = completion(**kwargs) - raw_output = _extract_response_content(response) - except Exception: - return None - - parsed = parse_preprocessor_output(raw_output, source_input=message) - if parsed is None: - return None - return parsed - - -def _state_before_last_message(user_transcript: Transcript) -> State | None: - if not user_transcript: - return None - prefix = user_transcript[:-1] - replay = compile_transcript(prefix) - if replay["kind"] != "state": - return None - return replay["state"] - - -def _preprocess_last_user_message(message: str, state: State | None) -> str | None: - try: - heuristic_result = preprocess_heuristic(message) - if ( - heuristic_result["outcome"] == PREPROCESS_OUTCOME_DIRECTIVE - and heuristic_result["directive"] - ): - parsed = parse_preprocessor_output(heuristic_result["directive"]) - if parsed is not None: - return parsed - except Exception: - logger.debug("litellm_proxy: heuristic_exception", exc_info=True) - - if state is None: - return None - - try: - return _llm_fallback_preprocess(message, state) - except Exception: - logger.debug("litellm_proxy: fallback_exception", exc_info=True) - return None - - -class ContextCompilerPreCallHookWithPreprocessor(CustomLogger): - async def async_pre_call_hook( - self, - user_api_key_dict: Any, - cache: Any, - data: dict[str, object], - call_type: str, - ) -> dict[str, object] | str: - del user_api_key_dict, cache - logger.debug("litellm_proxy: call_type=%s", call_type) - if call_type not in _SUPPORTED_CALL_TYPES: - return data - - request_messages = _extract_request_messages(data) - logger.debug("litellm_proxy: message_count=%d", len(request_messages)) - - user_transcript = _extract_user_transcript(request_messages) - logger.debug("litellm_proxy: transcript_len=%d", len(user_transcript)) - - transcript_for_replay = user_transcript - replaced_last_user_message = False - preprocessd: str | None = None - - if user_transcript: - last_user_content = cast(str, user_transcript[-1]["content"]) - prior_state = _state_before_last_message(user_transcript) - preprocessd = _preprocess_last_user_message(last_user_content, prior_state) - logger.debug("litellm_proxy: preprocessd=%r", preprocessd) - if preprocessd: - transcript_for_replay = [*user_transcript] - transcript_for_replay[-1] = {"role": "user", "content": preprocessd} - replaced_last_user_message = True - - logger.debug("litellm_proxy: replaced_last_user_message=%s", replaced_last_user_message) - - replay_result = compile_transcript(transcript_for_replay) - logger.debug("litellm_proxy: replay_kind=%s", replay_result["kind"]) - - if replay_result["kind"] == "confirm": - # Returning a string from this pre-call hook blocks the upstream - # LiteLLM model call under LiteLLM callback semantics. - logger.debug("litellm_proxy: blocking_on_confirm=true") - return replay_result["prompt_to_user"] or "Confirmation required." - - compiled_state = replay_result["state"] - system_message: dict[str, object] = { - "role": "system", - "content": "You are a helpful assistant.\n" - + _render_compiled_state_contract(compiled_state), - } - logger.debug("litellm_proxy: inject_system_message=true") - # Preserve original request messages; only compiler replay input uses - # the preprocessed latest user message when available. - data["messages"] = [system_message, *request_messages] - return data - - -proxy_handler_instance = ContextCompilerPreCallHookWithPreprocessor() diff --git a/examples/integrations/ollama_structured_output/README.md b/examples/integrations/ollama_structured_output/README.md deleted file mode 100644 index d5d8129..0000000 --- a/examples/integrations/ollama_structured_output/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Ollama structured output (host-side selection) - -This example shows a visible host behavior change that is different from prompt reinjection. - -Flow: - -`Context Compiler state -> host schema decision -> Ollama format request -> model call` - -The host reads compiled policy state, picks a JSON Schema (or none), and sends that choice through Ollama's `format` field. - -## What this example guarantees - -- Context Compiler provides deterministic state transitions. -- The host integration decides whether to request a schema. -- Ollama structured output is a runtime request made by the host. -- If policy state is unknown or insufficient, the host requests no schema. - -## What `prohibit shell_command` means here - -- The host will not request the `shell_command` schema. -- The host may still request a different schema when policy supports it (for example, `python_script`). -- This does not block normal language discussion about shell commands. - -## Observable behavior - -Given policy state: - -```text -use python_script -prohibit shell_command -``` - -this host selects `python_script` schema and does not request `shell_command` schema. - -## Test boundary - -Tests verify schema selection behavior only: - -- compiler state -> selected schema (or no schema) -- contradiction handling stays in compiler `clarify` - -Tests do not assert exact model wording. - -## Run without Ollama - -```shell -uv run python examples/integrations/ollama_structured_output/example.py -``` - -## Optional Ollama smoke run - -```shell -export RUN_OLLAMA_SMOKE=1 -export OLLAMA_MODEL=llama3.1 -uv run python examples/integrations/ollama_structured_output/example.py -``` - -When smoke mode is enabled, the host sends the selected JSON Schema through Ollama `format`. diff --git a/examples/integrations/ollama_structured_output/example.py b/examples/integrations/ollama_structured_output/example.py deleted file mode 100644 index ef2f3f6..0000000 --- a/examples/integrations/ollama_structured_output/example.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Minimal host-side Ollama structured-output schema selection. - -Flow: -Context Compiler state -> host selection logic -> Ollama `format` JSON Schema. - -This example keeps model execution optional so tests can validate behavior without Ollama. -""" - -import json -import os -import urllib.error -import urllib.request -from collections.abc import Mapping -from typing import Any, TypedDict, cast - -from context_compiler import ( - POLICY_PROHIBIT, - POLICY_USE, - State, - create_engine, - get_clarify_prompt, - get_decision_state, - get_policy_items, - is_clarify, -) -from context_compiler.engine import Engine - -PYTHON_SCRIPT_SCHEMA: dict[str, Any] = { - "type": "object", - "properties": { - "python_script": { - "type": "string", - "description": "A complete Python script.", - } - }, - "required": ["python_script"], - "additionalProperties": False, -} - -SHELL_COMMAND_SCHEMA: dict[str, Any] = { - "type": "object", - "properties": { - "shell_command": { - "type": "string", - "description": "A single shell command.", - } - }, - "required": ["shell_command"], - "additionalProperties": False, -} - -# Small, explicit mapping from policy item -> Ollama `format` schema. -_SCHEMA_BY_ITEM: dict[str, dict[str, Any]] = { - "python_script": PYTHON_SCRIPT_SCHEMA, - "shell_command": SHELL_COMMAND_SCHEMA, -} - - -class TurnPlan(TypedDict): - decision_kind: str - clarify_prompt: str | None - selected_schema_item: str | None - format_schema: dict[str, Any] | None - - -def select_ollama_format_schema(state: State) -> tuple[str | None, dict[str, Any] | None]: - """Return (policy_item, schema) or (None, None) when no safe match exists. - - Unknown/insufficient policy state intentionally selects no schema. - """ - - use_items = set(get_policy_items(state, POLICY_USE)) - prohibit_items = set(get_policy_items(state, POLICY_PROHIBIT)) - - for item, schema in _SCHEMA_BY_ITEM.items(): - if item in use_items and item not in prohibit_items: - return item, schema - - return None, None - - -def plan_turn(user_input: str, engine: Engine) -> TurnPlan: - """Run compiler step and decide whether to request Ollama structured output.""" - - decision = engine.step(user_input) - if is_clarify(decision): - return { - "decision_kind": "clarify", - "clarify_prompt": get_clarify_prompt(decision), - "selected_schema_item": None, - "format_schema": None, - } - - decision_state = get_decision_state(decision) - compiled_state = decision_state if decision_state is not None else engine.state - selected_item, format_schema = select_ollama_format_schema(compiled_state) - - return { - "decision_kind": str(decision["kind"]), - "clarify_prompt": None, - "selected_schema_item": selected_item, - "format_schema": format_schema, - } - - -def optional_ollama_call( - *, - user_input: str, - model: str, - format_schema: Mapping[str, Any] | None, - host: str | None = None, -) -> dict[str, Any]: - """Optional smoke call to Ollama's /api/chat. - - If `format_schema` is provided, it is passed through `format` exactly. - """ - - base_url = host or os.getenv("OLLAMA_BASE_URL") or "http://localhost:11434" - payload: dict[str, Any] = { - "model": model, - "messages": [{"role": "user", "content": user_input}], - "stream": False, - } - if format_schema is not None: - payload["format"] = dict(format_schema) - - request = urllib.request.Request( - url=f"{base_url.rstrip('/')}/api/chat", - data=json.dumps(payload).encode("utf-8"), - headers={"Content-Type": "application/json"}, - method="POST", - ) - - try: - with urllib.request.urlopen(request, timeout=30) as response: - raw = response.read().decode("utf-8") - except urllib.error.URLError as exc: - raise RuntimeError(f"Ollama call failed: {exc}") from exc - - decoded = cast(object, json.loads(raw)) - if not isinstance(decoded, dict): - raise RuntimeError("Ollama response must be a JSON object") - return cast(dict[str, Any], decoded) - - -def main() -> None: - engine = create_engine() - - # Demonstration setup. - engine.step("use python_script") - engine.step("prohibit shell_command") - - plan = plan_turn("Write a helper script to parse CSV files.", engine) - print("decision_kind:", plan["decision_kind"]) - print("selected_schema_item:", plan["selected_schema_item"]) - print("format_schema_selected:", plan["format_schema"] is not None) - - # Optional model execution path; disabled by default. - if os.getenv("RUN_OLLAMA_SMOKE") == "1": - response = optional_ollama_call( - user_input="Write a helper script to parse CSV files.", - model=os.getenv("OLLAMA_MODEL", "llama3.1"), - format_schema=plan["format_schema"], - ) - print("ollama_response_keys:", sorted(response.keys())) - - -if __name__ == "__main__": - main() diff --git a/examples/integrations/openwebui/README.md b/examples/integrations/openwebui/README.md deleted file mode 100644 index 1a1c21d..0000000 --- a/examples/integrations/openwebui/README.md +++ /dev/null @@ -1,151 +0,0 @@ -# Open WebUI Pipe Integration - -Examples of Open WebUI Pipe Functions that use Context Compiler. - -Tested target: Open WebUI `v0.8.12` (latest at time of testing). -Validated at runtime on stock Docker Open WebUI with a real backend model provider. - -Compatibility note: OpenWebUI `0.9.x` changed `Users.get_user_by_id` to async. -These examples support both sync (`0.8.x`) and async (`0.9.x`) user lookup. - -## Files - -- `open_webui_pipe.py`: basic integration, no preprocessor layer (recommended/default). -- `open_webui_pipe_with_preprocessor.py`: optional/experimental preprocessor layer (rule-based check first, then optional model fallback) before `engine.step(...)`. - -## Setup - -The minimal pipe path below is the easiest first-run flow and was runtime-validated in Docker via API flow with a real backend model. - -1. Import `open_webui_pipe.py` (recommended/default) as a Function by URL. -2. Open WebUI installs `context-compiler>=0.7.4` from the function frontmatter requirements. -3. Enable the function. -4. Set `BASE_MODEL_ID` to a valid Open WebUI model id (required). -5. Select the pipe model in chat. - -Open WebUI is a separate runtime and must already be installed/configured separately. -Open WebUI also needs at least one real backend model/provider configured (for example Ollama or OpenAI) so `BASE_MODEL_ID` resolves to an actual model. -Note: The `PROVIDER` environment contract used in LiteLLM examples/demos does not apply to OpenWebUI. OpenWebUI manages providers via its own connection settings and model IDs. - -Checkpoint continuation in these examples requires `context-compiler>=0.7.4`. - -### Model configuration - -- Open: `http://localhost:3000/admin/functions` -- Verify `BASE_MODEL_ID` matches an existing Open WebUI model id exactly. -- Example: - - `BASE_MODEL_ID = llama3.1:8b` -- Model ids are configured in: `Admin Panel → Settings → Models` - -If using `open_webui_pipe_with_preprocessor.py`: -- Install preprocessor support in the Open WebUI environment: - - `pip install "context-compiler[experimental]"` -- Open WebUI executes copied functions from a temp/cached location, so - preprocessor imports/resources must come from the installed package (not - repo-relative paths). -- Set `PREPROCESSOR_PROMPT_PROFILE` to `default` for heuristic-first usage. -- Use `llama` only for LLM-only preprocessing with Llama-family models. -- Prompt files are loaded from the installed package prompts (`default`/`llama` profiles). -- Optional: set `PREPROCESSOR_MODEL_ID` to route fallback precompilation through - a separate model. If unset, fallback uses `BASE_MODEL_ID`. -- Fallback routing is Open WebUI-native (no LiteLLM dependency for this pipe). -- The heuristic preprocessor is intentionally conservative and high-precision, and - may abstain on mixed-prose natural language (for example, `i think we should - use docker`). In those cases, behavior may remain passthrough unless fallback - precompilation returns a validated canonical directive. -- If you configure invalid model ids, the pipe returns explicit runtime errors: - - `BASE_MODEL_ID` not found in Open WebUI models - - `PREPROCESSOR_MODEL_ID` not found in Open WebUI models - -### Docker/manual install fallback - -If frontmatter dependency installs are disabled, offline, or unavailable: - -1. Open a shell in the Open WebUI container: - - `docker exec -it sh` -2. Install the package manually: - - Minimal pipe: `pip install "context-compiler>=0.7.4"` - - Preprocessor pipe: `pip install "context-compiler[experimental]>=0.7.4"` -3. Import and enable the function in Open WebUI, then configure valves. - -### Finding valid model ids - -Use the Open WebUI model picker/list to copy exact model ids for `BASE_MODEL_ID` -(and optional `PREPROCESSOR_MODEL_ID` for the preprocessor pipe). - -## Limitations - -- No durable external persistence (checkpoint continuation is in-process only). -- No multi-worker or cross-process guarantees. -- No Redis/DB/external storage. -- No Filters or Pipelines. -- No production hardening. -- Tied to Open WebUI internal helper/import paths by version. - -## Manual Validation - -Validate these behaviors: -- `clarify` short-circuits the LLM call. -- `passthrough` forwards input without state injection. -- `update` forwards with compiler state (`[[cc_state]]`) added to the request. -- chat isolation works with real chat ids. -- state is lost after restart (no external persistence). -- non-text input is bypassed. - -Note: In the OpenWebUI example pipes, recognized directive-only `update` -decisions return fixed local acknowledgments and do not call the -downstream LLM. -Both pipes support an exact local inspection command: `show state`. -When the latest user message is exactly `show state` (case-insensitive after trim), -the pipe returns current compiler state locally and does not call the downstream model. -When trace is enabled, responses include concise evidence of decision kind, -active state, downstream LLM call/no-call, and whether state was injected. - -Decision flow in both pipes: -- `passthrough`: call the downstream model with normal input. -- `clarify`: show `prompt_to_user`; do not change saved state. -- `update`: state changed; render local acknowledgment for directive-only input, or call downstream model with updated state injected. - -For the preprocessor pipe, if `engine.has_pending_clarification()` is true, bypass preprocessing and pass raw input directly to `engine.step(...)`. - -## Behavioral comparisons - -**Case 1** - -- prompt(s): `clear state` → `change premise to formal tone` -- base model: “To adjust the tone… provide the original content…” -- basic pipe: `No premise exists yet. Use 'set premise ...' first.` -- preprocessor pipe: `No premise exists yet. Use 'set premise ...' first.` -- why this matters: lifecycle rule is enforced in a fixed, repeatable way; base model drifts into generic rewriting help. - -**Case 2** - -- prompt(s): `clear state` → `use docker` → `prohibit docker` -- base model: generic Docker/prohibition guidance text -- basic pipe: `'docker' is already in use. Only one policy per item is allowed. Use 'reset policies' to change it.` -- preprocessor pipe: same conflict clarify -- why this matters: the app asks before applying a conflicting change. - -**Case 3** - -- prompt(s): `clear state` → `use podman instead of docker` -- base model: generic “how to switch to Podman” tutorial -- basic pipe: `No exact policy found for "docker". Replacement requires an exact policy match...` -- preprocessor pipe: same replacement clarify -- why this matters: the app only replaces a policy when the old item already exists. - -**Case 4** - -- prompt(s): `clear state` → `set premise to concise replies` -- base model: accepts conversational style phrasing -- basic pipe: `Did you mean 'set premise concise replies'?` -- preprocessor pipe: same clarify (near-miss is not rewritten) -- why this matters: near-miss text is not silently rewritten. - -**Case 5** - -- prompt(s): `clear state` → `change premise concise replies` -- base model: generic “please clarify changes” response -- basic pipe: `Did you mean 'change premise to concise replies'?` -- preprocessor pipe: same clarify (near-miss is passed through unchanged) -- why this matters: the app waits for explicit, valid directive text before changing state. diff --git a/examples/integrations/openwebui/open_webui_pipe.py b/examples/integrations/openwebui/open_webui_pipe.py deleted file mode 100644 index f3c373a..0000000 --- a/examples/integrations/openwebui/open_webui_pipe.py +++ /dev/null @@ -1,693 +0,0 @@ -""" -title: Context Compiler Pipe -author: rlippmann -author_url: https://github.com/rlippmann/context-compiler -funding_url: https://github.com/rlippmann/context-compiler -version: 0.9.3 -requirements: context-compiler>=0.7.4 - -Minimal Open WebUI Pipe integration for Context Compiler. - -This integration demonstrates mapping Context Compiler `Decision` output into -Open WebUI request flow. - -Scope is intentionally limited: -- Single Pipe Function for Open WebUI 0.8.x and 0.9.x. -- In-memory per-process engine map keyed by chat key. -- No persistence, no multi-worker coordination, no external storage. -""" - -import inspect -import json -import logging -import re -from collections.abc import AsyncIterator -from typing import Any, cast - -from fastapi import Request # type: ignore[import-not-found] -from open_webui.models.users import Users # type: ignore[import-not-found] -from open_webui.utils.chat import generate_chat_completion # type: ignore[import-not-found] - -try: - from pydantic import BaseModel, Field -except ModuleNotFoundError: - # Keep this import optional: CI/tests run without integration extras. - # These lightweight fallbacks keep import-time behavior deterministic so - # coverage exercises the pipe module without pydantic installed. - class BaseModel: # type: ignore[no-redef] - def __init__(self, **kwargs: object) -> None: - for key, value in kwargs.items(): - setattr(self, key, value) - - def Field(*, default: Any, description: str = "") -> Any: # type: ignore[no-redef] - del description - return default - - -from context_compiler import ( - DECISION_CLARIFY, - DECISION_PASSTHROUGH, - DECISION_UPDATE, - POLICY_PROHIBIT, - POLICY_USE, - State, - create_engine, - get_clarify_prompt, - get_decision_state, - get_policy_items, - get_premise_value, - is_clarify, - is_passthrough, - is_update, -) -from context_compiler.engine import Engine -from context_compiler.observability import build_compact_trace_text - -logger = logging.getLogger(__name__) - -_CC_MARKER = "[[cc_state]]" -_ENGINES_BY_CHAT_KEY: dict[str, Engine] = {} -# Example-only in-memory checkpoint store. -# This keeps continuation state only for the current process lifetime. -# Real deployments should persist checkpoints externally (DB/Redis/etc.), -# or restart continuity for pending flows will be lost. -_CHECKPOINTS_BY_CHAT_KEY: dict[str, str] = {} - - -def _resolve_chat_key( - user: dict[str, Any], - chat_id: str | None, - metadata: dict[str, Any] | None, -) -> str: - """Resolve chat key from reserved args with a minimal fallback. - - Resolution order: - 1. ``__chat_id__`` - 2. ``__metadata__["chat_id"]`` - 3. ``no-chat-id:`` - - The fallback key is a degraded convenience for this minimal integration and - is not a strong chat-isolation guarantee. - """ - if chat_id: - return chat_id - if isinstance(metadata, dict): - metadata_chat_id = metadata.get("chat_id") - if isinstance(metadata_chat_id, str) and metadata_chat_id: - return metadata_chat_id - user_id = str(user["id"]) - return f"no-chat-id:{user_id}" - - -def _extract_latest_user_text(messages: list[dict[str, Any]]) -> str | None: - """Return latest plain-text user content, scanning from the end. - - Uses the last message with ``role == "user"``. Only plain string content is - eligible for compilation. Non-text or missing-user cases return ``None`` so - the caller can bypass compiler behavior. - """ - for message in reversed(messages): - if message.get("role") != "user": - continue - content = message.get("content") - if isinstance(content, str): - return content - return None - return None - - -def _render_compiler_state_block(state: State) -> str: - """Render deterministic compiler-owned state block text. - - The first line is ``[[cc_state]]``. Optional lines follow for ``Premise``, - ``Use``, and ``Prohibit``. Policy items are rendered alphabetically, and - identical state must produce identical output bytes. - """ - lines: list[str] = [_CC_MARKER] - - premise = get_premise_value(state) - if premise is not None: - lines.append(f"Premise: {premise}") - - use_items = sorted(get_policy_items(state, POLICY_USE)) - if use_items: - lines.append("Use: " + ", ".join(use_items)) - - prohibit_items = sorted(get_policy_items(state, POLICY_PROHIBIT)) - if prohibit_items: - lines.append("Prohibit: " + ", ".join(prohibit_items)) - - return "\n".join(lines) - - -def _render_show_state_summary(engine: Engine) -> str: - premise = get_premise_value(engine.state) - use_items = sorted(get_policy_items(engine.state, POLICY_USE)) - prohibit_items = sorted(get_policy_items(engine.state, POLICY_PROHIBIT)) - pending = engine.has_pending_clarification() - - use_text = ", ".join(use_items) if use_items else "none" - prohibit_text = ", ".join(prohibit_items) if prohibit_items else "none" - premise_text = premise if premise is not None else "none" - pending_text = "yes" if pending else "no" - - return ( - f"Premise: {premise_text}\n" - f"Use: {use_text}\n" - f"Prohibit: {prohibit_text}\n" - f"Pending clarification: {pending_text}" - ) - - -def _replace_compiler_system_message( - messages: list[dict[str, Any]], - rendered_state_block: str, -) -> list[dict[str, Any]]: - """Replace compiler-owned state messages while preserving other order. - - Compiler-owned messages are identified by ``[[cc_state]]`` prefix. Existing - compiler-owned system messages are removed, and one fresh compiler-owned - system message is inserted after the last remaining system message, else at - index ``0``. Relative order of non-compiler messages is preserved. - - Invariant: exactly one compiler-owned state message exists afterward. - """ - filtered_messages: list[dict[str, Any]] = [] - last_system_index = -1 - - for message in messages: - role = message.get("role") - content = message.get("content") - if role == "system" and isinstance(content, str) and content.startswith(_CC_MARKER): - continue - - filtered_messages.append(message) - if role == "system": - last_system_index = len(filtered_messages) - 1 - - insert_at = last_system_index + 1 if last_system_index >= 0 else 0 - compiler_message: dict[str, Any] = {"role": "system", "content": rendered_state_block} - return [ - *filtered_messages[:insert_at], - compiler_message, - *filtered_messages[insert_at:], - ] - - -def _normalize_state(value: object) -> State: - if isinstance(value, dict): - return cast(State, value) - return {"premise": None, "policies": {}, "version": 2} - - -def _has_non_empty_authoritative_state(state: State) -> bool: - if get_premise_value(state) is not None: - return True - return bool(get_policy_items(state, POLICY_USE) or get_policy_items(state, POLICY_PROHIBIT)) - - -def _build_compact_trace_text( - *, - decision: object, - state_before: object, - state_after: object, - llm_called: bool, - state_injected: str, -) -> str: - return build_compact_trace_text( - decision=decision, - state_before=state_before, - state_after=state_after, - llm_called=llm_called, - state_injected=state_injected, - ) - - -def _strip_trace_block_from_text(content: str) -> str: - marker = "Context Compiler trace" - index = content.find(marker) - if index < 0: - return content - return content[:index].rstrip() - - -def _strip_trace_blocks_from_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: - cleaned: list[dict[str, Any]] = [] - for message in messages: - msg = dict(message) - content = msg.get("content") - if isinstance(content, str): - msg["content"] = _strip_trace_block_from_text(content) - cleaned.append(msg) - return cleaned - - -def _build_forward_messages( - raw_messages: object, - *, - state: State | None = None, -) -> list[dict[str, Any]]: - """Build forwarded messages with trace stripping and optional state injection.""" - messages = ( - _strip_trace_blocks_from_messages([msg for msg in raw_messages if isinstance(msg, dict)]) - if isinstance(raw_messages, list) - else [] - ) - if state is not None and _has_non_empty_authoritative_state(state): - return _replace_compiler_system_message( - messages, - _render_compiler_state_block(state), - ) - return messages - - -def _strip_existing_trace_from_chunk(chunk: object) -> object: - if isinstance(chunk, str): - return _strip_trace_block_from_text(chunk) - if isinstance(chunk, bytes): - decoded = chunk.decode("utf-8", errors="ignore") - cleaned = _strip_trace_block_from_text(decoded) - return cleaned.encode("utf-8") - return chunk - - -def _render_item_label(value: str) -> str: - return re.sub(r"\s+", " ", value).strip().lower() - - -def _near_miss_directive_clarify(value: str) -> str | None: - normalized = re.sub(r"\s+", " ", value.strip()) - lower = normalized.lower() - - if lower in {"reset premise", "reset premises", "clear premises"}: - return "Unknown directive.\nUse 'clear premise' or 'reset policies'." - if lower.startswith("set premise to "): - return "Invalid premise syntax.\nUse 'set premise '." - if lower.startswith("change premise ") and not lower.startswith("change premise to "): - return "Invalid premise syntax.\nUse 'change premise to '." - return None - - -def _summarize_update_from_input(user_input: str) -> str: - normalized = re.sub(r"\s+", " ", user_input.strip()) - lower = normalized.lower() - - if lower == "clear state": - return "State cleared." - if lower == "clear premise": - return "Premise cleared." - if lower == "reset policies": - return "Policies reset." - - replacement_match = re.match( - r"^use\s+(.+?)\s+instead\s+of\s+(.+)$", normalized, flags=re.IGNORECASE - ) - if replacement_match is not None: - item = _render_item_label(replacement_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Use {item}." - - use_match = re.match(r"^use\s+(.+)$", normalized, flags=re.IGNORECASE) - if use_match is not None: - item = _render_item_label(use_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Use {item}." - - prohibit_match = re.match(r"^prohibit\s+(.+)$", normalized, flags=re.IGNORECASE) - if prohibit_match is not None: - item = _render_item_label(prohibit_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Prohibit {item}." - - remove_policy_match = re.match(r"^remove\s+policy\s+(.+)$", normalized, flags=re.IGNORECASE) - if remove_policy_match is not None: - item = _render_item_label(remove_policy_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Removed policy {item}." - - return "State updated." - - -def _is_administrative_update_input(user_input: str) -> bool: - normalized = re.sub(r"\s+", " ", user_input.strip()).lower() - return ( - normalized == "clear state" - or normalized == "clear premise" - or normalized == "reset policies" - or normalized.startswith("remove policy ") - ) - - -class Pipe: - """Map Context Compiler decisions into Open WebUI pipe behavior. - - - ``clarify`` returns plain text and skips model forwarding. - - ``passthrough`` forwards with minimal mutation. - - ``update`` returns deterministic local acknowledgement (no model call). - """ - - class Valves(BaseModel): - BASE_MODEL_ID: str = Field( - default="", - description=( - "Required Open WebUI model id used for forwarding. Must exactly match a " - "configured model id in Open WebUI (not arbitrary text), for example: " - "llama3.1:8b." - ), - ) - SHOW_CONTEXT_COMPILER_TRACE: bool = Field( - default=False, - description="Include concise Context Compiler trace text in responses.", - ) - - def __init__(self) -> None: - self.valves = self.Valves() - - def _is_model_not_found_text(self, value: object) -> bool: - if not isinstance(value, str): - return False - return "model not found" in value.lower() - - def _contains_model_not_found(self, value: object) -> bool: - if self._is_model_not_found_text(value): - return True - if isinstance(value, dict): - return any(self._contains_model_not_found(v) for v in value.values()) - if isinstance(value, list): - return any(self._contains_model_not_found(v) for v in value) - return False - - def _normalize_forward_error(self, response: Any) -> str | None: - if self._contains_model_not_found(response): - return ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is invalid or not " - "configured in Open WebUI. Configure a valid model id in " - "Admin Panel → Settings → Models." - ) - return None - - def _normalize_forward_exception(self, exc: Exception) -> str | None: - detail = getattr(exc, "detail", None) - if self._contains_model_not_found(detail) or self._contains_model_not_found(str(exc)): - return ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is invalid or not " - "configured in Open WebUI. Configure a valid model id in " - "Admin Panel → Settings → Models." - ) - return None - - def _trace_enabled(self) -> bool: - return bool(getattr(self.valves, "SHOW_CONTEXT_COMPILER_TRACE", False)) - - def _append_trace_to_response(self, response: Any, trace_text: str) -> Any: - body_iterator = getattr(response, "body_iterator", None) - if body_iterator is not None and callable(getattr(body_iterator, "__aiter__", None)): - response.body_iterator = self._append_trace_to_stream( - cast(AsyncIterator[object], body_iterator), trace_text - ) - return response - aiter = getattr(response, "__aiter__", None) - if callable(aiter): - return self._append_trace_to_stream(cast(AsyncIterator[object], response), trace_text) - if isinstance(response, str): - cleaned = _strip_trace_block_from_text(response) - return f"{cleaned}\n\n{trace_text}" - if isinstance(response, dict): - choices = response.get("choices") - if isinstance(choices, list) and choices: - first = choices[0] - if isinstance(first, dict): - message = first.get("message") - if isinstance(message, dict): - content = message.get("content") - if isinstance(content, str): - cleaned = _strip_trace_block_from_text(content) - message["content"] = f"{cleaned}\n\n{trace_text}" - return response - choices_attr = getattr(response, "choices", None) - if isinstance(choices_attr, list) and choices_attr: - first_choice = choices_attr[0] - message_attr = getattr(first_choice, "message", None) - content_attr = getattr(message_attr, "content", None) - if message_attr is not None and isinstance(content_attr, str): - cleaned = _strip_trace_block_from_text(content_attr) - message_attr.content = f"{cleaned}\n\n{trace_text}" - return response - return response - - def _append_trace_to_stream( - self, stream: AsyncIterator[object], trace_text: str - ) -> AsyncIterator[object]: - async def _wrapped() -> AsyncIterator[object]: - chunk_type: type[str] | type[bytes] | None = None - saw_done = False - trace_json = json.dumps({"choices": [{"delta": {"content": f"\n\n{trace_text}"}}]}) - trace_event = f"data: {trace_json}\n\n" - - def _matches_done(value: str) -> bool: - normalized = value.strip() - return normalized == "data: [DONE]" or normalized == "[DONE]" - - async for chunk in stream: - if chunk_type is None: - if isinstance(chunk, bytes): - chunk_type = bytes - elif isinstance(chunk, str): - chunk_type = str - if isinstance(chunk, bytes): - decoded = chunk.decode("utf-8", errors="ignore") - if _matches_done(decoded): - saw_done = True - yield trace_event.encode("utf-8") - yield chunk - continue - elif isinstance(chunk, str) and _matches_done(chunk): - saw_done = True - yield trace_event - yield chunk - continue - yield _strip_existing_trace_from_chunk(chunk) - if saw_done: - return - suffix = f"\n\n{trace_text}" - if chunk_type is bytes: - yield suffix.encode("utf-8") - else: - yield suffix - - return _wrapped() - - def _with_trace( - self, - response: Any, - *, - original_input: str, - compiler_input: str, - decision: object, - state_before: object, - state_after: object, - llm_called: bool, - preprocessor_output: str | None = None, - state_injected: str = "no", - ) -> Any: - if not self._trace_enabled(): - return response - del original_input, compiler_input, preprocessor_output - trace_text = _build_compact_trace_text( - decision=decision, - state_before=state_before, - state_after=state_after, - llm_called=llm_called, - state_injected=state_injected, - ) - return self._append_trace_to_response(response, trace_text) - - async def _forward_passthrough( - self, - body: dict[str, Any], - user_payload: dict[str, Any], - request: Request, - *, - state: State | None = None, - ) -> Any: - """Forward with model override and optional compiler-owned state injection.""" - payload = {**body} - payload["model"] = self.valves.BASE_MODEL_ID - payload["messages"] = _build_forward_messages(body.get("messages"), state=state) - user = Users.get_user_by_id(user_payload["id"]) - if inspect.isawaitable(user): - user = await user - try: - response = await generate_chat_completion(request, payload, user) - except Exception as exc: - normalized_exception = self._normalize_forward_exception(exc) - if normalized_exception is not None: - return normalized_exception - raise - normalized_error = self._normalize_forward_error(response) - if normalized_error is not None: - return normalized_error - return response - - async def _forward_update( - self, - body: dict[str, Any], - user_payload: dict[str, Any], - request: Request, - state: State, - ) -> Any: - """Forward with one compiler-owned state message based on current state. - - The body is shallow-copied, ``model`` is overridden, and exactly one - compiler-owned message is inserted/replaced before forwarding. - """ - payload = {**body} - payload["model"] = self.valves.BASE_MODEL_ID - - payload["messages"] = _build_forward_messages(body.get("messages"), state=state) - - user = Users.get_user_by_id(user_payload["id"]) - if inspect.isawaitable(user): - user = await user - try: - response = await generate_chat_completion(request, payload, user) - except Exception as exc: - normalized_exception = self._normalize_forward_exception(exc) - if normalized_exception is not None: - return normalized_exception - raise - normalized_error = self._normalize_forward_error(response) - if normalized_error is not None: - return normalized_error - return response - - async def pipe( - self, - body: dict[str, Any], - __user__: dict[str, Any], - __request__: Request, - __chat_id__: str | None = None, - __metadata__: dict[str, Any] | None = None, - ) -> Any: - """Run minimal host flow around compiler decisions. - - Flow: - - Extract latest user text. - - Bypass compiler for non-text or missing-user turns. - - Resolve chat key and get/create per-chat engine. - - Call ``engine.step(...)``. - - Map ``clarify`` / ``passthrough`` / ``update`` outcomes. - """ - raw_messages = body.get("messages") - messages = ( - [msg for msg in raw_messages if isinstance(msg, dict)] - if isinstance(raw_messages, list) - else [] - ) - base_model_id = self.valves.BASE_MODEL_ID.strip() - current_model_id = str(body.get("model", "")).strip() - if not base_model_id: - return "Context Compiler pipe misconfigured: BASE_MODEL_ID is required." - if current_model_id and base_model_id == current_model_id: - return ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID must not match " - "the selected pipe model id to avoid recursive routing." - ) - - latest_user_text = _extract_latest_user_text(messages) - logger.debug("pipe: user_input_found=%s", latest_user_text is not None) - - if latest_user_text is None: - return await self._forward_passthrough(body, __user__, __request__) - - chat_key = _resolve_chat_key(__user__, __chat_id__, __metadata__) - engine = _ENGINES_BY_CHAT_KEY.get(chat_key) - if engine is None: - engine = create_engine() - checkpoint = _CHECKPOINTS_BY_CHAT_KEY.get(chat_key) - if checkpoint is not None: - engine.import_checkpoint_json(checkpoint) - _ENGINES_BY_CHAT_KEY[chat_key] = engine - - if latest_user_text.strip().lower() == "show state": - return _render_show_state_summary(engine) - - state_before = engine.state - logger.debug("pipe: engine_input=%r", latest_user_text) - decision = engine.step(latest_user_text) - if is_clarify(decision): - kind = DECISION_CLARIFY - elif is_update(decision): - kind = DECISION_UPDATE - else: - kind = DECISION_PASSTHROUGH - logger.debug("pipe: decision=%s", kind) - near_miss_prompt = _near_miss_directive_clarify(latest_user_text) - state_after = get_decision_state(decision) - if state_after is None: - state_after = engine.state - - if is_clarify(decision): - _CHECKPOINTS_BY_CHAT_KEY[chat_key] = engine.export_checkpoint_json() - return self._with_trace( - near_miss_prompt or get_clarify_prompt(decision) or "", - original_input=latest_user_text, - compiler_input=latest_user_text, - decision=decision, - state_before=state_before, - state_after=state_after, - llm_called=False, - ) - if near_miss_prompt is not None and is_passthrough(decision): - return self._with_trace( - near_miss_prompt, - original_input=latest_user_text, - compiler_input=latest_user_text, - decision={"kind": DECISION_CLARIFY, "prompt_to_user": near_miss_prompt}, - state_before=state_before, - state_after=state_after, - llm_called=False, - ) - if is_passthrough(decision): - compiled_state = _normalize_state(state_after) - state_injected = "yes" if _has_non_empty_authoritative_state(compiled_state) else "no" - response = await self._forward_passthrough( - body, __user__, __request__, state=compiled_state - ) - return self._with_trace( - response, - original_input=latest_user_text, - compiler_input=latest_user_text, - decision=decision, - state_before=state_before, - state_after=state_after, - llm_called=True, - state_injected=state_injected, - ) - if is_update(decision): - _CHECKPOINTS_BY_CHAT_KEY[chat_key] = engine.export_checkpoint_json() - return self._with_trace( - _summarize_update_from_input(latest_user_text), - original_input=latest_user_text, - compiler_input=latest_user_text, - decision=decision, - state_before=state_before, - state_after=state_after, - llm_called=False, - ) - - compiled_state = _normalize_state(state_after) - state_injected = "yes" if _has_non_empty_authoritative_state(compiled_state) else "no" - response = await self._forward_passthrough( - body, __user__, __request__, state=compiled_state - ) - return self._with_trace( - response, - original_input=latest_user_text, - compiler_input=latest_user_text, - decision=decision, - state_before=state_before, - state_after=state_after, - llm_called=True, - state_injected=state_injected, - ) diff --git a/examples/integrations/openwebui/open_webui_pipe_with_preprocessor.py b/examples/integrations/openwebui/open_webui_pipe_with_preprocessor.py deleted file mode 100644 index dce3c77..0000000 --- a/examples/integrations/openwebui/open_webui_pipe_with_preprocessor.py +++ /dev/null @@ -1,984 +0,0 @@ -""" -title: Context Compiler Pipe (Preprocessor) -author: rlippmann -author_url: https://github.com/rlippmann/context-compiler -funding_url: https://github.com/rlippmann/context-compiler -version: 0.9.3 -requirements: context-compiler[experimental]>=0.7.4 - -Open WebUI integration with Context Compiler preprocessor. - -This example extends `open_webui_pipe.py` by inserting a preprocessing step: - -1. Run heuristic preprocessor (fast, high-precision cases) -2. Fall back to Open WebUI-native model completion when needed -3. Pass resulting directive (or original input) to `engine.step(...)` - -Core decision handling remains the same as the base integration. -""" - -import inspect -import json -import logging -import re -from collections.abc import AsyncIterator -from importlib.resources import as_file, files -from importlib.resources.abc import Traversable -from typing import Any, Literal, cast - -from fastapi import Request # type: ignore[import-not-found] -from open_webui.models.users import Users # type: ignore[import-not-found] -from open_webui.utils.chat import generate_chat_completion # type: ignore[import-not-found] -from open_webui.utils.models import get_all_models # type: ignore[import-not-found] - -try: - from pydantic import BaseModel, Field -except ModuleNotFoundError: - # Keep this import optional: CI/tests run without integration extras. - # These lightweight fallbacks keep import-time behavior deterministic so - # coverage exercises the pipe module without pydantic installed. - class BaseModel: # type: ignore[no-redef] - def __init__(self, **kwargs: object) -> None: - for key, value in kwargs.items(): - setattr(self, key, value) - - def Field(*, default: Any, description: str = "") -> Any: # type: ignore[no-redef] - del description - return default - - -from context_compiler import ( - DECISION_CLARIFY, - DECISION_PASSTHROUGH, - DECISION_UPDATE, - POLICY_PROHIBIT, - POLICY_USE, - State, - create_engine, - get_clarify_prompt, - get_decision_state, - get_policy_items, - get_premise_value, - is_clarify, - is_passthrough, - is_update, -) -from context_compiler.engine import Engine -from context_compiler.observability import build_compact_trace_text -from experimental.preprocessor import ( - PREPROCESS_OUTCOME_DIRECTIVE, - parse_preprocessor_output, - preprocess_heuristic, - render_prompt, -) - -logger = logging.getLogger(__name__) - -_CC_MARKER = "[[cc_state]]" -_ENGINES_BY_CHAT_KEY: dict[str, Engine] = {} -# Example-only in-memory checkpoint store. -# This keeps continuation state only for the current process lifetime. -# Real deployments should persist checkpoints externally (DB/Redis/etc.), -# or restart continuity for pending flows will be lost. -_CHECKPOINTS_BY_CHAT_KEY: dict[str, str] = {} -_PROMPTS_DIR = files("experimental.preprocessor").joinpath("prompts") - - -def _is_directive_shaped_input(message: str) -> bool: - normalized = re.sub(r"\s+", " ", message.strip()).lower() - return ( - normalized.startswith("use") - or normalized.startswith("prohibit") - or normalized.startswith("remove policy") - or normalized.startswith("set premise") - or normalized.startswith("change premise") - or normalized.startswith("clear") - or normalized.startswith("reset") - ) - - -def _prompt_file_path(profile: str) -> Traversable: - # Runtime prompt selection for fallback precompilation: - # - default: most instruction-following models - # - llama: models that need tighter prompt guidance - if profile == "llama": - return _PROMPTS_DIR.joinpath("llama.txt") - return _PROMPTS_DIR.joinpath("default.txt") - - -def _resolve_chat_key( - user: dict[str, Any], - chat_id: str | None, - metadata: dict[str, Any] | None, -) -> str: - if chat_id: - return chat_id - if isinstance(metadata, dict): - metadata_chat_id = metadata.get("chat_id") - if isinstance(metadata_chat_id, str) and metadata_chat_id: - return metadata_chat_id - user_id = str(user["id"]) - return f"no-chat-id:{user_id}" - - -def _extract_latest_user_text(messages: list[dict[str, Any]]) -> str | None: - for message in reversed(messages): - if message.get("role") != "user": - continue - content = message.get("content") - if isinstance(content, str): - return content - return None - return None - - -def _has_pending_clarification(engine: Engine) -> bool: - return engine.has_pending_clarification() - - -def _render_compiler_state_block(state: State) -> str: - lines: list[str] = [_CC_MARKER] - - premise = get_premise_value(state) - if premise is not None: - lines.append(f"Premise: {premise}") - - use_items = sorted(get_policy_items(state, POLICY_USE)) - if use_items: - lines.append("Use: " + ", ".join(use_items)) - - prohibit_items = sorted(get_policy_items(state, POLICY_PROHIBIT)) - if prohibit_items: - lines.append("Prohibit: " + ", ".join(prohibit_items)) - - return "\n".join(lines) - - -def _render_show_state_summary(engine: Engine) -> str: - premise = get_premise_value(engine.state) - use_items = sorted(get_policy_items(engine.state, POLICY_USE)) - prohibit_items = sorted(get_policy_items(engine.state, POLICY_PROHIBIT)) - pending = engine.has_pending_clarification() - - use_text = ", ".join(use_items) if use_items else "none" - prohibit_text = ", ".join(prohibit_items) if prohibit_items else "none" - premise_text = premise if premise is not None else "none" - pending_text = "yes" if pending else "no" - - return ( - f"Premise: {premise_text}\n" - f"Use: {use_text}\n" - f"Prohibit: {prohibit_text}\n" - f"Pending clarification: {pending_text}" - ) - - -def _replace_compiler_system_message( - messages: list[dict[str, Any]], - rendered_state_block: str, -) -> list[dict[str, Any]]: - filtered_messages: list[dict[str, Any]] = [] - last_system_index = -1 - - for message in messages: - role = message.get("role") - content = message.get("content") - if role == "system" and isinstance(content, str) and content.startswith(_CC_MARKER): - continue - - filtered_messages.append(message) - if role == "system": - last_system_index = len(filtered_messages) - 1 - - insert_at = last_system_index + 1 if last_system_index >= 0 else 0 - compiler_message: dict[str, Any] = {"role": "system", "content": rendered_state_block} - return [ - *filtered_messages[:insert_at], - compiler_message, - *filtered_messages[insert_at:], - ] - - -def _normalize_state(value: object) -> State: - if isinstance(value, dict): - return cast(State, value) - return {"premise": None, "policies": {}, "version": 2} - - -def _has_non_empty_authoritative_state(state: State) -> bool: - if get_premise_value(state) is not None: - return True - return bool(get_policy_items(state, POLICY_USE) or get_policy_items(state, POLICY_PROHIBIT)) - - -def _build_compact_trace_text( - *, - decision: object, - state_before: object, - state_after: object, - llm_called: bool, - state_injected: str, -) -> str: - return build_compact_trace_text( - decision=decision, - state_before=state_before, - state_after=state_after, - llm_called=llm_called, - state_injected=state_injected, - ) - - -def _strip_trace_block_from_text(content: str) -> str: - marker = "Context Compiler trace" - index = content.find(marker) - if index < 0: - return content - return content[:index].rstrip() - - -def _strip_trace_blocks_from_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: - cleaned: list[dict[str, Any]] = [] - for message in messages: - msg = dict(message) - content = msg.get("content") - if isinstance(content, str): - msg["content"] = _strip_trace_block_from_text(content) - cleaned.append(msg) - return cleaned - - -def _build_forward_messages( - raw_messages: object, - *, - state: State | None = None, -) -> list[dict[str, Any]]: - """Build forwarded messages with trace stripping and optional state injection.""" - messages = ( - _strip_trace_blocks_from_messages([msg for msg in raw_messages if isinstance(msg, dict)]) - if isinstance(raw_messages, list) - else [] - ) - if state is not None and _has_non_empty_authoritative_state(state): - return _replace_compiler_system_message( - messages, - _render_compiler_state_block(state), - ) - return messages - - -def _strip_existing_trace_from_chunk(chunk: object) -> object: - if isinstance(chunk, str): - return _strip_trace_block_from_text(chunk) - if isinstance(chunk, bytes): - decoded = chunk.decode("utf-8", errors="ignore") - cleaned = _strip_trace_block_from_text(decoded) - return cleaned.encode("utf-8") - return chunk - - -def _render_item_label(value: str) -> str: - return re.sub(r"\s+", " ", value).strip().lower() - - -def _near_miss_directive_clarify(value: str) -> str | None: - normalized = re.sub(r"\s+", " ", value.strip()) - lower = normalized.lower() - - if lower in {"reset premise", "reset premises", "clear premises"}: - return "Unknown directive.\nUse 'clear premise' or 'reset policies'." - if lower.startswith("set premise to "): - return "Invalid premise syntax.\nUse 'set premise '." - if lower.startswith("change premise ") and not lower.startswith("change premise to "): - return "Invalid premise syntax.\nUse 'change premise to '." - return None - - -def _summarize_update_from_input(user_input: str) -> str: - normalized = re.sub(r"\s+", " ", user_input.strip()) - lower = normalized.lower() - - if lower == "clear state": - return "State cleared." - if lower == "clear premise": - return "Premise cleared." - if lower == "reset policies": - return "Policies reset." - - replacement_match = re.match( - r"^use\s+(.+?)\s+instead\s+of\s+(.+)$", normalized, flags=re.IGNORECASE - ) - if replacement_match is not None: - item = _render_item_label(replacement_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Use {item}." - - use_match = re.match(r"^use\s+(.+)$", normalized, flags=re.IGNORECASE) - if use_match is not None: - item = _render_item_label(use_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Use {item}." - - prohibit_match = re.match(r"^prohibit\s+(.+)$", normalized, flags=re.IGNORECASE) - if prohibit_match is not None: - item = _render_item_label(prohibit_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Prohibit {item}." - - remove_policy_match = re.match(r"^remove\s+policy\s+(.+)$", normalized, flags=re.IGNORECASE) - if remove_policy_match is not None: - item = _render_item_label(remove_policy_match.group(1).rstrip(" .!?")) - if item: - return f"State updated: Removed policy {item}." - - return "State updated." - - -def _is_administrative_update_input(user_input: str) -> bool: - normalized = re.sub(r"\s+", " ", user_input.strip()).lower() - return ( - normalized == "clear state" - or normalized == "clear premise" - or normalized == "reset policies" - or normalized.startswith("remove policy ") - ) - - -def _extract_completion_content(response: object) -> str | None: - choices_attr = getattr(response, "choices", None) - if isinstance(choices_attr, list) and choices_attr: - first_choice = choices_attr[0] - message_attr = getattr(first_choice, "message", None) - content_attr = getattr(message_attr, "content", None) - if isinstance(content_attr, str): - return content_attr - - if isinstance(response, dict): - choices = response.get("choices") - if isinstance(choices, list) and choices: - first_choice = choices[0] - if isinstance(first_choice, dict): - message = first_choice.get("message") - if isinstance(message, dict): - content = message.get("content") - if isinstance(content, str): - return content - - return None - - -def _normalize_model_id(value: str | None) -> str | None: - if value is None: - return None - value = value.strip() - return value or None - - -def _is_truthy_bool(value: object) -> bool: - if isinstance(value, bool): - return value - if isinstance(value, str): - normalized = value.strip().lower() - if normalized in {"true", "1", "on"}: - return True - if normalized in {"false", "0", "off"}: - return False - return False - - -class Pipe: - """Map Context Compiler decisions into Open WebUI pipe behavior. - - This variant adds a preprocessor stage before ``engine.step(...)``: - heuristic first, then Open WebUI-native LLM fallback. - Update decisions return deterministic local acknowledgement (no model call). - """ - - class Valves(BaseModel): - BASE_MODEL_ID: str = Field( - default="", - description=( - "Required Open WebUI model id used for forwarding. Must exactly match a " - "configured model id in Open WebUI (not arbitrary text), for example: " - "llama3.1:8b." - ), - ) - PREPROCESSOR_MODEL_ID: str | None = Field( - default=None, - description=( - "Optional model id for fallback precompilation (defaults to BASE_MODEL_ID)." - ), - ) - PREPROCESSOR_PROMPT_PROFILE: Literal["default", "llama"] = Field( - default="default", - description="Prompt profile for LLM fallback precompilation.", - ) - ALLOW_MISSING_BASE_MODEL_FOR_DEBUG: bool = Field( - default=False, - description="Allow missing BASE_MODEL_ID for debug/testing only.", - ) - SHOW_CONTEXT_COMPILER_TRACE: bool = Field( - default=False, - description="Include concise Context Compiler trace text in responses.", - ) - - def __init__(self) -> None: - self.valves = self.Valves() - - def _allow_missing_base_model_for_debug(self) -> bool: - return _is_truthy_bool(getattr(self.valves, "ALLOW_MISSING_BASE_MODEL_FOR_DEBUG", False)) - - def _trace_enabled(self) -> bool: - return bool(getattr(self.valves, "SHOW_CONTEXT_COMPILER_TRACE", False)) - - def _append_trace_to_response(self, response: Any, trace_text: str) -> Any: - body_iterator = getattr(response, "body_iterator", None) - if body_iterator is not None and callable(getattr(body_iterator, "__aiter__", None)): - response.body_iterator = self._append_trace_to_stream( - cast(AsyncIterator[object], body_iterator), trace_text - ) - return response - aiter = getattr(response, "__aiter__", None) - if callable(aiter): - return self._append_trace_to_stream(cast(AsyncIterator[object], response), trace_text) - if isinstance(response, str): - cleaned = _strip_trace_block_from_text(response) - return f"{cleaned}\n\n{trace_text}" - if isinstance(response, dict): - choices = response.get("choices") - if isinstance(choices, list) and choices: - first = choices[0] - if isinstance(first, dict): - message = first.get("message") - if isinstance(message, dict): - content = message.get("content") - if isinstance(content, str): - cleaned = _strip_trace_block_from_text(content) - message["content"] = f"{cleaned}\n\n{trace_text}" - return response - choices_attr = getattr(response, "choices", None) - if isinstance(choices_attr, list) and choices_attr: - first_choice = choices_attr[0] - message_attr = getattr(first_choice, "message", None) - content_attr = getattr(message_attr, "content", None) - if message_attr is not None and isinstance(content_attr, str): - cleaned = _strip_trace_block_from_text(content_attr) - message_attr.content = f"{cleaned}\n\n{trace_text}" - return response - return response - - def _append_trace_to_stream( - self, stream: AsyncIterator[object], trace_text: str - ) -> AsyncIterator[object]: - async def _wrapped() -> AsyncIterator[object]: - chunk_type: type[str] | type[bytes] | None = None - saw_done = False - trace_json = json.dumps({"choices": [{"delta": {"content": f"\n\n{trace_text}"}}]}) - trace_event = f"data: {trace_json}\n\n" - - def _matches_done(value: str) -> bool: - normalized = value.strip() - return normalized == "data: [DONE]" or normalized == "[DONE]" - - async for chunk in stream: - if chunk_type is None: - if isinstance(chunk, bytes): - chunk_type = bytes - elif isinstance(chunk, str): - chunk_type = str - if isinstance(chunk, bytes): - decoded = chunk.decode("utf-8", errors="ignore") - if _matches_done(decoded): - saw_done = True - yield trace_event.encode("utf-8") - yield chunk - continue - elif isinstance(chunk, str) and _matches_done(chunk): - saw_done = True - yield trace_event - yield chunk - continue - yield _strip_existing_trace_from_chunk(chunk) - if saw_done: - return - suffix = f"\n\n{trace_text}" - if chunk_type is bytes: - yield suffix.encode("utf-8") - else: - yield suffix - - return _wrapped() - - def _with_trace( - self, - response: Any, - *, - original_input: str, - compiler_input: str, - decision: object, - state_before: object, - state_after: object, - llm_called: bool, - preprocessor_output: str | None = None, - state_injected: str = "no", - ) -> Any: - if not self._trace_enabled(): - return response - del original_input, compiler_input, preprocessor_output - trace_text = _build_compact_trace_text( - decision=decision, - state_before=state_before, - state_after=state_after, - llm_called=llm_called, - state_injected=state_injected, - ) - return self._append_trace_to_response(response, trace_text) - - def _is_model_not_found_text(self, value: object) -> bool: - if not isinstance(value, str): - return False - return "model not found" in value.lower() - - def _contains_model_not_found(self, value: object) -> bool: - if self._is_model_not_found_text(value): - return True - if isinstance(value, dict): - return any(self._contains_model_not_found(v) for v in value.values()) - if isinstance(value, list): - return any(self._contains_model_not_found(v) for v in value) - return False - - def _normalize_forward_error(self, response: Any) -> str | None: - if self._contains_model_not_found(response): - return ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is invalid or not " - "configured in Open WebUI. Configure a valid model id in " - "Admin Panel → Settings → Models." - ) - return None - - def _normalize_forward_exception(self, exc: Exception) -> str | None: - detail = getattr(exc, "detail", None) - if self._contains_model_not_found(detail) or self._contains_model_not_found(str(exc)): - return ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is invalid or not " - "configured in Open WebUI. Configure a valid model id in " - "Admin Panel → Settings → Models." - ) - return None - - def _normalize_preprocessor_error(self, response: Any) -> str | None: - if self._contains_model_not_found(response): - return ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID is invalid or " - "not configured in Open WebUI. Configure a valid model id in " - "Admin Panel → Settings → Models." - ) - return None - - def _normalize_preprocessor_exception(self, exc: Exception) -> str | None: - detail = getattr(exc, "detail", None) - if self._contains_model_not_found(detail) or self._contains_model_not_found(str(exc)): - return ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID is invalid or " - "not configured in Open WebUI. Configure a valid model id in " - "Admin Panel → Settings → Models." - ) - return None - - def _resolve_preprocessor_model_id(self, base_model_id: str | None) -> str | None: - preprocessor_model_id = _normalize_model_id(self.valves.PREPROCESSOR_MODEL_ID) - return preprocessor_model_id or base_model_id - - async def _validate_configured_model_ids( - self, - request: Request, - user_payload: dict[str, Any], - *, - base_model_id: str | None, - preprocessor_model_id: str | None, - ) -> str | None: - base_model_id = _normalize_model_id(base_model_id) - preprocessor_model_id = _normalize_model_id(preprocessor_model_id) - # Best-effort preflight: fail closed only for clear missing-model mismatches. - # If model discovery fails, preserve runtime behavior and rely on call-path - # normalization below. - user = Users.get_user_by_id(user_payload["id"]) - if inspect.isawaitable(user): - user = await user - try: - models = await get_all_models(request, user=user) - except Exception: - return None - - known_model_ids: set[str] = set() - if isinstance(models, list): - for model in models: - if not isinstance(model, dict): - continue - model_id = model.get("id") - if isinstance(model_id, str): - known_model_ids.add(model_id) - - if base_model_id and base_model_id not in known_model_ids: - return ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID was not found " - "in Open WebUI models." - ) - if preprocessor_model_id and preprocessor_model_id not in known_model_ids: - return ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID was not found " - "in Open WebUI models." - ) - return None - - async def _llm_fallback_preprocess( - self, - message: str, - state: State, - *, - request: Request, - user_payload: dict[str, Any], - prompt_profile: str, - model_id: str | None, - ) -> tuple[str | None, str | None]: - model_id = _normalize_model_id(model_id) - if model_id is None: - return None, None - with as_file(_prompt_file_path(prompt_profile)) as prompt_path: - prompt = render_prompt(prompt_path, state) - if prompt is None: - return None, None - - payload: dict[str, Any] = { - "model": model_id, - "stream": False, - "messages": [ - {"role": "system", "content": prompt}, - {"role": "user", "content": message}, - ], - } - user = Users.get_user_by_id(user_payload["id"]) - if inspect.isawaitable(user): - user = await user - try: - response = await generate_chat_completion(request, payload, user) - except Exception as exc: - normalized_exception = self._normalize_preprocessor_exception(exc) - if normalized_exception is not None: - return None, normalized_exception - return None, None - - normalized_error = self._normalize_preprocessor_error(response) - if normalized_error is not None: - return None, normalized_error - - raw_output = _extract_completion_content(response) - parsed = parse_preprocessor_output(raw_output, source_input=message) - if parsed is None: - return None, None - return parsed, None - - async def _preprocess_user_input( - self, - message: str, - state: State, - *, - request: Request, - user_payload: dict[str, Any], - prompt_profile: str, - model_id: str | None, - ) -> tuple[str | None, str | None]: - # Heuristic first for precision, determinism, and low latency. - # If heuristic does not produce a directive, try Open WebUI-native fallback. - heuristic_result = preprocess_heuristic(message) - - if ( - heuristic_result["outcome"] == PREPROCESS_OUTCOME_DIRECTIVE - and heuristic_result["directive"] - ): - parsed = parse_preprocessor_output(heuristic_result["directive"]) - if parsed is not None: - return parsed, None - - if _is_directive_shaped_input(message): - return None, None - - # In debug mode with missing base/preprocessor model ids, skip fallback - # preprocess entirely so we never attempt an empty-model LLM call. - model_id = _normalize_model_id(model_id) - if model_id is None: - return None, None - - return await self._llm_fallback_preprocess( - message, - state, - request=request, - user_payload=user_payload, - prompt_profile=prompt_profile, - model_id=model_id, - ) - - async def _forward_passthrough( - self, - body: dict[str, Any], - user_payload: dict[str, Any], - request: Request, - *, - base_model_id: str | None, - state: State | None = None, - ) -> Any: - if base_model_id is None: - if self._allow_missing_base_model_for_debug(): - return ( - "Context Compiler debug mode: BASE_MODEL_ID is empty; " - "skipping model passthrough." - ) - return ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is required " - "(or set ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true for testing)." - ) - payload = {**body} - payload["model"] = base_model_id - payload["messages"] = _build_forward_messages(body.get("messages"), state=state) - user = Users.get_user_by_id(user_payload["id"]) - if inspect.isawaitable(user): - user = await user - try: - response = await generate_chat_completion(request, payload, user) - except Exception as exc: - normalized_exception = self._normalize_forward_exception(exc) - if normalized_exception is not None: - return normalized_exception - raise - normalized_error = self._normalize_forward_error(response) - if normalized_error is not None: - return normalized_error - return response - - async def _forward_update( - self, - body: dict[str, Any], - user_payload: dict[str, Any], - request: Request, - state: State, - *, - base_model_id: str | None, - ) -> Any: - if base_model_id is None: - if self._allow_missing_base_model_for_debug(): - return ( - "Context Compiler debug mode: BASE_MODEL_ID is empty; " - "skipping model passthrough." - ) - return ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is required " - "(or set ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true for testing)." - ) - payload = {**body} - payload["model"] = base_model_id - - payload["messages"] = _build_forward_messages(body.get("messages"), state=state) - - user = Users.get_user_by_id(user_payload["id"]) - if inspect.isawaitable(user): - user = await user - try: - response = await generate_chat_completion(request, payload, user) - except Exception as exc: - normalized_exception = self._normalize_forward_exception(exc) - if normalized_exception is not None: - return normalized_exception - raise - normalized_error = self._normalize_forward_error(response) - if normalized_error is not None: - return normalized_error - return response - - async def pipe( - self, - body: dict[str, Any], - __user__: dict[str, Any], - __request__: Request, - __chat_id__: str | None = None, - __metadata__: dict[str, Any] | None = None, - ) -> Any: - # Open WebUI integration entrypoint: - # 1) extract latest user input - # 2) run preprocess (heuristic -> LLM fallback) - # 3) pass directive or original input to engine.step(...) - # 4) map decision back to Open WebUI response behavior - raw_messages = body.get("messages") - messages = ( - [msg for msg in raw_messages if isinstance(msg, dict)] - if isinstance(raw_messages, list) - else [] - ) - base_model_id = _normalize_model_id(self.valves.BASE_MODEL_ID) - preprocessor_model_id = _normalize_model_id(self.valves.PREPROCESSOR_MODEL_ID) - effective_preprocessor_model = preprocessor_model_id or base_model_id - current_model_id = str(body.get("model", "")).strip() - - if not base_model_id and not self._allow_missing_base_model_for_debug(): - return ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is required " - "(or set ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true for testing)." - ) - if base_model_id and current_model_id and base_model_id == current_model_id: - return ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID must not match " - "the selected pipe model id to avoid recursive routing." - ) - if ( - effective_preprocessor_model - and current_model_id - and effective_preprocessor_model == current_model_id - ): - return ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID must not " - "match the selected pipe model id to avoid recursive routing." - ) - - preflight_error = await self._validate_configured_model_ids( - __request__, - __user__, - base_model_id=base_model_id, - preprocessor_model_id=effective_preprocessor_model, - ) - if preflight_error is not None: - return preflight_error - - latest_user_text = _extract_latest_user_text(messages) - logger.debug("preprocessor: user_input_found=%s", latest_user_text is not None) - - if latest_user_text is None: - return await self._forward_passthrough( - body, - __user__, - __request__, - base_model_id=base_model_id, - ) - - chat_key = _resolve_chat_key(__user__, __chat_id__, __metadata__) - engine = _ENGINES_BY_CHAT_KEY.get(chat_key) - if engine is None: - engine = create_engine() - checkpoint = _CHECKPOINTS_BY_CHAT_KEY.get(chat_key) - if checkpoint is not None: - engine.import_checkpoint_json(checkpoint) - _ENGINES_BY_CHAT_KEY[chat_key] = engine - - if latest_user_text.strip().lower() == "show state": - return _render_show_state_summary(engine) - - state_before = engine.state - - preprocessd: str | None = None - preprocess_error: str | None = None - if not _has_pending_clarification(engine): - preprocessd, preprocess_error = await self._preprocess_user_input( - latest_user_text, - engine.state, - request=__request__, - user_payload=__user__, - prompt_profile=self.valves.PREPROCESSOR_PROMPT_PROFILE, - model_id=effective_preprocessor_model, - ) - if preprocess_error is not None: - return preprocess_error - - logger.debug("preprocessor: preprocessd=%r", preprocessd) - # Preserve core behavior: if preprocess yields no directive, use raw user - # text so the compiler still decides clarify/passthrough/update. - compile_input = preprocessd if preprocessd is not None else latest_user_text - - logger.debug("preprocessor: engine_input=%r", compile_input) - decision = engine.step(compile_input) - if is_clarify(decision): - kind = DECISION_CLARIFY - elif is_update(decision): - kind = DECISION_UPDATE - else: - kind = DECISION_PASSTHROUGH - logger.debug("preprocessor: decision=%s", kind) - near_miss_prompt = _near_miss_directive_clarify(latest_user_text) - state_after = get_decision_state(decision) - if state_after is None: - state_after = engine.state - - if is_clarify(decision): - _CHECKPOINTS_BY_CHAT_KEY[chat_key] = engine.export_checkpoint_json() - return self._with_trace( - near_miss_prompt or get_clarify_prompt(decision) or "", - original_input=latest_user_text, - compiler_input=compile_input, - decision=decision, - state_before=state_before, - state_after=state_after, - preprocessor_output=preprocessd, - llm_called=False, - ) - if near_miss_prompt is not None and is_passthrough(decision): - return self._with_trace( - near_miss_prompt, - original_input=latest_user_text, - compiler_input=compile_input, - decision={"kind": DECISION_CLARIFY, "prompt_to_user": near_miss_prompt}, - state_before=state_before, - state_after=state_after, - preprocessor_output=preprocessd, - llm_called=False, - ) - if is_passthrough(decision): - compiled_state = _normalize_state(state_after) - state_injected = "yes" if _has_non_empty_authoritative_state(compiled_state) else "no" - response = await self._forward_passthrough( - body, - __user__, - __request__, - base_model_id=base_model_id, - state=compiled_state, - ) - return self._with_trace( - response, - original_input=latest_user_text, - compiler_input=compile_input, - decision=decision, - state_before=state_before, - state_after=state_after, - preprocessor_output=preprocessd, - llm_called=base_model_id is not None, - state_injected=state_injected, - ) - if is_update(decision): - _CHECKPOINTS_BY_CHAT_KEY[chat_key] = engine.export_checkpoint_json() - return self._with_trace( - _summarize_update_from_input(compile_input), - original_input=latest_user_text, - compiler_input=compile_input, - decision=decision, - state_before=state_before, - state_after=state_after, - preprocessor_output=preprocessd, - llm_called=False, - ) - - compiled_state = _normalize_state(state_after) - state_injected = "yes" if _has_non_empty_authoritative_state(compiled_state) else "no" - response = await self._forward_passthrough( - body, - __user__, - __request__, - base_model_id=base_model_id, - state=compiled_state, - ) - return self._with_trace( - response, - original_input=latest_user_text, - compiler_input=compile_input, - decision=decision, - state_before=state_before, - state_after=state_after, - preprocessor_output=preprocessd, - llm_called=base_model_id is not None, - state_injected=state_injected, - ) diff --git a/experimental/__init__.py b/experimental/__init__.py deleted file mode 100644 index a6869f1..0000000 --- a/experimental/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Experimental integration modules.""" diff --git a/experimental/preprocessor/README.md b/experimental/preprocessor/README.md deleted file mode 100644 index c74a963..0000000 --- a/experimental/preprocessor/README.md +++ /dev/null @@ -1,146 +0,0 @@ -# Experimental Preprocessor Package - -This package provides optional preprocessing helpers for app integrations with -Context Compiler. - -It is experimental and separate from the deterministic core engine in `src/`. - -Model or tool-description translation can help in simple direct cases, but raw -model output is not safe for state changes on its own. The app must validate -outputs before applying them. - -In MCP/tool-calling environments, over-eager tool calling on conversational or -ambiguous input is a known failure mode. Conservative preprocessing and -validation help reduce unintended mutation. - -Recommended install for integrations using this package: -`pip install "context-compiler[experimental]"`. - -Integrations should import this package from the installed environment rather -than using repo-relative preprocessor paths. - -Compatibility note: -- Use `heuristic_preprocessor.py` and `parse_preprocessor_output(...)`. - -## Modules - -- `heuristic_preprocessor.py`: conservative structural preprocessing pass. -- `output_validation.py`: shared normalization and validation checks. -- `prompt_utils.py`: state-aware prompt rendering helper. -- `constants.py`: shared protocol literals and directive validation patterns. -- `prompts/default.txt`: default runtime prompt. -- `prompts/llama.txt`: stricter prompt for Llama-family models in LLM-only mode. - -## Validation boundary (required) - -Public validator entry point: - -- `parse_preprocessor_output(raw_output: object, *, source_input: str | None = None) -> str | None` -- `validate_preprocessor_output(raw_output: object, *, source_input: str | None = None) -> dict` - -All preprocessor outputs (heuristic or LLM) must be validated with -`parse_preprocessor_output(...)` before being applied. - -Classification contract: - -- `directive`: safe, validated canonical directive (`output` is a directive string) -- `no_directive`: confident ordinary content (`output` is `null`) -- `unknown`: unsafe to rewrite (`output` is `null`) - -`unknown` is reject/abstain behavior. Malformed, ambiguous, mixed-intent, -quoted/reported, unsupported, or unsafe outputs must not be rewritten. - -Only validated `directive` output may be used as rewritten compiler input. -`no_directive` and `unknown` must fall back to original user input. - -`source_input` is optional at the API level for backward compatibility. -For integration behavior, it is REQUIRED for LLM fallback validation calls: -pass `source_input=` so source-aware reject rules can -block unsafe rewrites. - -Engine-owned near-misses are reject cases (for example `set premise to X`, -`change premise X`) and must remain `unknown` (not rewritten). - -Raw preprocessor/LLM outputs must not be passed directly to the compiler. -Raw model output must never directly change state. - -The preprocessor does not expand directive grammar. It may emit only validated -canonical directives that the compiler accepts. - -Conservative boundary policy: - -- Only process the full message, not pieces of it (no directive mining from prose). -- Emit at most one canonical directive; otherwise abstain. -- No sentence splitting or hidden multi-line extraction. -- No mixed directive + task extraction. -- No markdown/code-block extraction. -- No broad natural-language semantic rewriting. -- Prefer false negatives over false positives. -- Quoted payload tokens inside an otherwise canonical directive (for example - `use "docker"`) are preserved as-is; they are not silently unquoted. - -If you need natural-language proposal behavior, use an explicit host workflow -that shows suggestions first and asks for confirmation. - -## Safe usage pattern - -1. Run `preprocess_heuristic(message)`. -2. If a heuristic candidate directive exists, validate it with - `parse_preprocessor_output(...)`. -3. If no valid directive was produced, run LLM fallback preprocess. -4. Validate fallback output with - `parse_preprocessor_output(..., source_input=message)`. -5. If a valid directive is produced, pass it through a normal compiler input path. - For session-owned integrations, use `engine.step(...)`. - For transcript-based integrations that receive full chat history each turn: - - use `context_compiler.compile_transcript(...)` for stateless evaluation - - use `engine.apply_transcript(...)` to update an existing engine - Otherwise pass the original user input unchanged. - -Decision handling reminder: -- `passthrough`: no directive was applied; handle as ordinary user input. -- `clarify`: mutation is blocked; surface `prompt_to_user` and do not treat - state as updated. -- `update`: validated canonical directive applied; use updated state as - the source of truth. - -## Future direction (planning note) - -This section is architectural direction, not committed implementation. - -Future preprocessing may evolve beyond direct natural-language to directive -conversion. - -- Policy directives and premise-like facts have different risk profiles. -- Premise-like facts (for example, `I am vegetarian`) may be useful persistent - context, but should not be auto-persisted without confirmation. -- Likely direction: - - conservative directive preprocessing remains separate - - a possible suggestion layer is inspectable, non-mutating, and previewable - - user confirms before anything is saved - -## Prompt guidance - -- Use `prompts/default.txt` as the recommended default prompt. -- Use `prompts/llama.txt` only for LLM-only preprocessing with Llama-family - models. -- Heuristic-first integrations should still keep `default.txt` as the normal - fallback prompt unless there is a model-specific reason not to. - -## Prompt rendering helper - -`prompt_utils.py` exposes: - -- `render_prompt(path: Path, state: State) -> str | None` - -Behavior: - -- reads prompt text from `path` -- strips leading `#` header lines and leading blank lines -- replaces `` and `` using state -- returns `None` if prompt loading fails - -## Notes - -- This package does not mutate compiler state directly. -- State changes still occur only through compiler parsing/replay paths. diff --git a/experimental/preprocessor/__init__.py b/experimental/preprocessor/__init__.py deleted file mode 100644 index cf08253..0000000 --- a/experimental/preprocessor/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Experimental preprocessor integration helpers and prompts.""" - -from .constants import ( - PREPROCESS_OUTCOME_DIRECTIVE, - PREPROCESS_OUTCOME_NO_DIRECTIVE, - PREPROCESS_OUTCOME_UNKNOWN, - PREPROCESSOR_NO_DIRECTIVE_SENTINEL, - PreprocessOutcome, -) -from .heuristic_preprocessor import PreprocessResult, preprocess_heuristic -from .output_validation import ( - parse_preprocessor_output, - validate_preprocessor_output, -) -from .prompt_utils import render_prompt - -__all__ = [ - "PREPROCESS_OUTCOME_DIRECTIVE", - "PREPROCESS_OUTCOME_NO_DIRECTIVE", - "PREPROCESS_OUTCOME_UNKNOWN", - "PREPROCESSOR_NO_DIRECTIVE_SENTINEL", - "PreprocessResult", - "PreprocessOutcome", - "parse_preprocessor_output", - "preprocess_heuristic", - "render_prompt", - "validate_preprocessor_output", -] diff --git a/experimental/preprocessor/constants.py b/experimental/preprocessor/constants.py deleted file mode 100644 index c4da269..0000000 --- a/experimental/preprocessor/constants.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Shared protocol constants for experimental preprocessor paths.""" - -import re -from typing import Final, Literal - -PREPROCESSOR_NO_DIRECTIVE_SENTINEL: Final = "" - -PREPROCESS_OUTCOME_DIRECTIVE: Final = "directive" -PREPROCESS_OUTCOME_NO_DIRECTIVE: Final = "no_directive" -PREPROCESS_OUTCOME_UNKNOWN: Final = "unknown" -PreprocessOutcome = Literal["directive", "no_directive", "unknown"] - -PROMPT_TOKEN_NULL_OR_VALUE: Final = "" -PROMPT_TOKEN_POLICY_SET: Final = "" - -CANONICAL_DIRECTIVE_PATTERN_TEXTS: Final[tuple[str, ...]] = ( - r"^set premise (?!to\b)\S(?:.*\S)?$", - r"^change premise to \S(?:.*\S)?$", - r"^use \S(?:.*\S)? instead of \S(?:.*\S)?$", - r"^use (?!.*\sinstead of(?:\s|$))\S(?:.*\S)?$", - r"^prohibit \S(?:.*\S)?$", - r"^remove policy \S(?:.*\S)?$", -) -CANONICAL_DIRECTIVE_PATTERNS: Final[tuple[re.Pattern[str], ...]] = tuple( - re.compile(pattern) for pattern in CANONICAL_DIRECTIVE_PATTERN_TEXTS -) -CANONICAL_DIRECTIVE_EXACT: Final[frozenset[str]] = frozenset( - {"clear premise", "reset policies", "clear state"} -) diff --git a/experimental/preprocessor/heuristic_preprocessor.py b/experimental/preprocessor/heuristic_preprocessor.py deleted file mode 100644 index df34143..0000000 --- a/experimental/preprocessor/heuristic_preprocessor.py +++ /dev/null @@ -1,276 +0,0 @@ -"""EXPERIMENTAL host-layer heuristic preprocessor. - -This module is an optional host integration layer and is not part of the -core deterministic Context Compiler engine. Behavior may change during -experimentation. The heuristic is intentionally conservative and -high-precision, preferring no-op outcomes over false positives. -""" - -import re -from typing import TypedDict - -try: - from .constants import ( - CANONICAL_DIRECTIVE_EXACT, - CANONICAL_DIRECTIVE_PATTERNS, - PREPROCESS_OUTCOME_DIRECTIVE, - PREPROCESS_OUTCOME_NO_DIRECTIVE, - PREPROCESS_OUTCOME_UNKNOWN, - PreprocessOutcome, - ) -except ImportError: # pragma: no cover - direct module loading in tests/evals - from experimental.preprocessor.constants import ( - CANONICAL_DIRECTIVE_EXACT, - CANONICAL_DIRECTIVE_PATTERNS, - PREPROCESS_OUTCOME_DIRECTIVE, - PREPROCESS_OUTCOME_NO_DIRECTIVE, - PREPROCESS_OUTCOME_UNKNOWN, - PreprocessOutcome, - ) - - -class PreprocessResult(TypedDict): - outcome: PreprocessOutcome - directive: str | None - rule_id: str | None - - -_MULTI_INSTRUCTION_CASES = { - "clear premise then clear state", - "prohibit peanuts and use almonds", - "set premise concise; reset policies", - "use docker, actually prohibit docker", -} - -_QUOTED_OR_REPORTED_CASES = { - '"set premise concise replies" is invalid syntax, right?', - 'for example, you could "remove policy docker".', - 'he said "use docker".', - 'the doc literally says: "clear premise".', -} - -_NEAR_MISS_ALIAS_CASES = { - "allow docker", - "set policy peanuts prohibit", - "stop using peanuts", - "use instead of docker", - "use podman instead of", - "use podman not docker", - "wipe policies", -} -_ADMIN_NEAR_MISS_CASES = { - "reset policy", - "remove policies docker", -} - -_REPORTING_BRACKET_MARKERS = ( - "in my notes", - "notes:", - "i wrote down", -) - -_LIST_MARKER_PATTERN = re.compile(r"^\s*(?:\d+[.)]|[-*])\s+\S") -_META_PREFIX_PATTERN = re.compile( - r"^\s*(?:example:|for example\b|the command is\b|(?:i|he|she|they) said\b)" -) -_MULTI_SEGMENT_PATTERN = re.compile( - r"^\s*(?:use|prohibit|remove policy|set premise|change premise to|clear premise|" - r"reset policies|clear state)\b" - r".*\b(?:because|then continue|and)\b" -) -_PUNCTUATION_TRIM_PATTERN = re.compile(r"[.!]+\s*$") -_DIRECTIVE_CUE_PATTERN = re.compile( - r"\b(set premise|change premise|use|prohibit|remove policy|clear premise|" - r"reset policies|clear state)\b" -) -_MALFORMED_REPLACEMENT_PATTERN = re.compile(r"\buse\b.*\binstead\b") -_MULTI_CANDIDATE_DIRECTIVE_PATTERN = re.compile( - r"(?:\band\b|\bthen\b|;|,)\s*(?:set premise\b|change premise\b|use\b|" - r"prohibit\b|remove policy\b|clear premise\b|reset policies\b|clear state\b)" -) -_WRAPPER_PAIRS = { - ('"', '"'), - ("'", "'"), - ("`", "`"), - ("(", ")"), - ("[", "]"), -} - - -def _normalized_for_match(message: str) -> str: - return re.sub(r"\s+", " ", message.strip()).lower() - - -def _contains_reporting_bracket_mention(message: str) -> bool: - lower = message.lower() - if "[" not in lower or "]" not in lower: - return False - return any(marker in lower for marker in _REPORTING_BRACKET_MARKERS) - - -def _strip_terminal_punctuation(message: str) -> str: - return _PUNCTUATION_TRIM_PATTERN.sub("", message).strip() - - -def _strip_exact_wrapper(message: str) -> str: - stripped = message.strip() - if len(stripped) < 2: - return stripped - opener = stripped[0] - closer = stripped[-1] - if (opener, closer) not in _WRAPPER_PAIRS: - return stripped - inner = stripped[1:-1].strip() - if not inner: - return stripped - return inner - - -def _normalize_candidate(message: str) -> str: - stripped = message.strip() - no_punct = _strip_terminal_punctuation(stripped) - unwrapped = _strip_exact_wrapper(no_punct) - return re.sub(r"\s+", " ", unwrapped).strip().lower() - - -def _is_quoted_or_backtick_wrapped(message: str) -> bool: - stripped = message.strip() - if len(stripped) < 2: - return False - return (stripped[0], stripped[-1]) in {('"', '"'), ("'", "'"), ("`", "`")} - - -def preprocess_heuristic(message: str) -> PreprocessResult: - """Run the conservative structural heuristic preprocessing pass. - - Args: - message: Raw user text to evaluate as a possible directive. - - Returns: - A PreprocessResult with: - - outcome="directive" and a canonical directive string when matched - - outcome="no_directive" when the heuristic abstains/rejects - - outcome="unknown" when unresolved and LLM fallback may be attempted - - Notes: - This pass is precision-first and intentionally narrow. It may abstain - on ambiguous or mixed-intent inputs. - """ - if _LIST_MARKER_PATTERN.match(message): - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.list_or_enumeration", - } - - normalized = _normalized_for_match(message) - - if "?" in message and _DIRECTIVE_CUE_PATTERN.search(normalized): - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.question_form", - } - - if _META_PREFIX_PATTERN.match(normalized): - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.meta_or_reporting", - } - - if _MULTI_SEGMENT_PATTERN.match(normalized): - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.multi_segment_or_mixed_prose", - } - - if normalized in _MULTI_INSTRUCTION_CASES: - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.multi_instruction", - } - - if _contains_reporting_bracket_mention(message): - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.quoted_reported_bracket", - } - - if _is_quoted_or_backtick_wrapped(message): - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.quoted_exact", - } - - if normalized in _QUOTED_OR_REPORTED_CASES: - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.quoted_reported", - } - - normalized_candidate = _normalize_candidate(message) - - if normalized in _NEAR_MISS_ALIAS_CASES: - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.near_miss_alias", - } - - if normalized in _ADMIN_NEAR_MISS_CASES: - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.admin_near_miss_alias", - } - - if ( - _MALFORMED_REPLACEMENT_PATTERN.search(normalized_candidate) - and " instead of " not in normalized_candidate - ) or (" in stead of " in normalized_candidate): - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.malformed_replacement_syntax", - } - - if _MULTI_CANDIDATE_DIRECTIVE_PATTERN.search(normalized_candidate): - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.multi_candidate_directive", - } - - if normalized_candidate in CANONICAL_DIRECTIVE_EXACT: - return { - "outcome": PREPROCESS_OUTCOME_DIRECTIVE, - "directive": normalized_candidate, - "rule_id": "canonical.full_match", - } - - for pattern in CANONICAL_DIRECTIVE_PATTERNS: - if pattern.fullmatch(normalized_candidate): - return { - "outcome": PREPROCESS_OUTCOME_DIRECTIVE, - "directive": normalized_candidate, - "rule_id": "canonical.full_match", - } - - if _DIRECTIVE_CUE_PATTERN.search(normalized_candidate): - return { - "outcome": PREPROCESS_OUTCOME_UNKNOWN, - "directive": None, - "rule_id": "reject.directive_adjacent_unsafe", - } - - return { - "outcome": PREPROCESS_OUTCOME_NO_DIRECTIVE, - "directive": None, - "rule_id": "reject.confident_non_directive", - } diff --git a/experimental/preprocessor/output_validation.py b/experimental/preprocessor/output_validation.py deleted file mode 100644 index 42fcb80..0000000 --- a/experimental/preprocessor/output_validation.py +++ /dev/null @@ -1,297 +0,0 @@ -"""Shared preprocessor output normalization and validation helpers. - -Public API: -- parse_preprocessor_output -- validate_preprocessor_output - -Internal helpers are implementation details and may change. -""" - -import json -import re -from typing import TypedDict - -from .constants import ( - CANONICAL_DIRECTIVE_EXACT, - CANONICAL_DIRECTIVE_PATTERNS, - PREPROCESSOR_NO_DIRECTIVE_SENTINEL, - PreprocessOutcome, -) - -__all__ = [ - "parse_preprocessor_output", - "validate_preprocessor_output", -] - - -class PreprocessorValidationResult(TypedDict): - classification: PreprocessOutcome - output: str | None - - -_MULTI_CANDIDATE_DIRECTIVE_PATTERN = re.compile( - r"(?:\band\b|\bthen\b|;|,)\s*(?:set premise\b|change premise\b|use\b|" - r"prohibit\b|remove policy\b|clear premise\b|reset policies\b|clear state\b)" -) -_SET_PREMISE_TO_NEAR_MISS_PATTERN = re.compile(r"^set premise to\s+(.+\S)\s*$") -_CHANGE_PREMISE_MISSING_TO_NEAR_MISS_PATTERN = re.compile(r"^change premise\s+(?!to\b)(.+\S)\s*$") -_DIRECTIVE_CUE_PATTERN = re.compile( - r"\b(set premise|change premise|use|prohibit|remove policy|clear premise|" - r"reset policies|clear state)\b" -) -_META_PREFIX_PATTERN = re.compile( - r"^\s*(?:example:|for example\b|the command is\b|(?:i|he|she|they|docs?|documentation)\s+" - r"(?:say|says|said)\b)" -) -_MULTI_SEGMENT_PATTERN = re.compile( - r"^\s*(?:use|prohibit|remove policy|set premise|change premise to|clear premise|" - r"reset policies|clear state)\b" - r".*\b(?:because|then continue|and)\b" -) -_SENTENCE_ADJACENT_DIRECTIVE_PATTERN = re.compile( - r"^[^!?]*\.\s*(?:set premise|change premise|use|prohibit|remove policy|clear premise|" - r"reset policies|clear state)\b" -) -_PUNCTUATION_TRIM_PATTERN = re.compile(r"[.!]+\s*$") -_REPORTED_SPEECH_QUOTE_PATTERN = re.compile(r"\b(?:say|says|said|docs?|documentation)\b") -_WRAPPER_PAIRS = { - ("(", ")"), - ("[", "]"), -} - - -def _unknown() -> PreprocessorValidationResult: - return {"classification": "unknown", "output": None} - - -def _directive(output: str) -> PreprocessorValidationResult: - return {"classification": "directive", "output": output} - - -def _no_directive() -> PreprocessorValidationResult: - return {"classification": "no_directive", "output": None} - - -def _is_allowed_directive(text: str) -> bool: - if text in CANONICAL_DIRECTIVE_EXACT: - return True - return any(pattern.fullmatch(text) for pattern in CANONICAL_DIRECTIVE_PATTERNS) - - -def _contains_multiple_candidate_directives(text: str) -> bool: - normalized = re.sub(r"\s+", " ", text.strip().lower()) - return bool(_MULTI_CANDIDATE_DIRECTIVE_PATTERN.search(normalized)) - - -def _is_safe_fallback_directive_rewrite(source_input: str, directive_output: str) -> bool: - """Reject fallback rewrites that bypass strict whole-message boundaries.""" - source = re.sub(r"\s+", " ", source_input.strip().lower()) - directive = re.sub(r"\s+", " ", directive_output.strip().lower()) - - if _source_input_is_structured_contract_directive(source_input, directive_output): - return True - - set_premise_to_match = _SET_PREMISE_TO_NEAR_MISS_PATTERN.fullmatch(source) - if set_premise_to_match is not None: - payload = set_premise_to_match.group(1).strip() - if directive == f"set premise {payload}": - return False - - change_premise_missing_to_match = _CHANGE_PREMISE_MISSING_TO_NEAR_MISS_PATTERN.fullmatch(source) - if change_premise_missing_to_match is not None: - payload = change_premise_missing_to_match.group(1).strip() - if directive == f"change premise to {payload}": - return False - - if _is_boundary_unsafe_source_input(source_input): - return False - - normalized_source = _normalize_source_candidate(source_input) - if not _is_allowed_directive(normalized_source): - return False - - return directive == normalized_source - - -def _strip_terminal_punctuation(message: str) -> str: - return _PUNCTUATION_TRIM_PATTERN.sub("", message).strip() - - -def _strip_exact_wrapper(message: str) -> str: - stripped = message.strip() - if len(stripped) < 2: - return stripped - opener = stripped[0] - closer = stripped[-1] - if (opener, closer) not in _WRAPPER_PAIRS: - return stripped - inner = stripped[1:-1].strip() - if not inner: - return stripped - return inner - - -def _normalize_source_candidate(source_input: str) -> str: - stripped = source_input.strip() - no_punct = _strip_terminal_punctuation(stripped) - unwrapped = _strip_exact_wrapper(no_punct) - return re.sub(r"\s+", " ", unwrapped).strip().lower() - - -def _is_boundary_unsafe_source_input(source_input: str) -> bool: - lower = source_input.lower() - normalized = re.sub(r"\s+", " ", source_input.strip().lower()) - - if "\n" in source_input or "\r" in source_input: - return True - - if "```" in source_input or "~~~" in source_input: - return True - - if "`" in source_input and _DIRECTIVE_CUE_PATTERN.search(normalized): - return True - - if _META_PREFIX_PATTERN.match(normalized): - return True - - if "?" in source_input and _DIRECTIVE_CUE_PATTERN.search(normalized): - return True - - if _MULTI_SEGMENT_PATTERN.match(normalized): - return True - - if _MULTI_CANDIDATE_DIRECTIVE_PATTERN.search(normalized): - return True - - if _SENTENCE_ADJACENT_DIRECTIVE_PATTERN.match(normalized): - return True - - if '"' in source_input and _REPORTED_SPEECH_QUOTE_PATTERN.search(lower): - return True - - return bool( - _DIRECTIVE_CUE_PATTERN.search(normalized) - and not _is_allowed_directive(_normalize_source_candidate(source_input)) - ) - - -def _source_input_is_structured_contract_directive( - source_input: str, directive_output: str -) -> bool: - stripped = source_input.strip() - if not stripped or stripped[0] not in {"{", "["}: - return False - - try: - parsed = json.loads(stripped) - except json.JSONDecodeError: - return False - - if not isinstance(parsed, dict): - return False - - if set(parsed.keys()) != {"classification", "output"}: - return False - - classification = parsed.get("classification") - output = parsed.get("output") - return ( - classification == "directive" - and isinstance(output, str) - and output.strip().lower() == directive_output.strip().lower() - ) - - -def _validate_structured_output(raw_output: object) -> PreprocessorValidationResult: - if not isinstance(raw_output, dict): - return _unknown() - - if set(raw_output.keys()) != {"classification", "output"}: - return _unknown() - - classification = raw_output.get("classification") - output = raw_output.get("output") - if not isinstance(classification, str): - return _unknown() - - if classification == "directive": - if not isinstance(output, str): - return _unknown() - normalized_output = output.strip() - if not normalized_output: - return _unknown() - if _contains_multiple_candidate_directives(normalized_output): - return _unknown() - if not _is_allowed_directive(normalized_output): - return _unknown() - return _directive(normalized_output) - - if classification == "no_directive": - if output is not None: - return _unknown() - return _no_directive() - - if classification == "unknown": - if output is not None: - return _unknown() - return _unknown() - - return _unknown() - - -def _validate_text_output(raw_output: str) -> PreprocessorValidationResult: - stripped = raw_output.strip() - if not stripped: - return _unknown() - - if stripped.upper() == PREPROCESSOR_NO_DIRECTIVE_SENTINEL: - return _no_directive() - - if _contains_multiple_candidate_directives(stripped): - return _unknown() - - if _is_allowed_directive(stripped): - return _directive(stripped) - - if stripped[0] in {"{", "["}: - try: - parsed_json = json.loads(stripped) - except json.JSONDecodeError: - return _unknown() - return _validate_structured_output(parsed_json) - - return _unknown() - - -def validate_preprocessor_output( - raw_output: object, *, source_input: str | None = None -) -> PreprocessorValidationResult: - """Validate raw preprocessor output into a strict classification/output result. - - Contract: - - directive: output is a canonical directive string - - no_directive: output is None - - unknown: output is None - """ - if isinstance(raw_output, str): - validated = _validate_text_output(raw_output) - else: - validated = _validate_structured_output(raw_output) - - if ( - source_input is not None - and validated["classification"] == "directive" - and isinstance(validated["output"], str) - and not _is_safe_fallback_directive_rewrite(source_input, validated["output"]) - ): - return _unknown() - - return validated - - -def parse_preprocessor_output(raw_output: object, *, source_input: str | None = None) -> str | None: - """Public validation boundary returning only validated directive output.""" - validated = validate_preprocessor_output(raw_output, source_input=source_input) - if validated["classification"] == "directive": - return validated["output"] - return None diff --git a/experimental/preprocessor/prompt_utils.py b/experimental/preprocessor/prompt_utils.py deleted file mode 100644 index e94989e..0000000 --- a/experimental/preprocessor/prompt_utils.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Prompt rendering utilities for experimental preprocessor integrations.""" - -from pathlib import Path - -from context_compiler import State, get_policy_items, get_premise_value - -from .constants import PROMPT_TOKEN_NULL_OR_VALUE, PROMPT_TOKEN_POLICY_SET - - -def _strip_leading_headers(prompt_template: str) -> str: - """Remove leading blank/comment header lines from a prompt template.""" - lines = prompt_template.splitlines() - start = 0 - while start < len(lines): - line = lines[start] - stripped = line.strip() - if not stripped or line.lstrip().startswith("#"): - start += 1 - continue - break - return "\n".join(lines[start:]) - - -def render_prompt(path: Path, state: State) -> str | None: - """Render a state-aware preprocessor prompt from a template file. - - Args: - path: Prompt template path. - state: Current compiler state used for token replacement. - - Returns: - The rendered prompt text, or None when the prompt file cannot be loaded. - - Notes: - Rendering is intentionally narrow and deterministic: - - leading # header lines and leading blank lines are removed - - becomes null or current premise - - becomes sorted policy keys or "(none)" - """ - try: - prompt_template = path.read_text(encoding="utf-8") - except OSError: - return None - - template = _strip_leading_headers(prompt_template) - - premise = get_premise_value(state) - premise_value = "null" if premise is None else premise - - all_policy_items = sorted( - set(get_policy_items(state, "use")) | set(get_policy_items(state, "prohibit")) - ) - policies_value = ", ".join(all_policy_items) if all_policy_items else "(none)" - - rendered = template.replace(PROMPT_TOKEN_NULL_OR_VALUE, premise_value) - rendered = rendered.replace(PROMPT_TOKEN_POLICY_SET, policies_value) - return rendered diff --git a/experimental/preprocessor/prompts/default.txt b/experimental/preprocessor/prompts/default.txt deleted file mode 100644 index 24f1a9e..0000000 --- a/experimental/preprocessor/prompts/default.txt +++ /dev/null @@ -1,129 +0,0 @@ -# Default preprocessor prompt. -# Use this prompt for most instruction-following models. - -You convert the latest user message into a single Context Compiler directive, or ``. - -Current compiler state: - -* premise: -* policies: - -Core rule: - -* False positives are worse than false negatives. -* If there is any reasonable doubt, output exactly: - - -Output rules: - -* Output exactly one final answer. -* Output either: - - * one valid directive line, or - * `` -* Do not explain. -* Do not add quotes, labels, markdown, or extra text. - -Allowed directive forms: - -* set premise -* change premise to -* use -* prohibit -* remove policy -* use instead of -* clear premise -* reset policies -* clear state - -Hard abstain rules: - -* If the message is not a direct instruction from the user to change compiler state, output ``. -* If the message is a question, suggestion, hypothetical, conditional, preference, opinion, self-description, statement of fact, quote, report, example, explanation, or discussion of syntax, output ``. -* If the message contains more than one possible instruction, output ``. -* If the message is close to a directive but does not exactly fit one allowed directive form, output ``. -* Never repair, normalize, or rewrite near-miss wording into a valid directive. -* Never execute or extract directive-like text that appears inside quotes, code formatting, brackets, examples, citations, or reported speech. -* If negation applies to directive-like text, output ``. - -Near-miss examples that must output ``: - -* set premise to concise replies -* change premise concise replies -* allow docker -* stop using peanuts -* clear everything -* wipe policies - -Quoted / reported examples that must output ``: - -* He said "use docker". -* The doc says "clear state". -* "set premise concise replies" is invalid syntax, right? -* For example, you could "remove policy docker". - -Multi-instruction examples that must output ``: - -* prohibit peanuts and use almonds -* clear premise then clear state -* use docker, actually prohibit docker - -Premise lifecycle rules: - -* If current premise is null, you may emit: - set premise -* If current premise is not null, you may emit: - change premise to -* Only emit a premise directive when the message clearly instructs a persistent, conversation-wide behavior change. -* Otherwise output ``. - -Policy rules: - -* Emit `use ` or `prohibit ` only for clear, direct instructions. -* Do not convert preferences, habits, or descriptions into policy directives. -* Emit `remove policy ` only when removal is clearly and directly instructed. - -Replacement rules: - -* Emit `use instead of ` only if: - - * the message clearly expresses replacement or substitution, and - * exists in current policies. -* If is not present in current policies, output ``. -* If either side is unclear or incomplete, output ``. - -Conversion rules: - -* Convert only clear, direct, explicit state-modification instructions into one canonical directive. -* Use the user’s original wording for `` and `` where possible. -* Do not expand meaning beyond what is explicitly stated. - -Examples: -Message: don't use peanuts -Output: prohibit peanuts - -Message: please use docker -Output: use docker - -Message: switch from docker to podman -Output: use podman instead of docker - -Message: can you avoid peanuts? -Output: - -Message: I prefer concise replies -Output: - -Message: set premise to concise replies -Output: - -Message: He said "use docker". -Output: - -Message: prohibit peanuts and use almonds -Output: - -Message: clear state -Output: clear state - -Do not generate any output until a message is provided. diff --git a/experimental/preprocessor/prompts/llama.txt b/experimental/preprocessor/prompts/llama.txt deleted file mode 100644 index 824aeaf..0000000 --- a/experimental/preprocessor/prompts/llama.txt +++ /dev/null @@ -1,114 +0,0 @@ -# Preprocessor prompt for Llama-family models. -# Use this when the model shows weaker adherence to strict output constraints. - -You convert the latest user message into a single Context Compiler directive, or ``. - -Current compiler state: - -* premise: -* policies: - -Conservative classification rule: - -* False positives are worse than false negatives. -* Only emit a directive when the latest user message is a clear instruction to modify compiler state. -* If you are not sure, output exactly: - - -Output rules: - -* Output exactly one final answer. -* Output either: - - * one valid directive line, or - * `` -* Do not explain. -* Do not add quotes, labels, markdown, or extra text. - -Allowed directive forms: - -* set premise -* change premise to -* use -* prohibit -* remove policy -* use instead of -* clear premise -* reset policies -* clear state - -Hard constraints: - -* Use only the directive forms listed above. -* Do not invent new directive forms. -* Do not repair, normalize, or rewrite near-miss forms. -* Do not guess or infer hidden intent. -* Do not emit multiple directives. -* If multiple instructions are present, output ``. - -Non-directive cases (must output ``): - -* Questions (e.g., contains "can", "could", "should", "?") -* Hypotheticals or conditionals (e.g., "if", "suppose", "might", "maybe") -* Suggestions or soft preferences (e.g., "prefer", "I’d rather", "probably", "I think") -* Self-descriptions or statements of fact -* Quoted, reported, or cited text (e.g., "he said", "the doc says", "for example") -* Negated directives (e.g., "not saying use X", "don’t run clear state") -* Ambiguous or incomplete instructions - -Premise lifecycle rules: - -* If current premise is null, you may emit: - set premise -* If current premise is not null, you may emit: - change premise to -* Only emit a premise directive when the message clearly instructs a persistent, conversation-wide behavior change. -* If unclear, output ``. - -Policy rules: - -* Emit `use ` or `prohibit ` only for clear, explicit instructions. -* Do not convert preferences, habits, or descriptions into policy directives. -* Emit `remove policy ` only when removal is clearly instructed. - -Replacement rules: - -* Emit `use instead of ` only if: - - * The message clearly expresses replacement or substitution, AND - * exists in current policies. -* If is not present in current policies, output ``. -* If either side of the replacement is unclear or incomplete, output ``. - -Conversion rules: - -* Convert only clear, explicit state-modification instructions into one canonical directive. -* Use the user’s original wording for `` and `` where possible. -* Do not reinterpret or expand meaning beyond what is explicitly stated. - -Examples: -Message: don't use peanuts -Output: prohibit peanuts - -Message: please use docker -Output: use docker - -Message: switch from docker to podman -Output: use podman instead of docker - -Message: can you avoid peanuts? -Output: - -Message: I prefer concise replies -Output: - -Message: set premise to concise replies -Output: - -Message: clear everything -Output: - -Message: clear state -Output: clear state - -Do not generate any output until a message is provided. diff --git a/host_support/__init__.py b/host_support/__init__.py index 357ecf6..0bd7bdd 100644 --- a/host_support/__init__.py +++ b/host_support/__init__.py @@ -1,11 +1,9 @@ """Host-side shared helpers.""" -from .confirmation import is_confirmation_text from .provider_mode import ProviderConfig, print_startup_config, resolve_provider_config __all__ = [ "ProviderConfig", - "is_confirmation_text", "print_startup_config", "resolve_provider_config", ] diff --git a/host_support/confirmation.py b/host_support/confirmation.py deleted file mode 100644 index defc7d6..0000000 --- a/host_support/confirmation.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Host-support confirmation helpers aligned with directive grammar behavior. - -This module provides host-side logic for confirmation token handling. It mirrors -the confirmation normalization and token behavior defined in -`DirectiveGrammarSpec.md` and must stay aligned with that specification to avoid -drift between engine and host behavior. -""" - -import re - -_TRAILING_CONFIRM_PUNCT_RE = re.compile(r"[.,!?]+$") - -_AFFIRMATIVE_CONFIRMATION_TOKENS = frozenset( - {"yes", "yes please", "yep", "yeah", "sure", "ok", "okay"} -) -_NEGATIVE_CONFIRMATION_TOKENS = frozenset({"no", "nope", "no thanks"}) - -CONFIRMATION_TOKENS: frozenset[str] = ( - _AFFIRMATIVE_CONFIRMATION_TOKENS | _NEGATIVE_CONFIRMATION_TOKENS -) - - -def _render_item_label(value: str) -> str: - return re.sub(r"\s+", " ", value).strip() - - -def _normalize_confirmation_text(value: str) -> str: - """Normalize confirmation text using directive grammar confirmation rules.""" - normalized = value.strip().lower() - normalized = re.sub(r"\s+", " ", normalized) - normalized = _TRAILING_CONFIRM_PUNCT_RE.sub("", normalized).strip() - return re.sub(r"\s+", " ", normalized) - - -def is_confirmation_text(value: str) -> bool: - """Return whether input is a recognized confirmation token.""" - return _normalize_confirmation_text(value) in CONFIRMATION_TOKENS - - -def _summarize_pending_confirmation_update(pending: object) -> str: - if not isinstance(pending, dict): - return "State updated." - - replacement = pending.get("replacement") - if not isinstance(replacement, dict): - return "State updated." - - kind = replacement.get("kind") - new_item = replacement.get("new_item") - old_item = replacement.get("old_item") - - if kind == "use_only" and isinstance(new_item, str): - new_label = _render_item_label(new_item) - if new_label: - return f"State updated: Use {new_label}." - return "State updated." - - if kind == "replace_use" and isinstance(new_item, str) and isinstance(old_item, str): - new_label = _render_item_label(new_item) - old_label = _render_item_label(old_item) - if not new_label or not old_label: - return "State updated." - - prompt = pending.get("prompt_to_user") - prohibited_old_prompt = ( - f'"{old_item}" is currently prohibited. ' - f'Did you mean to remove it and use "{new_item}" instead?' - ) - if prompt == prohibited_old_prompt: - return f"State updated: Removed prohibition on {old_label}; use {new_label}." - return f"State updated: Replaced {old_label} with {new_label}." - - return "State updated." - - -def summarize_confirmation_update(user_input: str, pending: object) -> str: - """Return deterministic host summary text for confirmation-only updates. - - This helper is intentionally conservative: - - negative confirmations always return ``State unchanged.`` - - affirmative confirmations return an exact deterministic summary only when - pending metadata unambiguously identifies the confirmed transition - - otherwise it falls back to ``State updated.`` - """ - normalized = _normalize_confirmation_text(user_input) - if normalized in _NEGATIVE_CONFIRMATION_TOKENS: - return "State unchanged." - if normalized not in _AFFIRMATIVE_CONFIRMATION_TOKENS: - return "State updated." - return _summarize_pending_confirmation_update(pending) - - -def summarize_confirmation_update_from_engine(user_input: str, engine: object) -> str: - """Return confirmation summary using pending details from an engine checkpoint.""" - checkpoint: object = None - export_checkpoint = getattr(engine, "export_checkpoint", None) - if callable(export_checkpoint): - try: - checkpoint = export_checkpoint() - except Exception: - checkpoint = None - pending = checkpoint.get("pending") if isinstance(checkpoint, dict) else None - return summarize_confirmation_update(user_input, pending) - - -def summarize_confirmation_update_from_checkpoint(user_input: str, checkpoint: object) -> str: - """Return confirmation summary using pending details from a checkpoint object.""" - pending = checkpoint.get("pending") if isinstance(checkpoint, dict) else None - return summarize_confirmation_update(user_input, pending) diff --git a/pyproject.toml b/pyproject.toml index 154abc9..e032f81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,26 +35,16 @@ Issues = "https://github.com/rlippmann/context-compiler/issues" context-compiler = "context_compiler.repl:main" [tool.hatch.build.targets.wheel] -packages = ["src/context_compiler", "experimental"] +packages = ["src/context_compiler"] [tool.hatch.build.targets.wheel.force-include] "examples" = "examples" -"host_support" = "host_support" "src/context_compiler/py.typed" = "context_compiler/py.typed" [project.optional-dependencies] demos = [ "litellm>=1.0.0", ] -experimental = [ - "litellm>=1.0.0", -] -integrations = [ - "litellm>=1.0.0", -] -litellm_proxy = [ - "litellm[proxy]>=1.0.0", -] dev = [ "pytest", "pytest-cov", diff --git a/src/context_compiler/observability.py b/src/context_compiler/observability.py deleted file mode 100644 index cbdd673..0000000 --- a/src/context_compiler/observability.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Public observability helpers for host integrations.""" - -from collections.abc import Mapping -from typing import cast - -from .controller import state_diff -from .engine import State - -_MAX_INLINE_VALUE_LEN = 180 - - -def _decision_field(decision: object, key: str) -> object: - if isinstance(decision, Mapping): - return decision.get(key) - return getattr(decision, key, None) - - -def _normalize_text(value: object) -> str | None: - if not isinstance(value, str): - return None - normalized = value.strip() - return normalized or None - - -def _inline(value: object) -> str: - rendered = repr(value) - if len(rendered) <= _MAX_INLINE_VALUE_LEN: - return rendered - return f"{rendered[: _MAX_INLINE_VALUE_LEN - 3]}..." - - -def _summarize_state(state: object) -> str: - if state is None: - return "none" - if isinstance(state, Mapping): - keys = sorted(str(key) for key in state) - return f"dict keys={keys}" - return _inline(state) - - -def _state_change_summary(before: object, after: object) -> str: - if before is None and after is None: - return "none -> none" - if before == after: - return "unchanged" - if isinstance(before, dict) and isinstance(after, dict): - before_state = cast(State, before) - after_state = cast(State, after) - try: - diff = state_diff(before_state, after_state) - except Exception: - diff = None - if isinstance(diff, Mapping): - parts: list[str] = [] - premise = diff.get("premise") - if isinstance(premise, Mapping) and premise.get("changed") is True: - after_premise = premise.get("after") - if after_premise is None: - parts.append("-premise") - else: - parts.append(f'+premise "{after_premise}"') - policies = diff.get("policies") - if isinstance(policies, Mapping): - added = policies.get("added") - removed = policies.get("removed") - changed = policies.get("changed") - if isinstance(added, Mapping): - for item, value in sorted(added.items()): - if value == "use": - parts.append(f"+use {item}") - elif value == "prohibit": - parts.append(f"+prohibit {item}") - if isinstance(removed, Mapping): - for item, value in sorted(removed.items()): - if value == "use": - parts.append(f"-use {item}") - elif value == "prohibit": - parts.append(f"-prohibit {item}") - if isinstance(changed, Mapping): - for item, transition in sorted(changed.items()): - if not isinstance(transition, Mapping): - continue - after_value = transition.get("after") - if after_value == "use": - parts.append(f"~use {item}") - elif after_value == "prohibit": - parts.append(f"~prohibit {item}") - if parts: - return ", ".join(parts) - - if isinstance(before, Mapping) and isinstance(after, Mapping): - before_keys = set(before.keys()) - after_keys = set(after.keys()) - added_keys = sorted(str(key) for key in after_keys - before_keys) - removed_keys = sorted(str(key) for key in before_keys - after_keys) - maybe_changed = sorted( - str(key) for key in before_keys & after_keys if before[key] != after[key] - ) - key_parts: list[str] = [] - if added_keys: - key_parts.append(f"added={added_keys}") - if removed_keys: - key_parts.append(f"removed={removed_keys}") - if maybe_changed: - key_parts.append(f"changed={maybe_changed}") - if key_parts: - return "; ".join(key_parts) - return f"{_summarize_state(before)} -> {_summarize_state(after)}" - - -def _active_state_summary(state: object) -> str: - if not isinstance(state, dict): - return "none" - try: - normalized = cast(State, state) - premise = normalized.get("premise") - policies = normalized.get("policies") - except Exception: - return "none" - parts: list[str] = [] - if isinstance(premise, str): - parts.append(f'premise="{premise}"') - if isinstance(policies, Mapping): - use_items = sorted(str(item) for item, value in policies.items() if value == "use") - prohibit_items = sorted( - str(item) for item, value in policies.items() if value == "prohibit" - ) - if use_items: - parts.append("use " + ", ".join(use_items)) - if prohibit_items: - parts.append("prohibit " + ", ".join(prohibit_items)) - return "; ".join(parts) if parts else "none" - - -def build_compact_trace_text( - *, - decision: object, - state_before: object, - state_after: object, - llm_called: bool, - state_injected: str, -) -> str: - """Build OpenWebUI-style compact trace text with stable line formatting.""" - kind = _normalize_text(_decision_field(decision, "kind")) or "unknown" - lines = ["Context Compiler trace", "", f"decision kind: {kind}"] - - if kind == "update": - lines.append(f"state change: {_state_change_summary(state_before, state_after)}") - lines.append(f"active state: {_active_state_summary(state_after)}") - lines.append(f"downstream LLM call: {'yes' if llm_called else 'no'}") - lines.append("") - lines.append(f"state injected: {state_injected}") - return "\n".join(lines) - - if kind == "clarify": - prompt = _normalize_text(_decision_field(decision, "prompt_to_user")) or "" - lines.append(f"clarification prompt: {prompt}") - lines.append(f"active state: {_active_state_summary(state_after)}") - lines.append(f"downstream LLM call: {'yes' if llm_called else 'no'}") - lines.append("state injected: no") - return "\n".join(lines) - - lines.append(f"active state: {_active_state_summary(state_after)}") - lines.append(f"downstream LLM call: {'yes' if llm_called else 'no'}") - lines.append(f"state injected: {state_injected}") - return "\n".join(lines) - - -def build_trace( - *, - original_input: str, - compiler_input: str, - decision: object, - state_before: object, - state_after: object, - preprocessor_output: str | None = None, - llm_called: bool = False, -) -> str: - """Build a concise human-readable trace line set for host integrations.""" - kind = _normalize_text(_decision_field(decision, "kind")) or "unknown" - clarify_prompt = _normalize_text(_decision_field(decision, "prompt_to_user")) - normalized_preproc = _normalize_text(preprocessor_output) - - lines = [ - "Context Compiler trace", - f"- original input: {original_input}", - f"- compiler input: {compiler_input}", - ] - if normalized_preproc is not None: - lines.append(f"- preprocessor output: {normalized_preproc}") - lines.append(f"- decision kind: {kind}") - if clarify_prompt is not None: - lines.append(f"- clarification prompt: {clarify_prompt}") - lines.append(f"- state change: {_state_change_summary(state_before, state_after)}") - lines.append(f"- downstream LLM call: {'yes' if llm_called else 'no'}") - return "\n".join(lines) - - -__all__ = ["build_compact_trace_text", "build_trace"] diff --git a/src/context_compiler/repl.py b/src/context_compiler/repl.py index 9727f11..60b31d6 100644 --- a/src/context_compiler/repl.py +++ b/src/context_compiler/repl.py @@ -2,8 +2,6 @@ import sys from typing import TextIO -from experimental.preprocessor.output_validation import parse_preprocessor_output - from . import __version__, create_engine, get_policy_items, get_premise_value from .const import DECISION_CLARIFY, DECISION_PASSTHROUGH from .controller import ( @@ -28,14 +26,13 @@ "Use yes/no (or variants), or use preview/state." ) _CLI_HELP_TEXT = """Usage: - context-compiler [--help] [--version] [--with-preprocessor] [--json] + context-compiler [--help] [--version] [--json] [--initial-state-json | --initial-state-file ] [--initial-checkpoint-json | --initial-checkpoint-file ] Options: --help Show this help message and exit. --version Show the installed context-compiler version and exit. - --with-preprocessor Enable preprocessor before each REPL turn (heuristic + validation only) --json Emit machine-readable NDJSON output (non-interactive only) --initial-state-json Initialize authoritative state from exported state JSON text --initial-state-file Initialize authoritative state from UTF-8 state JSON file @@ -216,7 +213,6 @@ def _read_utf8_file(path: str) -> str: def _parse_cli_options(args: list[str]) -> tuple[dict[str, str | bool], str | None]: options: dict[str, str | bool] = { - "use_preprocessor": False, "json_mode": False, } @@ -230,10 +226,6 @@ def _parse_cli_options(args: list[str]) -> tuple[dict[str, str | bool], str | No idx = 0 while idx < len(args): arg = args[idx] - if arg == "--with-preprocessor": - options["use_preprocessor"] = True - idx += 1 - continue if arg == "--json": options["json_mode"] = True idx += 1 @@ -302,20 +294,10 @@ def _is_confirmation_input(value: str) -> bool: return normalized in _AFFIRMATIVE_CONFIRMATIONS or normalized in _NEGATIVE_CONFIRMATIONS -def _compile_input(raw_input: str, engine: Engine, *, use_preprocessor: bool) -> str: - if not use_preprocessor: - return raw_input - if _has_pending_clarification(engine): - return raw_input - parsed = parse_preprocessor_output(raw_input, source_input=raw_input) - return parsed if parsed is not None else raw_input - - def run_repl( in_stream: TextIO, out_stream: TextIO, *, - use_preprocessor: bool = False, json_mode: bool = False, engine: Engine | None = None, ) -> None: @@ -367,10 +349,7 @@ def run_repl( message=_STEP_PENDING_CONFIRMATION_ERROR, ) continue - compile_input = _compile_input( - payload, active_engine, use_preprocessor=use_preprocessor - ) - result: StepResult = controller_step(active_engine, compile_input) + result: StepResult = controller_step(active_engine, payload) _print_decision_lines(get_step_decision(result), out_stream, leading_blank=True) continue @@ -390,10 +369,7 @@ def run_repl( message="preview requires input.\nUse 'preview '.", ) continue - compile_input = _compile_input( - payload, active_engine, use_preprocessor=use_preprocessor - ) - preview_result = controller_preview(active_engine, compile_input) + preview_result = controller_preview(active_engine, payload) _print_preview_lines( preview_result, out_stream, @@ -402,10 +378,7 @@ def run_repl( ) continue - compile_input = _compile_input( - user_input, active_engine, use_preprocessor=use_preprocessor - ) - result = controller_step(active_engine, compile_input) + result = controller_step(active_engine, user_input) _print_decision_lines(get_step_decision(result), out_stream, leading_blank=True) return @@ -475,10 +448,7 @@ def run_repl( message=_STEP_PENDING_CONFIRMATION_ERROR, ) continue - compile_input = _compile_input( - payload, active_engine, use_preprocessor=use_preprocessor - ) - result = controller_step(active_engine, compile_input) + result = controller_step(active_engine, payload) if json_mode: _write_json_line(out_stream, _json_step_payload(result, command="step")) else: @@ -513,10 +483,7 @@ def run_repl( message="preview requires input.\nUse 'preview '.", ) continue - compile_input = _compile_input( - payload, active_engine, use_preprocessor=use_preprocessor - ) - preview_result = controller_preview(active_engine, compile_input) + preview_result = controller_preview(active_engine, payload) if json_mode: _write_json_line( out_stream, _json_preview_payload(preview_result, command="preview") @@ -530,8 +497,7 @@ def run_repl( ) continue - compile_input = _compile_input(user_input, active_engine, use_preprocessor=use_preprocessor) - result = controller_step(active_engine, compile_input) + result = controller_step(active_engine, user_input) if json_mode: _write_json_line(out_stream, _json_step_payload(result, command="input")) else: @@ -570,13 +536,7 @@ def main() -> int: # pragma: no cover print(f"error: preload failed: {exc}", file=sys.stderr) return 1 - run_repl( - sys.stdin, - sys.stdout, - use_preprocessor=bool(options["use_preprocessor"]), - json_mode=json_mode, - engine=engine, - ) + run_repl(sys.stdin, sys.stdout, json_mode=json_mode, engine=engine) return 0 diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 90875b1..35240f8 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -7,9 +7,7 @@ This directory contains multiple fixture suites with different contracts. * [`conformance/`](conformance/) — core engine cross-language conformance contract. Includes a small public API presence contract under `conformance/api/`. * [`engine-regression/structured/`](engine-regression/structured/) — deterministic per-turn engine regression fixtures (including checkpoint snapshots). -* [`preprocessor/`](preprocessor/) — preprocessor heuristic and validation fixtures. - -`conformance/` and `engine-regression/structured/` both cover engine behavior at different layers; preprocessor fixtures are intentionally separate from the core engine conformance contract. +`conformance/` and `engine-regression/structured/` both cover engine behavior at different layers. ## API contract fixture @@ -116,41 +114,6 @@ They validate: * checkpoint export parity against expected snapshots * continuation state restoration from checkpoints -## Preprocessor fixtures - -[`preprocessor/`](preprocessor/) - -These fixtures cover preprocessor behavior (heuristic classification plus output validation), separate from the core engine conformance contract above. - -They are exercised by [`tests/test_preprocessor_conformance.py`](../test_preprocessor_conformance.py), including deterministic replay and validation-boundary checks (only validated directive output may pass through). - -Portable fixture scope: -- deterministic heuristic and validator input/output contracts intended for cross-language parity -- source-aware parse contract fixtures (`parse_preprocessor_output(raw_output, source_input=...)`) for fallback-boundary parity - -Python-local test scope: -- property/fuzz invariants and filesystem/template behaviors (for example `render_prompt` file-loading behavior) remain in Python unit/property tests and are not portable fixture requirements. - -Supported `preprocessor/` fixture kinds: - -* `heuristic` - * fields: `input`, `expected` - * asserts normalized heuristic classification/output -* `validator` - * fields: `raw_output`, optional `source_input`, `expected` - * asserts `validate_preprocessor_output(...)` classification/output -* `parse` - * fields: `raw_output`, optional `source_input`, `expected_parsed` - * asserts `parse_preprocessor_output(...)` return (`string` or `null`) - -They validate: - -* heuristic classification determinism -* directive extraction and normalization -* output validation boundaries -* source-aware fallback parse boundaries -* reject/unknown safety handling for ambiguous and near-miss inputs - ## Test runner See [`tests/test_fixtures.py`](../test_fixtures.py) for execution details. diff --git a/tests/fixtures/engine-regression/structured/README.md b/tests/fixtures/engine-regression/structured/README.md index 800e5e7..6d27fe8 100644 --- a/tests/fixtures/engine-regression/structured/README.md +++ b/tests/fixtures/engine-regression/structured/README.md @@ -48,12 +48,12 @@ They do **not** cover: - REPL / user-facing formatting - LLM integration behavior -- preprocessor / heuristic directive generation +- acquisition-layer directive drafting These surfaces are tested separately because: - REPL output may intentionally differ from the underlying state representation -- preprocessor behavior is non-deterministic and outside the engine contract +- acquisition-layer drafting is outside the engine contract This fixture set is the **canonical engine-level conformance surface**, and may be reused by other implementations (e.g., TypeScript) to validate identical engine behavior. diff --git a/tests/fixtures/preprocessor/admin-alias-remove-policies-unknown.json b/tests/fixtures/preprocessor/admin-alias-remove-policies-unknown.json deleted file mode 100644 index a9fc713..0000000 --- a/tests/fixtures/preprocessor/admin-alias-remove-policies-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "admin-alias-remove-policies-unknown", - "input": "remove policies docker", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/admin-alias-reset-policy-unknown.json b/tests/fixtures/preprocessor/admin-alias-reset-policy-unknown.json deleted file mode 100644 index 4eca34c..0000000 --- a/tests/fixtures/preprocessor/admin-alias-reset-policy-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "admin-alias-reset-policy-unknown", - "input": "reset policy", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/ambiguous-directive-adjacent.json b/tests/fixtures/preprocessor/ambiguous-directive-adjacent.json deleted file mode 100644 index c0ef546..0000000 --- a/tests/fixtures/preprocessor/ambiguous-directive-adjacent.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "ambiguous-directive-adjacent", - "input": "could we maybe use uv later", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/canonical-directive-bracket-wrapper.json b/tests/fixtures/preprocessor/canonical-directive-bracket-wrapper.json deleted file mode 100644 index 44d956d..0000000 --- a/tests/fixtures/preprocessor/canonical-directive-bracket-wrapper.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "canonical-directive-bracket-wrapper", - "input": "[prohibit peanuts]", - "expected": { - "classification": "directive", - "output": "prohibit peanuts" - } -} diff --git a/tests/fixtures/preprocessor/canonical-directive-case-normalized-use-docker.json b/tests/fixtures/preprocessor/canonical-directive-case-normalized-use-docker.json deleted file mode 100644 index 68f54cc..0000000 --- a/tests/fixtures/preprocessor/canonical-directive-case-normalized-use-docker.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "canonical-directive-case-normalized-use-docker", - "input": "Use Docker", - "expected": { - "classification": "directive", - "output": "use docker" - } -} diff --git a/tests/fixtures/preprocessor/canonical-directive-clear-state-period.json b/tests/fixtures/preprocessor/canonical-directive-clear-state-period.json deleted file mode 100644 index ee769eb..0000000 --- a/tests/fixtures/preprocessor/canonical-directive-clear-state-period.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "canonical-directive-clear-state-period", - "input": "clear state.", - "expected": { - "classification": "directive", - "output": "clear state" - } -} diff --git a/tests/fixtures/preprocessor/canonical-directive-clear-state.json b/tests/fixtures/preprocessor/canonical-directive-clear-state.json deleted file mode 100644 index cf7e037..0000000 --- a/tests/fixtures/preprocessor/canonical-directive-clear-state.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "canonical-directive-clear-state", - "input": "clear state", - "expected": { - "classification": "directive", - "output": "clear state" - } -} diff --git a/tests/fixtures/preprocessor/canonical-directive-paren-wrapper.json b/tests/fixtures/preprocessor/canonical-directive-paren-wrapper.json deleted file mode 100644 index 3283b4d..0000000 --- a/tests/fixtures/preprocessor/canonical-directive-paren-wrapper.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "canonical-directive-paren-wrapper", - "input": "(clear state)", - "expected": { - "classification": "directive", - "output": "clear state" - } -} diff --git a/tests/fixtures/preprocessor/canonical-directive-prohibit-peanuts.json b/tests/fixtures/preprocessor/canonical-directive-prohibit-peanuts.json deleted file mode 100644 index 37292df..0000000 --- a/tests/fixtures/preprocessor/canonical-directive-prohibit-peanuts.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "canonical-directive-prohibit-peanuts", - "input": "prohibit peanuts", - "expected": { - "classification": "directive", - "output": "prohibit peanuts" - } -} diff --git a/tests/fixtures/preprocessor/canonical-directive-quoted-payload-use-docker.json b/tests/fixtures/preprocessor/canonical-directive-quoted-payload-use-docker.json deleted file mode 100644 index c095597..0000000 --- a/tests/fixtures/preprocessor/canonical-directive-quoted-payload-use-docker.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "canonical-directive-quoted-payload-use-docker", - "input": "use \"docker\"", - "expected": { - "classification": "directive", - "output": "use \"docker\"" - } -} diff --git a/tests/fixtures/preprocessor/canonical-directive-reset-policies-bang.json b/tests/fixtures/preprocessor/canonical-directive-reset-policies-bang.json deleted file mode 100644 index 993f280..0000000 --- a/tests/fixtures/preprocessor/canonical-directive-reset-policies-bang.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "canonical-directive-reset-policies-bang", - "input": "reset policies!", - "expected": { - "classification": "directive", - "output": "reset policies" - } -} diff --git a/tests/fixtures/preprocessor/canonical-directive-whitespace-collapsed-use-docker.json b/tests/fixtures/preprocessor/canonical-directive-whitespace-collapsed-use-docker.json deleted file mode 100644 index 0b82550..0000000 --- a/tests/fixtures/preprocessor/canonical-directive-whitespace-collapsed-use-docker.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "canonical-directive-whitespace-collapsed-use-docker", - "input": " use docker ", - "expected": { - "classification": "directive", - "output": "use docker" - } -} diff --git a/tests/fixtures/preprocessor/fenced-code-block-directive-unknown.json b/tests/fixtures/preprocessor/fenced-code-block-directive-unknown.json deleted file mode 100644 index 0e8dd39..0000000 --- a/tests/fixtures/preprocessor/fenced-code-block-directive-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "fenced-code-block-directive-unknown", - "input": "```\nuse docker\n```", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/inline-prose-code-directive-unknown.json b/tests/fixtures/preprocessor/inline-prose-code-directive-unknown.json deleted file mode 100644 index 4a1f381..0000000 --- a/tests/fixtures/preprocessor/inline-prose-code-directive-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "inline-prose-code-directive-unknown", - "input": "the command is `use docker`", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/list-prefix-directive-unknown.json b/tests/fixtures/preprocessor/list-prefix-directive-unknown.json deleted file mode 100644 index ecdb2b5..0000000 --- a/tests/fixtures/preprocessor/list-prefix-directive-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "list-prefix-directive-unknown", - "input": "1. use docker", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/malformed-replacement-instead-docker-unknown.json b/tests/fixtures/preprocessor/malformed-replacement-instead-docker-unknown.json deleted file mode 100644 index 70d847c..0000000 --- a/tests/fixtures/preprocessor/malformed-replacement-instead-docker-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "malformed-replacement-instead-docker-unknown", - "input": "use podman instead docker", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/meta-prefix-directive-unknown.json b/tests/fixtures/preprocessor/meta-prefix-directive-unknown.json deleted file mode 100644 index cedcaf7..0000000 --- a/tests/fixtures/preprocessor/meta-prefix-directive-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "meta-prefix-directive-unknown", - "input": "example: use docker", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/meta-prefix-the-command-is-clear-state-unknown.json b/tests/fixtures/preprocessor/meta-prefix-the-command-is-clear-state-unknown.json deleted file mode 100644 index 1e4c25b..0000000 --- a/tests/fixtures/preprocessor/meta-prefix-the-command-is-clear-state-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "meta-prefix-the-command-is-clear-state-unknown", - "input": "the command is clear state", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/mixed-directive-task-unknown.json b/tests/fixtures/preprocessor/mixed-directive-task-unknown.json deleted file mode 100644 index a412e07..0000000 --- a/tests/fixtures/preprocessor/mixed-directive-task-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "mixed-directive-task-unknown", - "input": "use docker and explain why", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/mixed-intent-clear-state-then-continue-unknown.json b/tests/fixtures/preprocessor/mixed-intent-clear-state-then-continue-unknown.json deleted file mode 100644 index ae5324b..0000000 --- a/tests/fixtures/preprocessor/mixed-intent-clear-state-then-continue-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "mixed-intent-clear-state-then-continue-unknown", - "input": "clear state then continue", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/mixed-intent-unknown.json b/tests/fixtures/preprocessor/mixed-intent-unknown.json deleted file mode 100644 index 50b0489..0000000 --- a/tests/fixtures/preprocessor/mixed-intent-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "mixed-intent-unknown", - "input": "use docker and then continue with the answer", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/mixed-intent-use-docker-because-repo-has-docker-unknown.json b/tests/fixtures/preprocessor/mixed-intent-use-docker-because-repo-has-docker-unknown.json deleted file mode 100644 index b9644c6..0000000 --- a/tests/fixtures/preprocessor/mixed-intent-use-docker-because-repo-has-docker-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "mixed-intent-use-docker-because-repo-has-docker-unknown", - "input": "use docker because this repo already has Docker", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/modal-please-change-premise-concise-replies-unknown.json b/tests/fixtures/preprocessor/modal-please-change-premise-concise-replies-unknown.json deleted file mode 100644 index b91b23c..0000000 --- a/tests/fixtures/preprocessor/modal-please-change-premise-concise-replies-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "modal-please-change-premise-concise-replies-unknown", - "input": "please change premise concise replies", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/modal-please-clear-state-unknown.json b/tests/fixtures/preprocessor/modal-please-clear-state-unknown.json deleted file mode 100644 index dc1c152..0000000 --- a/tests/fixtures/preprocessor/modal-please-clear-state-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "modal-please-clear-state-unknown", - "input": "please clear state", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/modal-please-set-premise-to-concise-replies-unknown.json b/tests/fixtures/preprocessor/modal-please-set-premise-to-concise-replies-unknown.json deleted file mode 100644 index 9a5a6f8..0000000 --- a/tests/fixtures/preprocessor/modal-please-set-premise-to-concise-replies-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "modal-please-set-premise-to-concise-replies-unknown", - "input": "please set premise to concise replies", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/multi-instruction-use-docker-actually-prohibit-unknown.json b/tests/fixtures/preprocessor/multi-instruction-use-docker-actually-prohibit-unknown.json deleted file mode 100644 index 290c8bc..0000000 --- a/tests/fixtures/preprocessor/multi-instruction-use-docker-actually-prohibit-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "multi-instruction-use-docker-actually-prohibit-unknown", - "input": "use docker, actually prohibit docker", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/multiline-multi-directive-unknown.json b/tests/fixtures/preprocessor/multiline-multi-directive-unknown.json deleted file mode 100644 index 28a40b8..0000000 --- a/tests/fixtures/preprocessor/multiline-multi-directive-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "multiline-multi-directive-unknown", - "input": "clear premise\nreset policies", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/natural-language-do-not-use-unknown.json b/tests/fixtures/preprocessor/natural-language-do-not-use-unknown.json deleted file mode 100644 index f9701ed..0000000 --- a/tests/fixtures/preprocessor/natural-language-do-not-use-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "natural-language-do-not-use-unknown", - "input": "do not use peanuts", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/natural-language-dont-use-unknown.json b/tests/fixtures/preprocessor/natural-language-dont-use-unknown.json deleted file mode 100644 index 4414f6a..0000000 --- a/tests/fixtures/preprocessor/natural-language-dont-use-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "natural-language-dont-use-unknown", - "input": "don't use peanuts", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-change-premise-missing-to-keyword-unknown.json b/tests/fixtures/preprocessor/near-miss-change-premise-missing-to-keyword-unknown.json deleted file mode 100644 index ad8600d..0000000 --- a/tests/fixtures/preprocessor/near-miss-change-premise-missing-to-keyword-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-change-premise-missing-to-keyword-unknown", - "input": "change premise formal tone", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-change-premise-missing-to-unknown.json b/tests/fixtures/preprocessor/near-miss-change-premise-missing-to-unknown.json deleted file mode 100644 index a5011c8..0000000 --- a/tests/fixtures/preprocessor/near-miss-change-premise-missing-to-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-change-premise-missing-to-unknown", - "input": "change premise concise replies", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-change-premise-to-empty-unknown.json b/tests/fixtures/preprocessor/near-miss-change-premise-to-empty-unknown.json deleted file mode 100644 index 95d8507..0000000 --- a/tests/fixtures/preprocessor/near-miss-change-premise-to-empty-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-change-premise-to-empty-unknown", - "input": "change premise to", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-prohibit-empty-unknown.json b/tests/fixtures/preprocessor/near-miss-prohibit-empty-unknown.json deleted file mode 100644 index 581b841..0000000 --- a/tests/fixtures/preprocessor/near-miss-prohibit-empty-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-prohibit-empty-unknown", - "input": "prohibit", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-remove-policy-empty-unknown.json b/tests/fixtures/preprocessor/near-miss-remove-policy-empty-unknown.json deleted file mode 100644 index fbdd343..0000000 --- a/tests/fixtures/preprocessor/near-miss-remove-policy-empty-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-remove-policy-empty-unknown", - "input": "remove policy", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-set-premise-empty-unknown.json b/tests/fixtures/preprocessor/near-miss-set-premise-empty-unknown.json deleted file mode 100644 index b52b7a6..0000000 --- a/tests/fixtures/preprocessor/near-miss-set-premise-empty-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-set-premise-empty-unknown", - "input": "set premise", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-set-premise-to-formal-tone-unknown.json b/tests/fixtures/preprocessor/near-miss-set-premise-to-formal-tone-unknown.json deleted file mode 100644 index bc4cdd7..0000000 --- a/tests/fixtures/preprocessor/near-miss-set-premise-to-formal-tone-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-set-premise-to-formal-tone-unknown", - "input": "set premise to formal tone", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-set-premise-to-unknown.json b/tests/fixtures/preprocessor/near-miss-set-premise-to-unknown.json deleted file mode 100644 index 32043eb..0000000 --- a/tests/fixtures/preprocessor/near-miss-set-premise-to-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-set-premise-to-unknown", - "input": "set premise to concise replies", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-use-empty-unknown.json b/tests/fixtures/preprocessor/near-miss-use-empty-unknown.json deleted file mode 100644 index 4b09561..0000000 --- a/tests/fixtures/preprocessor/near-miss-use-empty-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-use-empty-unknown", - "input": "use", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-use-instead-of-missing-new-item-unknown.json b/tests/fixtures/preprocessor/near-miss-use-instead-of-missing-new-item-unknown.json deleted file mode 100644 index 9de4fe6..0000000 --- a/tests/fixtures/preprocessor/near-miss-use-instead-of-missing-new-item-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-use-instead-of-missing-new-item-unknown", - "input": "use instead of docker", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/near-miss-use-instead-of-missing-old-item-unknown.json b/tests/fixtures/preprocessor/near-miss-use-instead-of-missing-old-item-unknown.json deleted file mode 100644 index f863535..0000000 --- a/tests/fixtures/preprocessor/near-miss-use-instead-of-missing-old-item-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "near-miss-use-instead-of-missing-old-item-unknown", - "input": "use podman instead of", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/nested-wrapper-clear-state-unknown.json b/tests/fixtures/preprocessor/nested-wrapper-clear-state-unknown.json deleted file mode 100644 index 050038a..0000000 --- a/tests/fixtures/preprocessor/nested-wrapper-clear-state-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "nested-wrapper-clear-state-unknown", - "input": "([clear state])", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/ordinary-non-directive.json b/tests/fixtures/preprocessor/ordinary-non-directive.json deleted file mode 100644 index d3043d9..0000000 --- a/tests/fixtures/preprocessor/ordinary-non-directive.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "ordinary-non-directive", - "input": "thanks for the help today", - "expected": { - "classification": "no_directive", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/parse-canonical-directive-prohibit-peanuts.json b/tests/fixtures/preprocessor/parse-canonical-directive-prohibit-peanuts.json deleted file mode 100644 index 7ff61df..0000000 --- a/tests/fixtures/preprocessor/parse-canonical-directive-prohibit-peanuts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "parse-canonical-directive-prohibit-peanuts", - "kind": "parse", - "raw_output": "prohibit peanuts", - "expected_parsed": "prohibit peanuts" -} diff --git a/tests/fixtures/preprocessor/parse-malformed-near-miss-rejected.json b/tests/fixtures/preprocessor/parse-malformed-near-miss-rejected.json deleted file mode 100644 index b3b094b..0000000 --- a/tests/fixtures/preprocessor/parse-malformed-near-miss-rejected.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "parse-malformed-near-miss-rejected", - "kind": "parse", - "raw_output": "set premise to concise replies", - "expected_parsed": null -} diff --git a/tests/fixtures/preprocessor/parse-multiple-directives-rejected.json b/tests/fixtures/preprocessor/parse-multiple-directives-rejected.json deleted file mode 100644 index c1c84fb..0000000 --- a/tests/fixtures/preprocessor/parse-multiple-directives-rejected.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "parse-multiple-directives-rejected", - "kind": "parse", - "raw_output": "prohibit peanuts and use almonds", - "expected_parsed": null -} diff --git a/tests/fixtures/preprocessor/parse-no-directive-sentinel.json b/tests/fixtures/preprocessor/parse-no-directive-sentinel.json deleted file mode 100644 index bda8d4d..0000000 --- a/tests/fixtures/preprocessor/parse-no-directive-sentinel.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "parse-no-directive-sentinel", - "kind": "parse", - "raw_output": "", - "expected_parsed": null -} diff --git a/tests/fixtures/preprocessor/parse-source-input-fenced-code-rejected.json b/tests/fixtures/preprocessor/parse-source-input-fenced-code-rejected.json deleted file mode 100644 index 972fc95..0000000 --- a/tests/fixtures/preprocessor/parse-source-input-fenced-code-rejected.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parse-source-input-fenced-code-rejected", - "kind": "parse", - "source_input": "```\nuse docker\n```", - "raw_output": "use docker", - "expected_parsed": null -} diff --git a/tests/fixtures/preprocessor/parse-source-input-inline-prose-code-rejected.json b/tests/fixtures/preprocessor/parse-source-input-inline-prose-code-rejected.json deleted file mode 100644 index 852e078..0000000 --- a/tests/fixtures/preprocessor/parse-source-input-inline-prose-code-rejected.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parse-source-input-inline-prose-code-rejected", - "kind": "parse", - "source_input": "the command is `use docker`", - "raw_output": "use docker", - "expected_parsed": null -} diff --git a/tests/fixtures/preprocessor/parse-source-input-mixed-directive-task-rejected.json b/tests/fixtures/preprocessor/parse-source-input-mixed-directive-task-rejected.json deleted file mode 100644 index bfa2d8e..0000000 --- a/tests/fixtures/preprocessor/parse-source-input-mixed-directive-task-rejected.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parse-source-input-mixed-directive-task-rejected", - "kind": "parse", - "source_input": "use docker and explain why", - "raw_output": "use docker", - "expected_parsed": null -} diff --git a/tests/fixtures/preprocessor/parse-source-input-multiline-rejected.json b/tests/fixtures/preprocessor/parse-source-input-multiline-rejected.json deleted file mode 100644 index 1ea11ab..0000000 --- a/tests/fixtures/preprocessor/parse-source-input-multiline-rejected.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parse-source-input-multiline-rejected", - "kind": "parse", - "source_input": "clear premise\nreset policies", - "raw_output": "clear state", - "expected_parsed": null -} diff --git a/tests/fixtures/preprocessor/parse-source-input-question-form-rejected.json b/tests/fixtures/preprocessor/parse-source-input-question-form-rejected.json deleted file mode 100644 index bcab0e1..0000000 --- a/tests/fixtures/preprocessor/parse-source-input-question-form-rejected.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parse-source-input-question-form-rejected", - "kind": "parse", - "source_input": "can you use docker?", - "raw_output": "use docker", - "expected_parsed": null -} diff --git a/tests/fixtures/preprocessor/parse-source-input-quoted-payload-locked.json b/tests/fixtures/preprocessor/parse-source-input-quoted-payload-locked.json deleted file mode 100644 index c700ae2..0000000 --- a/tests/fixtures/preprocessor/parse-source-input-quoted-payload-locked.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parse-source-input-quoted-payload-locked", - "kind": "parse", - "source_input": "use \"docker\"", - "raw_output": "use \"docker\"", - "expected_parsed": "use \"docker\"" -} diff --git a/tests/fixtures/preprocessor/parse-source-input-reported-speech-rejected.json b/tests/fixtures/preprocessor/parse-source-input-reported-speech-rejected.json deleted file mode 100644 index 2f1ea3f..0000000 --- a/tests/fixtures/preprocessor/parse-source-input-reported-speech-rejected.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parse-source-input-reported-speech-rejected", - "kind": "parse", - "source_input": "the docs say \"use docker\"", - "raw_output": "use docker", - "expected_parsed": null -} diff --git a/tests/fixtures/preprocessor/parse-source-input-safe-canonical-use-docker.json b/tests/fixtures/preprocessor/parse-source-input-safe-canonical-use-docker.json deleted file mode 100644 index b477de6..0000000 --- a/tests/fixtures/preprocessor/parse-source-input-safe-canonical-use-docker.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parse-source-input-safe-canonical-use-docker", - "kind": "parse", - "source_input": "use docker", - "raw_output": "use docker", - "expected_parsed": "use docker" -} diff --git a/tests/fixtures/preprocessor/parse-source-input-safe-canonicalization-use-docker.json b/tests/fixtures/preprocessor/parse-source-input-safe-canonicalization-use-docker.json deleted file mode 100644 index 24cfba5..0000000 --- a/tests/fixtures/preprocessor/parse-source-input-safe-canonicalization-use-docker.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parse-source-input-safe-canonicalization-use-docker", - "kind": "parse", - "source_input": "Use Docker", - "raw_output": "use docker", - "expected_parsed": "use docker" -} diff --git a/tests/fixtures/preprocessor/parse-source-input-structured-json-directive-self-accepted.json b/tests/fixtures/preprocessor/parse-source-input-structured-json-directive-self-accepted.json deleted file mode 100644 index 07ef0f4..0000000 --- a/tests/fixtures/preprocessor/parse-source-input-structured-json-directive-self-accepted.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "parse-source-input-structured-json-directive-self-accepted", - "kind": "parse", - "source_input": "{\"classification\":\"directive\",\"output\":\"prohibit peanuts\"}", - "raw_output": "{\"classification\":\"directive\",\"output\":\"prohibit peanuts\"}", - "expected_parsed": "prohibit peanuts" -} diff --git a/tests/fixtures/preprocessor/parse-unknown-directive-like-rejected.json b/tests/fixtures/preprocessor/parse-unknown-directive-like-rejected.json deleted file mode 100644 index 890281e..0000000 --- a/tests/fixtures/preprocessor/parse-unknown-directive-like-rejected.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "parse-unknown-directive-like-rejected", - "kind": "parse", - "raw_output": "", - "expected_parsed": null -} diff --git a/tests/fixtures/preprocessor/public-api-v1.json b/tests/fixtures/preprocessor/public-api-v1.json deleted file mode 100644 index 4067ea1..0000000 --- a/tests/fixtures/preprocessor/public-api-v1.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "id": "experimental-preprocessor-public-api-v1", - "kind": "api-contract", - "module": "experimental.preprocessor", - "required_exports": [ - "PREPROCESS_OUTCOME_DIRECTIVE", - "PREPROCESS_OUTCOME_NO_DIRECTIVE", - "PREPROCESS_OUTCOME_UNKNOWN", - "PREPROCESSOR_NO_DIRECTIVE_SENTINEL", - "PreprocessResult", - "PreprocessOutcome", - "parse_preprocessor_output", - "preprocess_heuristic", - "render_prompt", - "validate_preprocessor_output" - ] -} diff --git a/tests/fixtures/preprocessor/question-can-you-use-docker-unknown.json b/tests/fixtures/preprocessor/question-can-you-use-docker-unknown.json deleted file mode 100644 index 6bad320..0000000 --- a/tests/fixtures/preprocessor/question-can-you-use-docker-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "question-can-you-use-docker-unknown", - "input": "can you use docker?", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/question-help-with-lunch-no-directive.json b/tests/fixtures/preprocessor/question-help-with-lunch-no-directive.json deleted file mode 100644 index 8cc21ed..0000000 --- a/tests/fixtures/preprocessor/question-help-with-lunch-no-directive.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "question-help-with-lunch-no-directive", - "input": "can you help with lunch?", - "expected": { - "classification": "no_directive", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/question-use-docker-unknown.json b/tests/fixtures/preprocessor/question-use-docker-unknown.json deleted file mode 100644 index cb90ed7..0000000 --- a/tests/fixtures/preprocessor/question-use-docker-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "question-use-docker-unknown", - "input": "use docker?", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/quoted-exact-use-docker-backtick-unknown.json b/tests/fixtures/preprocessor/quoted-exact-use-docker-backtick-unknown.json deleted file mode 100644 index f336069..0000000 --- a/tests/fixtures/preprocessor/quoted-exact-use-docker-backtick-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "quoted-exact-use-docker-backtick-unknown", - "input": "`use docker`", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/quoted-exact-use-docker-single-unknown.json b/tests/fixtures/preprocessor/quoted-exact-use-docker-single-unknown.json deleted file mode 100644 index 3d3b09a..0000000 --- a/tests/fixtures/preprocessor/quoted-exact-use-docker-single-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "quoted-exact-use-docker-single-unknown", - "input": "'use docker'", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/quoted-exact-use-docker-unknown.json b/tests/fixtures/preprocessor/quoted-exact-use-docker-unknown.json deleted file mode 100644 index cd8c771..0000000 --- a/tests/fixtures/preprocessor/quoted-exact-use-docker-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "quoted-exact-use-docker-unknown", - "input": "\"use docker\"", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/quoted-reported-unknown.json b/tests/fixtures/preprocessor/quoted-reported-unknown.json deleted file mode 100644 index ada2bd8..0000000 --- a/tests/fixtures/preprocessor/quoted-reported-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "quoted-reported-unknown", - "input": "he said \"use docker\".", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/reported-speech-docs-say-unknown.json b/tests/fixtures/preprocessor/reported-speech-docs-say-unknown.json deleted file mode 100644 index bc8f7e2..0000000 --- a/tests/fixtures/preprocessor/reported-speech-docs-say-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "reported-speech-docs-say-unknown", - "input": "the docs say \"use docker\"", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/sentence-adjacent-directive-unknown.json b/tests/fixtures/preprocessor/sentence-adjacent-directive-unknown.json deleted file mode 100644 index fbb1121..0000000 --- a/tests/fixtures/preprocessor/sentence-adjacent-directive-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "sentence-adjacent-directive-unknown", - "input": "ok. prohibit peanuts", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/unsupported-alias-unknown.json b/tests/fixtures/preprocessor/unsupported-alias-unknown.json deleted file mode 100644 index 75e769f..0000000 --- a/tests/fixtures/preprocessor/unsupported-alias-unknown.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "unsupported-alias-unknown", - "input": "allow docker", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/validator-invalid-json-shape-unknown.json b/tests/fixtures/preprocessor/validator-invalid-json-shape-unknown.json deleted file mode 100644 index ef7bfe7..0000000 --- a/tests/fixtures/preprocessor/validator-invalid-json-shape-unknown.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "validator-invalid-json-shape-unknown", - "kind": "validator", - "raw_output": { - "classification": "directive" - }, - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/validator-malformed-json-text-unknown.json b/tests/fixtures/preprocessor/validator-malformed-json-text-unknown.json deleted file mode 100644 index da8ac6c..0000000 --- a/tests/fixtures/preprocessor/validator-malformed-json-text-unknown.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "validator-malformed-json-text-unknown", - "kind": "validator", - "raw_output": "{\"classification\":\"directive\",", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/validator-malformed-sentinel-unknown.json b/tests/fixtures/preprocessor/validator-malformed-sentinel-unknown.json deleted file mode 100644 index 040bd5b..0000000 --- a/tests/fixtures/preprocessor/validator-malformed-sentinel-unknown.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "validator-malformed-sentinel-unknown", - "kind": "validator", - "raw_output": "", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/validator-malformed-text-unknown.json b/tests/fixtures/preprocessor/validator-malformed-text-unknown.json deleted file mode 100644 index 0d6821b..0000000 --- a/tests/fixtures/preprocessor/validator-malformed-text-unknown.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "validator-malformed-text-unknown", - "kind": "validator", - "raw_output": "set premise to concise replies", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/validator-multi-candidate-directive-unknown.json b/tests/fixtures/preprocessor/validator-multi-candidate-directive-unknown.json deleted file mode 100644 index c22f4a0..0000000 --- a/tests/fixtures/preprocessor/validator-multi-candidate-directive-unknown.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "validator-multi-candidate-directive-unknown", - "kind": "validator", - "raw_output": "prohibit peanuts and use almonds", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/validator-sentinel-no-directive.json b/tests/fixtures/preprocessor/validator-sentinel-no-directive.json deleted file mode 100644 index ed1b659..0000000 --- a/tests/fixtures/preprocessor/validator-sentinel-no-directive.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "validator-sentinel-no-directive", - "kind": "validator", - "raw_output": "", - "expected": { - "classification": "no_directive", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/validator-source-input-allow-safe-directive.json b/tests/fixtures/preprocessor/validator-source-input-allow-safe-directive.json deleted file mode 100644 index 25e89b0..0000000 --- a/tests/fixtures/preprocessor/validator-source-input-allow-safe-directive.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "validator-source-input-allow-safe-directive", - "kind": "validator", - "raw_output": "prohibit peanuts", - "source_input": "what is a simple curry recipe?", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/validator-source-input-block-change-premise-rewrite.json b/tests/fixtures/preprocessor/validator-source-input-block-change-premise-rewrite.json deleted file mode 100644 index 8e93a16..0000000 --- a/tests/fixtures/preprocessor/validator-source-input-block-change-premise-rewrite.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "validator-source-input-block-change-premise-rewrite", - "kind": "validator", - "raw_output": "change premise to concise replies", - "source_input": "change premise concise replies", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/validator-source-input-block-set-premise-rewrite.json b/tests/fixtures/preprocessor/validator-source-input-block-set-premise-rewrite.json deleted file mode 100644 index 0415083..0000000 --- a/tests/fixtures/preprocessor/validator-source-input-block-set-premise-rewrite.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "validator-source-input-block-set-premise-rewrite", - "kind": "validator", - "raw_output": "set premise concise replies", - "source_input": "set premise to concise replies", - "expected": { - "classification": "unknown", - "output": null - } -} diff --git a/tests/fixtures/preprocessor/validator-structured-json-directive.json b/tests/fixtures/preprocessor/validator-structured-json-directive.json deleted file mode 100644 index e69177e..0000000 --- a/tests/fixtures/preprocessor/validator-structured-json-directive.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "validator-structured-json-directive", - "kind": "validator", - "raw_output": "{\"classification\":\"directive\",\"output\":\"use docker\"}", - "expected": { - "classification": "directive", - "output": "use docker" - } -} diff --git a/tests/test_example_integrations_imports.py b/tests/test_example_integrations_imports.py deleted file mode 100644 index fac21c3..0000000 --- a/tests/test_example_integrations_imports.py +++ /dev/null @@ -1,124 +0,0 @@ -import builtins -import importlib.util -import sys -import types -from pathlib import Path - -import pytest - -REPO_ROOT = Path(__file__).resolve().parents[1] -INTEGRATIONS_DIR = REPO_ROOT / "examples" / "integrations" - - -def _load_module(module_name: str, path: Path): - spec = importlib.util.spec_from_file_location(module_name, path) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def _install_openwebui_stubs(monkeypatch: pytest.MonkeyPatch) -> None: - fastapi_mod = types.ModuleType("fastapi") - - class _Request: - pass - - fastapi_mod.Request = _Request - - open_webui_mod = types.ModuleType("open_webui") - open_webui_models_mod = types.ModuleType("open_webui.models") - open_webui_models_users_mod = types.ModuleType("open_webui.models.users") - open_webui_utils_mod = types.ModuleType("open_webui.utils") - open_webui_utils_chat_mod = types.ModuleType("open_webui.utils.chat") - open_webui_utils_models_mod = types.ModuleType("open_webui.utils.models") - - class _Users: - @staticmethod - def get_user_by_id(user_id: object) -> dict[str, object]: - return {"id": user_id} - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - return {"choices": [{"message": {"content": payload.get("_mock_content", "")}}]} - - async def _all_models(_: object, user: object = None) -> list[dict[str, str]]: - del user - return [{"id": "base-model"}, {"id": "prep-model"}, {"id": "pipe-model"}] - - open_webui_models_users_mod.Users = _Users - open_webui_utils_chat_mod.generate_chat_completion = _chat_completion - open_webui_utils_models_mod.get_all_models = _all_models - - monkeypatch.setitem(sys.modules, "fastapi", fastapi_mod) - monkeypatch.setitem(sys.modules, "open_webui", open_webui_mod) - monkeypatch.setitem(sys.modules, "open_webui.models", open_webui_models_mod) - monkeypatch.setitem(sys.modules, "open_webui.models.users", open_webui_models_users_mod) - monkeypatch.setitem(sys.modules, "open_webui.utils", open_webui_utils_mod) - monkeypatch.setitem(sys.modules, "open_webui.utils.chat", open_webui_utils_chat_mod) - monkeypatch.setitem(sys.modules, "open_webui.utils.models", open_webui_utils_models_mod) - - -def _block_optional_imports( - monkeypatch: pytest.MonkeyPatch, blocked_prefixes: tuple[str, ...] -) -> None: - real_import = builtins.__import__ - - def _guarded_import( - name: str, - globals_: dict[str, object] | None = None, - locals_: dict[str, object] | None = None, - fromlist: tuple[str, ...] = (), - level: int = 0, - ) -> object: - if any(name == prefix or name.startswith(f"{prefix}.") for prefix in blocked_prefixes): - raise ModuleNotFoundError(f"No module named '{name}'") - return real_import(name, globals_, locals_, fromlist, level) - - monkeypatch.setattr(builtins, "__import__", _guarded_import) - - -@pytest.mark.parametrize( - ("module_path", "blocked_prefixes", "needs_openwebui_stubs"), - [ - (INTEGRATIONS_DIR / "litellm" / "basic.py", ("litellm",), False), - (INTEGRATIONS_DIR / "litellm" / "response_format.py", ("litellm",), False), - (INTEGRATIONS_DIR / "litellm" / "with_preprocessor.py", ("litellm",), False), - ( - INTEGRATIONS_DIR / "ollama_structured_output" / "example.py", - (), - False, - ), - ( - INTEGRATIONS_DIR / "litellm_proxy" / "context_compiler_precall_hook.py", - ("litellm",), - False, - ), - ( - INTEGRATIONS_DIR - / "litellm_proxy" - / "context_compiler_precall_hook_with_preprocessor.py", - ("litellm",), - False, - ), - (INTEGRATIONS_DIR / "openwebui" / "open_webui_pipe.py", ("pydantic",), True), - ( - INTEGRATIONS_DIR / "openwebui" / "open_webui_pipe_with_preprocessor.py", - ("pydantic",), - True, - ), - ], -) -def test_example_integration_modules_import_without_optional_dependencies( - module_path: Path, - blocked_prefixes: tuple[str, ...], - needs_openwebui_stubs: bool, - monkeypatch: pytest.MonkeyPatch, -) -> None: - if needs_openwebui_stubs: - _install_openwebui_stubs(monkeypatch) - _block_optional_imports(monkeypatch, blocked_prefixes) - - module = _load_module(f"test_import_{module_path.stem}", module_path) - assert module is not None diff --git a/tests/test_host_confirmation.py b/tests/test_host_confirmation.py deleted file mode 100644 index 6b614c0..0000000 --- a/tests/test_host_confirmation.py +++ /dev/null @@ -1,308 +0,0 @@ -from typing import Any - -import pytest -from hypothesis import assume, given -from hypothesis import strategies as st - -import host_support.confirmation as _confirmation -from context_compiler import create_engine - -_EXPECTED_CONFIRMATION_TOKENS = frozenset( - { - "yes", - "yes please", - "yep", - "yeah", - "sure", - "ok", - "okay", - "no", - "nope", - "no thanks", - } -) - -_TRAILING_PUNCT = st.text(alphabet=".,!?", min_size=0, max_size=4) -_EDGE_WHITESPACE = st.text(alphabet=" \t\n\r", min_size=0, max_size=4) -_INTERNAL_WHITESPACE = st.text(alphabet=" \t\n\r", min_size=1, max_size=4) - - -def _apply_mixed_case(value: str, flags: list[bool]) -> str: - if not flags: - return value - chars: list[str] = [] - for index, char in enumerate(value): - flag = flags[index % len(flags)] - chars.append(char.upper() if flag else char.lower()) - return "".join(chars) - - -def test_confirmation_tokens_match_spec_exactly() -> None: - assert _confirmation.CONFIRMATION_TOKENS == _EXPECTED_CONFIRMATION_TOKENS - - -def test_is_confirmation_text_normalization_examples() -> None: - assert _confirmation.is_confirmation_text(" YES! ") - assert _confirmation.is_confirmation_text("yes please.") - assert _confirmation.is_confirmation_text("no thanks.") - assert _confirmation.is_confirmation_text("Okay??") - - -def test_is_confirmation_text_accepts_spec_tokens() -> None: - for token in sorted(_EXPECTED_CONFIRMATION_TOKENS): - assert _confirmation.is_confirmation_text(token) - - -def test_is_confirmation_text_rejects_required_near_misses() -> None: - rejected = [ - "y", - "n", - "yes but explain", - "okay now answer", - "sure, explain", - "", - " \t\n ", - ] - for value in rejected: - assert not _confirmation.is_confirmation_text(value) - - -def test_summarize_confirmation_update_use_only_summary_with_label_normalization() -> None: - pending = { - "replacement": {"kind": "use_only", "new_item": " Docker Compose ", "old_item": None} - } - result = _confirmation.summarize_confirmation_update("yes", pending) - assert result == "State updated: Use Docker Compose." - - -def test_summarize_confirmation_update_replace_use_summary() -> None: - pending = { - "replacement": {"kind": "replace_use", "new_item": "podman", "old_item": "docker"}, - "prompt_to_user": 'Did you mean to replace "docker" with "podman"?', - } - result = _confirmation.summarize_confirmation_update("yes", pending) - assert result == "State updated: Replaced docker with podman." - - -def test_summarize_confirmation_update_replace_use_prohibited_old_prompt_summary() -> None: - pending = { - "replacement": {"kind": "replace_use", "new_item": "podman", "old_item": "docker"}, - "prompt_to_user": ( - '"docker" is currently prohibited. Did you mean to remove it and use "podman" instead?' - ), - } - result = _confirmation.summarize_confirmation_update("yes", pending) - assert result == "State updated: Removed prohibition on docker; use podman." - - -def test_summarize_confirmation_update_declined_returns_state_unchanged() -> None: - pending = { - "replacement": {"kind": "use_only", "new_item": "docker", "old_item": None}, - } - result = _confirmation.summarize_confirmation_update("no thanks.", pending) - assert result == "State unchanged." - - -def test_summarize_confirmation_update_unexpected_shape_falls_back_safely() -> None: - pending = {"replacement": {"kind": "replace_use", "new_item": None, "old_item": "docker"}} - result = _confirmation.summarize_confirmation_update("yes", pending) - assert result == "State updated." - - -def test_summarize_confirmation_update_non_affirmative_non_negative_falls_back() -> None: - pending = {"replacement": {"kind": "use_only", "new_item": "docker", "old_item": None}} - result = _confirmation.summarize_confirmation_update("maybe", pending) - assert result == "State updated." - - -def test_summarize_confirmation_update_affirmative_pending_not_dict_falls_back() -> None: - result = _confirmation.summarize_confirmation_update("yes", ["not-a-dict"]) - assert "State updated." in result - - -def test_summarize_confirmation_update_replacement_not_dict_falls_back() -> None: - pending = {"replacement": "not-a-dict"} - result = _confirmation.summarize_confirmation_update("yes", pending) - assert "State updated." in result - - -def test_summarize_confirmation_update_unrecognized_replacement_shape_falls_back() -> None: - pending = {"replacement": {"kind": "unknown_kind", "new_item": "docker", "old_item": None}} - result = _confirmation.summarize_confirmation_update("yes", pending) - assert "State updated." in result - - -def test_summarize_confirmation_update_use_only_empty_new_item_falls_back() -> None: - pending = {"replacement": {"kind": "use_only", "new_item": " ", "old_item": None}} - result = _confirmation.summarize_confirmation_update("yes", pending) - assert "State updated." in result - - -@pytest.mark.parametrize( - ("new_item", "old_item"), - [ - (" ", "docker"), - ("podman", " "), - ], -) -def test_summarize_confirmation_update_replace_use_empty_items_falls_back( - new_item: str, old_item: str -) -> None: - pending = {"replacement": {"kind": "replace_use", "new_item": new_item, "old_item": old_item}} - result = _confirmation.summarize_confirmation_update("yes", pending) - assert "State updated." in result - - -@given( - token=st.sampled_from(sorted(_EXPECTED_CONFIRMATION_TOKENS)), - leading_ws=_EDGE_WHITESPACE, - trailing_ws=_EDGE_WHITESPACE, - internal_ws=_INTERNAL_WHITESPACE, - trailing_punct=_TRAILING_PUNCT, - case_flags=st.lists(st.booleans(), min_size=1, max_size=8), -) -def test_accepted_tokens_survive_normalization_variants( - token: str, - leading_ws: str, - trailing_ws: str, - internal_ws: str, - trailing_punct: str, - case_flags: list[bool], -) -> None: - spaced = token.replace(" ", internal_ws) - cased = _apply_mixed_case(spaced, case_flags) - candidate = f"{leading_ws}{cased}{trailing_punct}{trailing_ws}" - assert _confirmation.is_confirmation_text(candidate) - - -@given( - token=st.sampled_from(sorted(_EXPECTED_CONFIRMATION_TOKENS)), - extra_text=st.text( - alphabet="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ", - min_size=1, - max_size=20, - ), -) -def test_semantic_extensions_after_valid_token_are_rejected(token: str, extra_text: str) -> None: - assume(extra_text.strip() != "") - candidate = f"{token} {extra_text}" - assert not _confirmation.is_confirmation_text(candidate) - - -@given( - st.text( - alphabet="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", - min_size=1, - max_size=40, - ) -) -def test_non_token_strings_are_rejected(value: str) -> None: - assume(value.lower() not in _confirmation.CONFIRMATION_TOKENS) - assert not _confirmation.is_confirmation_text(value) - - -def _create_engine_with_pending_confirmation() -> tuple[Any, dict[str, object], str]: - engine = create_engine() - pre_pending_state = engine.state - first = engine.step("use docker instead of kubectl") - assert first["kind"] == "clarify" - pending_prompt = first["prompt_to_user"] - assert isinstance(pending_prompt, str) - checkpoint = engine.export_checkpoint() - assert checkpoint["pending"] is not None - return engine, pre_pending_state, pending_prompt - - -def test_engine_host_confirmation_parity_for_required_candidates() -> None: - candidates = [ - "yes", - "yes please", - "yep", - "yeah", - "sure", - "ok", - "okay", - "no", - "nope", - "no thanks", - "YES!", - " yes please ", - "no thanks.", - "y", - "n", - "yes but explain", - "okay now answer", - "sure, explain", - "", - " \t\n ", - ] - - for candidate in candidates: - host_accepts = _confirmation.is_confirmation_text(candidate) - engine, pre_pending_state, pending_prompt = _create_engine_with_pending_confirmation() - - decision = engine.step(candidate) - checkpoint = engine.export_checkpoint() - - if host_accepts: - assert decision["kind"] == "update" - assert checkpoint["pending"] is None - else: - assert decision["kind"] == "clarify" - assert decision["prompt_to_user"] == pending_prompt - assert engine.state == pre_pending_state - assert checkpoint["pending"] is not None - - -@given( - st.text( - alphabet="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .,!?\t\n\r", - min_size=0, - max_size=40, - ) -) -def test_engine_host_confirmation_parity_property(value: str) -> None: - host_accepts = _confirmation.is_confirmation_text(value) - engine, pre_pending_state, pending_prompt = _create_engine_with_pending_confirmation() - - decision = engine.step(value) - checkpoint = engine.export_checkpoint() - engine_resolved = decision["kind"] == "update" - - assert engine_resolved == host_accepts - if host_accepts: - assert checkpoint["pending"] is None - else: - assert decision["prompt_to_user"] == pending_prompt - assert engine.state == pre_pending_state - assert checkpoint["pending"] is not None - - -def test_summarize_confirmation_update_from_engine_without_export_checkpoint_falls_back() -> None: - class _NoExport: - pass - - result = _confirmation.summarize_confirmation_update_from_engine("yes", _NoExport()) - assert result == "State updated." - - -def test_summarize_confirmation_update_from_engine_export_raises_falls_back() -> None: - class _BadExport: - def export_checkpoint(self) -> dict[str, object]: - raise RuntimeError("boom") - - result = _confirmation.summarize_confirmation_update_from_engine("yes", _BadExport()) - assert result == "State updated." - - -def test_summarize_confirmation_update_from_engine_uses_pending_when_available() -> None: - class _PendingExport: - def export_checkpoint(self) -> dict[str, object]: - return { - "pending": { - "replacement": {"kind": "use_only", "new_item": "docker", "old_item": None} - } - } - - result = _confirmation.summarize_confirmation_update_from_engine("yes", _PendingExport()) - assert result == "State updated: Use docker." diff --git a/tests/test_host_observability.py b/tests/test_host_observability.py deleted file mode 100644 index 219239a..0000000 --- a/tests/test_host_observability.py +++ /dev/null @@ -1,381 +0,0 @@ -import importlib -import sys -from dataclasses import dataclass -from pathlib import Path - -import pytest - -REPO_ROOT = Path(__file__).resolve().parents[1] - - -def _load_module(): - sys.path.insert(0, str(REPO_ROOT)) - module = importlib.import_module("context_compiler.observability") - return importlib.reload(module) - - -@dataclass -class _DecisionObject: - kind: str - prompt_to_user: str | None = None - - -class _VeryLongRepr: - def __repr__(self) -> str: - return "x" * 250 - - -class _BadStateDict(dict): - def get(self, key: object, default: object = None) -> object: - raise RuntimeError(f"bad key access: {key!r}") - - -def test_build_trace_passthrough_without_preprocessor_output() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - decision={"kind": "passthrough"}, - state_before={}, - state_after={}, - llm_called=True, - ) - - assert "original input: hello" in output - assert "compiler input: hello" in output - assert "decision kind: passthrough" in output - assert "state change: unchanged" in output - assert "downstream LLM call: yes" in output - assert "preprocessor output:" not in output - - -def test_build_trace_update_with_changed_state() -> None: - module = _load_module() - output = module.build_trace( - original_input="use docker", - compiler_input="use docker", - decision={"kind": "update"}, - state_before={"allowed_tools": []}, - state_after={"allowed_tools": ["docker"]}, - llm_called=False, - ) - - assert "decision kind: update" in output - assert "state change:" in output - assert "changed=['allowed_tools']" in output - assert "downstream LLM call: no" in output - - -def test_build_trace_clarify_includes_prompt() -> None: - module = _load_module() - output = module.build_trace( - original_input="use", - compiler_input="use", - decision=_DecisionObject(kind="clarify", prompt_to_user="Use what item?"), - state_before={"pending": None}, - state_after={"pending": None}, - llm_called=False, - ) - - assert "decision kind: clarify" in output - assert "clarification prompt: Use what item?" in output - assert "downstream LLM call: no" in output - - -def test_build_trace_includes_preprocessor_output_when_present() -> None: - module = _load_module() - output = module.build_trace( - original_input="pls use docker", - compiler_input="use docker", - preprocessor_output="use docker", - decision={"kind": "update"}, - state_before={"allowed_tools": []}, - state_after={"allowed_tools": ["docker"]}, - ) - - assert "preprocessor output: use docker" in output - assert "decision kind: update" in output - - -def test_build_trace_handles_none_states_defensively() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - decision={"kind": "passthrough"}, - state_before=None, - state_after=None, - ) - - assert "state change: none -> none" in output - assert "downstream LLM call: no" in output - - -def test_build_trace_uses_unknown_kind_when_missing() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - decision={}, - state_before={}, - state_after={}, - ) - - assert "decision kind: unknown" in output - - -def test_build_trace_uses_unknown_kind_when_invalid_object_kind() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - decision={"kind": 42}, - state_before={}, - state_after={}, - ) - - assert "decision kind: unknown" in output - - -def test_build_trace_reports_added_mapping_keys() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - decision={"kind": "update"}, - state_before={"a": 1}, - state_after={"a": 1, "b": 2}, - ) - - assert "state change:" in output - assert "added=['b']" in output - - -def test_build_trace_reports_removed_mapping_keys() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - decision={"kind": "update"}, - state_before={"a": 1, "b": 2}, - state_after={"a": 1}, - ) - - assert "state change:" in output - assert "removed=['b']" in output - - -def test_build_trace_suppresses_whitespace_only_preprocessor_output() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - preprocessor_output=" \n\t ", - decision={"kind": "passthrough"}, - state_before={}, - state_after={}, - ) - - assert "preprocessor output:" not in output - - -def test_build_trace_non_mapping_states_render_safely() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - decision={"kind": "passthrough"}, - state_before=["docker"], - state_after=["docker", "kubectl"], - ) - - assert "state change:" in output - assert "->" in output - assert "['docker']" in output - assert "['docker', 'kubectl']" in output - - -def test_context_compiler_observability_exports_build_trace() -> None: - sys.path.insert(0, str(REPO_ROOT)) - sys.modules.pop("context_compiler.observability", None) - from context_compiler.observability import build_trace - - output = build_trace( - original_input="hello", - compiler_input="hello", - decision={"kind": "passthrough"}, - state_before={}, - state_after={}, - ) - - assert "Context Compiler trace" in output - - -def test_build_trace_state_summary_uses_none_fallback_path() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - decision={"kind": "passthrough"}, - state_before=None, - state_after=["docker"], - ) - - assert "state change: none -> ['docker']" in output - - -def test_build_trace_state_summary_uses_mapping_fallback_path() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - decision={"kind": "passthrough"}, - state_before={"b": 2, "a": 1}, - state_after=["docker"], - ) - - assert "state change: dict keys=['a', 'b'] -> ['docker']" in output - - -def test_build_trace_state_summary_truncates_very_long_repr() -> None: - module = _load_module() - output = module.build_trace( - original_input="hello", - compiler_input="hello", - decision={"kind": "passthrough"}, - state_before=_VeryLongRepr(), - state_after=["docker"], - ) - - assert "state change:" in output - assert "..." in output - - -def test_build_compact_trace_text_update_shape() -> None: - module = _load_module() - output = module.build_compact_trace_text( - decision={"kind": "update"}, - state_before={"premise": None, "policies": {}, "version": 2}, - state_after={ - "premise": "concise replies", - "policies": {"docker": "use"}, - "version": 2, - }, - llm_called=False, - state_injected="yes", - ) - - assert output.startswith("Context Compiler trace\n\ndecision kind: update\n") - assert 'state change: +premise "concise replies", +use docker' in output - assert 'active state: premise="concise replies"; use docker' in output - assert "downstream LLM call: no" in output - assert output.endswith("state injected: yes") - - -def test_build_compact_trace_text_clarify_shape() -> None: - module = _load_module() - output = module.build_compact_trace_text( - decision={"kind": "clarify", "prompt_to_user": "Use what item?"}, - state_before={"premise": None, "policies": {}, "version": 2}, - state_after={"premise": None, "policies": {}, "version": 2}, - llm_called=False, - state_injected="no", - ) - - assert output.startswith("Context Compiler trace\n\ndecision kind: clarify\n") - assert "clarification prompt: Use what item?" in output - assert "active state: none" in output - assert "downstream LLM call: no" in output - assert output.endswith("state injected: no") - - -def test_build_compact_trace_text_update_handles_removed_and_changed_prohibit() -> None: - module = _load_module() - output = module.build_compact_trace_text( - decision={"kind": "update"}, - state_before={"premise": None, "policies": {"docker": "prohibit"}, "version": 2}, - state_after={"premise": None, "policies": {"docker": "use"}, "version": 2}, - llm_called=False, - state_injected="yes", - ) - - assert "state change: ~use docker" in output - - -def test_build_compact_trace_text_update_handles_removed_prohibit_policy() -> None: - module = _load_module() - output = module.build_compact_trace_text( - decision={"kind": "update"}, - state_before={"premise": None, "policies": {"docker": "prohibit"}, "version": 2}, - state_after={"premise": None, "policies": {}, "version": 2}, - llm_called=False, - state_injected="yes", - ) - - assert "state change: -prohibit docker" in output - - -def test_build_compact_trace_text_update_ignores_malformed_changed_transition() -> None: - module = _load_module() - output = module.build_compact_trace_text( - decision={"kind": "update"}, - state_before={"premise": None, "policies": {"docker": "use"}, "version": 2}, - state_after={"premise": None, "policies": {"docker": "prohibit"}, "version": 2}, - llm_called=False, - state_injected="yes", - ) - - assert "state change: ~prohibit docker" in output - - -def test_build_compact_trace_text_state_summary_non_dict_falls_back_to_none() -> None: - module = _load_module() - output = module.build_compact_trace_text( - decision={"kind": "passthrough"}, - state_before={"premise": None, "policies": {}, "version": 2}, - state_after=["not-a-dict-state"], - llm_called=False, - state_injected="no", - ) - assert "active state: none" in output - - -def test_build_compact_trace_text_state_summary_exception_falls_back_to_none() -> None: - module = _load_module() - output = module.build_compact_trace_text( - decision={"kind": "passthrough"}, - state_before={"premise": None, "policies": {}, "version": 2}, - state_after=_BadStateDict(), - llm_called=False, - state_injected="no", - ) - assert "active state: none" in output - - -def test_build_compact_trace_text_ignores_malformed_policy_changed_transitions( - monkeypatch: pytest.MonkeyPatch, -) -> None: - module = _load_module() - - def _bad_diff(_: object, __: object) -> dict[str, object]: - return { - "changed": True, - "premise": {"before": None, "after": None, "changed": False}, - "policies": { - "added": {}, - "removed": {}, - "changed": {"docker": "not-a-mapping"}, - }, - } - - monkeypatch.setattr(module, "state_diff", _bad_diff) - output = module.build_compact_trace_text( - decision={"kind": "update"}, - state_before={"premise": None, "policies": {"docker": "use"}, "version": 2}, - state_after={"premise": None, "policies": {"docker": "prohibit"}, "version": 2}, - llm_called=False, - state_injected="yes", - ) - - assert "state change: " in output - assert "~" not in output diff --git a/tests/test_litellm_checkpoint_integration.py b/tests/test_litellm_checkpoint_integration.py deleted file mode 100644 index 82f760c..0000000 --- a/tests/test_litellm_checkpoint_integration.py +++ /dev/null @@ -1,776 +0,0 @@ -import importlib.util -import json -from pathlib import Path -from typing import Any, cast - -import pytest - -from context_compiler import create_engine - - -class _FakeEngine: - def __init__(self, kind: str, checkpoint_out: str, *, has_pending: bool = False) -> None: - self.kind = kind - self.state: dict[str, object] = { - "premise": None, - "policies": {"peanuts": "prohibit"}, - "version": 2, - } - self._checkpoint_out = checkpoint_out - self._has_pending = has_pending - self.imported: list[str] = [] - self.step_calls = 0 - self.export_calls = 0 - - def import_checkpoint_json(self, payload: str) -> None: - self.imported.append(payload) - - def export_checkpoint_json(self) -> str: - self.export_calls += 1 - return self._checkpoint_out - - def export_checkpoint(self) -> dict[str, object]: - pending: object = None - if self._has_pending: - pending = { - "kind": "replacement", - "replacement": {"kind": "use_only", "new_item": "kubectl", "old_item": None}, - "prompt_to_user": "confirm?", - } - return { - "checkpoint_version": 1, - "authoritative_state": self.state, - "pending": pending, - } - - def has_pending_clarification(self) -> bool: - return self._has_pending - - def step(self, _text: str) -> dict[str, object]: - self.step_calls += 1 - if self.kind == "clarify": - return {"kind": "clarify", "state": None, "prompt_to_user": "confirm?"} - return {"kind": self.kind, "state": self.state} - - -def _load_module(module_name: str, path: Path): - spec = importlib.util.spec_from_file_location(module_name, path) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def _assert_checkpoint_behavior(module: object) -> None: - checkpoints = cast(dict[str, str], module._CHECKPOINTS_BY_SESSION_KEY) - restored = cast(dict[str, int], module._RESTORED_ENGINE_BY_SESSION_KEY) - checkpoints.clear() - restored.clear() - - call_litellm = cast(Any, module._call_litellm) - litellm_calls = 0 - - def _track_litellm(_messages: list[dict[str, str]]) -> str: - nonlocal litellm_calls - litellm_calls += 1 - return "ok" - - module._call_litellm = _track_litellm - if hasattr(module, "_preprocess_user_input"): - module._preprocess_user_input = lambda _text, _state: None - - try: - checkpoints["s1"] = "ckpt-in" - passthrough_engine = _FakeEngine("passthrough", "ckpt-passthrough") - result = module.handle_turn("hello", passthrough_engine, session_key="s1") - assert result == "ok" - assert litellm_calls == 1 - assert passthrough_engine.imported == ["ckpt-in"] - assert passthrough_engine.export_calls == 0 - assert checkpoints["s1"] == "ckpt-in" - - update_engine = _FakeEngine("update", "ckpt-update") - result = module.handle_turn("use docker", update_engine, session_key="s1") - assert result == "State updated: Use docker." - assert litellm_calls == 1 - assert update_engine.imported == ["ckpt-in"] - assert update_engine.export_calls == 1 - assert checkpoints["s1"] == "ckpt-update" - - remove_policy_engine = _FakeEngine("update", "ckpt-remove-policy") - result = module.handle_turn("remove policy peanuts", remove_policy_engine, session_key="s1") - assert result == "State updated: Removed policy peanuts." - assert litellm_calls == 1 - assert remove_policy_engine.imported == ["ckpt-update"] - assert remove_policy_engine.export_calls == 1 - assert checkpoints["s1"] == "ckpt-remove-policy" - - clarify_engine = _FakeEngine("clarify", "ckpt-clarify") - result = module.handle_turn( - "use kubectl instead of docker", clarify_engine, session_key="s1" - ) - assert result == "confirm?" - assert litellm_calls == 1 - assert clarify_engine.imported == ["ckpt-remove-policy"] - assert clarify_engine.export_calls == 1 - assert checkpoints["s1"] == "ckpt-clarify" - finally: - module._call_litellm = call_litellm - - -def test_litellm_basic_checkpoint_restore_and_persist_points() -> None: - module = _load_module( - "litellm_basic_checkpoint", Path("examples/integrations/litellm/basic.py") - ) - _assert_checkpoint_behavior(module) - - -def test_litellm_with_preprocessor_checkpoint_restore_and_persist_points() -> None: - module = _load_module( - "litellm_with_preprocessor_checkpoint", - Path("examples/integrations/litellm/with_preprocessor.py"), - ) - _assert_checkpoint_behavior(module) - - -@pytest.mark.parametrize( - ("confirmation", "expected_response"), - [ - ("yes", "State updated: Use kubectl."), - ("no", "State unchanged."), - ], -) -def test_litellm_with_preprocessor_bypasses_preprocess_while_pending( - confirmation: str, expected_response: str -) -> None: - module = _load_module( - "litellm_with_preprocessor_pending_bypass", - Path("examples/integrations/litellm/with_preprocessor.py"), - ) - - class _PendingEngine: - def __init__(self) -> None: - self.state: dict[str, object] = {"premise": None, "policies": {}, "version": 2} - self.pending = True - self.step_inputs: list[str] = [] - - def export_checkpoint(self) -> dict[str, object]: - pending: object = None - if self.pending: - pending = { - "kind": "replacement", - "replacement": {"kind": "use_only", "new_item": "kubectl", "old_item": None}, - "prompt_to_user": "confirm?", - } - return { - "checkpoint_version": 1, - "authoritative_state": self.state, - "pending": pending, - } - - def has_pending_clarification(self) -> bool: - return self.pending - - def step(self, text: str) -> dict[str, object]: - self.step_inputs.append(text) - if self.pending and text in {"yes", "no"}: - self.pending = False - return {"kind": "update", "state": self.state} - if self.pending: - return {"kind": "clarify", "state": None, "prompt_to_user": "confirm?"} - return {"kind": "passthrough", "state": None} - - call_litellm = cast(Any, module._call_litellm) - preprocess_user_input = cast(Any, module._preprocess_user_input) - - def _fail_preprocess(_text: str, _state: dict[str, object]) -> None: - raise AssertionError("should not preprocess") - - litellm_calls = 0 - - def _track_litellm(_messages: list[dict[str, str]]) -> str: - nonlocal litellm_calls - litellm_calls += 1 - return "ok" - - module._call_litellm = _track_litellm - module._preprocess_user_input = _fail_preprocess - try: - engine = _PendingEngine() - result = module.handle_turn(confirmation, engine) - finally: - module._call_litellm = call_litellm - module._preprocess_user_input = preprocess_user_input - - assert result == expected_response - assert engine.step_inputs == [confirmation] - assert litellm_calls == 0 - - -@pytest.mark.parametrize( - ("confirmation", "expected_policies", "expected_response"), - [ - ("yes", {"kubectl": "use"}, "State updated: Use kubectl."), - ("no", {}, "State unchanged."), - ], -) -def test_litellm_with_preprocessor_checkpoint_resume_yes_no_end_to_end( - confirmation: str, expected_policies: dict[str, str], expected_response: str -) -> None: - module = _load_module( - "litellm_with_preprocessor_checkpoint_resume_e2e", - Path("examples/integrations/litellm/with_preprocessor.py"), - ) - checkpoints = cast(dict[str, str], module._CHECKPOINTS_BY_SESSION_KEY) - restored = cast(dict[str, int], module._RESTORED_ENGINE_BY_SESSION_KEY) - checkpoints.clear() - restored.clear() - - call_litellm = cast(Any, module._call_litellm) - preprocess_user_input = cast(Any, module._preprocess_user_input) - - preprocess_inputs: list[str] = [] - - def _preprocess_before_pending(text: str, _state: dict[str, object]) -> None: - preprocess_inputs.append(text) - return None - - def _fail_preprocess(_text: str, _state: dict[str, object]) -> None: - raise AssertionError("preprocess should be bypassed while pending is restored") - - litellm_calls = 0 - - def _track_litellm(_messages: list[dict[str, str]]) -> str: - nonlocal litellm_calls - litellm_calls += 1 - return "ok" - - module._call_litellm = _track_litellm - module._preprocess_user_input = _preprocess_before_pending - session_key = "resume-e2e" - - try: - first_engine = create_engine() - clarify = module.handle_turn( - "use kubectl instead of docker", - first_engine, - session_key=session_key, - ) - assert clarify == 'Did you mean to use "kubectl" instead?' - assert preprocess_inputs == ["use kubectl instead of docker"] - assert session_key in checkpoints - - module._preprocess_user_input = _fail_preprocess - second_engine = create_engine() - resumed = module.handle_turn(confirmation, second_engine, session_key=session_key) - assert resumed == expected_response - assert second_engine.state == { - "premise": None, - "policies": expected_policies, - "version": 2, - } - resumed_checkpoint = json.loads(checkpoints[session_key]) - assert resumed_checkpoint["pending"] is None - assert litellm_calls == 0 - finally: - module._call_litellm = call_litellm - module._preprocess_user_input = preprocess_user_input - - -@pytest.mark.parametrize( - ("confirmation", "expected_policies", "expected_response"), - [ - ("yes", {"docker": "use"}, "State updated: Use docker."), - ("YES!", {"docker": "use"}, "State updated: Use docker."), - (" yes please ", {"docker": "use"}, "State updated: Use docker."), - ("no thanks.", {}, "State unchanged."), - ], -) -def test_litellm_basic_confirmation_update_returns_ack_without_downstream_model_call( - confirmation: str, expected_policies: dict[str, str], expected_response: str -) -> None: - module = _load_module( - "litellm_basic_confirmation_ack", - Path("examples/integrations/litellm/basic.py"), - ) - checkpoints = cast(dict[str, str], module._CHECKPOINTS_BY_SESSION_KEY) - restored = cast(dict[str, int], module._RESTORED_ENGINE_BY_SESSION_KEY) - checkpoints.clear() - restored.clear() - - call_litellm = cast(Any, module._call_litellm) - litellm_calls = 0 - - def _track_litellm(_messages: list[dict[str, str]]) -> str: - nonlocal litellm_calls - litellm_calls += 1 - raise AssertionError("downstream model should not be called") - - module._call_litellm = _track_litellm - session_key = "basic-confirmation-ack" - - try: - first_engine = create_engine() - clarify = module.handle_turn( - "use docker instead of kubectl", - first_engine, - session_key=session_key, - ) - assert clarify == 'Did you mean to use "docker" instead?' - assert session_key in checkpoints - first_checkpoint = json.loads(checkpoints[session_key]) - assert first_checkpoint["pending"] is not None - - second_engine = create_engine() - resumed = module.handle_turn(confirmation, second_engine, session_key=session_key) - assert resumed == expected_response - assert litellm_calls == 0 - assert second_engine.state == { - "premise": None, - "policies": expected_policies, - "version": 2, - } - resumed_checkpoint = json.loads(checkpoints[session_key]) - assert resumed_checkpoint["pending"] is None - finally: - module._call_litellm = call_litellm - - -def test_litellm_basic_confirmation_summary_true_replacement() -> None: - module = _load_module( - "litellm_basic_confirmation_replace_summary", - Path("examples/integrations/litellm/basic.py"), - ) - checkpoints = cast(dict[str, str], module._CHECKPOINTS_BY_SESSION_KEY) - restored = cast(dict[str, int], module._RESTORED_ENGINE_BY_SESSION_KEY) - checkpoints.clear() - restored.clear() - - call_litellm = cast(Any, module._call_litellm) - module._call_litellm = lambda _messages: "ok" - session_key = "basic-replace-summary" - - try: - engine = create_engine() - assert ( - module.handle_turn("use podman", engine, session_key=session_key) - == "State updated: Use podman." - ) - assert ( - module.handle_turn("prohibit docker", engine, session_key=session_key) - == "State updated: Prohibit docker." - ) - clarify = module.handle_turn( - "use docker instead of podman", engine, session_key=session_key - ) - assert ( - clarify == '"docker" is currently prohibited. ' - 'Did you mean to remove "podman" and use "docker" instead?' - ) - - resumed = module.handle_turn("yes", create_engine(), session_key=session_key) - assert resumed == "State updated: Replaced podman with docker." - finally: - module._call_litellm = call_litellm - - -def test_litellm_basic_confirmation_summary_prohibited_old_replacement() -> None: - module = _load_module( - "litellm_basic_confirmation_prohibited_old_summary", - Path("examples/integrations/litellm/basic.py"), - ) - checkpoints = cast(dict[str, str], module._CHECKPOINTS_BY_SESSION_KEY) - restored = cast(dict[str, int], module._RESTORED_ENGINE_BY_SESSION_KEY) - checkpoints.clear() - restored.clear() - - call_litellm = cast(Any, module._call_litellm) - module._call_litellm = lambda _messages: "ok" - session_key = "basic-prohibited-old-summary" - - try: - engine = create_engine() - assert ( - module.handle_turn("prohibit docker", engine, session_key=session_key) - == "State updated: Prohibit docker." - ) - clarify = module.handle_turn( - "use podman instead of docker", engine, session_key=session_key - ) - assert ( - clarify == '"docker" is currently prohibited. ' - 'Did you mean to remove it and use "podman" instead?' - ) - - resumed = module.handle_turn("yes", create_engine(), session_key=session_key) - assert resumed == "State updated: Removed prohibition on docker; use podman." - finally: - module._call_litellm = call_litellm - - -def test_litellm_basic_confirmation_summary_falls_back_for_unknown_pending_shape() -> None: - module = _load_module( - "litellm_basic_confirmation_summary_fallback", - Path("examples/integrations/litellm/basic.py"), - ) - - class _FallbackPendingEngine: - def __init__(self) -> None: - self.state: dict[str, object] = {"premise": None, "policies": {}, "version": 2} - self._pending = { - "kind": "replacement", - "replacement": { - "kind": "unknown_kind", - "new_item": "docker", - "old_item": "podman", - }, - "prompt_to_user": "confirm?", - } - - def export_checkpoint(self) -> dict[str, object]: - return { - "checkpoint_version": 1, - "authoritative_state": self.state, - "pending": self._pending, - } - - def has_pending_clarification(self) -> bool: - return self._pending is not None - - def export_checkpoint_json(self) -> str: - return "ckpt-fallback" - - def step(self, _text: str) -> dict[str, object]: - self._pending = None - return {"kind": "update", "state": self.state} - - checkpoints = cast(dict[str, str], module._CHECKPOINTS_BY_SESSION_KEY) - restored = cast(dict[str, int], module._RESTORED_ENGINE_BY_SESSION_KEY) - checkpoints.clear() - restored.clear() - - call_litellm = cast(Any, module._call_litellm) - litellm_calls = 0 - - def _track_litellm(_messages: list[dict[str, str]]) -> str: - nonlocal litellm_calls - litellm_calls += 1 - return "ok" - - module._call_litellm = _track_litellm - try: - result = module.handle_turn( - "yes", - _FallbackPendingEngine(), - session_key="basic-summary-fallback", - ) - finally: - module._call_litellm = call_litellm - - assert result == "State updated." - assert litellm_calls == 0 - assert checkpoints["basic-summary-fallback"] == "ckpt-fallback" - - -@pytest.mark.parametrize( - ("module_name", "path"), - [ - ("litellm_basic_literal_replace_summary", Path("examples/integrations/litellm/basic.py")), - ( - "litellm_with_preprocessor_literal_replace_summary", - Path("examples/integrations/litellm/with_preprocessor.py"), - ), - ], -) -def test_litellm_literal_replacement_update_summary_uses_new_item_only( - module_name: str, path: Path -) -> None: - module = _load_module(module_name, path) - checkpoints = cast(dict[str, str], module._CHECKPOINTS_BY_SESSION_KEY) - restored = cast(dict[str, int], module._RESTORED_ENGINE_BY_SESSION_KEY) - checkpoints.clear() - restored.clear() - - call_litellm = cast(Any, module._call_litellm) - litellm_calls = 0 - - def _track_litellm(_messages: list[dict[str, str]]) -> str: - nonlocal litellm_calls - litellm_calls += 1 - raise AssertionError("downstream model should not be called for update summaries") - - if hasattr(module, "_llm_fallback_preprocess"): - module._llm_fallback_preprocess = lambda _message, _state: None - - module._call_litellm = _track_litellm - try: - engine_use = create_engine() - assert module.handle_turn("use DOCKER", engine_use) == "State updated: Use docker." - - engine_prohibit = create_engine() - assert ( - module.handle_turn("prohibit DOCKER", engine_prohibit) - == "State updated: Prohibit docker." - ) - - engine_remove = create_engine() - assert ( - module.handle_turn("remove policy DOCKER", engine_remove) - == "State updated: Removed policy docker." - ) - - engine_replace = create_engine() - assert module.handle_turn("use docker", engine_replace) == "State updated: Use docker." - assert ( - module.handle_turn("use KUBECTL instead of DOCKER", engine_replace) - == "State updated: Use kubectl." - ) - - engine_premise = create_engine() - assert module.handle_turn("set premise concise answers", engine_premise) == "State updated." - assert module.handle_turn("clear premise", engine_premise) == "Premise cleared." - - engine_noop = create_engine() - assert module.handle_turn("use docker", engine_noop) == "State updated: Use docker." - assert ( - module.handle_turn("use docker instead of docker", engine_noop) - == "State updated: Use docker." - ) - finally: - module._call_litellm = call_litellm - - assert litellm_calls == 0 - - -@pytest.mark.parametrize( - ("module_name", "path"), - [ - ("litellm_basic_update_responses_no_llm", Path("examples/integrations/litellm/basic.py")), - ( - "litellm_with_preprocessor_update_responses_no_llm", - Path("examples/integrations/litellm/with_preprocessor.py"), - ), - ], -) -def test_litellm_update_responses_are_deterministic_and_skip_downstream_model( - module_name: str, path: Path -) -> None: - module = _load_module(module_name, path) - checkpoints = cast(dict[str, str], module._CHECKPOINTS_BY_SESSION_KEY) - restored = cast(dict[str, int], module._RESTORED_ENGINE_BY_SESSION_KEY) - checkpoints.clear() - restored.clear() - - call_litellm = cast(Any, module._call_litellm) - litellm_calls = 0 - - def _track_litellm(_messages: list[dict[str, str]]) -> str: - nonlocal litellm_calls - litellm_calls += 1 - raise AssertionError("downstream model should not be called for update summaries") - - module._call_litellm = _track_litellm - - try: - engine = create_engine() - assert module.handle_turn("set premise concise replies", engine) == "State updated." - assert module.handle_turn("use docker", engine) == "State updated: Use docker." - assert module.handle_turn("prohibit peanuts", engine) == "State updated: Prohibit peanuts." - assert ( - module.handle_turn("remove policy peanuts", engine) - == "State updated: Removed policy peanuts." - ) - assert module.handle_turn("reset policies", engine) == "Policies reset." - assert module.handle_turn("clear state", engine) == "State cleared." - finally: - module._call_litellm = call_litellm - - assert litellm_calls == 0 - - -@pytest.mark.parametrize( - ("module_name", "path"), - [ - ("litellm_basic_near_miss_clarify", Path("examples/integrations/litellm/basic.py")), - ( - "litellm_with_preprocessor_near_miss_clarify", - Path("examples/integrations/litellm/with_preprocessor.py"), - ), - ], -) -def test_litellm_near_miss_directives_return_deterministic_clarify_without_downstream( - module_name: str, path: Path -) -> None: - module = _load_module(module_name, path) - checkpoints = cast(dict[str, str], module._CHECKPOINTS_BY_SESSION_KEY) - restored = cast(dict[str, int], module._RESTORED_ENGINE_BY_SESSION_KEY) - checkpoints.clear() - restored.clear() - - call_litellm = cast(Any, module._call_litellm) - litellm_calls = 0 - - def _track_litellm(_messages: list[dict[str, str]]) -> str: - nonlocal litellm_calls - litellm_calls += 1 - raise AssertionError("downstream model should not be called for near-miss clarify") - - fallback_calls = 0 - fallback_original = None - if hasattr(module, "_llm_fallback_preprocess"): - fallback_original = module._llm_fallback_preprocess - - def _track_fallback(_message: str, _state: dict[str, object]) -> None: - nonlocal fallback_calls - fallback_calls += 1 - raise AssertionError("fallback should not be called for near-miss directive input") - - module._llm_fallback_preprocess = _track_fallback - - module._call_litellm = _track_litellm - try: - engine = create_engine() - assert ( - module.handle_turn("reset premise", engine) - == "Unknown directive.\nUse 'clear premise' or 'reset policies'." - ) - assert ( - module.handle_turn("reset premises", engine) - == "Unknown directive.\nUse 'clear premise' or 'reset policies'." - ) - assert ( - module.handle_turn("clear premises", engine) - == "Unknown directive.\nUse 'clear premise' or 'reset policies'." - ) - assert ( - module.handle_turn("set premise to concise answers", engine) - == "Invalid premise syntax.\nUse 'set premise '." - ) - assert ( - module.handle_turn("change premise formal tone", engine) - == "Invalid premise syntax.\nUse 'change premise to '." - ) - finally: - module._call_litellm = call_litellm - if fallback_original is not None: - module._llm_fallback_preprocess = fallback_original - - assert litellm_calls == 0 - - -def test_litellm_basic_trace_off_preserves_existing_output() -> None: - module = _load_module( - "litellm_basic_trace_off", - Path("examples/integrations/litellm/basic.py"), - ) - module.SHOW_CONTEXT_COMPILER_TRACE = False - module._call_litellm = lambda _messages: "ok" - engine = create_engine() - - result = module.handle_turn("hello", engine) - assert result == "ok" - - -def test_litellm_basic_trace_on_includes_trace_substrings() -> None: - module = _load_module( - "litellm_basic_trace_on", - Path("examples/integrations/litellm/basic.py"), - ) - module.SHOW_CONTEXT_COMPILER_TRACE = True - module._call_litellm = lambda _messages: "ok" - engine = create_engine() - - result = module.handle_turn("hello", engine) - assert "ok" in result - assert "Context Compiler trace" in result - assert "decision kind: passthrough" in result - assert "downstream LLM call: yes" in result - - -def test_litellm_with_preprocessor_trace_on_includes_preprocessor_output() -> None: - module = _load_module( - "litellm_with_preprocessor_trace_on", - Path("examples/integrations/litellm/with_preprocessor.py"), - ) - module.SHOW_CONTEXT_COMPILER_TRACE = True - module._call_litellm = lambda _messages: "ok" - module._preprocess_user_input = lambda _text, _state: "prohibit peanuts" - engine = create_engine() - - result = module.handle_turn("please use docker", engine) - assert "State updated: Prohibit peanuts." in result - assert "Context Compiler trace" in result - assert "decision kind: update" in result - assert "preprocessor output: prohibit peanuts" in result - assert "downstream LLM call: no" in result - - -def test_litellm_with_preprocessor_trace_on_passthrough_includes_trace() -> None: - module = _load_module( - "litellm_with_preprocessor_trace_on_passthrough", - Path("examples/integrations/litellm/with_preprocessor.py"), - ) - module.SHOW_CONTEXT_COMPILER_TRACE = True - module._call_litellm = lambda _messages: "ok" - module._preprocess_user_input = lambda _text, _state: None - engine = create_engine() - - result = module.handle_turn("hello", engine) - assert "ok" in result - assert "Context Compiler trace" in result - assert "decision kind: passthrough" in result - assert "downstream LLM call: yes" in result - - -@pytest.mark.parametrize( - "user_input", - [ - "ok. prohibit peanuts", - "```\nuse docker\n```", - "the command is `use docker`", - 'the docs say "use docker"', - "can you use docker?", - ], -) -def test_litellm_with_preprocessor_fallback_boundary_unsafe_sources_do_not_mutate_state( - monkeypatch: pytest.MonkeyPatch, user_input: str -) -> None: - module = _load_module( - "litellm_with_preprocessor_fallback_boundary_unsafe", - Path("examples/integrations/litellm/with_preprocessor.py"), - ) - - class _Config: - mode = "openai" - api_key = "test-key" - base_url = "http://example.invalid" - model = "base-model" - source = "test" - - completion_calls: list[str] = [] - - def _completion(**kwargs: object) -> dict[str, object]: - model = str(kwargs.get("model", "")) - completion_calls.append(model) - if model == "prep-model": - return {"choices": [{"message": {"content": "use docker"}}]} - return {"choices": [{"message": {"content": "downstream-ok"}}]} - - monkeypatch.setenv("PREPROCESSOR_MODEL", "prep-model") - monkeypatch.setattr(module, "resolve_provider_config", lambda **_kwargs: _Config()) - monkeypatch.setattr(module, "_get_litellm_completion", lambda: _completion) - monkeypatch.setattr(module, "render_prompt", lambda *_args, **_kwargs: "prompt") - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: {"outcome": "unknown", "directive": None, "rule_id": "test"}, - ) - - engine = create_engine() - initial_state = dict(engine.state["policies"]) - - result = module.handle_turn(user_input, engine) - - assert result == "downstream-ok" - assert engine.state["policies"] == initial_state - assert completion_calls == ["prep-model", "base-model"] diff --git a/tests/test_litellm_integration_error_paths.py b/tests/test_litellm_integration_error_paths.py deleted file mode 100644 index 2790df9..0000000 --- a/tests/test_litellm_integration_error_paths.py +++ /dev/null @@ -1,283 +0,0 @@ -import importlib.util -import types -from pathlib import Path -from typing import Any - -import pytest - -from context_compiler import create_engine - -LITELLM_BASIC_PATH = Path("examples/integrations/litellm/basic.py") -LITELLM_WITH_PREPROC_PATH = Path("examples/integrations/litellm/with_preprocessor.py") - - -def _load_module(module_name: str, path: Path): - spec = importlib.util.spec_from_file_location(module_name, path) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def _patch_completion_loader(monkeypatch, module: object, completion_fn: Any) -> None: - if hasattr(module, "_get_litellm_completion"): - monkeypatch.setattr(module, "_get_litellm_completion", lambda: completion_fn) - return - - module_proxy = types.SimpleNamespace(completion=completion_fn) - monkeypatch.setattr(module, "import_module", lambda _: module_proxy) # type: ignore[misc] - - -@pytest.mark.parametrize( - ("module_name", "path"), - [ - ("litellm_basic_error_paths", LITELLM_BASIC_PATH), - ("litellm_with_preproc_error_paths", LITELLM_WITH_PREPROC_PATH), - ], -) -def test_call_litellm_requires_api_key(monkeypatch, module_name: str, path: Path) -> None: - module = _load_module(module_name, path) - monkeypatch.delenv("PROVIDER", raising=False) - monkeypatch.delenv("OPENAI_BASE_URL", raising=False) - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - _patch_completion_loader(monkeypatch, module, lambda **_: {}) - - with pytest.raises(RuntimeError, match="OPENAI_API_KEY is required in openai mode"): - module._call_litellm([{"role": "user", "content": "hello"}]) - - -@pytest.mark.parametrize( - ("module_name", "path"), - [ - ("litellm_basic_missing_dep", LITELLM_BASIC_PATH), - ("litellm_with_preproc_missing_dep", LITELLM_WITH_PREPROC_PATH), - ], -) -def test_call_litellm_requires_litellm_dependency( - monkeypatch, module_name: str, path: Path -) -> None: - module = _load_module(module_name, path) - monkeypatch.delenv("PROVIDER", raising=False) - monkeypatch.delenv("OPENAI_BASE_URL", raising=False) - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - - def _raise_module_not_found() -> Any: - raise ModuleNotFoundError - - if hasattr(module, "_get_litellm_completion"): - monkeypatch.setattr(module, "_get_litellm_completion", _raise_module_not_found) - else: - monkeypatch.setattr( - module, - "import_module", - lambda _: _raise_module_not_found(), - ) - - with pytest.raises(RuntimeError, match="litellm is required"): - module._call_litellm([{"role": "user", "content": "hello"}]) - - -@pytest.mark.parametrize( - ("module_name", "path"), - [ - ("litellm_basic_malformed_response", LITELLM_BASIC_PATH), - ("litellm_with_preproc_malformed_response", LITELLM_WITH_PREPROC_PATH), - ], -) -def test_call_litellm_rejects_missing_content_shape( - monkeypatch, module_name: str, path: Path -) -> None: - module = _load_module(module_name, path) - monkeypatch.delenv("PROVIDER", raising=False) - monkeypatch.delenv("OPENAI_BASE_URL", raising=False) - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - - def _completion(**_: Any) -> dict[str, object]: - return {"choices": [{"message": {"content": None}}]} - - _patch_completion_loader(monkeypatch, module, _completion) - - with pytest.raises( - RuntimeError, match="LiteLLM response missing choices\\[0\\]\\.message\\.content" - ): - module._call_litellm([{"role": "user", "content": "hello"}]) - - -@pytest.mark.parametrize( - ("module_name", "path"), - [ - ("litellm_basic_provider_unknown", LITELLM_BASIC_PATH), - ("litellm_with_preproc_provider_unknown", LITELLM_WITH_PREPROC_PATH), - ], -) -def test_call_litellm_rejects_unknown_provider(monkeypatch, module_name: str, path: Path) -> None: - module = _load_module(module_name, path) - monkeypatch.setenv("PROVIDER", "bedrock") - monkeypatch.delenv("OPENAI_BASE_URL", raising=False) - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - _patch_completion_loader(monkeypatch, module, lambda **_: {}) - - with pytest.raises( - RuntimeError, - match=( - "Invalid PROVIDER value 'bedrock'. Allowed values: openai, ollama, openai_compatible" - ), - ): - module._call_litellm([{"role": "user", "content": "hello"}]) - - -@pytest.mark.parametrize( - ("module_name", "path"), - [ - ("litellm_basic_openai_compatible_missing_base", LITELLM_BASIC_PATH), - ("litellm_with_preproc_openai_compatible_missing_base", LITELLM_WITH_PREPROC_PATH), - ], -) -def test_call_litellm_openai_compatible_requires_base_url( - monkeypatch, module_name: str, path: Path -) -> None: - module = _load_module(module_name, path) - monkeypatch.setenv("PROVIDER", "openai_compatible") - monkeypatch.delenv("OPENAI_BASE_URL", raising=False) - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - _patch_completion_loader(monkeypatch, module, lambda **_: {}) - - with pytest.raises( - RuntimeError, match="OPENAI_BASE_URL is required when PROVIDER=openai_compatible." - ): - module._call_litellm([{"role": "user", "content": "hello"}]) - - -@pytest.mark.parametrize( - ("module_name", "path"), - [ - ("litellm_basic_ollama_optional_key", LITELLM_BASIC_PATH), - ("litellm_with_preproc_ollama_optional_key", LITELLM_WITH_PREPROC_PATH), - ], -) -def test_call_litellm_ollama_mode_allows_missing_api_key( - monkeypatch, module_name: str, path: Path -) -> None: - module = _load_module(module_name, path) - seen: dict[str, object] = {} - - def _completion(**kwargs: Any) -> dict[str, object]: - seen.update(kwargs) - return {"choices": [{"message": {"content": "ok"}}]} - - monkeypatch.setenv("PROVIDER", "ollama") - monkeypatch.delenv("OPENAI_BASE_URL", raising=False) - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - _patch_completion_loader(monkeypatch, module, _completion) - - result = module._call_litellm([{"role": "user", "content": "hello"}]) - assert result == "ok" - assert seen["api_base"] == "http://localhost:11434" - assert "api_key" not in seen - - -@pytest.mark.parametrize( - ("module_name", "path"), - [ - ("litellm_basic_base_url_override", LITELLM_BASIC_PATH), - ("litellm_with_preproc_base_url_override", LITELLM_WITH_PREPROC_PATH), - ], -) -def test_call_litellm_base_url_override_wins_over_provider( - monkeypatch, module_name: str, path: Path -) -> None: - module = _load_module(module_name, path) - seen: dict[str, object] = {} - - def _completion(**kwargs: Any) -> dict[str, object]: - seen.update(kwargs) - return {"choices": [{"message": {"content": "ok"}}]} - - monkeypatch.setenv("PROVIDER", "ollama") - monkeypatch.setenv("OPENAI_BASE_URL", "https://example.compat/v1") - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - _patch_completion_loader(monkeypatch, module, _completion) - - result = module._call_litellm([{"role": "user", "content": "hello"}]) - assert result == "ok" - assert seen["api_base"] == "https://example.compat/v1" - assert "api_key" not in seen - - -def test_call_litellm_logs_single_mode_resolution_line( - monkeypatch, caplog: pytest.LogCaptureFixture -) -> None: - import host_support.provider_mode as _provider - - module = _load_module("litellm_basic_logs_resolution", LITELLM_BASIC_PATH) - _provider._STARTUP_LOGGED = False - - def _completion(**_: Any) -> dict[str, object]: - return {"choices": [{"message": {"content": "ok"}}]} - - monkeypatch.setenv("MODEL", "openai/demo-model") - monkeypatch.setenv("OPENAI_BASE_URL", "http://localhost:11434/v1") - monkeypatch.setenv("PROVIDER", "ollama") - monkeypatch.delenv("OPENAI_API_KEY", raising=False) - _patch_completion_loader(monkeypatch, module, _completion) - - with caplog.at_level("INFO"): - first = module._call_litellm([{"role": "user", "content": "hello"}]) - second = module._call_litellm([{"role": "user", "content": "again"}]) - - assert first == "ok" - assert second == "ok" - matches = [ - rec - for rec in caplog.records - if rec.getMessage().startswith("litellm_config mode=openai_compatible") - ] - assert len(matches) == 1 - message = matches[0].getMessage() - assert "base_url=http://localhost:11434/v1" in message - assert "model=openai/demo-model" in message - assert "source=OPENAI_BASE_URL override" in message - - -def test_with_preprocessor_fallback_failure_preserves_basic_behavior(monkeypatch) -> None: - module = _load_module("litellm_with_preproc_fallback_failure", LITELLM_WITH_PREPROC_PATH) - - seen_preprocess_inputs: list[str] = [] - - def _heuristic(_text: str) -> dict[str, object]: - return {"outcome": "no_directive", "directive": None} - - def _fallback(_message: str, _state: dict[str, object]) -> str | None: - seen_preprocess_inputs.append("called") - raise RuntimeError("fallback failed") - - seen_engine_inputs: list[str] = [] - - class _ProxyEngine: - def __init__(self) -> None: - self._engine = create_engine() - - @property - def state(self) -> dict[str, object]: - return self._engine.state - - def export_checkpoint(self) -> dict[str, object]: - return self._engine.export_checkpoint() - - def has_pending_clarification(self) -> bool: - return self._engine.has_pending_clarification() - - def step(self, text: str) -> dict[str, object]: - seen_engine_inputs.append(text) - return self._engine.step(text) - - monkeypatch.setattr(module, "preprocess_heuristic", _heuristic) - monkeypatch.setattr(module, "_llm_fallback_preprocess", _fallback) - monkeypatch.setattr(module, "_call_litellm", lambda _messages: "ok") - - engine = _ProxyEngine() - result = module.handle_turn("hello world", engine) - - assert result == "ok" - assert seen_preprocess_inputs == ["called"] - assert seen_engine_inputs == ["hello world"] diff --git a/tests/test_litellm_preprocessor_model_config.py b/tests/test_litellm_preprocessor_model_config.py deleted file mode 100644 index 02739d6..0000000 --- a/tests/test_litellm_preprocessor_model_config.py +++ /dev/null @@ -1,232 +0,0 @@ -import importlib.util -import sys -import types -from pathlib import Path -from typing import Any - -from context_compiler import create_engine - -LITELLM_WITH_PREPROC_PATH = Path("examples/integrations/litellm/with_preprocessor.py") -LITELLM_PROXY_WITH_PREPROC_PATH = Path( - "examples/integrations/litellm_proxy/context_compiler_precall_hook_with_preprocessor.py" -) - - -def _load_module(module_name: str, path: Path): - spec = importlib.util.spec_from_file_location(module_name, path) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def _load_litellm_proxy_module_with_stubs(monkeypatch, module_name: str): - litellm_mod = types.ModuleType("litellm") - integrations_mod = types.ModuleType("litellm.integrations") - custom_logger_mod = types.ModuleType("litellm.integrations.custom_logger") - - class _CustomLogger: - pass - - custom_logger_mod.CustomLogger = _CustomLogger - - monkeypatch.setitem(sys.modules, "litellm", litellm_mod) - monkeypatch.setitem(sys.modules, "litellm.integrations", integrations_mod) - monkeypatch.setitem(sys.modules, "litellm.integrations.custom_logger", custom_logger_mod) - - return _load_module(module_name, LITELLM_PROXY_WITH_PREPROC_PATH) - - -def test_litellm_preprocessor_model_defaults_to_model(monkeypatch): - module = _load_module("litellm_with_preproc_cfg_default", LITELLM_WITH_PREPROC_PATH) - - seen: dict[str, Any] = {} - - def _completion(**kwargs): - seen["model"] = kwargs["model"] - return {"choices": [{"message": {"content": "use docker"}}]} - - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - monkeypatch.setenv("MODEL", "openai/main-model") - monkeypatch.delenv("PREPROCESSOR_MODEL", raising=False) - - monkeypatch.setattr(module, "_get_litellm_completion", lambda: _completion) - monkeypatch.setattr(module, "render_prompt", lambda *_: "prompt") - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - result = module._llm_fallback_preprocess("please use docker", None) - - assert result == "use docker" - assert seen["model"] == "openai/main-model" - - -def test_litellm_preprocessor_model_override(monkeypatch): - module = _load_module("litellm_with_preproc_cfg_override", LITELLM_WITH_PREPROC_PATH) - - seen: dict[str, Any] = {} - - def _completion(**kwargs): - seen["model"] = kwargs["model"] - return {"choices": [{"message": {"content": "use docker"}}]} - - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - monkeypatch.setenv("MODEL", "openai/main-model") - monkeypatch.setenv("PREPROCESSOR_MODEL", "openai/preprocessor-model") - - monkeypatch.setattr(module, "_get_litellm_completion", lambda: _completion) - monkeypatch.setattr(module, "render_prompt", lambda *_: "prompt") - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - result = module._llm_fallback_preprocess("please use docker", None) - - assert result == "use docker" - assert seen["model"] == "openai/preprocessor-model" - - -def test_litellm_proxy_preprocessor_model_defaults_to_model(monkeypatch): - module = _load_litellm_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_with_preproc_cfg_default" - ) - - seen: dict[str, Any] = {} - - def _completion(**kwargs): - seen["model"] = kwargs["model"] - return {"choices": [{"message": {"content": "use docker"}}]} - - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - monkeypatch.setenv("MODEL", "openai/main-model") - monkeypatch.delenv("PREPROCESSOR_MODEL", raising=False) - - monkeypatch.setattr(module, "_get_litellm_completion", lambda: _completion) - monkeypatch.setattr(module, "render_prompt", lambda *_: "prompt") - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - result = module._llm_fallback_preprocess("please use docker", None) - - assert result == "use docker" - assert seen["model"] == "openai/main-model" - - -def test_litellm_proxy_preprocessor_model_override(monkeypatch): - module = _load_litellm_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_with_preproc_cfg_override" - ) - - seen: dict[str, Any] = {} - - def _completion(**kwargs): - seen["model"] = kwargs["model"] - return {"choices": [{"message": {"content": "use docker"}}]} - - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - monkeypatch.setenv("MODEL", "openai/main-model") - monkeypatch.setenv("PREPROCESSOR_MODEL", "openai/preprocessor-model") - - monkeypatch.setattr(module, "_get_litellm_completion", lambda: _completion) - monkeypatch.setattr(module, "render_prompt", lambda *_: "prompt") - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - result = module._llm_fallback_preprocess("please use docker", None) - - assert result == "use docker" - assert seen["model"] == "openai/preprocessor-model" - - -def test_litellm_fallback_rejects_premise_near_miss_rewrite(monkeypatch): - module = _load_module( - "litellm_with_preproc_cfg_reject_premise_rewrite", LITELLM_WITH_PREPROC_PATH - ) - - def _completion(**kwargs): - del kwargs - return {"choices": [{"message": {"content": "set premise concise replies"}}]} - - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - monkeypatch.setenv("MODEL", "openai/main-model") - monkeypatch.delenv("PREPROCESSOR_MODEL", raising=False) - - monkeypatch.setattr(module, "_get_litellm_completion", lambda: _completion) - monkeypatch.setattr(module, "render_prompt", lambda *_: "prompt") - - result = module._llm_fallback_preprocess("set premise to concise replies", None) - assert result is None - - -def test_litellm_proxy_fallback_rejects_premise_near_miss_rewrite(monkeypatch): - module = _load_litellm_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_with_preproc_cfg_reject_premise_rewrite" - ) - - def _completion(**kwargs): - del kwargs - return {"choices": [{"message": {"content": "change premise to concise replies"}}]} - - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - monkeypatch.setenv("MODEL", "openai/main-model") - monkeypatch.delenv("PREPROCESSOR_MODEL", raising=False) - - monkeypatch.setattr(module, "_get_litellm_completion", lambda: _completion) - monkeypatch.setattr(module, "render_prompt", lambda *_: "prompt") - - result = module._llm_fallback_preprocess("change premise concise replies", None) - assert result is None - - -def test_litellm_preprocess_skips_fallback_for_directive_shaped_malformed_inputs(monkeypatch): - module = _load_module("litellm_with_preproc_skip_fallback_malformed", LITELLM_WITH_PREPROC_PATH) - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: {"outcome": "no_directive", "directive": None}, - ) - - fallback_calls = 0 - - def _fallback(_message: str, _state: dict[str, object]) -> None: - nonlocal fallback_calls - fallback_calls += 1 - raise AssertionError("fallback should not be called for directive-shaped malformed input") - - downstream_calls = 0 - - def _downstream(_messages: list[dict[str, str]]) -> str: - nonlocal downstream_calls - downstream_calls += 1 - raise AssertionError("downstream should not be called for clarify output") - - monkeypatch.setattr(module, "_llm_fallback_preprocess", _fallback) - monkeypatch.setattr(module, "_call_litellm", _downstream) - - cases = [ - ( - "use", - "Policy item cannot be empty.\nUse 'use ' with a non-empty value.", - ), - ( - "prohibit", - "Policy item cannot be empty.\nUse 'prohibit ' with a non-empty value.", - ), - ( - "remove policy", - "Policy item cannot be empty.\nUse 'remove policy ' with a non-empty value.", - ), - ( - "use docker instead of", - "Replacement requires both new and old items.\n" - "Use 'use instead of ' with non-empty values.", - ), - ( - "use instead of docker", - "Replacement requires both new and old items.\n" - "Use 'use instead of ' with non-empty values.", - ), - ] - - for text, expected in cases: - engine = create_engine() - assert module.handle_turn(text, engine) == expected - - assert fallback_calls == 0 - assert downstream_calls == 0 diff --git a/tests/test_litellm_proxy_hooks.py b/tests/test_litellm_proxy_hooks.py deleted file mode 100644 index 75313e0..0000000 --- a/tests/test_litellm_proxy_hooks.py +++ /dev/null @@ -1,317 +0,0 @@ -import asyncio -import importlib.util -import sys -import types -from copy import deepcopy -from pathlib import Path - -BASIC_PROXY_PATH = Path("examples/integrations/litellm_proxy/context_compiler_precall_hook.py") -PREPROC_PROXY_PATH = Path( - "examples/integrations/litellm_proxy/context_compiler_precall_hook_with_preprocessor.py" -) - - -def _load_module(module_name: str, path: Path): - spec = importlib.util.spec_from_file_location(module_name, path) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def _load_proxy_module_with_stubs(monkeypatch, module_name: str, path: Path): - litellm_mod = types.ModuleType("litellm") - integrations_mod = types.ModuleType("litellm.integrations") - custom_logger_mod = types.ModuleType("litellm.integrations.custom_logger") - - class _CustomLogger: - pass - - custom_logger_mod.CustomLogger = _CustomLogger - - monkeypatch.setitem(sys.modules, "litellm", litellm_mod) - monkeypatch.setitem(sys.modules, "litellm.integrations", integrations_mod) - monkeypatch.setitem(sys.modules, "litellm.integrations.custom_logger", custom_logger_mod) - - return _load_module(module_name, path) - - -def _state( - *, premise: str | None = None, policies: dict[str, str] | None = None -) -> dict[str, object]: - return { - "premise": premise, - "policies": {} if policies is None else policies, - "version": 2, - } - - -def test_basic_proxy_hook_unsupported_call_type_returns_data_unchanged(monkeypatch) -> None: - module = _load_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_basic_unsupported_call_type", BASIC_PROXY_PATH - ) - hook = module.ContextCompilerPreCallHook() - data = {"messages": [{"role": "user", "content": "hello"}], "model": "demo"} - - result = asyncio.run(hook.async_pre_call_hook(None, None, data, "embeddings")) - - assert result is data - - -def test_basic_proxy_hook_clarify_returns_prompt_string_and_blocks_upstream(monkeypatch) -> None: - module = _load_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_basic_clarify", BASIC_PROXY_PATH - ) - hook = module.ContextCompilerPreCallHook() - captured: list[list[dict[str, str]]] = [] - - def _compile_transcript(transcript: list[dict[str, str]]) -> dict[str, object]: - captured.append(transcript) - return {"kind": "confirm", "prompt_to_user": 'Did you mean to use "kubectl" instead?'} - - monkeypatch.setattr(module, "compile_transcript", _compile_transcript) - data = { - "model": "demo", - "messages": [{"role": "user", "content": "use kubectl instead of docker"}], - } - - result = asyncio.run(hook.async_pre_call_hook(None, None, data, "completion")) - - assert result == 'Did you mean to use "kubectl" instead?' - assert captured == [[{"role": "user", "content": "use kubectl instead of docker"}]] - assert data["messages"] == [{"role": "user", "content": "use kubectl instead of docker"}] - - -def test_basic_proxy_hook_update_prepends_one_system_contract_message(monkeypatch) -> None: - module = _load_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_basic_update", BASIC_PROXY_PATH - ) - hook = module.ContextCompilerPreCallHook() - original_messages = [ - {"role": "system", "content": "original system"}, - {"role": "user", "content": "prohibit peanuts"}, - ] - data = {"model": "demo", "messages": deepcopy(original_messages)} - - monkeypatch.setattr( - module, - "compile_transcript", - lambda _transcript: {"kind": "state", "state": _state(policies={"peanuts": "prohibit"})}, - ) - - result = asyncio.run(hook.async_pre_call_hook(None, None, data, "completion")) - - assert result is data - assert isinstance(data["messages"], list) - messages = data["messages"] - assert len(messages) == len(original_messages) + 1 - assert messages[0]["role"] == "system" - assert "Host policy contract:" in messages[0]["content"] - assert "Never recommend or use prohibited items: peanuts." in messages[0]["content"] - assert messages[1:] == original_messages - - -def test_basic_proxy_hook_passthrough_preserves_original_messages_after_injected_contract( - monkeypatch, -) -> None: - module = _load_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_basic_passthrough_contract", BASIC_PROXY_PATH - ) - hook = module.ContextCompilerPreCallHook() - original_messages = [ - {"role": "system", "content": "original system"}, - {"role": "assistant", "content": "earlier reply"}, - {"role": "user", "content": "hello there"}, - ] - data = {"model": "demo", "messages": deepcopy(original_messages)} - - monkeypatch.setattr( - module, - "compile_transcript", - lambda _transcript: {"kind": "state", "state": _state()}, - ) - - result = asyncio.run(hook.async_pre_call_hook(None, None, data, "chat_completion")) - - assert result is data - assert data["messages"][1:] == original_messages - assert data["messages"][0]["role"] == "system" - assert "Host policy contract:" in data["messages"][0]["content"] - - -def test_basic_proxy_hook_extracts_only_user_text_messages_from_mixed_content_shapes( - monkeypatch, -) -> None: - module = _load_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_basic_extract_user_text", BASIC_PROXY_PATH - ) - messages = [ - {"role": "system", "content": "system"}, - {"role": "assistant", "content": "assistant text"}, - {"role": "user", "content": "plain user text"}, - { - "role": "user", - "content": [ - {"type": "text", "text": "alpha"}, - {"type": "image_url", "image_url": {"url": "https://example.test/image.png"}}, - {"type": "text", "text": "beta"}, - ], - }, - {"role": "user", "content": [{"type": "image_url", "image_url": {"url": "ignored"}}]}, - {"role": "user", "content": {"type": "text", "text": "ignored non-list shape"}}, - ] - - transcript = module._extract_user_transcript(messages) - - assert transcript == [ - {"role": "user", "content": "plain user text"}, - {"role": "user", "content": "alpha beta"}, - ] - - -def test_preprocessor_proxy_hook_only_rewrites_latest_user_message_for_replay(monkeypatch) -> None: - module = _load_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_preproc_rewrites_latest_only", PREPROC_PROXY_PATH - ) - hook = module.ContextCompilerPreCallHookWithPreprocessor() - compile_calls: list[list[dict[str, str]]] = [] - - def _compile_transcript(transcript: list[dict[str, str]]) -> dict[str, object]: - compile_calls.append(transcript) - if len(compile_calls) == 1: - return {"kind": "state", "state": _state()} - return {"kind": "state", "state": _state(policies={"docker": "use"})} - - monkeypatch.setattr(module, "compile_transcript", _compile_transcript) - monkeypatch.setattr( - module, "_preprocess_last_user_message", lambda _message, _state: "use docker" - ) - - data = { - "model": "demo", - "messages": [ - {"role": "user", "content": "hello there"}, - {"role": "assistant", "content": "hi"}, - {"role": "user", "content": "please use docker"}, - ], - } - - result = asyncio.run(hook.async_pre_call_hook(None, None, data, "completion")) - - assert result is data - assert compile_calls == [ - [{"role": "user", "content": "hello there"}], - [ - {"role": "user", "content": "hello there"}, - {"role": "user", "content": "use docker"}, - ], - ] - - -def test_preprocessor_proxy_hook_does_not_rewrite_forwarded_request_messages(monkeypatch) -> None: - module = _load_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_preproc_preserves_forwarded_messages", PREPROC_PROXY_PATH - ) - hook = module.ContextCompilerPreCallHookWithPreprocessor() - original_messages = [ - {"role": "system", "content": "original system"}, - {"role": "user", "content": "please use docker"}, - ] - data = {"model": "demo", "messages": deepcopy(original_messages)} - - compile_calls: list[list[dict[str, str]]] = [] - - def _compile_transcript(transcript: list[dict[str, str]]) -> dict[str, object]: - compile_calls.append(transcript) - if len(compile_calls) == 1: - return {"kind": "state", "state": _state()} - return {"kind": "state", "state": _state(policies={"docker": "use"})} - - monkeypatch.setattr(module, "compile_transcript", _compile_transcript) - monkeypatch.setattr( - module, "_preprocess_last_user_message", lambda _message, _state: "use docker" - ) - - result = asyncio.run(hook.async_pre_call_hook(None, None, data, "completion")) - - assert result is data - assert data["messages"][1:] == original_messages - assert data["messages"][2]["content"] == "please use docker" - assert compile_calls[-1] == [{"role": "user", "content": "use docker"}] - - -def test_preprocessor_proxy_hook_abstain_leaves_replay_input_unchanged(monkeypatch) -> None: - module = _load_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_preproc_abstain_unchanged", PREPROC_PROXY_PATH - ) - hook = module.ContextCompilerPreCallHookWithPreprocessor() - compile_calls: list[list[dict[str, str]]] = [] - - def _compile_transcript(transcript: list[dict[str, str]]) -> dict[str, object]: - compile_calls.append(transcript) - return {"kind": "state", "state": _state()} - - monkeypatch.setattr(module, "compile_transcript", _compile_transcript) - monkeypatch.setattr(module, "_state_before_last_message", lambda _transcript: _state()) - monkeypatch.setattr(module, "_preprocess_last_user_message", lambda _message, _state: None) - - data = { - "model": "demo", - "messages": [ - {"role": "user", "content": "first user turn"}, - {"role": "assistant", "content": "assistant reply"}, - {"role": "user", "content": "i think we should use docker"}, - ], - } - - result = asyncio.run(hook.async_pre_call_hook(None, None, data, "completion")) - - assert result is data - assert compile_calls == [ - [ - {"role": "user", "content": "first user turn"}, - {"role": "user", "content": "i think we should use docker"}, - ] - ] - - -def test_preprocessor_proxy_hook_invalid_fallback_output_does_not_mutate_replay_input( - monkeypatch, -) -> None: - module = _load_proxy_module_with_stubs( - monkeypatch, "litellm_proxy_preproc_invalid_fallback_unchanged", PREPROC_PROXY_PATH - ) - hook = module.ContextCompilerPreCallHookWithPreprocessor() - compile_calls: list[list[dict[str, str]]] = [] - - def _compile_transcript(transcript: list[dict[str, str]]) -> dict[str, object]: - compile_calls.append(transcript) - return {"kind": "state", "state": _state()} - - monkeypatch.setattr(module, "compile_transcript", _compile_transcript) - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: {"outcome": "no_directive", "directive": None}, - ) - monkeypatch.setattr(module, "_llm_fallback_preprocess", lambda _message, _state: None) - - data = { - "model": "demo", - "messages": [ - {"role": "user", "content": "first user turn"}, - {"role": "assistant", "content": "assistant reply"}, - {"role": "user", "content": "please use docker"}, - ], - } - - result = asyncio.run(hook.async_pre_call_hook(None, None, data, "completion")) - - assert result is data - assert compile_calls == [ - [{"role": "user", "content": "first user turn"}], - [ - {"role": "user", "content": "first user turn"}, - {"role": "user", "content": "please use docker"}, - ], - ] diff --git a/tests/test_litellm_response_format_example.py b/tests/test_litellm_response_format_example.py deleted file mode 100644 index d217c7b..0000000 --- a/tests/test_litellm_response_format_example.py +++ /dev/null @@ -1,112 +0,0 @@ -import importlib.util -from pathlib import Path -from typing import Any - -from context_compiler import create_engine - -REPO_ROOT = Path(__file__).resolve().parents[1] -EXAMPLE_PATH = REPO_ROOT / "examples" / "integrations" / "litellm" / "response_format.py" - - -def _load_module(): - spec = importlib.util.spec_from_file_location("litellm_response_format_example", EXAMPLE_PATH) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def test_no_supported_policy_selects_no_response_format() -> None: - module = _load_module() - engine = create_engine() - - plan = module.plan_turn("hello", engine) - assert plan["decision_kind"] == "passthrough" - assert plan["selected_response_format_item"] is None - assert plan["response_format"] is None - - -def test_use_compact_summary_selects_compact_summary_response_format() -> None: - module = _load_module() - engine = create_engine() - - assert engine.step("use compact_summary")["kind"] == "update" - - plan = module.plan_turn("summarize this", engine) - assert plan["selected_response_format_item"] == "compact_summary" - assert plan["response_format"] == module.COMPACT_SUMMARY_RESPONSE_FORMAT - - -def test_use_action_plan_selects_action_plan_response_format() -> None: - module = _load_module() - engine = create_engine() - - assert engine.step("use action_plan")["kind"] == "update" - - plan = module.plan_turn("what should i do next?", engine) - assert plan["selected_response_format_item"] == "action_plan" - assert plan["response_format"] == module.ACTION_PLAN_RESPONSE_FORMAT - - -def test_prohibit_compact_summary_omits_compact_summary_response_format() -> None: - module = _load_module() - engine = create_engine() - - assert engine.step("prohibit compact_summary")["kind"] == "update" - - plan = module.plan_turn("summarize this", engine) - assert plan["selected_response_format_item"] is None - assert plan["response_format"] is None - - -def test_contradiction_path_returns_clarify_and_skips_response_format() -> None: - module = _load_module() - engine = create_engine() - - assert engine.step("use compact_summary")["kind"] == "update" - - plan = module.plan_turn("prohibit compact_summary", engine) - assert plan["decision_kind"] == "clarify" - assert plan["clarify_prompt"] is not None - assert plan["selected_response_format_item"] is None - assert plan["response_format"] is None - - -def test_optional_litellm_call_includes_response_format_when_selected(monkeypatch) -> None: - module = _load_module() - seen: dict[str, object] = {} - - def _completion(**kwargs: Any) -> dict[str, object]: - seen.update(kwargs) - return {"choices": [{"message": {"content": "ok"}}]} - - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - monkeypatch.setattr(module, "_get_litellm_completion", lambda: _completion) - - result = module.optional_litellm_call( - user_input="summarize this", - response_format=module.COMPACT_SUMMARY_RESPONSE_FORMAT, - ) - - assert result == "ok" - assert seen["response_format"] == module.COMPACT_SUMMARY_RESPONSE_FORMAT - - -def test_optional_litellm_call_omits_response_format_when_not_selected(monkeypatch) -> None: - module = _load_module() - seen: dict[str, object] = {} - - def _completion(**kwargs: Any) -> dict[str, object]: - seen.update(kwargs) - return {"choices": [{"message": {"content": "ok"}}]} - - monkeypatch.setenv("OPENAI_API_KEY", "dummy") - monkeypatch.setattr(module, "_get_litellm_completion", lambda: _completion) - - result = module.optional_litellm_call( - user_input="hello", - response_format=None, - ) - - assert result == "ok" - assert "response_format" not in seen diff --git a/tests/test_ollama_structured_output_example.py b/tests/test_ollama_structured_output_example.py deleted file mode 100644 index 656c260..0000000 --- a/tests/test_ollama_structured_output_example.py +++ /dev/null @@ -1,60 +0,0 @@ -import importlib.util -from pathlib import Path - -from context_compiler import create_engine - -REPO_ROOT = Path(__file__).resolve().parents[1] -EXAMPLE_PATH = REPO_ROOT / "examples" / "integrations" / "ollama_structured_output" / "example.py" - - -def _load_module(): - spec = importlib.util.spec_from_file_location("ollama_structured_output_example", EXAMPLE_PATH) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def test_use_python_script_selects_python_schema() -> None: - module = _load_module() - engine = create_engine() - - first = engine.step("use python_script") - assert first["kind"] == "update" - - plan = module.plan_turn("write script", engine) - assert plan["selected_schema_item"] == "python_script" - assert plan["format_schema"] == module.PYTHON_SCRIPT_SCHEMA - - -def test_prohibit_shell_command_does_not_select_shell_schema() -> None: - module = _load_module() - engine = create_engine() - - assert engine.step("use python_script")["kind"] == "update" - assert engine.step("prohibit shell_command")["kind"] == "update" - - plan = module.plan_turn("do task", engine) - assert plan["selected_schema_item"] == "python_script" - assert plan["format_schema"] != module.SHELL_COMMAND_SCHEMA - - -def test_unknown_or_no_matching_state_selects_no_schema() -> None: - module = _load_module() - engine = create_engine() - - plan = module.plan_turn("hello", engine) - assert plan["decision_kind"] == "passthrough" - assert plan["selected_schema_item"] is None - assert plan["format_schema"] is None - - -def test_contradictory_input_is_compiler_clarify_not_host_resolution() -> None: - module = _load_module() - engine = create_engine() - - assert engine.step("use python_script")["kind"] == "update" - conflict_plan = module.plan_turn("prohibit python_script", engine) - assert conflict_plan["decision_kind"] == "clarify" - assert conflict_plan["selected_schema_item"] is None - assert conflict_plan["format_schema"] is None diff --git a/tests/test_openwebui_pipe.py b/tests/test_openwebui_pipe.py deleted file mode 100644 index 6554607..0000000 --- a/tests/test_openwebui_pipe.py +++ /dev/null @@ -1,1394 +0,0 @@ -import asyncio -import builtins -import importlib.util -import json -import sys -import types -from pathlib import Path - -import pytest - -MODULE_PATH = Path("examples/integrations/openwebui/open_webui_pipe.py") - - -def _load_module_with_openwebui_stubs(module_name: str, monkeypatch): - fastapi_mod = types.ModuleType("fastapi") - - class _Request: - pass - - fastapi_mod.Request = _Request - - open_webui_mod = types.ModuleType("open_webui") - open_webui_models_mod = types.ModuleType("open_webui.models") - open_webui_models_users_mod = types.ModuleType("open_webui.models.users") - open_webui_utils_mod = types.ModuleType("open_webui.utils") - open_webui_utils_chat_mod = types.ModuleType("open_webui.utils.chat") - - class _Users: - @staticmethod - def get_user_by_id(user_id: object) -> dict[str, object]: - return {"id": user_id} - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - return {"choices": [{"message": {"content": payload.get("_mock_content", "")}}]} - - open_webui_models_users_mod.Users = _Users - open_webui_utils_chat_mod.generate_chat_completion = _chat_completion - - sys.modules["fastapi"] = fastapi_mod - sys.modules["open_webui"] = open_webui_mod - sys.modules["open_webui.models"] = open_webui_models_mod - sys.modules["open_webui.models.users"] = open_webui_models_users_mod - sys.modules["open_webui.utils"] = open_webui_utils_mod - sys.modules["open_webui.utils.chat"] = open_webui_utils_chat_mod - - real_import = builtins.__import__ - - def _guarded_import( - name: str, - globals_: dict[str, object] | None = None, - locals_: dict[str, object] | None = None, - fromlist: tuple[str, ...] = (), - level: int = 0, - ) -> object: - if name == "pydantic": - raise ModuleNotFoundError("No module named 'pydantic'") - return real_import(name, globals_, locals_, fromlist, level) - - monkeypatch.setattr(builtins, "__import__", _guarded_import) - - spec = importlib.util.spec_from_file_location(module_name, MODULE_PATH) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def test_openwebui_pipe_imports_without_pydantic(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_no_pydantic", monkeypatch) - pipe = module.Pipe() - assert pipe.valves.BASE_MODEL_ID == "" - - -def test_pipe_restore_and_persist_checkpoint_points(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_checkpoint", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY["chat-1"] = "ckpt-in" - - class _FakeEngine: - def __init__(self, kind: str, checkpoint_out: str) -> None: - self.kind = kind - self.state = {"premise": None, "policies": {}, "version": 2} - self.imported: list[str] = [] - self._checkpoint_out = checkpoint_out - self.export_calls = 0 - - def import_checkpoint_json(self, payload: str) -> None: - self.imported.append(payload) - - def export_checkpoint_json(self) -> str: - self.export_calls += 1 - return self._checkpoint_out - - def export_checkpoint(self) -> dict[str, object]: - return { - "checkpoint_version": 1, - "authoritative_state": self.state, - "pending": None, - } - - def step(self, _text: str) -> dict[str, object]: - if self.kind == "clarify": - return {"kind": "clarify", "prompt_to_user": "confirm?", "state": None} - return {"kind": self.kind, "state": self.state} - - created: list[_FakeEngine] = [] - - def _create_engine(): - engine = _FakeEngine("clarify", "ckpt-clarify") - created.append(engine) - return engine - - monkeypatch.setattr(module, "create_engine", _create_engine) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "test"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-1", - ) - ) - assert result == "confirm?" - assert created[0].imported == ["ckpt-in"] - assert module._CHECKPOINTS_BY_CHAT_KEY["chat-1"] == "ckpt-clarify" - assert created[0].export_calls == 1 - - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY["chat-2"] = "ckpt-keep" - - passthrough_engine = _FakeEngine("passthrough", "ckpt-new") - monkeypatch.setattr(module, "create_engine", lambda: passthrough_engine) - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-2", - ) - ) - assert isinstance(result, dict) - assert passthrough_engine.imported == ["ckpt-keep"] - assert passthrough_engine.export_calls == 0 - assert module._CHECKPOINTS_BY_CHAT_KEY["chat-2"] == "ckpt-keep" - - -def test_pipe_requires_base_model_id(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_requires_base", monkeypatch) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "" - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert result == "Context Compiler pipe misconfigured: BASE_MODEL_ID is required." - - -def test_pipe_blocks_recursive_base_model_id(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_recursion_guard", monkeypatch) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "pipe-model" - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert result == ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID must not match " - "the selected pipe model id to avoid recursive routing." - ) - - -def test_pipe_normalizes_model_not_found_response(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_model_not_found_response", monkeypatch) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - del payload - return {"error": {"message": "MODEL NOT FOUND"}} - - module.generate_chat_completion = _chat_completion - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert result == ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is invalid or not " - "configured in Open WebUI. Configure a valid model id in " - "Admin Panel → Settings → Models." - ) - - -def test_pipe_normalizes_model_not_found_exception(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_model_not_found_exception", monkeypatch) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - - class _ForwardError(Exception): - def __init__(self) -> None: - super().__init__("forward failed") - self.detail = {"error": {"message": "model not found"}} - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - del payload - raise _ForwardError() - - module.generate_chat_completion = _chat_completion - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert result == ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is invalid or not " - "configured in Open WebUI. Configure a valid model id in " - "Admin Panel → Settings → Models." - ) - - -def test_pipe_supports_async_user_lookup(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_async_user_lookup", monkeypatch) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - - async def _get_user_by_id(user_id: object) -> dict[str, object]: - return {"id": user_id} - - monkeypatch.setattr(module.Users, "get_user_by_id", _get_user_by_id) - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert isinstance(result, dict) - - -def test_pipe_normal_update_returns_local_ack_and_persists_checkpoint(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_update_forward", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"ok": True} - - module.generate_chat_completion = _chat_completion - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - chat_key = "chat-normal-update" - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "prohibit peanuts"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_key, - ) - ) - - assert result == "State updated: Prohibit peanuts." - assert len(forwarded_payloads) == 0 - - checkpoint = json.loads(module._CHECKPOINTS_BY_CHAT_KEY[chat_key]) - assert checkpoint["pending"] is None - assert checkpoint["authoritative_state"]["policies"] == {"peanuts": "prohibit"} - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "remove policy peanuts"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_key, - ) - ) - - assert result == "State updated: Removed policy peanuts." - assert len(forwarded_payloads) == 0 - - checkpoint = json.loads(module._CHECKPOINTS_BY_CHAT_KEY[chat_key]) - assert checkpoint["pending"] is None - assert checkpoint["authoritative_state"]["policies"] == {} - - -def test_pipe_update_directives_return_local_ack_across_shapes(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_update_shapes", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - forwarded_payloads: list[dict[str, object]] = [] - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"ok": True} - - module.generate_chat_completion = _track_downstream - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use DOCKER"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-use", - ) - ) - assert result == "State updated: Use docker." - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "prohibit DOCKER"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-prohibit", - ) - ) - assert result == "State updated: Prohibit docker." - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-idempotent", - ) - ) - assert result == "State updated: Use docker." - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "use docker"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-replace", - ) - ) - assert result == "State updated: Use docker." - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "use KUBECTL instead of DOCKER"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-replace", - ) - ) - assert result == "State updated: Use kubectl." - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "set premise concise answers"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-premise", - ) - ) - assert result == "State updated." - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "clear premise"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-premise", - ) - ) - assert result == "Premise cleared." - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "use docker instead of docker"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-replace-noop", - ) - ) - assert result == "State updated: Use docker." - assert len(forwarded_payloads) == 0 - - -def test_pipe_show_state_returns_local_summary_and_no_downstream(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_show_state", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - del payload - nonlocal downstream_calls - downstream_calls += 1 - return {"choices": [{"message": {"content": "downstream"}}]} - - module.generate_chat_completion = _track_downstream - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - chat_id = "chat-show-state" - - no_pending = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "show state"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert no_pending == ("Premise: none\nUse: none\nProhibit: none\nPending clarification: no") - - assert downstream_calls == 0 - assert "Context Compiler trace" not in no_pending - - -def test_pipe_show_state_reports_pending_yes(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_show_state_pending", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - class _PendingEngine: - state = {"premise": None, "policies": {}, "version": 2} - - def has_pending_clarification(self) -> bool: - return True - - def step(self, _: str) -> dict[str, object]: - raise AssertionError("show state should not step engine") - - monkeypatch.setattr(module, "create_engine", lambda: _PendingEngine()) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "show state"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-show-state-pending", - ) - ) - assert result == "Premise: none\nUse: none\nProhibit: none\nPending clarification: yes" - - -def test_pipe_show_state_non_exact_routes_normally(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_show_state_non_exact", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - del payload - nonlocal downstream_calls - downstream_calls += 1 - return {"choices": [{"message": {"content": "downstream"}}]} - - module.generate_chat_completion = _track_downstream - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "show state please"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-show-state-non-exact", - ) - ) - assert result == {"choices": [{"message": {"content": "downstream"}}]} - assert downstream_calls == 1 - - -def test_pipe_show_state_exact_match_is_case_insensitive_after_trim(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_show_state_case_trim", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - del payload - nonlocal downstream_calls - downstream_calls += 1 - return {"choices": [{"message": {"content": "downstream"}}]} - - module.generate_chat_completion = _track_downstream - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": " ShOw StAtE "}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-show-state-case-trim", - ) - ) - - assert result == "Premise: none\nUse: none\nProhibit: none\nPending clarification: no" - assert downstream_calls == 0 - - -def test_pipe_near_miss_directives_return_deterministic_clarify_without_downstream( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_near_miss_clarify", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - nonlocal downstream_calls - downstream_calls += 1 - raise AssertionError(f"downstream model should not be called: {payload.get('model')}") - - module.generate_chat_completion = _track_downstream - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - - cases = [ - ("reset premise", "Unknown directive.\nUse 'clear premise' or 'reset policies'."), - ("reset premises", "Unknown directive.\nUse 'clear premise' or 'reset policies'."), - ("clear premises", "Unknown directive.\nUse 'clear premise' or 'reset policies'."), - ("set premise to concise answers", "Invalid premise syntax.\nUse 'set premise '."), - ( - "change premise formal tone", - "Invalid premise syntax.\nUse 'change premise to '.", - ), - ] - - for idx, (user_input, expected) in enumerate(cases): - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": user_input}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=f"chat-near-miss-{idx}", - ) - ) - assert result == expected - - assert downstream_calls == 0 - - -def test_pipe_incomplete_replacement_clarify_has_no_model_call(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs( - "owui_pipe_incomplete_replacement_clarify", monkeypatch - ) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - nonlocal downstream_calls - downstream_calls += 1 - raise AssertionError(f"downstream model should not be called: {payload.get('model')}") - - module.generate_chat_completion = _track_downstream - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - chat_id = "chat-incomplete-replacement-clarify" - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "use docker instead of"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert result == ( - "Replacement requires both new and old items.\n" - "Use 'use instead of ' with non-empty values." - ) - assert downstream_calls == 0 - - state_view = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "show state"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert state_view == "Premise: none\nUse: none\nProhibit: none\nPending clarification: no" - - -@pytest.mark.parametrize( - ("confirmation", "expected_policies"), - [ - ("yes", {"docker": "use"}), - ("YES!", {"docker": "use"}), - (" yes please ", {"docker": "use"}), - ("no thanks.", {}), - ], -) -def test_pipe_confirmation_update_returns_local_ack( - monkeypatch, - confirmation: str, - expected_policies: dict[str, str], -) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_confirmation_ack", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - forwarded_payloads: list[dict[str, object]] = [] - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"ok": True} - - module.generate_chat_completion = _track_downstream - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - chat_key = "chat-confirm-ack" - - clarify = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "use docker instead of kubectl"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_key, - ) - ) - assert clarify == 'Did you mean to use "docker" instead?' - first_checkpoint = json.loads(module._CHECKPOINTS_BY_CHAT_KEY[chat_key]) - assert first_checkpoint["pending"] is not None - - resumed = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": confirmation}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_key, - ) - ) - assert resumed == "State updated." - assert len(forwarded_payloads) == 0 - - resumed_engine = module._ENGINES_BY_CHAT_KEY[chat_key] - assert resumed_engine.state == { - "premise": None, - "policies": expected_policies, - "version": 2, - } - resumed_checkpoint = json.loads(module._CHECKPOINTS_BY_CHAT_KEY[chat_key]) - assert resumed_checkpoint["pending"] is None - - -def test_pipe_trace_off_keeps_existing_response_shape(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_trace_off_shape", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - del payload - return {"choices": [{"message": {"content": "downstream"}}]} - - module.generate_chat_completion = _chat_completion - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = False - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-trace-off", - ) - ) - assert result == {"choices": [{"message": {"content": "downstream"}}]} - - -def test_pipe_trace_on_appends_trace_to_user_visible_output(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_trace_on", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - del payload - return {"choices": [{"message": {"content": "downstream"}}]} - - module.generate_chat_completion = _chat_completion - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-trace-on", - ) - ) - content = result["choices"][0]["message"]["content"] - assert "downstream" in content - assert "Context Compiler trace" in content - assert "decision kind: passthrough" in content - assert "downstream LLM call: yes" in content - assert "active state:" in content - assert "state injected: no" in content - - -def test_pipe_trace_on_passthrough_stream_appends_trace_after_chunks(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_trace_on_stream", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - async def _streaming_response() -> object: - for part in ("down", "stream"): - yield part - - async def _chat_completion(_: object, payload: dict[str, object], __: object) -> object: - del payload - return _streaming_response() - - module.generate_chat_completion = _chat_completion - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-trace-on-stream", - ) - ) - - async def _collect() -> str: - parts: list[str] = [] - async for chunk in result: - assert isinstance(chunk, str) - parts.append(chunk) - return "".join(parts) - - content = asyncio.run(_collect()) - assert content.startswith("downstream") - assert "Context Compiler trace" in content - assert "decision kind: passthrough" in content - assert "downstream LLM call: yes" in content - assert "active state:" in content - assert "state injected: no" in content - - -def test_pipe_trace_on_update_shows_local_ack_and_no_downstream_call(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_trace_update", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - nonlocal downstream_calls - downstream_calls += 1 - del payload - return {"choices": [{"message": {"content": "downstream"}}]} - - module.generate_chat_completion = _track_downstream - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "prohibit peanuts"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-trace-update", - ) - ) - assert isinstance(result, str) - content = result - assert content.startswith("State updated: Prohibit peanuts.") - assert "Context Compiler trace" in content - assert "decision kind: update" in content - assert "downstream LLM call: no" in content - assert "state change:" in content - assert "active state:" in content - assert "state injected: no" in content - assert "\n\ndecision kind: update" in content - assert "\n\nstate injected: no" in content - assert downstream_calls == 0 - - -def test_pipe_trace_on_clarify_shows_prompt_and_no_downstream_call(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_trace_clarify", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - nonlocal downstream_calls - downstream_calls += 1 - del payload - return {"choices": [{"message": {"content": "downstream"}}]} - - module.generate_chat_completion = _track_downstream - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "set premise to concise answers"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-trace-clarify", - ) - ) - assert isinstance(result, str) - assert "decision kind: clarify" in result - assert "active state:" in result - assert "clarification prompt:" in result - assert "downstream LLM call: no" in result - assert "state injected: no" in result - assert downstream_calls == 0 - - -def test_pipe_trace_appends_on_object_response_for_passthrough_and_update(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_trace_object_response", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - class _Message: - def __init__(self, content: str) -> None: - self.content = content - - class _Choice: - def __init__(self, content: str) -> None: - self.message = _Message(content) - - class _Response: - def __init__(self, content: str) -> None: - self.choices = [_Choice(content)] - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion(_: object, payload: dict[str, object], __: object) -> object: - forwarded_payloads.append(payload) - return _Response("downstream") - - module.generate_chat_completion = _chat_completion - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - passthrough = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-object-passthrough", - ) - ) - assert hasattr(passthrough, "choices") - passthrough_content = passthrough.choices[0].message.content - assert "Context Compiler trace" in passthrough_content - assert "decision kind: passthrough" in passthrough_content - assert "downstream LLM call: yes" in passthrough_content - - update = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-object-update", - ) - ) - assert isinstance(update, str) - update_content = update - assert "Context Compiler trace" in update_content - assert "decision kind: update" in update_content - assert "downstream LLM call: no" in update_content - assert "state injected: no" in update_content - assert len(forwarded_payloads) == 1 - - -def test_pipe_trace_appends_on_streaming_response_wrapper_passthrough_and_update( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_trace_streaming_wrapper", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - class _StreamingResponse: - def __init__(self, parts: tuple[str, ...]) -> None: - async def _iter() -> object: - for part in parts: - yield part - - self.body_iterator = _iter() - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion(_: object, payload: dict[str, object], __: object) -> object: - forwarded_payloads.append(payload) - return _StreamingResponse(("data: stub\n\n", "data: [DONE]\n\n")) - - module.generate_chat_completion = _chat_completion - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - passthrough = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-stream-wrapper-passthrough", - ) - ) - - async def _collect_stream(wrapper: object) -> str: - parts: list[str] = [] - async for chunk in wrapper.body_iterator: - assert isinstance(chunk, str) - parts.append(chunk) - return "".join(parts) - - passthrough_stream = asyncio.run(_collect_stream(passthrough)) - assert "data: [DONE]" in passthrough_stream - assert "Context Compiler trace" in passthrough_stream - assert "decision kind: passthrough" in passthrough_stream - - update = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-stream-wrapper-update", - ) - ) - assert isinstance(update, str) - assert "Context Compiler trace" in update - assert "decision kind: update" in update - assert "state injected: no" in update - assert len(forwarded_payloads) == 1 - - -@pytest.mark.parametrize( - ("steps", "expected_ack"), - [ - ( - ["use docker", "clear state"], - "State cleared.", - ), - ( - ["set premise concise replies", "clear premise"], - "Premise cleared.", - ), - ( - ["use docker", "use pytest", "reset policies"], - "Policies reset.", - ), - ( - ["use docker", "remove policy docker"], - "State updated: Removed policy docker.", - ), - ], -) -def test_pipe_trace_update_clear_reset_paths_single_and_consistent( - monkeypatch, - steps: list[str], - expected_ack: str, -) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_trace_clear_reset", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - nonlocal downstream_calls - downstream_calls += 1 - del payload - return {"choices": [{"message": {"content": "downstream"}}]} - - module.generate_chat_completion = _chat_completion - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result: object = "" - for idx, user_input in enumerate(steps): - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": user_input}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=f"chat-trace-clear-reset-{hash(tuple(steps))}", - ) - ) - if idx < len(steps) - 1: - continue - - assert isinstance(result, str) - assert result.startswith(expected_ack) - content = result - assert content.count("Context Compiler trace") == 1 - assert "decision kind: update" in content - assert "downstream LLM call: no" in content - assert "downstream LLM call: yes" not in content - assert "active state: none" in content - assert "state injected: no" in content - assert downstream_calls == 0 - - -def test_pipe_clear_state_trace_not_duplicated_when_model_echoes_history(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_trace_echo_dedupe", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - messages = payload.get("messages") - echoed = "" - if isinstance(messages, list): - assistant_contents = [ - str(msg.get("content", "")) - for msg in messages - if isinstance(msg, dict) and msg.get("role") == "assistant" - ] - echoed = "\n".join(assistant_contents) - content = "downstream" - if echoed: - content += f"\n{echoed}" - return {"choices": [{"message": {"content": content}}]} - - module.generate_chat_completion = _chat_completion - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - chat_id = "chat-trace-echo-dedupe" - - first = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(first, str) - first_content = first - assert first_content.count("Context Compiler trace") == 1 - - second = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [ - {"role": "assistant", "content": first_content}, - {"role": "user", "content": "clear state"}, - ], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(second, str) - second_content = second - assert second_content.count("Context Compiler trace") == 1 - assert "decision kind: update" in second_content - assert "downstream LLM call: no" in second_content - assert "downstream LLM call: yes" not in second_content - assert "active state: none" in second_content - assert "state injected: no" in second_content - - -def test_pipe_clear_state_strips_preexisting_contradictory_trace_from_model_output( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_trace_strip_contradiction", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - old_trace = ( - "Context Compiler trace\n\n" - "decision kind: update\n" - "active state: none\n" - "downstream LLM call: no\n" - "\n" - "state injected: none" - ) - - call_count = 0 - - async def _chat_completion( - _: object, - payload: dict[str, object], - __: object, - ) -> dict[str, object]: - nonlocal call_count - call_count += 1 - if call_count == 1: - return {"choices": [{"message": {"content": "downstream"}}]} - del payload - return {"choices": [{"message": {"content": f"downstream\n{old_trace}"}}]} - - module.generate_chat_completion = _chat_completion - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - chat_id = "chat-trace-strip-contradiction" - - _ = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - - second = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "clear state"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(second, str) - second_content = second - assert second_content.count("Context Compiler trace") == 1 - assert "downstream LLM call: no" in second_content - assert "downstream LLM call: yes" not in second_content - - -def test_pipe_passthrough_injects_active_state_and_trace_reports_yes(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_passthrough_state_injection", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"choices": [{"message": {"content": "answer"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - chat_id = "chat-passthrough-injected-state" - - update = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert "State updated: Use docker." in update - assert len(forwarded_payloads) == 0 - - passthrough = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "what container runtime should i use?"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(passthrough, dict) - assert len(forwarded_payloads) == 1 - messages = forwarded_payloads[0]["messages"] - assert isinstance(messages, list) - assert any( - isinstance(msg, dict) - and msg.get("role") == "system" - and isinstance(msg.get("content"), str) - and msg["content"].startswith("[[cc_state]]") - and "Use: docker" in msg["content"] - for msg in messages - ) - content = passthrough["choices"][0]["message"]["content"] - assert "state injected: yes" in content - - -def test_pipe_empty_state_passthrough_does_not_inject_and_trace_reports_no(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_empty_state_passthrough", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"choices": [{"message": {"content": "answer"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - passthrough = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-empty-state-passthrough", - ) - ) - assert isinstance(passthrough, dict) - assert len(forwarded_payloads) == 1 - messages = forwarded_payloads[0]["messages"] - assert isinstance(messages, list) - assert not any( - isinstance(msg, dict) - and msg.get("role") == "system" - and isinstance(msg.get("content"), str) - and msg["content"].startswith("[[cc_state]]") - for msg in messages - ) - content = passthrough["choices"][0]["message"]["content"] - assert "downstream LLM call: yes" in content - assert "state injected: no" in content - - -def test_pipe_repeated_passthrough_does_not_duplicate_compiler_state_prompt(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_pipe_repeated_passthrough_no_dup", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"choices": [{"message": {"content": "answer"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - chat_id = "chat-repeated-passthrough-no-dup" - - _ = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - for idx in range(2): - _ = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": f"question {idx}"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - - assert len(forwarded_payloads) == 2 - for payload in forwarded_payloads: - messages = payload.get("messages") - assert isinstance(messages, list) - cc_messages = [ - msg - for msg in messages - if isinstance(msg, dict) - and msg.get("role") == "system" - and isinstance(msg.get("content"), str) - and msg["content"].startswith("[[cc_state]]") - ] - assert len(cc_messages) == 1 diff --git a/tests/test_openwebui_preprocessor_pipe.py b/tests/test_openwebui_preprocessor_pipe.py deleted file mode 100644 index 6ace403..0000000 --- a/tests/test_openwebui_preprocessor_pipe.py +++ /dev/null @@ -1,2390 +0,0 @@ -import asyncio -import builtins -import importlib.util -import json -import sys -import types -from pathlib import Path -from typing import Any, cast - -import pytest - -MODULE_PATH = Path("examples/integrations/openwebui/open_webui_pipe_with_preprocessor.py") - - -class DummyValves: - def __init__( - self, - base_model_id: str | None, - preprocessor_model_id: str | None, - allow_missing_base: bool = False, - ) -> None: - self.BASE_MODEL_ID = base_model_id - self.PREPROCESSOR_MODEL_ID = preprocessor_model_id - self.ALLOW_MISSING_BASE_MODEL_FOR_DEBUG = allow_missing_base - self.PREPROCESSOR_PROMPT_PROFILE = "default" - self.SHOW_CONTEXT_COMPILER_TRACE = False - - -def _load_module_with_openwebui_stubs( - module_name: str, monkeypatch, *, block_pydantic: bool = True -): - fastapi_mod = types.ModuleType("fastapi") - - class _Request: # minimal placeholder for type import - pass - - fastapi_mod.Request = _Request - - open_webui_mod = types.ModuleType("open_webui") - open_webui_models_mod = types.ModuleType("open_webui.models") - open_webui_models_users_mod = types.ModuleType("open_webui.models.users") - open_webui_utils_mod = types.ModuleType("open_webui.utils") - open_webui_utils_chat_mod = types.ModuleType("open_webui.utils.chat") - open_webui_utils_models_mod = types.ModuleType("open_webui.utils.models") - - class _Users: - @staticmethod - def get_user_by_id(user_id: object) -> dict[str, object]: - return {"id": user_id} - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - return {"choices": [{"message": {"content": payload.get("_mock_content", "")}}]} - - async def _all_models(_: object, user: object = None) -> list[dict[str, str]]: - del user - return [{"id": "base-model"}, {"id": "prep-model"}, {"id": "pipe-model"}] - - open_webui_models_users_mod.Users = _Users - open_webui_utils_chat_mod.generate_chat_completion = _chat_completion - open_webui_utils_models_mod.get_all_models = _all_models - - sys.modules["fastapi"] = fastapi_mod - sys.modules["open_webui"] = open_webui_mod - sys.modules["open_webui.models"] = open_webui_models_mod - sys.modules["open_webui.models.users"] = open_webui_models_users_mod - sys.modules["open_webui.utils"] = open_webui_utils_mod - sys.modules["open_webui.utils.chat"] = open_webui_utils_chat_mod - sys.modules["open_webui.utils.models"] = open_webui_utils_models_mod - if block_pydantic: - real_import = builtins.__import__ - - def _guarded_import( - name: str, - globals_: dict[str, object] | None = None, - locals_: dict[str, object] | None = None, - fromlist: tuple[str, ...] = (), - level: int = 0, - ) -> object: - if name == "pydantic": - raise ModuleNotFoundError("No module named 'pydantic'") - return real_import(name, globals_, locals_, fromlist, level) - - monkeypatch.setattr(builtins, "__import__", _guarded_import) - - spec = importlib.util.spec_from_file_location(module_name, MODULE_PATH) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def test_preprocessor_model_defaults_to_base_model(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_defaults", monkeypatch) - pipe = module.Pipe() - pipe.valves = DummyValves("base-model", None) - - assert pipe._resolve_preprocessor_model_id("base-model") == "base-model" - - -def test_preprocessor_model_empty_string_defaults_to_base_model(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_defaults_empty", monkeypatch) - pipe = module.Pipe() - pipe.valves = DummyValves("base-model", "") - - assert pipe._resolve_preprocessor_model_id("base-model") == "base-model" - - -def test_pipe_requires_base_model_id_when_debug_disabled(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_requires_base", monkeypatch) - pipe = module.Pipe() - pipe.valves = DummyValves("", None, allow_missing_base=False) - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hi"}]}, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert result == ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is required " - "(or set ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true for testing)." - ) - - -def test_preprocessor_model_can_be_overridden(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_override", monkeypatch) - pipe = module.Pipe() - pipe.valves = DummyValves("base-model", "prep-model") - - assert pipe._resolve_preprocessor_model_id("base-model") == "prep-model" - - -def test_pipe_debug_false_missing_base_blocks_without_llm_calls(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_debug_false_blocked", monkeypatch) - pipe = module.Pipe() - pipe.valves = DummyValves(None, None, allow_missing_base=False) - - llm_calls = 0 - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - del payload - nonlocal llm_calls - llm_calls += 1 - return {"ok": True} - - module.generate_chat_completion = _chat_completion - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hi"}]}, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert result == ( - "Context Compiler pipe misconfigured: BASE_MODEL_ID is required " - "(or set ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true for testing)." - ) - assert llm_calls == 0 - - -def test_pipe_debug_true_missing_base_skips_update_forwarding_call(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_debug_true_allowed", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - pipe = module.Pipe() - pipe.valves = DummyValves(None, None, allow_missing_base=True) - - llm_calls = 0 - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - del payload - nonlocal llm_calls - llm_calls += 1 - return {"ok": True} - - module.generate_chat_completion = _chat_completion - module.preprocess_heuristic = lambda _text: { - "outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, - "directive": "use docker", - } - module.parse_preprocessor_output = lambda value, **_kwargs: value - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "please use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-missing-base-debug", - ) - ) - - assert result == "State updated: Use docker." - assert llm_calls == 0 - - -def test_pipe_debug_true_missing_base_hello_returns_base_misconfig_without_llm_calls( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_debug_true_hello_safe", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - pipe = module.Pipe() - pipe.valves = DummyValves(None, None, allow_missing_base=True) - - llm_calls = 0 - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - del payload - nonlocal llm_calls - llm_calls += 1 - return {"ok": True} - - module.generate_chat_completion = _chat_completion - module.preprocess_heuristic = lambda _text: { - "outcome": "no_directive", - "directive": None, - } - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-missing-base-debug-hello", - ) - ) - - assert result == ( - "Context Compiler debug mode: BASE_MODEL_ID is empty; skipping model passthrough." - ) - assert llm_calls == 0 - - -@pytest.mark.parametrize( - "flag_value", - ["true", "1", "on", True], -) -def test_pipe_debug_true_like_values_missing_base_hello_uses_debug_passthrough_message( - monkeypatch, flag_value -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_debug_truthy_values", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - pipe = module.Pipe() - pipe.valves = DummyValves(None, None, allow_missing_base=False) - pipe.valves.ALLOW_MISSING_BASE_MODEL_FOR_DEBUG = flag_value - - llm_calls = 0 - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - del payload - nonlocal llm_calls - llm_calls += 1 - return {"ok": True} - - module.generate_chat_completion = _chat_completion - module.preprocess_heuristic = lambda _text: { - "outcome": "no_directive", - "directive": None, - } - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-missing-base-debug-truthy", - ) - ) - - assert result == ( - "Context Compiler debug mode: BASE_MODEL_ID is empty; skipping model passthrough." - ) - assert llm_calls == 0 - - -def test_preprocessor_pipe_supports_async_user_lookup(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_async_user_lookup", monkeypatch) - pipe = module.Pipe() - - async def _get_user_by_id(user_id: object) -> dict[str, object]: - return {"id": user_id} - - monkeypatch.setattr(module.Users, "get_user_by_id", _get_user_by_id) - - error = asyncio.run( - pipe._validate_configured_model_ids( - request=object(), - user_payload={"id": "u1"}, - base_model_id="base-model", - preprocessor_model_id="prep-model", - ) - ) - - assert error is None - - -def test_invalid_preprocessor_model_is_normalized(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_invalid", monkeypatch) - pipe = module.Pipe() - - async def _models(_: object, user: object = None) -> list[dict[str, str]]: - del user - return [{"id": "base-model"}] - - module.get_all_models = _models - - error = asyncio.run( - pipe._validate_configured_model_ids( - request=object(), - user_payload={"id": "u1"}, - base_model_id="base-model", - preprocessor_model_id="missing-prep-model", - ) - ) - - assert error == ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID was not found " - "in Open WebUI models." - ) - - -def test_preprocessor_fallback_uses_preprocessor_model_only(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_routing", monkeypatch) - pipe = module.Pipe() - - calls: list[str] = [] - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - calls.append(str(payload.get("model", ""))) - # First call is fallback preprocess completion; return no directive. - # Second call is main forward passthrough. - if len(calls) == 1: - return {"choices": [{"message": {"content": "no_directive"}}]} - return {"ok": True} - - async def _models(_: object, user: object = None) -> list[dict[str, str]]: - del user - return [{"id": "base-model"}, {"id": "prep-model"}, {"id": "pipe-model"}] - - def _heuristic(_: str) -> dict[str, object]: - return {"outcome": "no_directive", "directive": None} - - module.generate_chat_completion = _chat_completion - module.get_all_models = _models - module.preprocess_heuristic = _heuristic - - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - body = { - "model": "pipe-model", - "messages": [{"role": "user", "content": "please use docker"}], - } - result = asyncio.run( - pipe.pipe( - body, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert result == {"ok": True} - assert calls == ["prep-model", "base-model"] - - -def test_preprocessor_fallback_rejects_premise_near_miss_rewrite(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_reject_premise_rewrite", monkeypatch) - pipe = module.Pipe() - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - del payload - return {"choices": [{"message": {"content": "set premise concise replies"}}]} - - module.generate_chat_completion = _chat_completion - module.render_prompt = lambda *_: "prompt" - - directive, error = asyncio.run( - pipe._llm_fallback_preprocess( - "set premise to concise replies", - {"premise": None, "policies": {}, "version": 2}, - request=object(), - user_payload={"id": "u1"}, - prompt_profile="default", - model_id="prep-model", - ) - ) - - assert directive is None - assert error is None - - -def test_recursion_guard_for_preprocessor_model(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_recursion", monkeypatch) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "pipe-model" - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hi"}]}, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert result == ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID must not " - "match the selected pipe model id to avoid recursive routing." - ) - - -def test_pipe_normalizes_preprocessor_model_not_found_response(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_model_not_found_response", monkeypatch) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - if payload.get("model") == "prep-model": - return {"error": {"message": "model not found"}} - return {"ok": True} - - module.generate_chat_completion = _chat_completion - module.preprocess_heuristic = lambda _text: {"outcome": "no_directive", "directive": None} - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert result == ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID is invalid or " - "not configured in Open WebUI. Configure a valid model id in " - "Admin Panel → Settings → Models." - ) - - -def test_pipe_normalizes_preprocessor_model_not_found_exception(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs( - "owui_preproc_model_not_found_exception", monkeypatch - ) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - class _PreprocessorError(Exception): - def __init__(self) -> None: - super().__init__("preprocessor failed") - self.detail = {"error": {"message": "model not found"}} - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - if payload.get("model") == "prep-model": - raise _PreprocessorError() - return {"ok": True} - - module.generate_chat_completion = _chat_completion - module.preprocess_heuristic = lambda _text: {"outcome": "no_directive", "directive": None} - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - ) - ) - - assert result == ( - "Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID is invalid or " - "not configured in Open WebUI. Configure a valid model id in " - "Admin Panel → Settings → Models." - ) - - -def test_preprocessor_pipe_restore_and_persist_checkpoint_points(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_checkpoint", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY["chat-1"] = "ckpt-in" - - class _FakeEngine: - def __init__(self, kind: str, checkpoint_out: str, *, has_pending: bool = False) -> None: - self.kind = kind - self.state = {"premise": None, "policies": {}, "version": 2} - self.has_pending = has_pending - self.imported: list[str] = [] - self._checkpoint_out = checkpoint_out - self.export_calls = 0 - - def import_checkpoint_json(self, payload: str) -> None: - self.imported.append(payload) - - def export_checkpoint_json(self) -> str: - self.export_calls += 1 - return self._checkpoint_out - - def export_checkpoint(self) -> dict[str, object]: - pending: object = None - if self.has_pending: - pending = { - "kind": "replacement", - "replacement": {"kind": "use_only", "new_item": "kubectl", "old_item": None}, - "prompt_to_user": "confirm?", - } - return { - "checkpoint_version": 1, - "authoritative_state": self.state, - "pending": pending, - } - - def has_pending_clarification(self) -> bool: - return self.has_pending - - def step(self, _text: str) -> dict[str, object]: - if self.kind == "clarify": - return {"kind": "clarify", "prompt_to_user": "confirm?", "state": None} - return {"kind": self.kind, "state": self.state} - - created: list[_FakeEngine] = [] - - def _create_engine(): - engine = _FakeEngine("clarify", "ckpt-clarify") - created.append(engine) - return engine - - monkeypatch.setattr(module, "create_engine", _create_engine) - monkeypatch.setattr(module, "preprocess_heuristic", lambda _text: {"outcome": "no_directive"}) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda _value, **_kwargs: None) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "test"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-1", - ) - ) - assert result == "confirm?" - assert created[0].imported == ["ckpt-in"] - assert module._CHECKPOINTS_BY_CHAT_KEY["chat-1"] == "ckpt-clarify" - assert created[0].export_calls == 1 - - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY["chat-2"] = "ckpt-keep" - - passthrough_engine = _FakeEngine("passthrough", "ckpt-new") - monkeypatch.setattr(module, "create_engine", lambda: passthrough_engine) - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-2", - ) - ) - assert isinstance(result, dict) - assert passthrough_engine.imported == ["ckpt-keep"] - assert passthrough_engine.export_calls == 0 - assert module._CHECKPOINTS_BY_CHAT_KEY["chat-2"] == "ckpt-keep" - - -def test_preprocessor_pipe_normal_update_returns_local_ack_and_persists_checkpoint( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_update_forward", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda text: ( - { - "outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, - "directive": "remove policy peanuts", - } - if "remove policy peanuts" in text.lower() - else { - "outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, - "directive": "prohibit peanuts", - } - ), - ) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"ok": True} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - chat_key = "chat-preproc-update" - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "please disallow peanuts"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_key, - ) - ) - - assert result == "State updated: Prohibit peanuts." - assert len(forwarded_payloads) == 0 - - checkpoint = json.loads(module._CHECKPOINTS_BY_CHAT_KEY[chat_key]) - assert checkpoint["pending"] is None - assert checkpoint["authoritative_state"]["policies"] == {"peanuts": "prohibit"} - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "remove policy peanuts"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_key, - ) - ) - - assert result == "State updated: Removed policy peanuts." - assert len(forwarded_payloads) == 0 - - checkpoint = json.loads(module._CHECKPOINTS_BY_CHAT_KEY[chat_key]) - assert checkpoint["pending"] is None - assert checkpoint["authoritative_state"]["policies"] == {} - - -def test_preprocessor_pipe_update_directives_return_local_ack_across_shapes( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_update_shapes", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - forwarded_payloads: list[dict[str, object]] = [] - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"ok": True} - - monkeypatch.setattr(module, "generate_chat_completion", _track_downstream) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use DOCKER"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-use", - ) - ) - assert result == "State updated: Use docker." - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "prohibit DOCKER"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-prohibit", - ) - ) - assert result == "State updated: Prohibit docker." - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-idempotent", - ) - ) - assert result == "State updated: Use docker." - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "use docker"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-replace", - ) - ) - assert result == "State updated: Use docker." - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "use KUBECTL instead of DOCKER"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-replace", - ) - ) - assert result == "State updated: Use kubectl." - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "set premise concise answers"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-premise", - ) - ) - assert result == "State updated." - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-idempotent", - ) - ) - assert result == "State updated: Use docker." - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "use docker instead of docker"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-replace-noop", - ) - ) - assert result == "State updated: Use docker." - assert len(forwarded_payloads) == 0 - - -def test_preprocessor_pipe_show_state_returns_local_summary_and_bypasses_preprocess_and_model( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_show_state", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - preprocess_calls = 0 - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - del payload - nonlocal downstream_calls - downstream_calls += 1 - return {"choices": [{"message": {"content": "downstream"}}]} - - async def _track_preprocess( - self, *args: object, **kwargs: object - ) -> tuple[str | None, str | None]: - del self, args, kwargs - nonlocal preprocess_calls - preprocess_calls += 1 - return None, None - - monkeypatch.setattr(module, "generate_chat_completion", _track_downstream) - monkeypatch.setattr(module.Pipe, "_preprocess_user_input", _track_preprocess) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - chat_id = "chat-preproc-show-state" - - no_pending = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "show state"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert no_pending == ("Premise: none\nUse: none\nProhibit: none\nPending clarification: no") - - assert downstream_calls == 0 - assert preprocess_calls == 0 - assert "Context Compiler trace" not in no_pending - - -def test_preprocessor_pipe_show_state_reports_pending_yes(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_show_state_pending", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - class _PendingEngine: - state = {"premise": None, "policies": {}, "version": 2} - - def has_pending_clarification(self) -> bool: - return True - - def step(self, _: str) -> dict[str, object]: - raise AssertionError("show state should not step engine") - - monkeypatch.setattr(module, "create_engine", lambda: _PendingEngine()) - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "show state"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-show-state-pending", - ) - ) - assert result == "Premise: none\nUse: none\nProhibit: none\nPending clarification: yes" - - -def test_preprocessor_pipe_show_state_non_exact_routes_normally(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_show_state_non_exact", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - del payload - nonlocal downstream_calls - downstream_calls += 1 - return {"choices": [{"message": {"content": "downstream"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _track_downstream) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "show state please"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-show-state-non-exact", - ) - ) - assert result == {"choices": [{"message": {"content": "downstream"}}]} - assert downstream_calls >= 1 - - -def test_preprocessor_pipe_show_state_exact_match_is_case_insensitive_after_trim( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_show_state_case_trim", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - downstream_calls = 0 - preprocess_calls = 0 - - async def _track_downstream( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - del payload - nonlocal downstream_calls - downstream_calls += 1 - return {"choices": [{"message": {"content": "downstream"}}]} - - async def _track_preprocess( - self, *args: object, **kwargs: object - ) -> tuple[str | None, str | None]: - del self, args, kwargs - nonlocal preprocess_calls - preprocess_calls += 1 - return None, None - - monkeypatch.setattr(module, "generate_chat_completion", _track_downstream) - monkeypatch.setattr(module.Pipe, "_preprocess_user_input", _track_preprocess) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": " ShOw StAtE "}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-show-state-case-trim", - ) - ) - - assert result == "Premise: none\nUse: none\nProhibit: none\nPending clarification: no" - assert downstream_calls == 0 - assert preprocess_calls == 0 - - -@pytest.mark.parametrize( - ("confirmation",), - [ - ("yes",), - ("no",), - ], -) -def test_preprocessor_pipe_bypasses_preprocess_while_pending( - monkeypatch, confirmation: str -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_pending_bypass", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - class _PendingEngine: - def __init__(self) -> None: - self.state = {"premise": None, "policies": {}, "version": 2} - self.pending = True - self.step_inputs: list[str] = [] - - def export_checkpoint(self) -> dict[str, object]: - pending: object = None - if self.pending: - pending = { - "kind": "replacement", - "replacement": {"kind": "use_only", "new_item": "kubectl", "old_item": None}, - "prompt_to_user": "confirm?", - } - return { - "checkpoint_version": 1, - "authoritative_state": self.state, - "pending": pending, - } - - def has_pending_clarification(self) -> bool: - return self.pending - - def export_checkpoint_json(self) -> str: - return "ckpt-out" - - def step(self, text: str) -> dict[str, object]: - self.step_inputs.append(text) - if self.pending and text in {"yes", "no"}: - self.pending = False - return {"kind": "update", "state": self.state} - if self.pending: - return {"kind": "clarify", "state": None, "prompt_to_user": "confirm?"} - return {"kind": "passthrough", "state": None} - - engine = _PendingEngine() - monkeypatch.setattr(module, "create_engine", lambda: engine) - - def _fail_preprocess(_: str) -> dict[str, object]: - raise AssertionError("should not preprocess") - - monkeypatch.setattr(module, "preprocess_heuristic", _fail_preprocess) - - forwarded_payloads: list[dict[str, Any]] = [] - - async def _track_downstream_model( - _: object, payload: dict[str, Any], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"ok": True} - - monkeypatch.setattr(module, "generate_chat_completion", _track_downstream_model) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": confirmation}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-pending", - ) - ) - - assert result == "State updated." - assert engine.step_inputs == [confirmation] - assert module._CHECKPOINTS_BY_CHAT_KEY["chat-pending"] == "ckpt-out" - assert len(forwarded_payloads) == 0 - - -@pytest.mark.parametrize( - ("confirmation", "expected_policies"), - [ - ("yes", {"kubectl": "use"}), - ("no", {}), - ], -) -def test_preprocessor_pipe_checkpoint_resume_yes_no_end_to_end( - monkeypatch, - confirmation: str, - expected_policies: dict[str, str], -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_resume_e2e", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - heuristic_inputs: list[str] = [] - - def _heuristic(text: str) -> dict[str, object]: - if text in {"yes", "no"}: - raise AssertionError("heuristic preprocess should be bypassed while pending") - heuristic_inputs.append(text) - return {"outcome": "no_directive", "directive": None} - - monkeypatch.setattr(module, "preprocess_heuristic", _heuristic) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - chat_key = "chat-resume-e2e" - clarify = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "use kubectl instead of docker"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_key, - ) - ) - assert isinstance(clarify, str) - assert clarify == 'Did you mean to use "kubectl" instead?' - assert heuristic_inputs == ["use kubectl instead of docker"] - - module._ENGINES_BY_CHAT_KEY.clear() - - forwarded_payloads: list[dict[str, Any]] = [] - - async def _track_downstream_model( - _: object, payload: dict[str, Any], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"ok": True} - - monkeypatch.setattr(module, "generate_chat_completion", _track_downstream_model) - - resumed = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": confirmation}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_key, - ) - ) - assert resumed == "State updated." - resumed_engine = cast(Any, module._ENGINES_BY_CHAT_KEY[chat_key]) - assert resumed_engine.state == { - "premise": None, - "policies": expected_policies, - "version": 2, - } - resumed_checkpoint = json.loads(module._CHECKPOINTS_BY_CHAT_KEY[chat_key]) - assert resumed_checkpoint["pending"] is None - assert len(forwarded_payloads) == 0 - - -@pytest.mark.parametrize( - ("user_input", "expected"), - [ - ( - "use", - "Policy item cannot be empty.\nUse 'use ' with a non-empty value.", - ), - ( - "prohibit", - "Policy item cannot be empty.\nUse 'prohibit ' with a non-empty value.", - ), - ( - "remove policy", - "Policy item cannot be empty.\nUse 'remove policy ' with a non-empty value.", - ), - ( - "use docker instead of", - "Replacement requires both new and old items.\n" - "Use 'use instead of ' with non-empty values.", - ), - ( - "use instead of docker", - "Replacement requires both new and old items.\n" - "Use 'use instead of ' with non-empty values.", - ), - ], -) -def test_preprocessor_pipe_skips_fallback_for_directive_shaped_malformed_inputs( - monkeypatch, - user_input: str, - expected: str, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_skip_fallback_malformed", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: {"outcome": "no_directive", "directive": None}, - ) - - fallback_calls = 0 - - async def _fallback( - self, - message: str, - state: dict[str, object], - *, - request: object, - user_payload: dict[str, object], - prompt_profile: str, - model_id: str, - ) -> tuple[str | None, str | None]: - nonlocal fallback_calls - del self, message, state, request, user_payload, prompt_profile, model_id - fallback_calls += 1 - raise AssertionError("fallback should not be called for directive-shaped malformed input") - - downstream_calls = 0 - - async def _downstream(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - nonlocal downstream_calls - downstream_calls += 1 - raise AssertionError(f"downstream model should not be called: {payload.get('model')}") - - monkeypatch.setattr(module.Pipe, "_llm_fallback_preprocess", _fallback) - monkeypatch.setattr(module, "generate_chat_completion", _downstream) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": user_input}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-malformed", - ) - ) - - assert result == expected - assert fallback_calls == 0 - assert downstream_calls == 0 - - -def test_preprocessor_pipe_near_miss_directives_return_deterministic_clarify_without_model_calls( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_near_miss_clarify", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: {"outcome": "no_directive", "directive": None}, - ) - - fallback_calls = 0 - - async def _fallback( - self, - message: str, - state: dict[str, object], - *, - request: object, - user_payload: dict[str, object], - prompt_profile: str, - model_id: str, - ) -> tuple[str | None, str | None]: - nonlocal fallback_calls - del self, message, state, request, user_payload, prompt_profile, model_id - fallback_calls += 1 - raise AssertionError("fallback should not be called for near-miss directive input") - - downstream_calls = 0 - - async def _downstream(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - nonlocal downstream_calls - downstream_calls += 1 - raise AssertionError(f"downstream model should not be called: {payload.get('model')}") - - monkeypatch.setattr(module.Pipe, "_llm_fallback_preprocess", _fallback) - monkeypatch.setattr(module, "generate_chat_completion", _downstream) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - cases = [ - ("reset premise", "Unknown directive.\nUse 'clear premise' or 'reset policies'."), - ("reset premises", "Unknown directive.\nUse 'clear premise' or 'reset policies'."), - ("clear premises", "Unknown directive.\nUse 'clear premise' or 'reset policies'."), - ("set premise to concise answers", "Invalid premise syntax.\nUse 'set premise '."), - ( - "change premise formal tone", - "Invalid premise syntax.\nUse 'change premise to '.", - ), - ] - - for idx, (user_input, expected) in enumerate(cases): - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": user_input}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=f"chat-near-miss-{idx}", - ) - ) - assert result == expected - - assert fallback_calls == 0 - assert downstream_calls == 0 - - -@pytest.mark.parametrize( - "user_input", - [ - "ok. prohibit peanuts", - "```\nuse docker\n```", - "the command is `use docker`", - 'the docs say "use docker"', - "can you use docker?", - ], -) -def test_preprocessor_pipe_fallback_boundary_unsafe_sources_do_not_mutate_state( - monkeypatch, user_input: str -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_fallback_boundary_unsafe", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - calls: list[str] = [] - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - model = str(payload.get("model", "")) - calls.append(model) - if model == "prep-model": - return {"choices": [{"message": {"content": "use docker"}}]} - return {"ok": True} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - monkeypatch.setattr(module, "render_prompt", lambda *_args, **_kwargs: "prompt") - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: {"outcome": "unknown", "directive": None, "rule_id": "test"}, - ) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - - chat_id = "chat-fallback-boundary-unsafe" - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": user_input}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - - assert result == {"ok": True} - assert calls == ["prep-model", "base-model"] - assert chat_id in module._ENGINES_BY_CHAT_KEY - assert module._ENGINES_BY_CHAT_KEY[chat_id].state["policies"] == {} - - -def test_preprocessor_pipe_trace_off_keeps_existing_response_shape(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_trace_off_shape", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - if payload.get("model") == "prep-model": - return {"choices": [{"message": {"content": "no_directive"}}]} - return {"choices": [{"message": {"content": "downstream"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - monkeypatch.setattr(module, "preprocess_heuristic", lambda _text: {"outcome": "no_directive"}) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda _value, **_kwargs: None) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = False - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-trace-off", - ) - ) - assert result == {"choices": [{"message": {"content": "downstream"}}]} - - -def test_preprocessor_pipe_trace_on_appends_trace_to_user_visible_output(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_trace_on", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - del payload - return {"choices": [{"message": {"content": "downstream"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: { - "outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, - "directive": "prohibit peanuts", - }, - ) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "please use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-trace-on", - ) - ) - assert isinstance(result, str) - content = result - assert content.startswith("State updated: Prohibit peanuts.") - assert "Context Compiler trace" in content - assert "decision kind: update" in content - assert "preprocessor output:" not in content - assert "downstream LLM call: no" in content - assert "state change:" in content - assert "active state:" in content - assert "state injected: no" in content - assert "\n\nstate injected: no" in content - - -def test_preprocessor_pipe_trace_on_passthrough_appends_trace_to_llm_content(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_trace_on_passthrough", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - if payload.get("model") == "prep-model": - return {"choices": [{"message": {"content": "no_directive"}}]} - return {"choices": [{"message": {"content": "downstream"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - monkeypatch.setattr(module, "preprocess_heuristic", lambda _text: {"outcome": "no_directive"}) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda _value, **_kwargs: None) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-trace-on-passthrough", - ) - ) - content = result["choices"][0]["message"]["content"] - assert "downstream" in content - assert "Context Compiler trace" in content - assert "decision kind: passthrough" in content - assert "downstream LLM call: yes" in content - assert "active state:" in content - assert "state injected: no" in content - - -def test_preprocessor_pipe_trace_on_passthrough_stream_appends_trace_after_chunks( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_trace_on_stream", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - async def _streaming_response() -> Any: - for part in ("down", "stream"): - yield part - - async def _chat_completion(_: object, payload: dict[str, Any], __: object) -> Any: - if payload.get("model") == "prep-model": - return {"choices": [{"message": {"content": "no_directive"}}]} - return _streaming_response() - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - monkeypatch.setattr(module, "preprocess_heuristic", lambda _text: {"outcome": "no_directive"}) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda _value, **_kwargs: None) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-trace-on-stream", - ) - ) - - async def _collect() -> str: - parts: list[str] = [] - async for chunk in result: - assert isinstance(chunk, str) - parts.append(chunk) - return "".join(parts) - - content = asyncio.run(_collect()) - assert content.startswith("downstream") - assert "Context Compiler trace" in content - assert "decision kind: passthrough" in content - assert "downstream LLM call: yes" in content - assert "active state:" in content - assert "state injected: no" in content - - -def test_preprocessor_pipe_trace_on_clarify_shows_prompt_and_no_downstream_call( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_trace_clarify", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: {"outcome": "no_directive", "directive": None}, - ) - - downstream_calls = 0 - - async def _downstream(_: object, payload: dict[str, Any], __: object) -> dict[str, object]: - nonlocal downstream_calls - downstream_calls += 1 - del payload - return {"choices": [{"message": {"content": "downstream"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _downstream) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "set premise to concise answers"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-trace-clarify", - ) - ) - assert isinstance(result, str) - assert "decision kind: clarify" in result - assert "active state:" in result - assert "clarification prompt:" in result - assert "downstream LLM call: no" in result - assert "state injected: no" in result - assert downstream_calls == 0 - - -def test_preprocessor_pipe_trace_appends_on_object_response_for_passthrough_and_update( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_trace_object_response", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - class _Message: - def __init__(self, content: str) -> None: - self.content = content - - class _Choice: - def __init__(self, content: str) -> None: - self.message = _Message(content) - - class _Response: - def __init__(self, content: str) -> None: - self.choices = [_Choice(content)] - - def _heuristic(text: str) -> dict[str, object]: - if "use docker" in text.lower(): - return {"outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, "directive": "use docker"} - return {"outcome": "no_directive", "directive": None} - - monkeypatch.setattr(module, "preprocess_heuristic", _heuristic) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion(_: object, payload: dict[str, object], __: object) -> object: - forwarded_payloads.append(payload) - return _Response("downstream") - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - passthrough = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-object-passthrough", - ) - ) - passthrough_content = passthrough.choices[0].message.content - assert "Context Compiler trace" in passthrough_content - assert "decision kind: passthrough" in passthrough_content - assert "downstream LLM call: yes" in passthrough_content - - update = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-object-update", - ) - ) - assert isinstance(update, str) - update_content = update - assert "Context Compiler trace" in update_content - assert "decision kind: update" in update_content - assert "downstream LLM call: no" in update_content - assert "state injected: no" in update_content - assert len(forwarded_payloads) == 2 - - -def test_preprocessor_pipe_trace_appends_on_streaming_response_wrapper_passthrough_and_update( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_trace_streaming_wrapper", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - class _StreamingResponse: - def __init__(self, parts: tuple[str, ...]) -> None: - async def _iter() -> object: - for part in parts: - yield part - - self.body_iterator = _iter() - - def _heuristic(text: str) -> dict[str, object]: - if "use docker" in text.lower(): - return {"outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, "directive": "use docker"} - return {"outcome": "no_directive", "directive": None} - - monkeypatch.setattr(module, "preprocess_heuristic", _heuristic) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion(_: object, payload: dict[str, object], __: object) -> object: - forwarded_payloads.append(payload) - return _StreamingResponse(("data: stub\n\n", "data: [DONE]\n\n")) - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - passthrough = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-stream-wrapper-passthrough", - ) - ) - - async def _collect_stream(wrapper: object) -> str: - parts: list[str] = [] - async for chunk in wrapper.body_iterator: - assert isinstance(chunk, str) - parts.append(chunk) - return "".join(parts) - - passthrough_stream = asyncio.run(_collect_stream(passthrough)) - assert "data: [DONE]" in passthrough_stream - assert "Context Compiler trace" in passthrough_stream - assert "decision kind: passthrough" in passthrough_stream - - update = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-stream-wrapper-update", - ) - ) - assert isinstance(update, str) - assert "Context Compiler trace" in update - assert "decision kind: update" in update - assert "state injected: no" in update - assert len(forwarded_payloads) == 2 - - -@pytest.mark.parametrize( - ("steps", "expected_ack"), - [ - ( - ["use docker", "clear state"], - "State cleared.", - ), - ( - ["set premise concise replies", "clear premise"], - "Premise cleared.", - ), - ( - ["use docker", "use pytest", "reset policies"], - "Policies reset.", - ), - ( - ["use docker", "remove policy docker"], - "State updated: Removed policy docker.", - ), - ], -) -def test_preprocessor_pipe_trace_update_clear_reset_paths_single_and_consistent( - monkeypatch, - steps: list[str], - expected_ack: str, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_trace_clear_reset", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: {"outcome": "no_directive", "directive": None}, - ) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda _value, **_kwargs: None) - - downstream_calls = 0 - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - nonlocal downstream_calls - if payload.get("model") == "prep-model": - return {"choices": [{"message": {"content": "no_directive"}}]} - downstream_calls += 1 - return {"choices": [{"message": {"content": "downstream"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result: object = "" - for idx, user_input in enumerate(steps): - result = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": user_input}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=f"chat-preproc-trace-clear-reset-{hash(tuple(steps))}", - ) - ) - if idx < len(steps) - 1: - continue - - assert isinstance(result, str) - assert result.startswith(expected_ack) - content = result - assert content.count("Context Compiler trace") == 1 - assert "decision kind: update" in content - assert "downstream LLM call: no" in content - assert "downstream LLM call: yes" not in content - assert "active state: none" in content - assert "state injected: no" in content - assert downstream_calls == 0 - - -def test_preprocessor_pipe_clear_state_trace_not_duplicated_when_model_echoes_history( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_trace_echo_dedupe", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: {"outcome": "no_directive", "directive": None}, - ) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda _value, **_kwargs: None) - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - messages = payload.get("messages") - echoed = "" - if isinstance(messages, list): - assistant_contents = [ - str(msg.get("content", "")) - for msg in messages - if isinstance(msg, dict) and msg.get("role") == "assistant" - ] - echoed = "\n".join(assistant_contents) - content = "downstream" - if echoed: - content += f"\n{echoed}" - return {"choices": [{"message": {"content": content}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - chat_id = "chat-preproc-trace-echo-dedupe" - - first = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(first, str) - first_content = first - assert first_content.count("Context Compiler trace") == 1 - - second = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [ - {"role": "assistant", "content": first_content}, - {"role": "user", "content": "clear state"}, - ], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(second, str) - second_content = second - assert second_content.count("Context Compiler trace") == 1 - assert "decision kind: update" in second_content - assert "downstream LLM call: no" in second_content - assert "downstream LLM call: yes" not in second_content - assert "active state: none" in second_content - assert "state injected: no" in second_content - - -def test_preprocessor_pipe_clear_state_strips_preexisting_contradictory_trace_from_model_output( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs( - "owui_preproc_trace_strip_contradiction", monkeypatch - ) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda _text: {"outcome": "no_directive", "directive": None}, - ) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda _value, **_kwargs: None) - - old_trace = ( - "Context Compiler trace\n\n" - "decision kind: update\n" - "active state: none\n" - "downstream LLM call: no\n" - "\n" - "state injected: none" - ) - - call_count = 0 - - async def _chat_completion( - _: object, - payload: dict[str, object], - __: object, - ) -> dict[str, object]: - nonlocal call_count - call_count += 1 - if call_count == 1: - return {"choices": [{"message": {"content": "downstream"}}]} - del payload - return {"choices": [{"message": {"content": f"downstream\n{old_trace}"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - chat_id = "chat-preproc-trace-strip-contradiction" - - _ = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - - second = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "clear state"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(second, str) - second_content = second - assert second_content.count("Context Compiler trace") == 1 - assert "downstream LLM call: no" in second_content - assert "downstream LLM call: yes" not in second_content - - -def test_preprocessor_pipe_update_trace_local_ack_when_heuristic_emits_directive( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_nl_use_docker", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda text: ( - {"outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, "directive": "use docker"} - if "i think we should use docker" in text.lower() - else {"outcome": "no_directive", "directive": None} - ), - ) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion( - _: object, - payload: dict[str, object], - __: object, - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"choices": [{"message": {"content": "downstream"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "i think we should use docker"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-nl-use-docker", - ) - ) - assert isinstance(result, str) - content = result - assert content.count("Context Compiler trace") == 1 - assert "decision kind: update" in content - assert "downstream LLM call: no" in content - assert "downstream LLM call: yes" not in content - assert "active state: use docker" in content - assert "state injected: no" in content - assert len(forwarded_payloads) == 0 - - -def test_preprocessor_pipe_replacement_update_trace_local_ack_when_heuristic_emits_directive( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_nl_replace", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda text: ( - {"outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, "directive": "use docker"} - if text.strip().lower() == "use docker" - else ( - { - "outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, - "directive": "use podman instead of docker", - } - if "switch to podman instead of docker" in text.lower() - else {"outcome": "no_directive", "directive": None} - ) - ), - ) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - async def _chat_completion( - _: object, - payload: dict[str, object], - __: object, - ) -> dict[str, object]: - del payload - return {"choices": [{"message": {"content": "downstream"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - chat_id = "chat-preproc-nl-replace" - - _ = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "switch to podman instead of docker"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(result, str) - content = result - assert content.count("Context Compiler trace") == 1 - assert "decision kind: update" in content - assert "downstream LLM call: no" in content - assert "downstream LLM call: yes" not in content - assert "active state: use podman" in content - assert "state injected: no" in content - - -def test_preprocessor_pipe_ambiguous_text_passthrough_trace_streaming(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs( - "owui_preproc_ambiguous_passthrough_stream", monkeypatch - ) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr(module, "preprocess_heuristic", lambda _text: {"outcome": "no_directive"}) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda _value, **_kwargs: None) - - class _StreamingResponse: - def __init__(self) -> None: - async def _iter() -> object: - for part in ("data: downstream\n\n", "data: [DONE]\n\n"): - yield part - - self.body_iterator = _iter() - - async def _chat_completion(_: object, payload: dict[str, object], __: object) -> object: - if payload.get("model") == "prep-model": - return {"choices": [{"message": {"content": "no_directive"}}]} - return _StreamingResponse() - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "docker is interesting"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-ambiguous-stream", - ) - ) - - async def _collect_stream(wrapper: object) -> str: - parts: list[str] = [] - async for chunk in wrapper.body_iterator: - assert isinstance(chunk, str) - parts.append(chunk) - return "".join(parts) - - content = asyncio.run(_collect_stream(result)) - assert content.count("Context Compiler trace") == 1 - assert "decision kind: passthrough" in content - assert "downstream LLM call: yes" in content - assert "downstream LLM call: no" not in content - assert "state injected: no" in content - - -def test_preprocessor_pipe_natural_language_abstention_can_passthrough_with_trace( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_nl_abstention", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - # Use the real heuristic behavior for this phrase (unknown/no_directive), - # and force fallback to abstain so runtime stays conservative. - monkeypatch.setattr(module, "parse_preprocessor_output", lambda _value, **_kwargs: None) - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - if payload.get("model") == "prep-model": - return {"choices": [{"message": {"content": "no_directive"}}]} - return {"choices": [{"message": {"content": "downstream"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - result = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "i think we should use docker"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-nl-abstention", - ) - ) - assert isinstance(result, dict) - content = result["choices"][0]["message"]["content"] - assert content.count("Context Compiler trace") == 1 - assert "decision kind: passthrough" in content - assert "active state: none" in content - assert "downstream LLM call: yes" in content - assert "state injected: no" in content - - -def test_preprocessor_pipe_pending_clarification_bypasses_preprocessing_for_ambiguous_yes( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_pending_yeah_probably", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - heuristic_calls: list[str] = [] - - def _heuristic(text: str) -> dict[str, object]: - if text.strip().lower() == "yeah probably": - raise AssertionError("heuristic preprocess should be bypassed while pending") - heuristic_calls.append(text) - if "use podman instead of kubectl" in text.lower(): - return { - "outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, - "directive": "use podman instead of kubectl", - } - return {"outcome": "no_directive", "directive": None} - - monkeypatch.setattr(module, "preprocess_heuristic", _heuristic) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - downstream_calls = 0 - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - nonlocal downstream_calls - downstream_calls += 1 - del payload - return {"choices": [{"message": {"content": "downstream"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - chat_id = "chat-preproc-pending-yeah-probably" - - first = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "use podman instead of kubectl"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(first, str) - assert first.count("Context Compiler trace") == 1 - assert "decision kind: clarify" in first - assert "downstream LLM call: no" in first - assert "state injected: no" in first - - second = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "yeah probably"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(second, str) - assert second.count("Context Compiler trace") == 1 - assert "decision kind: clarify" in second - assert "downstream LLM call: no" in second - assert "downstream LLM call: yes" not in second - assert "state injected: no" in second - assert downstream_calls == 0 - assert heuristic_calls == ["use podman instead of kubectl"] - - -def test_preprocessor_pipe_passthrough_injects_active_state_and_trace_reports_yes( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs( - "owui_preproc_passthrough_state_injection", monkeypatch - ) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda text: ( - {"outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, "directive": "use docker"} - if text.strip().lower() == "use docker" - else {"outcome": "no_directive", "directive": None} - ), - ) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"choices": [{"message": {"content": "answer"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - chat_id = "chat-preproc-passthrough-injected-state" - - update = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert "State updated: Use docker." in update - assert len(forwarded_payloads) == 0 - - passthrough = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": "what container runtime should i use?"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - assert isinstance(passthrough, dict) - assert len(forwarded_payloads) == 2 - messages = forwarded_payloads[-1]["messages"] - assert isinstance(messages, list) - assert any( - isinstance(msg, dict) - and msg.get("role") == "system" - and isinstance(msg.get("content"), str) - and msg["content"].startswith("[[cc_state]]") - and "Use: docker" in msg["content"] - for msg in messages - ) - content = passthrough["choices"][0]["message"]["content"] - assert "state injected: yes" in content - - -def test_preprocessor_pipe_empty_state_passthrough_does_not_inject_and_trace_reports_no( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_empty_state_passthrough", monkeypatch) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - monkeypatch.setattr(module, "preprocess_heuristic", lambda _text: {"outcome": "no_directive"}) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda _value, **_kwargs: None) - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"choices": [{"message": {"content": "answer"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True - - passthrough = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__="chat-preproc-empty-state-passthrough", - ) - ) - assert isinstance(passthrough, dict) - assert len(forwarded_payloads) == 2 - messages = forwarded_payloads[-1]["messages"] - assert isinstance(messages, list) - assert not any( - isinstance(msg, dict) - and msg.get("role") == "system" - and isinstance(msg.get("content"), str) - and msg["content"].startswith("[[cc_state]]") - for msg in messages - ) - content = passthrough["choices"][0]["message"]["content"] - assert "downstream LLM call: yes" in content - assert "state injected: no" in content - - -def test_preprocessor_pipe_repeated_passthrough_does_not_duplicate_compiler_state_prompt( - monkeypatch, -) -> None: - module = _load_module_with_openwebui_stubs( - "owui_preproc_repeated_passthrough_no_dup", monkeypatch - ) - module._ENGINES_BY_CHAT_KEY.clear() - module._CHECKPOINTS_BY_CHAT_KEY.clear() - monkeypatch.setattr( - module, - "preprocess_heuristic", - lambda text: ( - {"outcome": module.PREPROCESS_OUTCOME_DIRECTIVE, "directive": "use docker"} - if text.strip().lower() == "use docker" - else {"outcome": "no_directive", "directive": None} - ), - ) - monkeypatch.setattr(module, "parse_preprocessor_output", lambda value, **_kwargs: value) - - forwarded_payloads: list[dict[str, object]] = [] - - async def _chat_completion( - _: object, payload: dict[str, object], __: object - ) -> dict[str, object]: - forwarded_payloads.append(payload) - return {"choices": [{"message": {"content": "answer"}}]} - - monkeypatch.setattr(module, "generate_chat_completion", _chat_completion) - - pipe = module.Pipe() - pipe.valves.BASE_MODEL_ID = "base-model" - pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model" - chat_id = "chat-preproc-repeated-passthrough-no-dup" - - _ = asyncio.run( - pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "use docker"}]}, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - for idx in range(2): - _ = asyncio.run( - pipe.pipe( - { - "model": "pipe-model", - "messages": [{"role": "user", "content": f"question {idx}"}], - }, - __user__={"id": "u1"}, - __request__=object(), - __chat_id__=chat_id, - ) - ) - - assert len(forwarded_payloads) == 4 - passthrough_payloads = [forwarded_payloads[1], forwarded_payloads[3]] - for payload in passthrough_payloads: - messages = payload.get("messages") - assert isinstance(messages, list) - cc_messages = [ - msg - for msg in messages - if isinstance(msg, dict) - and msg.get("role") == "system" - and isinstance(msg.get("content"), str) - and msg["content"].startswith("[[cc_state]]") - ] - assert len(cc_messages) == 1 - - -def test_preprocessor_pipe_frontmatter_title_and_public_symbol_stability(monkeypatch) -> None: - module = _load_module_with_openwebui_stubs("owui_preproc_frontmatter_title", monkeypatch) - assert "title: Context Compiler Pipe (Preprocessor)" in (module.__doc__ or "") - assert hasattr(module, "Pipe") diff --git a/tests/test_precompiler_prompt_utils.py b/tests/test_precompiler_prompt_utils.py deleted file mode 100644 index 09a026a..0000000 --- a/tests/test_precompiler_prompt_utils.py +++ /dev/null @@ -1,67 +0,0 @@ -from pathlib import Path - -from context_compiler import create_engine -from context_compiler.engine import State -from experimental.preprocessor.constants import PROMPT_TOKEN_NULL_OR_VALUE, PROMPT_TOKEN_POLICY_SET -from experimental.preprocessor.prompt_utils import render_prompt - - -def _write_prompt(path: Path, body: str) -> None: - path.write_text(body, encoding="utf-8") - - -def _empty_state() -> State: - return create_engine().state - - -def _populated_state() -> State: - engine = create_engine() - engine.step("set premise concise replies") - engine.step("use zeta") - engine.step("use beta") - engine.step("prohibit alpha") - return engine.state - - -def test_render_prompt_returns_none_when_file_missing(tmp_path: Path) -> None: - missing = tmp_path / "missing.txt" - assert render_prompt(missing, _empty_state()) is None - - -def test_render_prompt_strips_leading_header_comments_and_blank_lines(tmp_path: Path) -> None: - prompt_file = tmp_path / "prompt.txt" - _write_prompt( - prompt_file, - "\n# header one\n # header two\n\n" - f"premise={PROMPT_TOKEN_NULL_OR_VALUE}\n" - f"policies={PROMPT_TOKEN_POLICY_SET}\n", - ) - - rendered = render_prompt(prompt_file, _empty_state()) - assert rendered is not None - assert rendered.startswith("premise=null\n") - assert "# header" not in rendered - - -def test_render_prompt_replaces_tokens_for_empty_state(tmp_path: Path) -> None: - prompt_file = tmp_path / "prompt.txt" - _write_prompt( - prompt_file, - f"premise={PROMPT_TOKEN_NULL_OR_VALUE}\npolicies={PROMPT_TOKEN_POLICY_SET}\n", - ) - - rendered = render_prompt(prompt_file, _empty_state()) - assert rendered == "premise=null\npolicies=(none)" - - -def test_render_prompt_replaces_tokens_for_populated_state_with_sorted_policy_keys( - tmp_path: Path, -) -> None: - prompt_file = tmp_path / "prompt.txt" - _write_prompt( - prompt_file, - f"premise={PROMPT_TOKEN_NULL_OR_VALUE}\npolicies={PROMPT_TOKEN_POLICY_SET}\n", - ) - - rendered = render_prompt(prompt_file, _populated_state()) - assert rendered == "premise=concise replies\npolicies=alpha, beta, zeta" diff --git a/tests/test_preprocessor_api_contract_fixture.py b/tests/test_preprocessor_api_contract_fixture.py deleted file mode 100644 index 61a5c78..0000000 --- a/tests/test_preprocessor_api_contract_fixture.py +++ /dev/null @@ -1,29 +0,0 @@ -import json -from pathlib import Path - -import experimental.preprocessor as preprocessor - -_CONTRACT_PATH = ( - Path(__file__).resolve().parent / "fixtures" / "preprocessor" / "public-api-v1.json" -) - - -def _load_contract() -> dict[str, object]: - return json.loads(_CONTRACT_PATH.read_text(encoding="utf-8")) - - -def test_preprocessor_api_contract_fixture_matches_public_surface() -> None: - contract = _load_contract() - - assert contract["kind"] == "api-contract" - - for name in contract["required_exports"]: - assert hasattr(preprocessor, name), name - assert name in preprocessor.__all__, name - - -def test_preprocessor_api_contract_fixture_has_unique_entries() -> None: - contract = _load_contract() - - required_exports = contract["required_exports"] - assert len(required_exports) == len(set(required_exports)) diff --git a/tests/test_preprocessor_conformance.py b/tests/test_preprocessor_conformance.py deleted file mode 100644 index ce9312a..0000000 --- a/tests/test_preprocessor_conformance.py +++ /dev/null @@ -1,143 +0,0 @@ -import json -import re -from pathlib import Path - -from experimental.preprocessor.heuristic_preprocessor import preprocess_heuristic -from experimental.preprocessor.output_validation import ( - parse_preprocessor_output, - validate_preprocessor_output, -) - -_PREPROCESSOR_FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures" / "preprocessor" - - -def _behavior_fixture_paths() -> list[Path]: - return sorted(path for path in _PREPROCESSOR_FIXTURES_DIR.glob("*.json")) - - -def _load_fixture(path: Path) -> dict[str, object]: - return json.loads(path.read_text(encoding="utf-8")) - - -def _normalize_result(message: str) -> dict[str, object]: - result = preprocess_heuristic(message) - output = result["directive"] if result["outcome"] == "directive" else None - normalized = { - "classification": result["outcome"], - "output": output, - } - - # Enforce the validation boundary: only validated directive output may pass. - validated = validate_preprocessor_output(output) - if normalized["classification"] == "directive": - assert validated["classification"] == "directive" - assert validated["output"] == output - else: - assert output is None - assert validated["output"] is None - - return normalized - - -def _normalize_validator_result( - raw_output: object, source_input: str | None = None -) -> dict[str, object]: - validated = validate_preprocessor_output(raw_output, source_input=source_input) - return { - "classification": validated["classification"], - "output": validated["output"], - } - - -def _normalize_parse_result(raw_output: object, source_input: str | None = None) -> str | None: - return parse_preprocessor_output(raw_output, source_input=source_input) - - -def _derived_risky_rewrite_candidates(source_input: str) -> list[str]: - normalized = re.sub(r"\s+", " ", source_input.strip().lower()) - candidates: list[str] = [] - - set_premise_to_match = re.fullmatch(r"set premise to\s+(.+\S)", normalized) - if set_premise_to_match is not None: - payload = set_premise_to_match.group(1).strip() - candidates.append(f"set premise {payload}") - - change_premise_missing_to_match = re.fullmatch(r"change premise\s+(?!to\b)(.+\S)", normalized) - if change_premise_missing_to_match is not None: - payload = change_premise_missing_to_match.group(1).strip() - candidates.append(f"change premise to {payload}") - - return candidates - - -def test_preprocessor_conformance_fixtures() -> None: - for path in _behavior_fixture_paths(): - fixture = _load_fixture(path) - if path.name.startswith("public-api-"): - continue - - fixture_name = fixture.get("name", path.name) - kind = fixture.get("kind", "heuristic") - - if kind == "heuristic": - expected = fixture.get("expected") - assert isinstance(expected, dict), fixture_name - input_text = fixture.get("input") - assert isinstance(input_text, str), fixture_name - - # Deterministic replay check. - first = _normalize_result(input_text) - second = _normalize_result(input_text) - assert first == second, fixture_name - assert first == expected, fixture_name - continue - - assert "raw_output" in fixture, fixture_name - raw_output = fixture["raw_output"] - source_input_obj = fixture.get("source_input") - source_input = source_input_obj if isinstance(source_input_obj, str) else None - - if kind == "validator": - expected = fixture.get("expected") - assert isinstance(expected, dict), fixture_name - # Deterministic replay check. - first = _normalize_validator_result(raw_output, source_input=source_input) - second = _normalize_validator_result(raw_output, source_input=source_input) - assert first == second, fixture_name - assert first == expected, fixture_name - continue - - assert kind == "parse", fixture_name - assert "expected_parsed" in fixture, fixture_name - expected_parsed = fixture["expected_parsed"] - assert isinstance(expected_parsed, str) or expected_parsed is None, fixture_name - - # Deterministic replay check. - first_parsed = _normalize_parse_result(raw_output, source_input=source_input) - second_parsed = _normalize_parse_result(raw_output, source_input=source_input) - assert first_parsed == second_parsed, fixture_name - assert first_parsed == expected_parsed, fixture_name - - -def test_engine_owned_near_misses_are_reject_only_for_fallback_rewrites() -> None: - # Engine-owned near-misses must not be canonicalized by the preprocessor and - # must remain unknown even if fallback proposes a plausible canonical rewrite. - for path in _behavior_fixture_paths(): - fixture = _load_fixture(path) - if path.name.startswith("public-api-"): - continue - if fixture.get("kind", "heuristic") != "heuristic": - continue - expected = fixture["expected"] - input_text = fixture["input"] - fixture_name = fixture["name"] - - assert isinstance(expected, dict), fixture_name - assert isinstance(input_text, str), fixture_name - - if expected.get("classification") != "unknown" or expected.get("output") is not None: - continue - - for candidate in _derived_risky_rewrite_candidates(input_text): - validated = validate_preprocessor_output(candidate, source_input=input_text) - assert validated["classification"] != "directive", fixture_name diff --git a/tests/test_preprocessor_heuristic.py b/tests/test_preprocessor_heuristic.py deleted file mode 100644 index 8744ad7..0000000 --- a/tests/test_preprocessor_heuristic.py +++ /dev/null @@ -1,320 +0,0 @@ -import pytest - -from experimental.preprocessor.heuristic_preprocessor import preprocess_heuristic - - -def test_heuristic_rejects_consistent_high_risk_non_directives() -> None: - cases = [ - "allow docker", - "set policy peanuts prohibit", - "stop using peanuts", - "use instead of docker", - "use podman instead of", - "use podman not docker", - "wipe policies", - "clear premise then clear state", - "prohibit peanuts and use almonds", - "set premise concise; reset policies", - "use docker, actually prohibit docker", - '"set premise concise replies" is invalid syntax, right?', - 'For example, you could "remove policy docker".', - 'He said "use docker".', - 'The doc literally says: "clear premise".', - ] - - for message in cases: - result = preprocess_heuristic(message) - assert result["outcome"] == "unknown" - assert result["directive"] is None - assert result["rule_id"] is not None - - -def test_heuristic_accepts_trailing_period_or_bang_for_whole_message_directives() -> None: - cases = [ - ("clear state.", "clear state"), - ("reset policies!", "reset policies"), - ("use docker.", "use docker"), - ] - for message, expected in cases: - assert preprocess_heuristic(message) == { - "outcome": "directive", - "directive": expected, - "rule_id": "canonical.full_match", - } - - -def test_heuristic_allows_exact_full_message_wrappers_for_directives() -> None: - cases = [ - ("(reset policies)", "reset policies"), - ("[prohibit peanuts]", "prohibit peanuts"), - ] - for message, expected in cases: - assert preprocess_heuristic(message) == { - "outcome": "directive", - "directive": expected, - "rule_id": "canonical.full_match", - } - - -def test_heuristic_rejects_quoted_or_backticked_exact_directives() -> None: - cases = [ - "`use docker`", - '"clear state"', - "'reset policies'", - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.quoted_exact", - } - - -def test_heuristic_case_normalizes_exact_command_shapes() -> None: - cases = [ - ("CLEAR STATE", "clear state"), - ("Use Docker", "use docker"), - ("Prohibit Peanuts", "prohibit peanuts"), - ] - for message, expected in cases: - assert preprocess_heuristic(message) == { - "outcome": "directive", - "directive": expected, - "rule_id": "canonical.full_match", - } - - -def test_heuristic_question_mark_only_non_directive_is_confident() -> None: - assert preprocess_heuristic("can you help with lunch?") == { - "outcome": "no_directive", - "directive": None, - "rule_id": "reject.confident_non_directive", - } - - -def test_heuristic_rejects_directive_adjacent_question_mark_as_unknown() -> None: - cases = [ - "use docker?", - "clear state?", - "can you use pytest instead of unittest?", - ] - for message in cases: - result = preprocess_heuristic(message) - assert result["outcome"] == "unknown" - assert result["directive"] is None - assert result["rule_id"] is not None - - -def test_heuristic_rejects_meta_reporting_or_example_prefixes() -> None: - cases = [ - "example: use docker", - "the command is clear state", - 'I said "use docker"', - 'he said "reset policies"', - 'example: "use docker"', - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.meta_or_reporting", - } - - -def test_heuristic_rejects_list_or_enumeration_inputs() -> None: - cases = [ - "1. use docker", - "- clear state", - "* prohibit peanuts", - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.list_or_enumeration", - } - - -def test_heuristic_rejects_multi_segment_or_mixed_prose_inputs() -> None: - cases = [ - "use docker because this repo already has Docker", - "clear state then continue", - "use docker and prohibit peanuts", - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.multi_segment_or_mixed_prose", - } - - -def test_heuristic_rejects_malformed_replacement_syntax() -> None: - cases = [ - "use podman instead docker", - "use podman in stead of docker", - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.malformed_replacement_syntax", - } - - -def test_heuristic_rejects_admin_near_miss_aliases() -> None: - cases = [ - "reset policy", - "remove policies docker", - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.admin_near_miss_alias", - } - - -def test_heuristic_rejects_notes_and_reporting_with_bracketed_mentions() -> None: - cases = [ - "In my notes: [clear state] [reset policies]", - "Notes: [use docker] [prohibit peanuts]", - "I wrote down [change premise to concise replies] yesterday", - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.quoted_reported_bracket", - } - - -def test_heuristic_accepts_bracket_wrapper_without_reporting_marker() -> None: - assert preprocess_heuristic("[clear state]") == { - "outcome": "directive", - "directive": "clear state", - "rule_id": "canonical.full_match", - } - - -def test_heuristic_set_premise_to_forms_are_unknown_not_rewritten() -> None: - cases = [ - "set premise to concise replies", - "set premise to formal tone", - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.directive_adjacent_unsafe", - } - - -def test_heuristic_dont_use_forms_are_unknown_not_rewritten() -> None: - cases = [ - "don't use peanuts", - "do not use peanuts", - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.directive_adjacent_unsafe", - } - - -def test_heuristic_does_not_canonicalize_set_premise_to_with_empty_payload() -> None: - assert preprocess_heuristic("set premise to ") == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.directive_adjacent_unsafe", - } - - -def test_heuristic_does_not_canonicalize_set_premise_to_when_not_whole_message() -> None: - assert preprocess_heuristic("please set premise to concise replies") == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.directive_adjacent_unsafe", - } - - -def test_heuristic_change_premise_missing_to_forms_are_unknown_not_rewritten() -> None: - cases = [ - "change premise concise replies", - "change premise formal tone", - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.directive_adjacent_unsafe", - } - - -def test_heuristic_does_not_canonicalize_change_premise_with_empty_payload() -> None: - assert preprocess_heuristic("change premise ") == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.directive_adjacent_unsafe", - } - - -def test_heuristic_does_not_canonicalize_change_premise_when_not_whole_message() -> None: - assert preprocess_heuristic("please change premise concise replies") == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.directive_adjacent_unsafe", - } - - -def test_heuristic_accepts_strict_canonical_directives() -> None: - directives = [ - "set premise concise replies", - "change premise to concise replies", - "use docker", - "prohibit peanuts", - "remove policy docker", - "use podman instead of docker", - "clear premise", - "reset policies", - "clear state", - ] - - for directive in directives: - result = preprocess_heuristic(directive) - assert result == { - "outcome": "directive", - "directive": directive, - "rule_id": "canonical.full_match", - } - - -def test_heuristic_returns_unknown_for_unresolved_cases() -> None: - unresolved = ["Could we maybe use uv later"] - - for message in unresolved: - assert preprocess_heuristic(message) == { - "outcome": "unknown", - "directive": None, - "rule_id": "reject.directive_adjacent_unsafe", - } - - -def test_heuristic_returns_no_directive_for_ordinary_non_directive_content() -> None: - cases = [ - "not sure this is right", - "thanks for the help", - ] - for message in cases: - assert preprocess_heuristic(message) == { - "outcome": "no_directive", - "directive": None, - "rule_id": "reject.confident_non_directive", - } - - -@pytest.mark.parametrize("message", ['""', "''", "()", "[]", "``"]) -def test_heuristic_empty_wrappers_do_not_produce_directive(message: str) -> None: - result = preprocess_heuristic(message) - assert result["directive"] is None diff --git a/tests/test_preprocessor_heuristic_properties.py b/tests/test_preprocessor_heuristic_properties.py deleted file mode 100644 index a28193e..0000000 --- a/tests/test_preprocessor_heuristic_properties.py +++ /dev/null @@ -1,161 +0,0 @@ -import re - -from hypothesis import assume, given -from hypothesis import strategies as st - -from experimental.preprocessor.constants import ( - PREPROCESS_OUTCOME_DIRECTIVE, - PREPROCESS_OUTCOME_NO_DIRECTIVE, - PREPROCESS_OUTCOME_UNKNOWN, -) -from experimental.preprocessor.heuristic_preprocessor import preprocess_heuristic -from experimental.preprocessor.output_validation import ( - _is_allowed_directive, - parse_preprocessor_output, -) - -CANONICAL_DIRECTIVES = [ - "set premise concise replies", - "change premise to formal tone", - "use docker", - "prohibit peanuts", - "remove policy docker", - "use podman instead of docker", - "clear premise", - "reset policies", - "clear state", -] - -NON_EMPTY_TEXT = st.text(min_size=1, max_size=40).filter(lambda s: s.strip() != "") -WRAPPERS = st.sampled_from( - [ - ("(", ")"), - ("[", "]"), - ] -) -QUOTED_WRAPPERS = st.sampled_from( - [ - ('"', '"'), - ("'", "'"), - ("`", "`"), - ] -) - - -@given(st.sampled_from(CANONICAL_DIRECTIVES), st.sampled_from([".", "!"])) -def test_heuristic_accepts_canonical_directive_with_trailing_period_or_bang( - directive: str, punctuation: str -) -> None: - result = preprocess_heuristic(f"{directive}{punctuation}") - assert result["outcome"] == PREPROCESS_OUTCOME_DIRECTIVE - parsed = parse_preprocessor_output(result["directive"]) - assert parsed == result["directive"] - - -@given(st.sampled_from(CANONICAL_DIRECTIVES)) -def test_heuristic_question_suffix_never_produces_directive(directive: str) -> None: - result = preprocess_heuristic(f"{directive}?") - assert result["outcome"] == PREPROCESS_OUTCOME_UNKNOWN - assert result["directive"] is None - - -@given(st.sampled_from(CANONICAL_DIRECTIVES), WRAPPERS) -def test_heuristic_accepts_single_layer_exact_wrapper( - directive: str, wrapper: tuple[str, str] -) -> None: - left, right = wrapper - result = preprocess_heuristic(f"{left}{directive}{right}") - assert result["outcome"] == PREPROCESS_OUTCOME_DIRECTIVE - parsed = parse_preprocessor_output(result["directive"]) - assert parsed == result["directive"] - - -@given(st.sampled_from(CANONICAL_DIRECTIVES), QUOTED_WRAPPERS) -def test_heuristic_quoted_exact_wrappers_never_directive( - directive: str, wrapper: tuple[str, str] -) -> None: - left, right = wrapper - result = preprocess_heuristic(f"{left}{directive}{right}") - assert result["outcome"] == PREPROCESS_OUTCOME_UNKNOWN - assert result["directive"] is None - - -@given( - st.sampled_from(CANONICAL_DIRECTIVES), - WRAPPERS, - st.sampled_from(["example:", "for example", "i said", "he said", "the command is"]), -) -def test_heuristic_rejects_wrapped_directive_with_surrounding_meta_text( - directive: str, wrapper: tuple[str, str], prefix: str -) -> None: - left, right = wrapper - message = f"{prefix} {left}{directive}{right}" - result = preprocess_heuristic(message) - assert result["outcome"] != PREPROCESS_OUTCOME_DIRECTIVE - - -@given(st.text(max_size=60), st.text(max_size=60)) -def test_heuristic_question_mark_is_always_rejected(prefix: str, suffix: str) -> None: - message = f"{prefix}?{suffix}" - result = preprocess_heuristic(message) - assert result["outcome"] in {PREPROCESS_OUTCOME_NO_DIRECTIVE, PREPROCESS_OUTCOME_UNKNOWN} - assert result["directive"] is None - - -@given(st.text(max_size=120)) -def test_heuristic_directive_output_is_always_validator_safe(message: str) -> None: - result = preprocess_heuristic(message) - if result["outcome"] != PREPROCESS_OUTCOME_DIRECTIVE: - return - directive = result["directive"] - assert isinstance(directive, str) - assert parse_preprocessor_output(directive) == directive - - -@given(st.sampled_from(CANONICAL_DIRECTIVES), NON_EMPTY_TEXT, NON_EMPTY_TEXT) -def test_heuristic_whole_message_discipline_for_surrounded_directive( - directive: str, prefix: str, suffix: str -) -> None: - message = f"{prefix} {directive} {suffix}" - normalized = re.sub(r"\s+", " ", message.strip().lower()) - assume(prefix.strip() not in {'"', "'", "`", "(", "["}) - assume(suffix.strip() not in {'"', "'", "`", ")", "]"}) - assume(not message.strip().lower().startswith("change premise ")) - assume(not _is_allowed_directive(normalized)) - result = preprocess_heuristic(message) - assert result["outcome"] != PREPROCESS_OUTCOME_DIRECTIVE - - -@given(NON_EMPTY_TEXT) -def test_heuristic_list_or_enumeration_prefix_never_directive(rest: str) -> None: - for prefix in ("1. ", "- ", "* "): - result = preprocess_heuristic(f"{prefix}{rest}") - assert result["outcome"] != PREPROCESS_OUTCOME_DIRECTIVE - - -@given(st.sampled_from(CANONICAL_DIRECTIVES)) -def test_heuristic_meta_reporting_prefix_never_directive(directive: str) -> None: - samples = [ - f"example: {directive}", - f"for example {directive}", - f"the command is {directive}", - f'i said "{directive}"', - f'he said "{directive}"', - ] - for message in samples: - result = preprocess_heuristic(message) - assert result["outcome"] != PREPROCESS_OUTCOME_DIRECTIVE - - -@given(st.sampled_from(["use docker", "clear state", "prohibit peanuts"]), NON_EMPTY_TEXT) -def test_heuristic_mixed_prose_connector_forms_never_directive( - directive_seed: str, detail: str -) -> None: - messages = [ - f"{directive_seed} because {detail}", - f"{directive_seed} then continue {detail}", - f"{directive_seed} and {detail}", - ] - for message in messages: - result = preprocess_heuristic(message) - assert result["outcome"] != PREPROCESS_OUTCOME_DIRECTIVE diff --git a/tests/test_preprocessor_output_validation.py b/tests/test_preprocessor_output_validation.py deleted file mode 100644 index df36007..0000000 --- a/tests/test_preprocessor_output_validation.py +++ /dev/null @@ -1,214 +0,0 @@ -from experimental.preprocessor.output_validation import ( - _is_allowed_directive, - parse_preprocessor_output, - validate_preprocessor_output, -) - - -def test_is_allowed_directive_accepts_canonical_shapes() -> None: - assert _is_allowed_directive("clear state") - assert _is_allowed_directive("set premise concise replies") - assert _is_allowed_directive("change premise to formal tone") - assert _is_allowed_directive("use podman instead of docker") - - -def test_validate_text_accepts_canonical_directive() -> None: - result = validate_preprocessor_output("prohibit peanuts") - assert result == { - "classification": "directive", - "output": "prohibit peanuts", - } - - -def test_validate_text_accepts_exact_no_directive_sentinel() -> None: - result = validate_preprocessor_output("") - assert result == { - "classification": "no_directive", - "output": None, - } - - -def test_validate_text_rejects_malformed_or_mixed_output_as_unknown() -> None: - assert validate_preprocessor_output("") == { - "classification": "unknown", - "output": None, - } - assert validate_preprocessor_output("set premise to concise replies") == { - "classification": "unknown", - "output": None, - } - assert validate_preprocessor_output("prohibit peanuts and use almonds") == { - "classification": "unknown", - "output": None, - } - - -def test_validate_structured_output_accepts_strict_contract_shape() -> None: - assert validate_preprocessor_output( - { - "classification": "directive", - "output": "clear state", - } - ) == { - "classification": "directive", - "output": "clear state", - } - - assert validate_preprocessor_output( - { - "classification": "no_directive", - "output": None, - } - ) == { - "classification": "no_directive", - "output": None, - } - - assert validate_preprocessor_output( - { - "classification": "unknown", - "output": None, - } - ) == { - "classification": "unknown", - "output": None, - } - - -def test_validate_structured_output_rejects_malformed_shape_or_payload_as_unknown() -> None: - cases = [ - None, - 123, - {}, - {"classification": "directive"}, - {"output": "clear state"}, - {"classification": "directive", "output": None}, - {"classification": "directive", "output": ""}, - {"classification": "directive", "output": "set premise to concise replies"}, - {"classification": "no_directive", "output": "clear state"}, - {"classification": "unknown", "output": "clear state"}, - {"classification": "unsupported_action", "output": None}, - {"classification": "directive", "output": "clear state\nreset policies"}, - {"classification": "directive", "output": "clear state", "extra": True}, - {"action": "prohibit", "item": "peanuts"}, - ] - for raw in cases: - assert validate_preprocessor_output(raw) == { - "classification": "unknown", - "output": None, - } - - -def test_validate_text_parses_and_validates_json_contract() -> None: - raw = '{"classification":"directive","output":"use docker"}' - assert validate_preprocessor_output(raw) == { - "classification": "directive", - "output": "use docker", - } - - -def test_parse_returns_validated_directive_only() -> None: - assert parse_preprocessor_output("prohibit peanuts") == "prohibit peanuts" - assert parse_preprocessor_output("") is None - assert parse_preprocessor_output("set premise to concise replies") is None - - -def test_parse_with_source_input_rejects_premise_near_miss_canonicalization() -> None: - assert ( - parse_preprocessor_output( - "set premise concise replies", - source_input="set premise to concise replies", - ) - is None - ) - assert ( - parse_preprocessor_output( - "change premise to concise replies", - source_input="change premise concise replies", - ) - is None - ) - - -def test_validation_with_source_input_rejects_premise_near_miss_canonicalization() -> None: - assert validate_preprocessor_output( - "set premise concise replies", - source_input="set premise to concise replies", - ) == { - "classification": "unknown", - "output": None, - } - assert validate_preprocessor_output( - "change premise to concise replies", - source_input="change premise concise replies", - ) == { - "classification": "unknown", - "output": None, - } - - -def test_validation_with_source_input_allows_other_directives() -> None: - assert validate_preprocessor_output( - "prohibit peanuts", - source_input="prohibit peanuts", - ) == { - "classification": "directive", - "output": "prohibit peanuts", - } - assert validate_preprocessor_output( - "use coconut milk", - source_input="what is a simple curry recipe?", - ) == { - "classification": "unknown", - "output": None, - } - - -def test_validation_with_source_input_rejects_boundary_unsafe_fallback_rewrites() -> None: - cases = [ - ("ok. prohibit peanuts", "prohibit peanuts"), - ("clear premise\nreset policies", "clear premise"), - ("```\nuse docker\n```", "use docker"), - ("the command is `use docker`", "use docker"), - ('the docs say "use docker"', "use docker"), - ("use docker and explain why", "use docker"), - ("can you use docker?", "use docker"), - ] - for source_input, fallback_output in cases: - assert validate_preprocessor_output( - fallback_output, - source_input=source_input, - ) == { - "classification": "unknown", - "output": None, - } - - -def test_validation_with_source_input_preserves_safe_whole_message_canonicalization() -> None: - cases = [ - ("Use Docker", "use docker"), - (" use docker ", "use docker"), - ("clear state.", "clear state"), - ("reset policies!", "reset policies"), - ("(clear state)", "clear state"), - ("[clear state]", "clear state"), - ('use "docker"', 'use "docker"'), - ] - for source_input, fallback_output in cases: - assert validate_preprocessor_output( - fallback_output, - source_input=source_input, - ) == { - "classification": "directive", - "output": fallback_output, - } - - -def test_validation_with_source_input_allows_strict_structured_contract_self_input() -> None: - assert validate_preprocessor_output( - '{"classification":"directive","output":"prohibit peanuts"}', - source_input='{"classification":"directive","output":"prohibit peanuts"}', - ) == { - "classification": "directive", - "output": "prohibit peanuts", - } diff --git a/tests/test_preprocessor_validator_properties.py b/tests/test_preprocessor_validator_properties.py deleted file mode 100644 index 2c7b69e..0000000 --- a/tests/test_preprocessor_validator_properties.py +++ /dev/null @@ -1,185 +0,0 @@ -from copy import deepcopy - -from hypothesis import assume, given -from hypothesis import strategies as st - -from context_compiler import create_engine -from experimental.preprocessor.output_validation import ( - _is_allowed_directive, - parse_preprocessor_output, - validate_preprocessor_output, -) - -CANONICAL_DIRECTIVES = [ - "set premise concise replies", - "change premise to formal tone", - "use docker", - "prohibit peanuts", - "remove policy docker", - "use podman instead of docker", - "clear premise", - "reset policies", - "clear state", -] - -SPACE_TEXT = st.text(alphabet=" \t\n", min_size=0, max_size=4) -NON_EMPTY_TEXT = st.text(min_size=1, max_size=40).filter(lambda s: s.strip() != "") - -NOISY_TEXT = st.one_of( - st.text(min_size=0, max_size=80), - st.builds(lambda a, b: f"{a}\n{b}", st.text(max_size=40), st.text(max_size=40)), - st.builds(lambda t: f'"{t}"', st.text(max_size=60)), - st.builds(lambda t: f"`{t}`", st.text(max_size=60)), - st.builds(lambda t: f"[{t}]", st.text(max_size=60)), - st.builds(lambda a, b: f"{a}; {b}", st.text(max_size=30), st.text(max_size=30)), - st.builds(lambda a, b: f"{a}: {b}", st.text(max_size=30), st.text(max_size=30)), -) -UNSAFE_SOURCE_TEMPLATES = st.sampled_from( - [ - "ok. {}", - "```\n{}\n```", - "the command is `{}`", - 'the docs say "{}"', - "can you {}?", - ] -) - - -@given( - st.one_of( - st.none(), - st.integers(), - st.floats(), - st.booleans(), - st.binary(), - st.lists(st.integers()), - st.dictionaries(st.text(max_size=10), st.integers()), - ) -) -def test_parse_non_string_never_produces_directive(raw_output: object) -> None: - assert parse_preprocessor_output(raw_output) is None - - -@given(NOISY_TEXT) -def test_parse_invalid_text_never_becomes_directive(text: str) -> None: - stripped = text.strip() - assume(not _is_allowed_directive(stripped)) - assert parse_preprocessor_output(text) is None - - -@given(st.sampled_from(CANONICAL_DIRECTIVES), SPACE_TEXT, SPACE_TEXT) -def test_parse_valid_canonical_directive_always_passes( - directive: str, leading_ws: str, trailing_ws: str -) -> None: - raw = f"{leading_ws}{directive}{trailing_ws}" - assert parse_preprocessor_output(raw) == directive - - -@given(st.sampled_from(CANONICAL_DIRECTIVES), NON_EMPTY_TEXT, NON_EMPTY_TEXT) -def test_parse_rejects_directive_with_surrounding_text( - directive: str, prefix: str, suffix: str -) -> None: - raw = f"{prefix} {directive} {suffix}" - stripped = raw.strip() - assume(not _is_allowed_directive(stripped)) - assert parse_preprocessor_output(raw) is None - - -@given( - st.sampled_from(CANONICAL_DIRECTIVES), - st.sampled_from( - [ - 'example: "{}"', - "for example `{}`", - "notes: [{}]", - 'he said "{}"', - 'command = "{}"', - ] - ), -) -def test_parse_rejects_directive_in_constrained_wrappers(directive: str, wrapper: str) -> None: - wrapped = wrapper.format(directive) - assert parse_preprocessor_output(wrapped) is None - - -def test_validate_malformed_abstain_negative_boundaries_are_unknown() -> None: - cases = { - "": "unknown", - "": "unknown", - "": "unknown", - "notes: ": "unknown", - "prefix ": "unknown", - "": "unknown", - "": "unknown", - "": "no_directive", - } - for raw, expected_cls in cases.items(): - validated = validate_preprocessor_output(raw) - assert validated["classification"] == expected_cls - assert validated["output"] is None - - -def test_parse_rejects_near_miss_directives_when_wrapped_or_prefixed() -> None: - cases = [ - "`set premise to concise replies`", - '"use podman not docker"', - "example: clear state", - "notes: [set premise to concise replies]", - 'he said "use docker"', - ] - for raw in cases: - assert parse_preprocessor_output(raw) is None - - -@given(st.one_of(st.none(), st.integers(), st.text(max_size=120))) -def test_parse_output_idempotent(raw_output: object) -> None: - first = parse_preprocessor_output(raw_output) - second = parse_preprocessor_output(first) - if first is None: - assert second is None - else: - assert second == first - - -@given( - st.one_of( - st.none(), st.integers(), st.text(max_size=120), st.dictionaries(st.text(), st.none()) - ) -) -def test_validate_output_always_has_null_for_non_directive(raw_output: object) -> None: - validated = validate_preprocessor_output(raw_output) - if validated["classification"] == "directive": - assert isinstance(validated["output"], str) - else: - assert validated["output"] is None - - -@given( - st.sampled_from(CANONICAL_DIRECTIVES), - st.sampled_from(CANONICAL_DIRECTIVES), - UNSAFE_SOURCE_TEMPLATES, -) -def test_source_aware_validation_rejects_unsafe_wrapped_sources( - directive_in_source: str, fallback_directive: str, template: str -) -> None: - source_input = template.format(directive_in_source) - validated = validate_preprocessor_output(fallback_directive, source_input=source_input) - assert validated == {"classification": "unknown", "output": None} - - -@given(st.sampled_from(CANONICAL_DIRECTIVES), UNSAFE_SOURCE_TEMPLATES) -def test_source_aware_rejection_prevents_engine_state_mutation( - directive_in_source: str, template: str -) -> None: - source_input = template.format(directive_in_source) - fallback_directive = "use docker" - parsed = parse_preprocessor_output(fallback_directive, source_input=source_input) - compile_input = parsed if parsed is not None else source_input - - engine = create_engine() - before = deepcopy(engine.state) - decision = engine.step(compile_input) - after = engine.state - - assume(decision["kind"] != "update") - assert before == after diff --git a/tests/test_repl.py b/tests/test_repl.py index 844961d..3c661db 100644 --- a/tests/test_repl.py +++ b/tests/test_repl.py @@ -88,15 +88,13 @@ def test_main_help_flag_prints_usage_and_exits_zero( assert result == 0 assert captured.out == ( "Usage:\n" - " context-compiler [--help] [--version] [--with-preprocessor] [--json]\n" + " context-compiler [--help] [--version] [--json]\n" " [--initial-state-json | --initial-state-file ]\n" " [--initial-checkpoint-json | --initial-checkpoint-file ]\n" "\n" "Options:\n" " --help Show this help message and exit.\n" " --version Show the installed context-compiler version and exit.\n" - " --with-preprocessor Enable preprocessor before each REPL turn " - "(heuristic + validation only)\n" " --json Emit machine-readable NDJSON output (non-interactive only)\n" " --initial-state-json Initialize authoritative state from exported state JSON text\n" " --initial-state-file Initialize authoritative state from UTF-8 state JSON file\n" @@ -128,13 +126,11 @@ def _fake_run_repl( in_stream: TextIO, out_stream: TextIO, *, - use_preprocessor: bool = False, json_mode: bool = False, engine: object | None = None, ) -> None: called["in_stream"] = in_stream called["out_stream"] = out_stream - called["use_preprocessor"] = use_preprocessor called["json_mode"] = json_mode called["engine"] = engine @@ -146,39 +142,10 @@ def _fake_run_repl( assert result == 0 assert called["in_stream"] is sys.stdin assert called["out_stream"] is sys.stdout - assert called["use_preprocessor"] is False assert called["json_mode"] is False assert called["engine"] is None -def test_main_with_preprocessor_flag_runs_repl_with_flag(monkeypatch: pytest.MonkeyPatch) -> None: - called: dict[str, object] = {} - - def _fake_run_repl( - in_stream: TextIO, - out_stream: TextIO, - *, - use_preprocessor: bool = False, - json_mode: bool = False, - engine: object | None = None, - ) -> None: - called["in_stream"] = in_stream - called["out_stream"] = out_stream - called["use_preprocessor"] = use_preprocessor - called["json_mode"] = json_mode - - monkeypatch.setattr(repl_module, "run_repl", _fake_run_repl) - monkeypatch.setattr(sys, "argv", ["context-compiler", "--with-preprocessor"]) - - result = repl_module.main() - - assert result == 0 - assert called["in_stream"] is sys.stdin - assert called["out_stream"] is sys.stdout - assert called["use_preprocessor"] is True - assert called["json_mode"] is False - - def test_main_with_json_flag_runs_repl_with_json_mode(monkeypatch: pytest.MonkeyPatch) -> None: called: dict[str, object] = {} @@ -186,13 +153,11 @@ def _fake_run_repl( in_stream: TextIO, out_stream: TextIO, *, - use_preprocessor: bool = False, json_mode: bool = False, engine: object | None = None, ) -> None: called["in_stream"] = in_stream called["out_stream"] = out_stream - called["use_preprocessor"] = use_preprocessor called["json_mode"] = json_mode monkeypatch.setattr(repl_module, "run_repl", _fake_run_repl) @@ -204,38 +169,6 @@ def _fake_run_repl( assert result == 0 assert called["in_stream"] is sys.stdin assert called["out_stream"] is sys.stdout - assert called["use_preprocessor"] is False - assert called["json_mode"] is True - - -def test_main_with_json_and_preprocessor_runs_repl_with_both( - monkeypatch: pytest.MonkeyPatch, -) -> None: - called: dict[str, object] = {} - - def _fake_run_repl( - in_stream: TextIO, - out_stream: TextIO, - *, - use_preprocessor: bool = False, - json_mode: bool = False, - engine: object | None = None, - ) -> None: - called["in_stream"] = in_stream - called["out_stream"] = out_stream - called["use_preprocessor"] = use_preprocessor - called["json_mode"] = json_mode - - monkeypatch.setattr(repl_module, "run_repl", _fake_run_repl) - monkeypatch.setattr(repl_module, "_is_interactive", lambda _in, _out: False) - monkeypatch.setattr(sys, "argv", ["context-compiler", "--with-preprocessor", "--json"]) - - result = repl_module.main() - - assert result == 0 - assert called["in_stream"] is sys.stdin - assert called["out_stream"] is sys.stdout - assert called["use_preprocessor"] is True assert called["json_mode"] is True @@ -268,7 +201,7 @@ def test_main_unknown_flag_prints_error_hint_and_exits_nonzero( ) -@pytest.mark.parametrize("args", [["--help", "--version"], ["--version", "--with-preprocessor"]]) +@pytest.mark.parametrize("args", [["--help", "--version"], ["--version", "--json"]]) def test_cli_rejects_non_single_flag_argument_forms(args: list[str]) -> None: result = _run_repl_cli(*args) @@ -279,7 +212,7 @@ def test_cli_rejects_non_single_flag_argument_forms(args: list[str]) -> None: def test_cli_rejects_unknown_positional_after_flag() -> None: - result = _run_repl_cli("--with-preprocessor", "foo") + result = _run_repl_cli("--json", "foo") assert result.returncode != 0 assert result.stdout == "" assert ( @@ -524,7 +457,7 @@ def test_apply_preload_from_options_state_and_checkpoint_file_paths( state_engine = create_engine() repl_module._apply_preload_from_options( state_engine, - {"use_preprocessor": False, "json_mode": False, "initial_state_file": str(state_path)}, + {"json_mode": False, "initial_state_file": str(state_path)}, ) assert state_engine.state["premise"] == "concise" @@ -532,7 +465,6 @@ def test_apply_preload_from_options_state_and_checkpoint_file_paths( repl_module._apply_preload_from_options( checkpoint_engine, { - "use_preprocessor": False, "json_mode": False, "initial_checkpoint_file": str(checkpoint_path), }, @@ -552,7 +484,6 @@ def test_apply_preload_from_options_state_and_checkpoint_json() -> None: repl_module._apply_preload_from_options( state_engine, { - "use_preprocessor": False, "json_mode": False, "initial_state_json": source_state.export_json(), }, @@ -563,7 +494,6 @@ def test_apply_preload_from_options_state_and_checkpoint_json() -> None: repl_module._apply_preload_from_options( checkpoint_engine, { - "use_preprocessor": False, "json_mode": False, "initial_checkpoint_json": source_checkpoint.export_checkpoint_json(), }, @@ -915,83 +845,6 @@ def test_repl_preview_idempotent_admin_action_reports_no_mutation() -> None: assert _contains_subsequence(lines, ["would_mutate: no", "diff:", "- (none)"]) -def test_repl_with_preprocessor_parses_directive_before_engine_step() -> None: - out = StringIO() - run_repl( - StringIO('{"classification":"directive","output":"prohibit peanuts"}\nquit\n'), - out, - use_preprocessor=True, - ) - - lines = out.getvalue().splitlines() - assert lines == ["updated", "premise: (none)", "policies:", "- prohibit peanuts"] - - -def test_repl_with_preprocessor_near_miss_passes_through_and_clarifies() -> None: - out = StringIO() - run_repl(StringIO("set premise to concise replies\nquit\n"), out, use_preprocessor=True) - - lines = out.getvalue().splitlines() - assert lines == ["confirm: Did you mean 'set premise concise replies'?"] - - -def test_repl_with_preprocessor_non_directive_passthrough() -> None: - out = StringIO() - run_repl(StringIO("what is a simple curry recipe?\nquit\n"), out, use_preprocessor=True) - - lines = out.getvalue().splitlines() - assert lines == ["passthrough"] - - -def test_cli_with_preprocessor_pipe_smoke_emits_clarify_without_update() -> None: - result = _run_repl_cli("--with-preprocessor", input_text="set premise to concise replies\n") - - assert result.returncode == 0 - assert "Did you mean 'set premise concise replies'?" in result.stdout - assert "updated" not in result.stdout - assert result.stderr == "" - - -def test_repl_with_preprocessor_bypasses_parsing_while_pending( - monkeypatch: pytest.MonkeyPatch, -) -> None: - seen: list[tuple[object, str | None]] = [] - - def _parse(raw_output: object, *, source_input: str | None = None) -> str | None: - seen.append((raw_output, source_input)) - if raw_output == "use podman instead of docker": - return "use podman instead of docker" - raise AssertionError("parse_preprocessor_output should be bypassed while pending") - - monkeypatch.setattr(repl_module, "parse_preprocessor_output", _parse) - - out = StringIO() - run_repl( - StringIO("use podman instead of docker\nyes\nquit\n"), - out, - use_preprocessor=True, - ) - - assert seen == [("use podman instead of docker", "use podman instead of docker")] - lines = out.getvalue().splitlines() - assert _contains_subsequence(lines, ['confirm: Did you mean to use "podman" instead?']) - assert _contains_subsequence(lines, ["updated", "premise: (none)", "policies:", "- use podman"]) - - -def test_repl_without_preprocessor_does_not_parse_inputs(monkeypatch: pytest.MonkeyPatch) -> None: - def _fail_parse(_raw: object, *, source_input: str | None = None) -> str | None: - del source_input - raise AssertionError("parse_preprocessor_output should not be called") - - monkeypatch.setattr(repl_module, "parse_preprocessor_output", _fail_parse) - - out = StringIO() - run_repl(StringIO('{"classification":"directive","output":"prohibit peanuts"}\nquit\n'), out) - - lines = out.getvalue().splitlines() - assert lines == ["passthrough"] - - def test_repl_interactive_rejects_multi_command_chunk() -> None: out = _TTYStringIO() run_repl( diff --git a/uv.lock b/uv.lock index 1a1d30c..b443450 100644 --- a/uv.lock +++ b/uv.lock @@ -157,27 +157,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] -[[package]] -name = "apscheduler" -version = "3.11.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tzlocal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683, upload-time = "2025-12-22T00:39:34.884Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" }, -] - -[[package]] -name = "async-timeout" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, -] - [[package]] name = "attrs" version = "26.1.0" @@ -187,87 +166,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] -[[package]] -name = "azure-core" -version = "1.39.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/83/bbde3faa84ddcb8eb0eca4b3ffb3221252281db4ce351300fe248c5c70b1/azure_core-1.39.0.tar.gz", hash = "sha256:8a90a562998dd44ce84597590fff6249701b98c0e8797c95fcdd695b54c35d74", size = 367531, upload-time = "2026-03-19T01:31:29.461Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d6/8ebcd05b01a580f086ac9a97fb9fac65c09a4b012161cc97c21a336e880b/azure_core-1.39.0-py3-none-any.whl", hash = "sha256:4ac7b70fab5438c3f68770649a78daf97833caa83827f91df9c14e0e0ea7d34f", size = 218318, upload-time = "2026-03-19T01:31:31.25Z" }, -] - -[[package]] -name = "azure-identity" -version = "1.25.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "cryptography" }, - { name = "msal" }, - { name = "msal-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/0e/3a63efb48aa4a5ae2cfca61ee152fbcb668092134d3eb8bfda472dd5c617/azure_identity-1.25.3.tar.gz", hash = "sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6", size = 286304, upload-time = "2026-03-13T01:12:20.892Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/9a/417b3a533e01953a7c618884df2cb05a71e7b68bdbce4fbdb62349d2a2e8/azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c", size = 192138, upload-time = "2026-03-13T01:12:22.951Z" }, -] - -[[package]] -name = "azure-storage-blob" -version = "12.28.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "cryptography" }, - { name = "isodate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/24/072ba8e27b0e2d8fec401e9969b429d4f5fc4c8d4f0f05f4661e11f7234a/azure_storage_blob-12.28.0.tar.gz", hash = "sha256:e7d98ea108258d29aa0efbfd591b2e2075fa1722a2fae8699f0b3c9de11eff41", size = 604225, upload-time = "2026-01-06T23:48:57.282Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/3a/6ef2047a072e54e1142718d433d50e9514c999a58f51abfff7902f3a72f8/azure_storage_blob-12.28.0-py3-none-any.whl", hash = "sha256:00fb1db28bf6a7b7ecaa48e3b1d5c83bfadacc5a678b77826081304bd87d6461", size = 431499, upload-time = "2026-01-06T23:48:58.995Z" }, -] - -[[package]] -name = "backoff" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, -] - -[[package]] -name = "boto3" -version = "1.42.89" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "botocore" }, - { name = "jmespath" }, - { name = "s3transfer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/0c/f7bccb22b245cabf392816baba20f9e95f78ace7dbc580fd40136e80e732/boto3-1.42.89.tar.gz", hash = "sha256:3e43aacc0801bba9bcd23a8c271c089af297a69565f783fcdd357ae0e330bf1e", size = 113165, upload-time = "2026-04-13T19:36:17.516Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/33/55103ba5ef9975ea54b8d39e69b76eb6e9fded3beae5f01065e26951a3a1/boto3-1.42.89-py3-none-any.whl", hash = "sha256:6204b189f4d0c655535f43d7eaa57ff4e8d965b8463c97e45952291211162932", size = 140556, upload-time = "2026-04-13T19:36:13.894Z" }, -] - -[[package]] -name = "botocore" -version = "1.42.89" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jmespath" }, - { name = "python-dateutil" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/cc/e6be943efa9051bd15c2ee14077c2b10d6e27c9e9385fc43a03a5c4ed8b5/botocore-1.42.89.tar.gz", hash = "sha256:95ac52f472dad29942f3088b278ab493044516c16dbf9133c975af16527baa99", size = 15206290, upload-time = "2026-04-13T19:36:02.321Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/f1/90a7b8eda38b7c3a65ca7ee0075bdf310b6b471cb1b95fab6e8994323a50/botocore-1.42.89-py3-none-any.whl", hash = "sha256:d9b786c8d9db6473063b4cc5be0ba7e6a381082307bd6afb69d4216f9fa95f35", size = 14887287, upload-time = "2026-04-13T19:35:56.677Z" }, -] - [[package]] name = "certifi" version = "2026.2.25" @@ -277,76 +175,6 @@ 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" }, ] -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - [[package]] name = "cfgv" version = "3.5.0" @@ -484,31 +312,19 @@ dev = [ { name = "pytest-cov" }, { name = "ruff" }, ] -experimental = [ - { name = "litellm" }, -] -integrations = [ - { name = "litellm" }, -] -litellm-proxy = [ - { name = "litellm", extra = ["proxy"] }, -] [package.metadata] requires-dist = [ { name = "coverage", marker = "extra == 'dev'" }, { name = "hypothesis", marker = "extra == 'dev'" }, { name = "litellm", marker = "extra == 'demos'", specifier = ">=1.0.0" }, - { name = "litellm", marker = "extra == 'experimental'", specifier = ">=1.0.0" }, - { name = "litellm", marker = "extra == 'integrations'", specifier = ">=1.0.0" }, - { name = "litellm", extras = ["proxy"], marker = "extra == 'litellm-proxy'", specifier = ">=1.0.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.12,<2.0" }, { name = "pre-commit", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-cov", marker = "extra == 'dev'" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.12,<1.0" }, ] -provides-extras = ["demos", "experimental", "integrations", "litellm-proxy", "dev"] +provides-extras = ["demos", "dev"] [[package]] name = "coverage" @@ -614,77 +430,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "croniter" -version = "6.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/de/5832661ed55107b8a09af3f0a2e71e0957226a59eb1dcf0a445cce6daf20/croniter-6.2.2.tar.gz", hash = "sha256:ba60832a5ec8e12e51b8691c3309a113d1cf6526bdf1a48150ce8ec7a532d0ab", size = 113762, upload-time = "2026-03-15T08:43:48.112Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/39/783980e78cb92c2d7bdb1fc7dbc86e94ccc6d58224d76a7f1f51b6c51e30/croniter-6.2.2-py3-none-any.whl", hash = "sha256:a5d17b1060974d36251ea4faf388233eca8acf0d09cbd92d35f4c4ac8f279960", size = 45422, upload-time = "2026-03-15T08:43:46.626Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, - { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, - { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, - { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, - { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, - { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, - { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, - { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, - { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, - { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -703,59 +448,6 @@ 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 = "email-validator" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, -] - -[[package]] -name = "fastapi" -version = "0.136.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4e/d9/e66315807e41e69e7f6a1b42a162dada2f249c5f06ad3f1a95f84ab336ef/fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e", size = 396607, upload-time = "2026-04-16T11:47:13.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/a3/0bd5f0cdb0bbc92650e8dc457e9250358411ee5d1b65e42b6632387daf81/fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4", size = 117556, upload-time = "2026-04-16T11:47:11.922Z" }, -] - -[[package]] -name = "fastapi-sso" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fastapi" }, - { name = "httpx" }, - { name = "oauthlib" }, - { name = "pydantic", extra = ["email"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/9b/25c43c928b46ec919cb8941d3de53dd2e12bab12e1c0182646425dbefd60/fastapi_sso-0.16.0.tar.gz", hash = "sha256:f3941f986347566b7d3747c710cf474a907f581bfb6697ff3bb3e44eb76b438c", size = 16555, upload-time = "2024-11-04T11:54:38.579Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/84/df15745ff06c1b44e478b72759d5cf48e4583e221389d4cdea76c472dd1c/fastapi_sso-0.16.0-py3-none-any.whl", hash = "sha256:3a66a942474ef9756d3a9d8b945d55bd9faf99781facdb9b87a40b73d6d6b0c3", size = 23942, upload-time = "2024-11-04T11:54:37.189Z" }, -] - [[package]] name = "fastuuid" version = "0.14.0" @@ -931,18 +623,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] -[[package]] -name = "gunicorn" -version = "23.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -1012,15 +692,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - [[package]] name = "huggingface-hub" version = "1.7.2" @@ -1092,15 +763,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "isodate" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -1198,15 +860,6 @@ wheels = [ { 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]] -name = "jmespath" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, -] - [[package]] name = "jsonschema" version = "4.26.0" @@ -1330,53 +983,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/6c/5327667e6dbe9e98cbfbd4261c8e91386a52e38f41419575854248bbab6a/litellm-1.82.6-py3-none-any.whl", hash = "sha256:164a3ef3e19f309e3cabc199bef3d2045212712fefdfa25fc7f75884a5b5b205", size = 15591595, upload-time = "2026-03-22T06:35:56.795Z" }, ] -[package.optional-dependencies] -proxy = [ - { name = "apscheduler" }, - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "backoff" }, - { name = "boto3" }, - { name = "cryptography" }, - { name = "fastapi" }, - { name = "fastapi-sso" }, - { name = "gunicorn" }, - { name = "litellm-enterprise" }, - { name = "litellm-proxy-extras" }, - { name = "mcp" }, - { name = "orjson" }, - { name = "polars" }, - { name = "pyjwt" }, - { name = "pynacl" }, - { name = "pyroscope-io", marker = "sys_platform != 'win32'" }, - { name = "python-multipart" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "rq" }, - { name = "soundfile" }, - { name = "uvicorn" }, - { name = "uvloop", marker = "sys_platform != 'win32'" }, - { name = "websockets" }, -] - -[[package]] -name = "litellm-enterprise" -version = "0.1.35" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/5f/e593f335698a5c70d7e96e8ab9fdc4cfd4cc9249c524723fe64ed7f00cbb/litellm_enterprise-0.1.35.tar.gz", hash = "sha256:b752d07e538424743fcc08ba0d3d9d83d1f04a45c115811ac7828d789b6d87cc", size = 58817, upload-time = "2026-03-21T15:06:16.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/fa/39efe3dfa680ca5bc5795b9c904c914b09a65278c2970c8fece6e0e30e47/litellm_enterprise-0.1.35-py3-none-any.whl", hash = "sha256:8d2d9c925de8ee35e308c0f4975483b60f5e22beb50506e261e555e466f019c5", size = 122659, upload-time = "2026-03-21T15:06:15.586Z" }, -] - -[[package]] -name = "litellm-proxy-extras" -version = "0.4.65" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/21/134126176621c4e50bc9703f693b90d68899fc68fdebf6958704c5c483e8/litellm_proxy_extras-0.4.65.tar.gz", hash = "sha256:2dd4b75b17008a36e05ef8e234fe4ab1cf1e1e2231baeb8b8f8d2f48162e2b08", size = 32575, upload-time = "2026-04-05T00:20:13.68Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/90/d0cc4d0a614494cd3b9153584e11f8f1d06045706de92901dafaf1b12403/litellm_proxy_extras-0.4.65-py3-none-any.whl", hash = "sha256:d165269a0e9ec536784483a5b1aaefe989c9ae7ff2ddb4b3dda132ad96c95b29", size = 78106, upload-time = "2026-04-05T00:20:12.046Z" }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1463,31 +1069,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 = "mcp" -version = "1.27.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -1497,32 +1078,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "msal" -version = "1.36.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/cb/b02b0f748ac668922364ccb3c3bff5b71628a05f5adfec2ba2a5c3031483/msal-1.36.0.tar.gz", hash = "sha256:3f6a4af2b036b476a4215111c4297b4e6e236ed186cd804faefba23e4990978b", size = 174217, upload-time = "2026-04-09T10:20:33.525Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/d3/414d1f0a5f6f4fe5313c2b002c54e78a3332970feb3f5fed14237aa17064/msal-1.36.0-py3-none-any.whl", hash = "sha256:36ecac30e2ff4322d956029aabce3c82301c29f0acb1ad89b94edcabb0e58ec4", size = 121547, upload-time = "2026-04-09T10:20:32.336Z" }, -] - -[[package]] -name = "msal-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "msal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, -] - [[package]] name = "multidict" version = "6.7.1" @@ -1697,15 +1252,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] -[[package]] -name = "oauthlib" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, -] - [[package]] name = "openai" version = "2.26.0" @@ -1725,74 +1271,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/2e/3f73e8ca53718952222cacd0cf7eecc9db439d020f0c1fe7ae717e4e199a/openai-2.26.0-py3-none-any.whl", hash = "sha256:6151bf8f83802f036117f06cc8a57b3a4da60da9926826cc96747888b57f394f", size = 1136409, upload-time = "2026-03-05T23:17:34.072Z" }, ] -[[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" }, -] - [[package]] name = "packaging" version = "26.0" @@ -1829,34 +1307,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "polars" -version = "1.39.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "polars-runtime-32" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/93/ab/f19e592fce9e000da49c96bf35e77cef67f9cb4b040bfa538a2764c0263e/polars-1.39.3.tar.gz", hash = "sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c", size = 728987, upload-time = "2026-03-20T11:16:24.836Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/db/08f4ca10c5018813e7e0b59e4472302328b3d2ab1512f5a2157a814540e0/polars-1.39.3-py3-none-any.whl", hash = "sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56", size = 823985, upload-time = "2026-03-20T11:14:23.619Z" }, -] - -[[package]] -name = "polars-runtime-32" -version = "1.39.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/17/39/c8688696bc22b6c501e3b82ef3be10e543c07a785af5660f30997cd22dd2/polars_runtime_32-1.39.3.tar.gz", hash = "sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d", size = 2872335, upload-time = "2026-03-20T11:16:26.581Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/74/1b41205f7368c9375ab1dea91178eaa20435fe3eff036390a53a7660b416/polars_runtime_32-1.39.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9", size = 45273243, upload-time = "2026-03-20T11:14:26.691Z" }, - { url = "https://files.pythonhosted.org/packages/90/bf/297716b3095fe719be20fcf7af1d2b6ab069c38199bbace2469608a69b3a/polars_runtime_32-1.39.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562", size = 40842924, upload-time = "2026-03-20T11:14:31.154Z" }, - { url = "https://files.pythonhosted.org/packages/3d/3e/e65236d9d0d9babfa0ecba593413c06530fca60a8feb8f66243aa5dba92e/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14", size = 43220650, upload-time = "2026-03-20T11:14:35.458Z" }, - { url = "https://files.pythonhosted.org/packages/b0/15/fc3e43f3fdf3f20b7dfb5abe871ab6162cf8fb4aeabf4cfad822d5dc4c79/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed", size = 46877498, upload-time = "2026-03-20T11:14:40.14Z" }, - { url = "https://files.pythonhosted.org/packages/3c/81/bd5f895919e32c6ab0a7786cd0c0ca961cb03152c47c3645808b54383f31/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2", size = 43380176, upload-time = "2026-03-20T11:14:45.566Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3e/c86433c3b5ec0315bdfc7640d0c15d41f1216c0103a0eab9a9b5147d6c4c/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4", size = 46485933, upload-time = "2026-03-20T11:14:51.155Z" }, - { url = "https://files.pythonhosted.org/packages/54/ce/200b310cf91f98e652eb6ea09fdb3a9718aa0293ebf113dce325797c8572/polars_runtime_32-1.39.3-cp310-abi3-win_amd64.whl", hash = "sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339", size = 46995458, upload-time = "2026-03-20T11:14:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/da/76/2d48927e0aa2abbdde08cbf4a2536883b73277d47fbeca95e952de86df34/polars_runtime_32-1.39.3-cp310-abi3-win_arm64.whl", hash = "sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860", size = 41857648, upload-time = "2026-03-20T11:15:01.142Z" }, -] - [[package]] name = "pre-commit" version = "4.5.1" @@ -1972,15 +1422,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - [[package]] name = "pydantic" version = "2.12.5" @@ -1996,11 +1437,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] -[package.optional-dependencies] -email = [ - { name = "email-validator" }, -] - [[package]] name = "pydantic-core" version = "2.41.5" @@ -2098,20 +1534,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] -[[package]] -name = "pydantic-settings" -version = "2.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, -] - [[package]] name = "pygments" version = "2.19.2" @@ -2121,69 +1543,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pyjwt" -version = "2.12.1" -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" } -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" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "pynacl" -version = "1.6.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, - { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, - { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, - { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, - { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, - { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, - { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, - { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, - { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, - { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, - { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, - { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, - { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, - { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, - { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, - { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, - { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, -] - -[[package]] -name = "pyroscope-io" -version = "0.8.16" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/50/607b38b120ba8adad954119ba512c53590c793f0cf7f009ba6549e4e1d77/pyroscope_io-0.8.16-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:e07edcfd59f5bdce42948b92c9b118c824edbd551730305f095a6b9af401a9e8", size = 3138869, upload-time = "2026-01-22T06:23:24.664Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c1/90fc335f2224da86d49016ebe15fb4f709c7b8853d4b5beced5a052d9ea3/pyroscope_io-0.8.16-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:dc98355e27c0b7b61f27066500fe1045b70e9459bb8b9a3082bc4755cb6392b6", size = 3375865, upload-time = "2026-01-22T06:23:27.736Z" }, - { url = "https://files.pythonhosted.org/packages/39/7a/261f53ede16b7db19984ec80480572b8e9aa3be0ffc82f62650c4b9ca7d6/pyroscope_io-0.8.16-py2.py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:86f0f047554ff62bd92c3e5a26bc2809ccd467d11fbacb9fef898ba299dbda59", size = 3236172, upload-time = "2026-01-22T06:23:29.107Z" }, - { url = "https://files.pythonhosted.org/packages/eb/8f/88d792e9cacd6ff3bd9a50100586ddc665e02a917662c17d30931f778542/pyroscope_io-0.8.16-py2.py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6b91ce5b240f8de756c16a17022ca8e25ef8a4eed461c7d074b8a0841cf7b445", size = 3485288, upload-time = "2026-01-22T06:23:32Z" }, -] - [[package]] name = "pytest" version = "9.0.2" @@ -2214,18 +1573,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - [[package]] name = "python-discovery" version = "1.1.0" @@ -2248,34 +1595,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] -[[package]] -name = "python-multipart" -version = "0.0.26" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -2331,18 +1650,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 = "redis" -version = "7.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7b/7f/3759b1d0d72b7c92f0d70ffd9dc962b7b7b5ee74e135f9d7d8ab06b8a318/redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad", size = 4943913, upload-time = "2026-03-24T09:14:37.53Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/3a/95deec7db1eb53979973ebd156f3369a72732208d1391cd2e5d127062a32/redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec", size = 409772, upload-time = "2026-03-24T09:14:35.968Z" }, -] - [[package]] name = "referencing" version = "0.37.0" @@ -2597,20 +1904,6 @@ wheels = [ { 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 = "rq" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "croniter" }, - { name = "redis" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/9b/93b7180220fe462b4128425e687665bcdeffddc51683d41e7fbe509c2d2e/rq-2.7.0.tar.gz", hash = "sha256:c2156fc7249b5d43dda918c4355cfbf8d0d299a5cdd3963918e9c8daf4b1e0c0", size = 679396, upload-time = "2026-02-22T11:10:50.775Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/1a/3b64696bc0c33aa1d86d3e6add03c4e0afe51110264fd41208bd95c2665c/rq-2.7.0-py3-none-any.whl", hash = "sha256:4b320e95968208d2e249fa0d3d90ee309478e2d7ea60a116f8ff9aa343a4c117", size = 115728, upload-time = "2026-02-22T11:10:48.401Z" }, -] - [[package]] name = "ruff" version = "0.15.4" @@ -2636,18 +1929,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, ] -[[package]] -name = "s3transfer" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "botocore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -2657,15 +1938,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -2684,50 +1956,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] -[[package]] -name = "soundfile" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/96/5ff33900998bad58d5381fd1acfcdac11cbea4f08fc72ac1dc25ffb13f6a/soundfile-0.12.1.tar.gz", hash = "sha256:e8e1017b2cf1dda767aef19d2fd9ee5ebe07e050d430f77a0a7c66ba08b8cdae", size = 43184, upload-time = "2023-02-15T15:37:32.011Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/bc/cd845c2dbb4d257c744cd58a5bcdd9f6d235ca317e7e22e49564ec88dcd9/soundfile-0.12.1-py2.py3-none-any.whl", hash = "sha256:828a79c2e75abab5359f780c81dccd4953c45a2c4cd4f05ba3e233ddf984b882", size = 24030, upload-time = "2023-02-15T15:37:16.077Z" }, - { url = "https://files.pythonhosted.org/packages/c8/73/059c84343be6509b480013bf1eeb11b96c5f9eb48deff8f83638011f6b2c/soundfile-0.12.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d922be1563ce17a69582a352a86f28ed8c9f6a8bc951df63476ffc310c064bfa", size = 1213305, upload-time = "2023-02-15T15:37:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/71/87/31d2b9ed58975cec081858c01afaa3c43718eb0f62b5698a876d94739ad0/soundfile-0.12.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:bceaab5c4febb11ea0554566784bcf4bc2e3977b53946dda2b12804b4fe524a8", size = 1075977, upload-time = "2023-02-15T15:37:21.938Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/0602167a213d9184fc688b1086dc6d374b7ae8c33eccf169f9b50ce6568c/soundfile-0.12.1-py2.py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:2dc3685bed7187c072a46ab4ffddd38cef7de9ae5eb05c03df2ad569cf4dacbc", size = 1257765, upload-time = "2023-03-24T08:21:58.716Z" }, - { url = "https://files.pythonhosted.org/packages/c1/07/7591f4efd29e65071c3a61b53725036ea8f73366a4920a481ebddaf8d0ca/soundfile-0.12.1-py2.py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:074247b771a181859d2bc1f98b5ebf6d5153d2c397b86ee9e29ba602a8dfe2a6", size = 1174746, upload-time = "2023-02-15T15:37:24.771Z" }, - { url = "https://files.pythonhosted.org/packages/03/0f/49941ed8a2d94e5b36ea94346fb1d2b22e847fede902e05be4c96f26be7d/soundfile-0.12.1-py2.py3-none-win32.whl", hash = "sha256:59dfd88c79b48f441bbf6994142a19ab1de3b9bb7c12863402c2bc621e49091a", size = 888234, upload-time = "2023-02-15T15:37:27.078Z" }, - { url = "https://files.pythonhosted.org/packages/50/ff/26a4ee48d0b66625a4e4028a055b9f25bc9d7c7b2d17d21a45137621a50d/soundfile-0.12.1-py2.py3-none-win_amd64.whl", hash = "sha256:0d86924c00b62552b650ddd28af426e3ff2d4dc2e9047dae5b3d8452e0a49a77", size = 1009109, upload-time = "2023-02-15T15:37:29.41Z" }, -] - -[[package]] -name = "sse-starlette" -version = "3.3.4" -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" } -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" }, -] - -[[package]] -name = "starlette" -version = "1.0.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" } -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 = "tiktoken" version = "0.12.0" @@ -2910,27 +2138,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 = "tzlocal" -version = "5.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tzdata", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, -] - [[package]] name = "urllib3" version = "2.6.3" @@ -2940,45 +2147,6 @@ 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" }, ] -[[package]] -name = "uvicorn" -version = "0.44.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, -] - -[[package]] -name = "uvloop" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" }, - { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, - { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, - { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, - { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" }, - { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" }, - { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, -] - [[package]] name = "virtualenv" version = "21.1.0" @@ -2994,48 +2162,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, ] -[[package]] -name = "websockets" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, -] - [[package]] name = "yarl" version = "1.23.0"