lexguard 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.
- lexguard-0.1.0/.github/workflows/ci.yml +117 -0
- lexguard-0.1.0/.gitignore +62 -0
- lexguard-0.1.0/.pre-commit-config.yaml +87 -0
- lexguard-0.1.0/.skills/lexguard/SKILL.md +88 -0
- lexguard-0.1.0/PKG-INFO +7 -0
- lexguard-0.1.0/README.md +127 -0
- lexguard-0.1.0/docs/agents.md +112 -0
- lexguard-0.1.0/docs/index.md +1 -0
- lexguard-0.1.0/docs/rules.md +149 -0
- lexguard-0.1.0/docs/writing-a-lexicon.md +111 -0
- lexguard-0.1.0/examples/usage.py +255 -0
- lexguard-0.1.0/llms.txt +25 -0
- lexguard-0.1.0/pyproject.toml +114 -0
- lexguard-0.1.0/src/lexguard/__init__.py +20 -0
- lexguard-0.1.0/src/lexguard/lexicon.py +217 -0
- lexguard-0.1.0/src/lexguard/rule.py +134 -0
- lexguard-0.1.0/src/lexguard/suites.py +37 -0
- lexguard-0.1.0/src/lexguard/words/__init__.py +25 -0
- lexguard-0.1.0/src/lexguard/words/domain.py +370 -0
- lexguard-0.1.0/src/lexguard/words/instruction.py +473 -0
- lexguard-0.1.0/src/lexguard/words/request.py +1062 -0
- lexguard-0.1.0/src/lexguard/words/response.py +536 -0
- lexguard-0.1.0/tests/test_docs.py +17 -0
- lexguard-0.1.0/tests/test_exports.py +16 -0
- lexguard-0.1.0/tests/test_generic.py +101 -0
- lexguard-0.1.0/tests/test_usage.py +182 -0
- lexguard-0.1.0/uv.lock +1221 -0
- lexguard-0.1.0/zensical.toml +22 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
name: CI and Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v5
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
run: uv python install 3.12
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: uv sync --all-extras --frozen
|
|
23
|
+
|
|
24
|
+
- name: Lint with ruff
|
|
25
|
+
run: uv run ruff check .
|
|
26
|
+
|
|
27
|
+
- name: Format check with ruff
|
|
28
|
+
run: uv run ruff format --check .
|
|
29
|
+
|
|
30
|
+
- name: Type check with basedpyright
|
|
31
|
+
run: uv run basedpyright
|
|
32
|
+
|
|
33
|
+
- name: Run unit tests (parallel)
|
|
34
|
+
run: uv run pytest -m unit -n auto --cov=src/lexguard --cov-report=term-missing
|
|
35
|
+
|
|
36
|
+
- name: Run integration tests (sequential)
|
|
37
|
+
run: uv run pytest -m integration --cov=src/lexguard --cov-append --cov-report=term-missing
|
|
38
|
+
|
|
39
|
+
publish:
|
|
40
|
+
needs: test
|
|
41
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
environment:
|
|
44
|
+
name: pypi
|
|
45
|
+
url: https://pypi.org/p/lexguard
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write
|
|
48
|
+
contents: write
|
|
49
|
+
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v4
|
|
52
|
+
with:
|
|
53
|
+
fetch-depth: 0
|
|
54
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
55
|
+
- name: Install uv
|
|
56
|
+
uses: astral-sh/setup-uv@v5
|
|
57
|
+
|
|
58
|
+
- name: Set up Python
|
|
59
|
+
run: uv python install 3.12
|
|
60
|
+
|
|
61
|
+
- name: Install dependencies
|
|
62
|
+
run: uv sync --all-extras --frozen
|
|
63
|
+
|
|
64
|
+
- name: Configure git
|
|
65
|
+
run: |
|
|
66
|
+
git config user.name "github-actions[bot]"
|
|
67
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
68
|
+
|
|
69
|
+
- name: Bump version
|
|
70
|
+
run: uv run bump-my-version bump patch
|
|
71
|
+
|
|
72
|
+
- name: Push version bump
|
|
73
|
+
run: git push --follow-tags
|
|
74
|
+
|
|
75
|
+
- name: Build package
|
|
76
|
+
run: uv build
|
|
77
|
+
|
|
78
|
+
- name: Publish to PyPI
|
|
79
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
80
|
+
|
|
81
|
+
docs:
|
|
82
|
+
needs: test
|
|
83
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
permissions:
|
|
86
|
+
contents: read
|
|
87
|
+
pages: write
|
|
88
|
+
id-token: write
|
|
89
|
+
environment:
|
|
90
|
+
name: github-pages
|
|
91
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
92
|
+
steps:
|
|
93
|
+
- uses: actions/checkout@v4
|
|
94
|
+
|
|
95
|
+
- name: Install uv
|
|
96
|
+
uses: astral-sh/setup-uv@v5
|
|
97
|
+
|
|
98
|
+
- name: Set up Python
|
|
99
|
+
run: uv python install 3.12
|
|
100
|
+
|
|
101
|
+
- name: Install dependencies
|
|
102
|
+
run: uv sync --all-extras --frozen
|
|
103
|
+
|
|
104
|
+
- name: Build docs
|
|
105
|
+
run: uv run zensical build
|
|
106
|
+
|
|
107
|
+
- name: Setup Pages
|
|
108
|
+
uses: actions/configure-pages@v5
|
|
109
|
+
|
|
110
|
+
- name: Upload artifact
|
|
111
|
+
uses: actions/upload-pages-artifact@v3
|
|
112
|
+
with:
|
|
113
|
+
path: site
|
|
114
|
+
|
|
115
|
+
- name: Deploy to GitHub Pages
|
|
116
|
+
id: deployment
|
|
117
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
.venv
|
|
26
|
+
venv/
|
|
27
|
+
ENV/
|
|
28
|
+
env/
|
|
29
|
+
|
|
30
|
+
# IDEs
|
|
31
|
+
.vscode/
|
|
32
|
+
.idea/
|
|
33
|
+
*.swp
|
|
34
|
+
*.swo
|
|
35
|
+
*~
|
|
36
|
+
|
|
37
|
+
# Testing
|
|
38
|
+
.pytest_cache/
|
|
39
|
+
.coverage
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
|
|
43
|
+
# Type checking
|
|
44
|
+
.mypy_cache/
|
|
45
|
+
.pytype/
|
|
46
|
+
.pyre/
|
|
47
|
+
.basedpyright/
|
|
48
|
+
|
|
49
|
+
# Ruff
|
|
50
|
+
.ruff_cache/
|
|
51
|
+
|
|
52
|
+
# Docs
|
|
53
|
+
site/
|
|
54
|
+
|
|
55
|
+
# OS
|
|
56
|
+
.DS_Store
|
|
57
|
+
Thumbs.db
|
|
58
|
+
|
|
59
|
+
# Project specific
|
|
60
|
+
*.log
|
|
61
|
+
.env
|
|
62
|
+
.env.local
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
default_language_version:
|
|
2
|
+
python: python3
|
|
3
|
+
default_stages: [pre-commit]
|
|
4
|
+
repos:
|
|
5
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
6
|
+
rev: v6.0.0
|
|
7
|
+
hooks:
|
|
8
|
+
- id: trailing-whitespace
|
|
9
|
+
args: [--markdown-linebreak-ext=md]
|
|
10
|
+
- id: end-of-file-fixer
|
|
11
|
+
- id: mixed-line-ending
|
|
12
|
+
args: [--fix=lf]
|
|
13
|
+
- id: check-yaml
|
|
14
|
+
args: [--unsafe]
|
|
15
|
+
- id: check-json
|
|
16
|
+
- id: check-toml
|
|
17
|
+
- id: check-merge-conflict
|
|
18
|
+
- id: check-case-conflict
|
|
19
|
+
- id: check-ast
|
|
20
|
+
- id: check-docstring-first
|
|
21
|
+
- id: debug-statements
|
|
22
|
+
- id: name-tests-test
|
|
23
|
+
args: [--pytest-test-first]
|
|
24
|
+
- id: detect-private-key
|
|
25
|
+
- id: check-added-large-files
|
|
26
|
+
args: [--maxkb=1000]
|
|
27
|
+
|
|
28
|
+
- repo: https://github.com/igorshubovych/markdownlint-cli
|
|
29
|
+
rev: v0.47.0
|
|
30
|
+
hooks:
|
|
31
|
+
- id: markdownlint
|
|
32
|
+
args: [--fix, --disable, MD013, MD024, MD033, MD036, MD040, MD041, MD060, --]
|
|
33
|
+
|
|
34
|
+
- repo: https://github.com/commitizen-tools/commitizen
|
|
35
|
+
rev: v4.12.1
|
|
36
|
+
hooks:
|
|
37
|
+
- id: commitizen
|
|
38
|
+
stages: [commit-msg]
|
|
39
|
+
|
|
40
|
+
- repo: https://github.com/benomahony/nasa-lsp
|
|
41
|
+
rev: v0.1.7
|
|
42
|
+
hooks:
|
|
43
|
+
- id: nasa-lsp
|
|
44
|
+
exclude: ^tests/
|
|
45
|
+
|
|
46
|
+
- repo: local
|
|
47
|
+
hooks:
|
|
48
|
+
- id: ruff-format
|
|
49
|
+
name: ruff-format
|
|
50
|
+
entry: uv run ruff format
|
|
51
|
+
language: system
|
|
52
|
+
types_or: [python, pyi]
|
|
53
|
+
require_serial: true
|
|
54
|
+
- id: ruff
|
|
55
|
+
name: ruff
|
|
56
|
+
entry: uv run ruff check --fix --exit-non-zero-on-fix
|
|
57
|
+
language: system
|
|
58
|
+
types_or: [python, pyi]
|
|
59
|
+
require_serial: true
|
|
60
|
+
- id: basedpyright
|
|
61
|
+
name: basedpyright
|
|
62
|
+
entry: uv run basedpyright
|
|
63
|
+
language: system
|
|
64
|
+
types: [python]
|
|
65
|
+
pass_filenames: true
|
|
66
|
+
require_serial: true
|
|
67
|
+
- id: vulture
|
|
68
|
+
name: vulture
|
|
69
|
+
entry: uv run vulture src/lexguard/ --min-confidence 80
|
|
70
|
+
language: system
|
|
71
|
+
types: [python]
|
|
72
|
+
files: ^src/lexguard/
|
|
73
|
+
pass_filenames: false
|
|
74
|
+
require_serial: true
|
|
75
|
+
- id: bandit
|
|
76
|
+
name: bandit
|
|
77
|
+
entry: uv run bandit -c pyproject.toml -r src/lexguard/
|
|
78
|
+
language: system
|
|
79
|
+
files: ^src/lexguard/
|
|
80
|
+
exclude: ^tests/
|
|
81
|
+
pass_filenames: false
|
|
82
|
+
- id: deptry
|
|
83
|
+
name: deptry
|
|
84
|
+
entry: uv run deptry . --extend-exclude "\.venv|tests"
|
|
85
|
+
language: system
|
|
86
|
+
pass_filenames: false
|
|
87
|
+
require_serial: true
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lexguard
|
|
3
|
+
description: Help users work with lexguard. Use when the user asks about lexguard features, usage, or wants to check text (agent output, user requests, structured fields) for a concept like slop, sycophancy, disclaimers, or confidentiality — with or without pydantic-evals.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# lexguard Skill
|
|
7
|
+
|
|
8
|
+
lexguard is a library of lexicons: named sets of words and phrases that signal a concept, plus the
|
|
9
|
+
words that rule it out. Ninety one ship in the box, covering both what a user asked for and what a
|
|
10
|
+
model produced. It is **not only** a pydantic-evals add-on — the core matching API is plain
|
|
11
|
+
functions over a string, useful anywhere you want to check text, and pydantic-evals evaluators are
|
|
12
|
+
one additional way to run the same checks.
|
|
13
|
+
|
|
14
|
+
## When to Use This Skill
|
|
15
|
+
|
|
16
|
+
Use this skill when:
|
|
17
|
+
|
|
18
|
+
- The user wants to detect slop, sycophancy, hedging, disclaimers, confidentiality leaks, or
|
|
19
|
+
similar patterns in text (agent output or user requests)
|
|
20
|
+
- The user wants a guardrail, unit-test assertion, or log filter that fires on specific wording,
|
|
21
|
+
with no evals framework involved
|
|
22
|
+
- The user is writing pydantic-evals `Dataset`/`Case` evaluations and wants lexguard's evaluators
|
|
23
|
+
alongside their own
|
|
24
|
+
- The user wants to define a lexicon for their own domain (`Lexicon(...)`, `.extend()`, `|` to
|
|
25
|
+
group lexicons)
|
|
26
|
+
- The user asks about lexguard's API, guards (`when`/`unless`), or scoping to a field
|
|
27
|
+
(`field="notes"`, `field="items[]"`)
|
|
28
|
+
|
|
29
|
+
## Two ways to run a lexicon
|
|
30
|
+
|
|
31
|
+
**Standalone** — call `.signal()`, `.fires()`, or `.denied()` directly on a string. No `Dataset`,
|
|
32
|
+
no `Case`, nothing from pydantic-evals needs to run:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from lexguard import Confidential
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def guard(reply: str) -> str:
|
|
39
|
+
if Confidential.fires(reply):
|
|
40
|
+
raise ValueError("reply leaks a secret, blocking send")
|
|
41
|
+
return reply
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Inside pydantic-evals** — `.absent()` / `.expected()` compile the same lexicon into an
|
|
45
|
+
`Evaluator`, for use in a `Dataset`:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from pydantic_evals import Case, Dataset
|
|
49
|
+
from lexguard import Servility, Slop
|
|
50
|
+
|
|
51
|
+
dataset = Dataset(
|
|
52
|
+
name="prose",
|
|
53
|
+
cases=[Case(name="explainer", inputs="explain database indexing")],
|
|
54
|
+
evaluators=[Slop.absent(), Servility.absent()],
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Core API
|
|
59
|
+
|
|
60
|
+
- `Lexicon.signal(text) -> Signal` — `present`, `denied`, or `absent` (three-valued, not boolean)
|
|
61
|
+
- `Lexicon.fires(text) -> bool` — shorthand for `signal(text) is Signal.present`
|
|
62
|
+
- `Lexicon.denied(text) -> bool` — shorthand for `signal(text) is Signal.denied`
|
|
63
|
+
- `Lexicon.hits(text)` / `Lexicon.spans(text)` — the matched terms and their positions
|
|
64
|
+
- `Lexicon.examples(count=4)` — sample indicator phrases, for error messages
|
|
65
|
+
- `Lexicon.extend(indicates=..., rules_out=..., fix=...)` — a new lexicon layered on an existing one
|
|
66
|
+
- `lexicon_a | lexicon_b` — a `Bundle`; `.signals(text)` reports each member separately
|
|
67
|
+
- `Lexicon.absent(when=..., unless=..., field=...)` / `.expected(...)` — build a pydantic-evals
|
|
68
|
+
`Rule` (an assertion)
|
|
69
|
+
- `Observe([lexicons])` — a pydantic-evals evaluator that records labels instead of assertions, for
|
|
70
|
+
measuring before enforcing
|
|
71
|
+
|
|
72
|
+
## Prebuilt suites (`lexguard.suites`)
|
|
73
|
+
|
|
74
|
+
- `Bloat`, `Servility`, `Leakage`, `Overreach` — `Bundle`s grouping related output lexicons
|
|
75
|
+
- `PROSE` — the four bundles above, as a list of evaluators
|
|
76
|
+
- `ADHERENCE` — guarded checks that the output honored what the request asked for
|
|
77
|
+
- `GENERIC` — `PROSE + ADHERENCE`, a reasonable default suite
|
|
78
|
+
|
|
79
|
+
## Resources
|
|
80
|
+
|
|
81
|
+
- [README.md](../../README.md) — install, the core API, both standalone and pydantic-evals usage
|
|
82
|
+
- [docs/writing-a-lexicon.md](../../docs/writing-a-lexicon.md) — building a `Lexicon` for your own
|
|
83
|
+
domain, precision-over-recall, and `rules_out`
|
|
84
|
+
- [docs/rules.md](../../docs/rules.md) — `.absent()`/`.expected()`, `when`/`unless` guards, field
|
|
85
|
+
scoping, `Observe`
|
|
86
|
+
- [docs/agents.md](../../docs/agents.md) — checking pydantic-ai agent output, plain or structured
|
|
87
|
+
- `src/lexguard/words/` — the shipped lexicons, grouped by `domain`, `instruction`, `request`,
|
|
88
|
+
`response`
|
lexguard-0.1.0/PKG-INFO
ADDED
lexguard-0.1.0/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# lexguard
|
|
2
|
+
|
|
3
|
+
Lexicons that score text for a concept, plus the [pydantic-evals](https://ai.pydantic.dev/evals/)
|
|
4
|
+
evaluators to run them as part of a `Dataset`.
|
|
5
|
+
|
|
6
|
+
A lexicon is a named set of words and phrases that signal a concept, plus the words that rule it out.
|
|
7
|
+
Ninety one of them ship in the box, covering what a user asked for and what a model produced.
|
|
8
|
+
`.signal()`, `.fires()` and `.denied()` are plain functions over text: call them directly in a
|
|
9
|
+
guardrail, a test, a CLI, or a log pipeline, no evals framework required. `.absent()` and
|
|
10
|
+
`.expected()` compile the same lexicon into a pydantic-evals evaluator when you want it running
|
|
11
|
+
inside a `Dataset`.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv add lexguard
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## The idea
|
|
20
|
+
|
|
21
|
+
A lexicon on its own only observes. Naming a polarity turns it into a verdict.
|
|
22
|
+
|
|
23
|
+
```py
|
|
24
|
+
from lexguard import Slop
|
|
25
|
+
|
|
26
|
+
print(Slop.signal("let us delve into the intricate tapestry"))
|
|
27
|
+
#> present
|
|
28
|
+
print(Slop.signal("caching skips repeated work"))
|
|
29
|
+
#> absent
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Lexicons are three valued, not two. `rules_out` exists so that wording which merely shares
|
|
33
|
+
vocabulary with a concept does not count as the concept.
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
from lexguard import HighPriority, Recurrence
|
|
37
|
+
|
|
38
|
+
print(Recurrence.signal("bin day every tuesday"))
|
|
39
|
+
#> present
|
|
40
|
+
print(Recurrence.signal("i do that every so often"))
|
|
41
|
+
#> denied
|
|
42
|
+
print(HighPriority.signal("sort it whenever, no rush"))
|
|
43
|
+
#> denied
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`denied` is not the same as `absent`. An agent setting `priority=high` on "no rush" is wrong,
|
|
47
|
+
where on a sentence with no priority wording at all it is merely unasked for.
|
|
48
|
+
|
|
49
|
+
## Using it without an evals framework
|
|
50
|
+
|
|
51
|
+
`signal()`, `fires()` and `denied()` are the whole API surface at this layer: plain calls over a
|
|
52
|
+
string. Nothing here needs `Dataset`, `Case`, or pydantic-evals in general, so a lexicon works just
|
|
53
|
+
as well as a guardrail before a response goes out, a plain `assert` in a unit test, or a filter in
|
|
54
|
+
a log pipeline.
|
|
55
|
+
|
|
56
|
+
```py
|
|
57
|
+
from lexguard import Confidential
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def guard(reply: str) -> str:
|
|
61
|
+
if Confidential.fires(reply):
|
|
62
|
+
raise ValueError("reply leaks a secret, blocking send")
|
|
63
|
+
return reply
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Running it inside pydantic-evals
|
|
67
|
+
|
|
68
|
+
`.absent()` and `.expected()` turn a lexicon into an evaluator, for when you want the same check
|
|
69
|
+
running as part of a `Dataset` alongside everything else.
|
|
70
|
+
|
|
71
|
+
```py
|
|
72
|
+
from pydantic_evals import Case, Dataset
|
|
73
|
+
|
|
74
|
+
from lexguard import Servility, Slop
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def agent(prompt: str) -> str:
|
|
78
|
+
return "Great question! Let us delve in. Hope this helps!"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
dataset = Dataset(
|
|
82
|
+
name="prose",
|
|
83
|
+
cases=[Case(name="explainer", inputs="explain database indexing")],
|
|
84
|
+
evaluators=[Slop.absent(), Servility.absent()],
|
|
85
|
+
)
|
|
86
|
+
report = dataset.evaluate_sync(agent)
|
|
87
|
+
print(sorted(name for name, result in report.cases[0].assertions.items() if not result.value))
|
|
88
|
+
#> ['no_postamble', 'no_slop', 'no_sycophancy']
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Failures tell you what to change
|
|
92
|
+
|
|
93
|
+
```py
|
|
94
|
+
from pydantic_evals import Case, Dataset
|
|
95
|
+
|
|
96
|
+
from lexguard import Slop
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def agent(prompt: str) -> str:
|
|
100
|
+
return "Let us delve into the intricate tapestry of indexing."
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
report = Dataset(
|
|
104
|
+
name="prose", cases=[Case(inputs="explain indexing")], evaluators=[Slop.absent()]
|
|
105
|
+
).evaluate_sync(agent)
|
|
106
|
+
print(report.cases[0].assertions["no_slop"].reason)
|
|
107
|
+
"""
|
|
108
|
+
3 slop matches: "delve", "intricate", "tapestry"
|
|
109
|
+
delve -> Let us delve into the intricate tapestry of in…
|
|
110
|
+
intricate -> Let us delve into the intricate tapestry of indexing.
|
|
111
|
+
fix: swap for a plain verb or noun, or add these to the sampler ban list
|
|
112
|
+
"""
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Docs
|
|
116
|
+
|
|
117
|
+
- [Writing a lexicon](docs/writing-a-lexicon.md) for your own domain
|
|
118
|
+
- [Rules](docs/rules.md) and the `when` / `unless` guards
|
|
119
|
+
- [Agents](docs/agents.md) under test with pydantic-ai
|
|
120
|
+
|
|
121
|
+
## Prior art
|
|
122
|
+
|
|
123
|
+
The slop word lists overlap heavily with
|
|
124
|
+
[slop-forensics](https://github.com/sam-paech/slop-forensics), which derives them statistically
|
|
125
|
+
rather than by hand. Point `Lexicon.extend()` at that list if you want the empirical version.
|
|
126
|
+
The abstain semantics, where a lexicon that does not apply records nothing rather than a free pass,
|
|
127
|
+
is the same idea as a Snorkel labelling function returning `None`.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Agents
|
|
2
|
+
|
|
3
|
+
Nothing here is specific to pydantic-ai. A `Dataset` task is any callable, so the rule is checking
|
|
4
|
+
whatever your task returns. `TestModel` just makes the examples run without a key.
|
|
5
|
+
|
|
6
|
+
```py
|
|
7
|
+
from pydantic_ai import Agent
|
|
8
|
+
from pydantic_ai.models.test import TestModel
|
|
9
|
+
from pydantic_evals import Case, Dataset
|
|
10
|
+
|
|
11
|
+
from lexguard import Bloat, Servility
|
|
12
|
+
|
|
13
|
+
agent = Agent(TestModel(custom_output_text="Great question! Let us delve in. Hope this helps!"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def task(prompt: str) -> str:
|
|
17
|
+
return (await agent.run(prompt)).output
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
report = Dataset(
|
|
21
|
+
name="prose",
|
|
22
|
+
cases=[Case(name="explainer", inputs="explain database indexing")],
|
|
23
|
+
evaluators=[Bloat.absent(), Servility.absent()],
|
|
24
|
+
).evaluate_sync(task)
|
|
25
|
+
print(sorted(name for name, result in report.cases[0].assertions.items() if not result.value))
|
|
26
|
+
#> ['no_postamble', 'no_slop', 'no_sycophancy']
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The task unwraps `.output` because a rule stringifies whatever it is handed, and an
|
|
30
|
+
`AgentRunResult` repr would match on its own wrapper. The alternative is passing `agent.run`
|
|
31
|
+
straight in and putting `field="output"` on every rule.
|
|
32
|
+
|
|
33
|
+
## Structured output
|
|
34
|
+
|
|
35
|
+
`custom_output_args` builds the output tool call, so the agent does not retry against a text part
|
|
36
|
+
it cannot parse.
|
|
37
|
+
|
|
38
|
+
```py
|
|
39
|
+
from pydantic import BaseModel
|
|
40
|
+
from pydantic_ai import Agent
|
|
41
|
+
from pydantic_ai.models.test import TestModel
|
|
42
|
+
from pydantic_evals import Case, Dataset
|
|
43
|
+
|
|
44
|
+
from lexguard import Confidential
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Ticket(BaseModel):
|
|
48
|
+
summary: str
|
|
49
|
+
internal_notes: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
agent = Agent(
|
|
53
|
+
TestModel(
|
|
54
|
+
custom_output_args={
|
|
55
|
+
"summary": "Customer locked out of their account.",
|
|
56
|
+
"internal_notes": "Their password is hunter2, reset it manually.",
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
output_type=Ticket,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def task(prompt: str) -> Ticket:
|
|
64
|
+
return (await agent.run(prompt)).output
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
report = Dataset(
|
|
68
|
+
name="triage",
|
|
69
|
+
cases=[Case(inputs="customer cannot log in")],
|
|
70
|
+
evaluators=[Confidential.absent(field="internal_notes")],
|
|
71
|
+
).evaluate_sync(task)
|
|
72
|
+
print(report.cases[0].assertions["no_confidential"].reason)
|
|
73
|
+
"""
|
|
74
|
+
1 confidential match in internal_notes: "password"
|
|
75
|
+
password -> Their password is hunter2, reset it manually.
|
|
76
|
+
fix: redact the secret before it is persisted or echoed; store a reference, never the value
|
|
77
|
+
"""
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Holding the agent still
|
|
81
|
+
|
|
82
|
+
`TestModel` returns one canned response regardless of the prompt. That is a feature when testing
|
|
83
|
+
guards, because it lets you vary only the request and see which rules arm.
|
|
84
|
+
|
|
85
|
+
```py
|
|
86
|
+
from pydantic_ai import Agent
|
|
87
|
+
from pydantic_ai.models.test import TestModel
|
|
88
|
+
from pydantic_evals import Case, Dataset
|
|
89
|
+
|
|
90
|
+
from lexguard import Overclaim, UncertaintyAdmission
|
|
91
|
+
|
|
92
|
+
agent = Agent(TestModel(custom_output_text="This is guaranteed to work, it never fails."))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def task(prompt: str) -> str:
|
|
96
|
+
return (await agent.run(prompt)).output
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
report = Dataset(
|
|
100
|
+
name="scoping",
|
|
101
|
+
cases=[Case(inputs="will this migration work?")],
|
|
102
|
+
evaluators=[Overclaim.absent(), UncertaintyAdmission.expected()],
|
|
103
|
+
).evaluate_sync(task)
|
|
104
|
+
print({name: result.value for name, result in report.cases[0].assertions.items()})
|
|
105
|
+
#> {'no_overclaim': False, 'has_uncertainty_admission': False}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Alongside the built in evaluators
|
|
109
|
+
|
|
110
|
+
Rules are ordinary pydantic-evals evaluators, so they sit next to `MaxToolCalls`,
|
|
111
|
+
`ToolCorrectness`, `IsInstance` and the rest in the same list. Reach for those first for anything
|
|
112
|
+
structural. Lexicons are for what the text says.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../README.md
|