aicordon-haystack 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aicordon_haystack-0.1.0/.gitignore +41 -0
- aicordon_haystack-0.1.0/CATALOG-CARD.md +133 -0
- aicordon_haystack-0.1.0/CHANGELOG.md +32 -0
- aicordon_haystack-0.1.0/LICENSE +202 -0
- aicordon_haystack-0.1.0/NOTES.md +100 -0
- aicordon_haystack-0.1.0/PKG-INFO +199 -0
- aicordon_haystack-0.1.0/README.md +177 -0
- aicordon_haystack-0.1.0/eval/costturn.py +175 -0
- aicordon_haystack-0.1.0/eval/measure.py +165 -0
- aicordon_haystack-0.1.0/eval/measure_dialog.py +178 -0
- aicordon_haystack-0.1.0/eval/result-costturn.json +74 -0
- aicordon_haystack-0.1.0/eval/result-dialog.json +12 -0
- aicordon_haystack-0.1.0/eval/result-redact-disclose.json +8014 -0
- aicordon_haystack-0.1.0/eval/result-redact-disclose_exfiltrate.json +9614 -0
- aicordon_haystack-0.1.0/eval/result-redact-exfiltrate.json +6414 -0
- aicordon_haystack-0.1.0/eval/result-redact-manipulate_hijack_task_exfiltrate_unauthorized_action.json +12014 -0
- aicordon_haystack-0.1.0/eval/result-redact.json +8014 -0
- aicordon_haystack-0.1.0/example/chat_pipeline.py +53 -0
- aicordon_haystack-0.1.0/example/indexing_pipeline.py +42 -0
- aicordon_haystack-0.1.0/pyproject.toml +37 -0
- aicordon_haystack-0.1.0/src/haystack_integrations/components/preprocessors/aicordon/__init__.py +3 -0
- aicordon_haystack-0.1.0/src/haystack_integrations/components/preprocessors/aicordon/prompt_injection_filter.py +78 -0
- aicordon_haystack-0.1.0/src/haystack_integrations/components/validators/aicordon/__init__.py +3 -0
- aicordon_haystack-0.1.0/src/haystack_integrations/components/validators/aicordon/prompt_injection_guard.py +114 -0
- aicordon_haystack-0.1.0/tests/test_prompt_injection_filter.py +95 -0
- aicordon_haystack-0.1.0/tests/test_prompt_injection_guard.py +141 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Secrets never live next to code. This line is a fuse, not a description of what is here.
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
.venv/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
|
|
12
|
+
# The injection bank and any datasets never travel: the bank is both a resource of the project and
|
|
13
|
+
# its only honest held-out test. Insurance against an accidental `git add`, not a precaution in the
|
|
14
|
+
# abstract — memory is not the thing to rely on here.
|
|
15
|
+
datasets/
|
|
16
|
+
data/pools/
|
|
17
|
+
*.jsonl
|
|
18
|
+
|
|
19
|
+
# Three exceptions, each of them ours end to end and rebuildable from the script beside it: the ten
|
|
20
|
+
# demonstration exchanges and the two ground-truth manifests. Without them a clone can neither read
|
|
21
|
+
# the example sets nor run `show_material.py` / `show_request.py` over them.
|
|
22
|
+
!integrations/examples/dialogues.jsonl
|
|
23
|
+
!integrations/examples/dialog_manifest.jsonl
|
|
24
|
+
!integrations/examples/manifest.jsonl
|
|
25
|
+
|
|
26
|
+
# The sources the base is built from stay out: the slot dictionaries and the rule file are at once
|
|
27
|
+
# the recipe for the base and the instructions for walking around it. The product does not claim its
|
|
28
|
+
# rules are readable; what ships is the built base, one file (`src/aicordon/picket/data/*.bin`).
|
|
29
|
+
src/aicordon/picket/data/slots/
|
|
30
|
+
src/aicordon/picket/data/*.json
|
|
31
|
+
|
|
32
|
+
# Research notes live in the research repository and do not travel. This line is not caution in the
|
|
33
|
+
# abstract: an append to RESULTS.md from this directory has already dropped fifty lines of
|
|
34
|
+
# measurement prose in here once. Better the mistake is caught by git than by eye.
|
|
35
|
+
RESULTS.md
|
|
36
|
+
HANDOFF.md
|
|
37
|
+
PLAN*.md
|
|
38
|
+
|
|
39
|
+
# Working notes and release plans: ours, not part of what ships. Same rule as the research
|
|
40
|
+
# notes above -- the repository holds the product, not the scaffolding around a release.
|
|
41
|
+
TODO/
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
STAGING COPY of the integration card for the Haystack catalogue.
|
|
3
|
+
|
|
4
|
+
When submitting: copy this file to `integrations/aicordon-haystack.md` in a fork of
|
|
5
|
+
`deepset-ai/haystack-integrations` and open a PR. Schema confirmed 2026-08-19 from that repo's
|
|
6
|
+
README:
|
|
7
|
+
- required frontmatter: layout, name, description, authors (>=1 with a name), type
|
|
8
|
+
- at least one of `pypi` or `repo` must be present for a merge
|
|
9
|
+
- allowed `type`: Document Store | Model Provider | Data Ingestion | Monitoring Tool |
|
|
10
|
+
Evaluation Framework | Custom Component | Tool Integration | Other
|
|
11
|
+
- one card per PACKAGE, not per component: `aicordon-haystack` ships two components under one card.
|
|
12
|
+
|
|
13
|
+
SUBMIT ONLY AFTER `aicordon-haystack` IS ON PyPI. Order:
|
|
14
|
+
1. merge the integration branch into `main` and push - the `repo` link and the README links in
|
|
15
|
+
this card resolve off `main`;
|
|
16
|
+
2. publish `aicordon` >= 1.1.0;
|
|
17
|
+
3. publish `aicordon-haystack`;
|
|
18
|
+
4. open this PR.
|
|
19
|
+
|
|
20
|
+
`logo` is optional and deliberately unset: it lives in the catalogue repository, so it is a separate
|
|
21
|
+
PR whenever we want one.
|
|
22
|
+
-->
|
|
23
|
+
---
|
|
24
|
+
layout: integration
|
|
25
|
+
name: AI Cordon Picket
|
|
26
|
+
description: Detect prompt injection in what an LLM is given - documents at ingest and the turn it answers - with a local rule base, no GPU or network.
|
|
27
|
+
authors:
|
|
28
|
+
- name: Mike Gribov
|
|
29
|
+
socials:
|
|
30
|
+
github: mihail-gribov
|
|
31
|
+
pypi: https://pypi.org/project/aicordon-haystack
|
|
32
|
+
repo: https://github.com/AICordon/aicordon
|
|
33
|
+
report_issue: https://github.com/AICordon/aicordon/issues
|
|
34
|
+
type: Custom Component
|
|
35
|
+
version: Haystack 3.0
|
|
36
|
+
toc: true
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# AI Cordon Picket for Haystack
|
|
40
|
+
|
|
41
|
+
Check what an LLM is given for prompt injection - in both places it can arrive.
|
|
42
|
+
|
|
43
|
+
| component | reads | with |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `PromptInjectionFilter` | **material**: documents at ingest, before they are chunked and embedded | Picket's `ipi` rules |
|
|
46
|
+
| `PromptInjectionGuard` | **the request**: the turn the model is about to answer | Picket's `dpi` rules |
|
|
47
|
+
|
|
48
|
+
The two rule sets are disjoint, and neither is a stricter version of the other - this is not a
|
|
49
|
+
sensitivity knob. Pick by role: material is what the model works on, the request is what it answers.
|
|
50
|
+
Your code knows which is which; it puts them in different places when it assembles the call.
|
|
51
|
+
|
|
52
|
+
The check is a rule, not a model: no GPU, no network, no key, a few hundred kilobytes of base, and a
|
|
53
|
+
fraction of a millisecond per turn on one core.
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install aicordon-haystack
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Material at ingest
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from haystack import Pipeline
|
|
65
|
+
from haystack.components.preprocessors import DocumentSplitter
|
|
66
|
+
from haystack.components.writers import DocumentWriter
|
|
67
|
+
from haystack_integrations.components.preprocessors.aicordon import PromptInjectionFilter
|
|
68
|
+
|
|
69
|
+
pipe = Pipeline()
|
|
70
|
+
pipe.add_component("ipi_filter", PromptInjectionFilter(mode="redact"))
|
|
71
|
+
pipe.add_component("splitter", DocumentSplitter(split_by="word", split_length=200))
|
|
72
|
+
pipe.add_component("writer", DocumentWriter(document_store=store))
|
|
73
|
+
|
|
74
|
+
pipe.connect("converter.documents", "ipi_filter.documents")
|
|
75
|
+
pipe.connect("ipi_filter.documents", "splitter.documents")
|
|
76
|
+
pipe.connect("splitter.documents", "writer.documents")
|
|
77
|
+
pipe.connect("ipi_filter.rejected", "quarantine.documents") # optional; nothing disappears quietly
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The component sits **before the splitter**: a cut here takes the injection out of the chunks, the
|
|
81
|
+
embeddings and the store at once, with no offsets to reconcile across chunk boundaries. Modes:
|
|
82
|
+
`annotate`, `blank` (keeps the length), `mask`, `redact` (default), `drop`, `fail`. The cut takes
|
|
83
|
+
the whole line holding the span, not the matched characters alone.
|
|
84
|
+
|
|
85
|
+
## The turn the model answers
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from haystack_integrations.components.validators.aicordon import PromptInjectionGuard
|
|
89
|
+
|
|
90
|
+
pipe.add_component("guard", PromptInjectionGuard()) # mode="drop" is the default
|
|
91
|
+
pipe.connect("prompt.messages", "guard.messages")
|
|
92
|
+
pipe.connect("guard.messages", "llm.messages") # the model is called on this path
|
|
93
|
+
pipe.connect("guard.blocked", "refusal.messages") # and not on this one
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Two sockets, one value.** On a flagged exchange `run` returns `blocked` and no `messages` key, so
|
|
97
|
+
the generator is not called at all. Connect `blocked` to whatever answers the user instead. The
|
|
98
|
+
decision is for the exchange, not for one message. This side never edits a turn: a typed jailbreak
|
|
99
|
+
is not spliced into anything - it *is* the turn.
|
|
100
|
+
|
|
101
|
+
## Measured
|
|
102
|
+
|
|
103
|
+
Not the detector's recall - that ships with the detector - but what the pipeline delivers with the
|
|
104
|
+
component and without.
|
|
105
|
+
|
|
106
|
+
**Material** - [Quadrat-IPI v1.0.1](https://huggingface.co/datasets/mihailgribov/quadrat-ipi),
|
|
107
|
+
1000 injected + 1000 clean documents, `mode="redact"`:
|
|
108
|
+
|
|
109
|
+
| | whole corpus | injections that ask the model to reveal something |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| payload reaches the store intact, without the filter | 100% | 100% |
|
|
112
|
+
| payload reaches the store intact, with it | **85.4%** | **42.8%** |
|
|
113
|
+
| payload gone without a trace | 13.1% | **52.3%** |
|
|
114
|
+
| clean documents dropped or trimmed | 0 of 1000 | 0 of 1000 |
|
|
115
|
+
|
|
116
|
+
**The request** - held-out forum jailbreaks from
|
|
117
|
+
[TrustAIRLab in-the-wild](https://huggingface.co/datasets/TrustAIRLab/in-the-wild-jailbreak-prompts)
|
|
118
|
+
(537) against 20 000 real [WildChat](https://huggingface.co/datasets/allenai/WildChat-1M) turns,
|
|
119
|
+
`mode="drop"`:
|
|
120
|
+
|
|
121
|
+
| | |
|
|
122
|
+
|---|---|
|
|
123
|
+
| attacks reaching the model, without the guard | 100% (537 of 537) |
|
|
124
|
+
| attacks reaching the model, with it | **65.2%** |
|
|
125
|
+
| turns not answered, out of 20 000 real ones | 0.070% (14) |
|
|
126
|
+
| verdicts differing from the bare detector | **0** |
|
|
127
|
+
|
|
128
|
+
Adding the component costs **0.39 ms for a turn of median length**; loading the base costs 15 ms,
|
|
129
|
+
once per process. No findings does not mean no injection.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
Apache-2.0, the same as the detector it wraps.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-08-21
|
|
4
|
+
|
|
5
|
+
First release. Two components, one per place a pipeline holds text a model is about to read.
|
|
6
|
+
|
|
7
|
+
* **`PromptInjectionFilter`** — `haystack_integrations.components.preprocessors.aicordon`. Reads
|
|
8
|
+
documents at ingest with the `ipi` rules, before chunking and embedding. Six modes (`annotate`,
|
|
9
|
+
`blank`, `mask`, `redact`, `drop`, `fail`); the cut takes the whole line holding the span, or the
|
|
10
|
+
sentence when the line is long. Two output sockets, so a dropped document goes to quarantine
|
|
11
|
+
instead of disappearing.
|
|
12
|
+
* **`PromptInjectionGuard`** — `haystack_integrations.components.validators.aicordon`. Reads the
|
|
13
|
+
message list on its way into the generator with the `dpi` rules. The decision is for the whole
|
|
14
|
+
exchange: when it fires, `run` returns `blocked` and **no** `messages` key, which is how a
|
|
15
|
+
pipeline branches — the generator is not called at all. This side never edits a turn.
|
|
16
|
+
|
|
17
|
+
Both are thin; the policy lives in `aicordon.guard`, which ships with the detector. Hence
|
|
18
|
+
`aicordon>=1.1.0`: against 1.0.0 the install resolves and the import fails.
|
|
19
|
+
|
|
20
|
+
Measured on the pipeline, not on the detector:
|
|
21
|
+
|
|
22
|
+
| | |
|
|
23
|
+
|---|---|
|
|
24
|
+
| injected payload reaching the store, `mode="redact"` | 85.4% with the filter, 100% without |
|
|
25
|
+
| clean documents damaged | 0 of 1000 |
|
|
26
|
+
| held-out forum jailbreaks reaching the generator, `mode="drop"` | 65.2% with the guard, 100% without |
|
|
27
|
+
| real WildChat turns left unanswered | 0.070% (14 of 20 000), an upper bound |
|
|
28
|
+
| **verdicts differing from the bare detector** | **0** |
|
|
29
|
+
| cost per turn of median length | 0.39 ms; 15 ms once, to load the base |
|
|
30
|
+
|
|
31
|
+
The last row licenses the others: a wrapper that loses or adds text makes every number above it
|
|
32
|
+
meaningless.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 Mikhail Gribov
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Haystack: the contract, established from the sources
|
|
2
|
+
|
|
3
|
+
Checked 2026-08-13. The clone in `upstream/` at `ba92ec9` is **3.1.0-rc0**, i.e. the development
|
|
4
|
+
branch; the last RELEASED version is **v3.0.0** (2026-07-20). The wrapper is written against 3.0.x,
|
|
5
|
+
and the clone is what tells us whether the contract is drifting by 3.1.
|
|
6
|
+
|
|
7
|
+
Repository: `deepset-ai/haystack`, Apache-2.0, 26 201 stars. The catalogue of third-party
|
|
8
|
+
integrations is a separate repository, `deepset-ai/haystack-integrations` (a card via PR); what
|
|
9
|
+
deepset maintains itself lives in `haystack-core-integrations`.
|
|
10
|
+
|
|
11
|
+
## What a component is obliged to do
|
|
12
|
+
|
|
13
|
+
Source of truth: the docstring of `haystack/core/component/component.py`, which says it outright.
|
|
14
|
+
|
|
15
|
+
* The class is marked `@component`.
|
|
16
|
+
* **`run()`** is mandatory; output types are declared with `@component.output_types(...)`, and a
|
|
17
|
+
`dict` with those same keys is returned.
|
|
18
|
+
* **`__init__` must be cheap** — it is called when the pipeline is assembled and validated. Heavy
|
|
19
|
+
initialisation goes into the optional `warm_up()`, which the pipeline calls before a run.
|
|
20
|
+
* **`__init__` parameters must be primitives** (strings, numbers, lists and dicts of them). Objects
|
|
21
|
+
and functions are forbidden: parameters must be JSON-serialisable or the pipeline cannot be saved
|
|
22
|
+
or loaded. If an object is needed, take its import path as a string and resolve it inside.
|
|
23
|
+
* **Do not modify the input in place**: work on a copy and return that (`dataclasses.replace` for a
|
|
24
|
+
single field, `deepcopy` for anything richer). Otherwise the edit leaks into other branches of the
|
|
25
|
+
pipeline.
|
|
26
|
+
|
|
27
|
+
## What follows for us
|
|
28
|
+
|
|
29
|
+
1. The detector is heavy state (a 376 KB base, ~17 ms to load): raise it in `warm_up()`, not in
|
|
30
|
+
`__init__`. Otherwise the cost is paid on every pipeline assembly, validation included.
|
|
31
|
+
2. Pass only strings into `__init__`: the mode (`redact` / `drop` / `annotate`), the metadata
|
|
32
|
+
prefix. No "hand the detector object in here" — that breaks saving the pipeline.
|
|
33
|
+
3. Copy documents before editing their text: in an indexing pipeline the same list may go into a
|
|
34
|
+
second branch.
|
|
35
|
+
4. Give the component two outputs: the clean stream and the rejected one. Connections are named, so
|
|
36
|
+
the rejected pile goes to its own store in one `connect` line instead of vanishing quietly.
|
|
37
|
+
|
|
38
|
+
## Where we sit
|
|
39
|
+
|
|
40
|
+
Two places, one per role the text plays in the prompt.
|
|
41
|
+
|
|
42
|
+
**Material** — the indexing pipeline: `converter → (our component) → cleaner → splitter → embedder →
|
|
43
|
+
writer`. BEFORE the splitter: what is cut then reaches neither the embeddings nor the store, and no
|
|
44
|
+
chunk boundaries need stitching.
|
|
45
|
+
|
|
46
|
+
**The request** — the chat pipeline: `prompt builder → (our component) → chat generator`, with a
|
|
47
|
+
second wire `blocked → whoever answers instead of the model`. RIGHT NEXT to the generator, on the
|
|
48
|
+
message list about to enter it: anything between the check and the call is one more place the text
|
|
49
|
+
could change.
|
|
50
|
+
|
|
51
|
+
## Traps the mock-up caught (2026-08-13)
|
|
52
|
+
|
|
53
|
+
1. **Settings are lost silently when a pipeline is saved.** Without a `to_dict` of its own, Haystack
|
|
54
|
+
restores parameters through `getattr(obj, "<parameter name>")`, and where the attribute is
|
|
55
|
+
missing it **substitutes the default from the signature** and says nothing. A component built
|
|
56
|
+
with `mode="annotate"` came back out of YAML as `redact`. The cure: keep the parameters on the
|
|
57
|
+
object under those same names AND declare `to_dict`/`from_dict` via
|
|
58
|
+
`default_to_dict`/`default_from_dict`. Covered by a YAML round trip in the tests.
|
|
59
|
+
2. **The order of chunks out of a store is not guaranteed**, and `split_overlap` repeats the tail of
|
|
60
|
+
the previous chunk. A measurement that glued chunks back together reported 92.5% where the answer
|
|
61
|
+
is 100% because of it. The acceptance measurement now has no splitter in it at all: it stands
|
|
62
|
+
AFTER us and cannot affect the result.
|
|
63
|
+
3. **Logging is not a mode.** It is wanted under `drop` as much as under `annotate`, so it goes
|
|
64
|
+
through `haystack.logging` at `warning` level rather than being a value of the parameter.
|
|
65
|
+
|
|
66
|
+
## What carries over to LlamaIndex or LangChain
|
|
67
|
+
|
|
68
|
+
The policy (`aicordon.guard`) carries over as it is — modes, cut boundaries, metadata. A wrapper is
|
|
69
|
+
the host's document type translated into a string and back, plus the host's contract: for LlamaIndex
|
|
70
|
+
`TransformComponent.__call__(nodes)`, for LangChain `BaseDocumentTransformer.transform_documents`.
|
|
71
|
+
Check their serialisation separately — Haystack is not the only framework that loses a mode without
|
|
72
|
+
a word.
|
|
73
|
+
|
|
74
|
+
## The request side: contract and traps (2026-08-18)
|
|
75
|
+
|
|
76
|
+
The second component is `PromptInjectionGuard`, in
|
|
77
|
+
`haystack_integrations.components.validators.aicordon`. Category `validators` rather than
|
|
78
|
+
`preprocessors`: it prepares nothing, it decides whether to call the model.
|
|
79
|
+
|
|
80
|
+
1. **A branch in Haystack is an ABSENT key in the returned dict, not an empty list.** The receiver
|
|
81
|
+
gets `_NoOutputProduced` (`core/pipeline/component_checks.py`) and does not run at all. Returning
|
|
82
|
+
`{"messages": [], "blocked": [...]}` means calling the generator with an empty list. So `run`
|
|
83
|
+
returns exactly one of the two keys. Covered by a test with a control: on a clean turn the next
|
|
84
|
+
component IS in the pipeline output, on a flagged one it is not.
|
|
85
|
+
2. **`ChatMessage.text` is the FIRST text part, not the whole message.** In a message with an image
|
|
86
|
+
the parts come as a list, and an attack in the second text part is invisible to `text` — with no
|
|
87
|
+
error of any kind. We read `texts` and join them. Covered by a test.
|
|
88
|
+
3. **The text of a tool result is NOT in `texts`.** For role `tool` the content sits in
|
|
89
|
+
`tool_call_result.result` and `texts` is empty. A wrapper reading only `texts` would check the
|
|
90
|
+
role against an empty string and write "read, clean", which is worse than a crash. We read `texts`
|
|
91
|
+
plus the call results; the calls themselves (`tool_calls`) we do not, they are the model's output
|
|
92
|
+
rather than what it was given. Covered by a test.
|
|
93
|
+
4. **Edit a message only through a copy**: `ChatMessage` carries `@_warn_on_inplace_mutation`, and
|
|
94
|
+
the same list may go into a second branch of the pipeline. The copy is
|
|
95
|
+
`dataclasses.replace(msg, _meta=...)` (the underscored dataclass fields are their real names in
|
|
96
|
+
`__init__`).
|
|
97
|
+
5. **The same serialisation trap as in the filter**, now with a dict parameter: `roles` has to sit
|
|
98
|
+
on the object under its own name and be listed in `to_dict`. Covered by a YAML round trip.
|
|
99
|
+
6. **The decision is for the exchange, not for a message.** Dropping the flagged turn and calling
|
|
100
|
+
the model with the rest is not allowed: the model then answers the message before it.
|