promptdiff-ai 1.0.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.
@@ -0,0 +1,43 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg-info/
7
+ *.egg
8
+ dist/
9
+ build/
10
+ .eggs/
11
+ *.whl
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ ENV/
17
+
18
+ # IDE
19
+ .idea/
20
+ .vscode/
21
+ *.swp
22
+ *.swo
23
+ *~
24
+
25
+ # Testing
26
+ .pytest_cache/
27
+ .coverage
28
+ htmlcov/
29
+ .mypy_cache/
30
+
31
+ # OS
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # Secrets - NEVER commit these
36
+ .env
37
+ .env.*
38
+ secrets/
39
+ *.key
40
+ *.pem
41
+
42
+ # Cache
43
+ .prompttools-cache/
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: promptdiff-ai
3
+ Version: 1.0.0
4
+ Summary: Semantic diff for LLM prompt changes
5
+ Project-URL: Homepage, https://github.com/scottconverse/promptdiff
6
+ Project-URL: Repository, https://github.com/scottconverse/promptdiff
7
+ Project-URL: Issues, https://github.com/scottconverse/promptdiff/issues
8
+ Author: Scott Converse
9
+ License: MIT
10
+ Keywords: ai,breaking-changes,diff,llm,prompt,semantic
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: prompttools-core-ai<2.0,>=1.0
22
+ Requires-Dist: rich>=13.0
23
+ Requires-Dist: typer[all]>=0.12
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy; extra == 'dev'
26
+ Requires-Dist: pytest-cov; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Requires-Dist: ruff; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # promptdiff
32
+
33
+ Semantic diff for LLM prompt changes. Part of the [prompttools](https://github.com/scottconverse/prompttools) suite.
34
+
35
+ Unlike generic text diffing, promptdiff understands prompt structure: messages, variables, metadata, and token counts. It classifies changes as breaking or non-breaking and outputs structured reports suitable for CI/CD pipelines and GitHub PR comments.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install promptdiff-ai
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```bash
46
+ # Compare two prompt files
47
+ promptdiff old_prompt.yaml new_prompt.yaml
48
+
49
+ # JSON output for CI pipelines
50
+ promptdiff old.yaml new.yaml --format json
51
+
52
+ # Markdown output for GitHub PR comments
53
+ promptdiff old.yaml new.yaml --format markdown
54
+
55
+ # Exit with code 1 if breaking changes are found (CI gate)
56
+ promptdiff old.yaml new.yaml --exit-on-breaking
57
+
58
+ # Show per-message token breakdowns
59
+ promptdiff old.yaml new.yaml --token-detail
60
+ ```
61
+
62
+ ## What It Detects
63
+
64
+ ### Message-Level Changes
65
+ - Added, removed, or modified messages
66
+ - Per-message token deltas
67
+ - Unified content diffs within modified messages
68
+
69
+ ### Variable Changes
70
+ - New variables (with or without defaults)
71
+ - Removed variables
72
+ - Modified default values
73
+
74
+ ### Metadata Changes
75
+ - Model changes
76
+ - Added/removed/modified metadata keys
77
+
78
+ ### Token Deltas
79
+ - Total token count comparison
80
+ - Percentage change
81
+ - Per-message breakdowns (with `--token-detail`)
82
+
83
+ ## Breaking Change Classification
84
+
85
+ ### Breaking (High Severity)
86
+ - **New required variable** -- a variable added without a default value; existing callers will fail
87
+ - **Removed variable** -- callers referencing this variable will break
88
+ - **Removed message** -- changes the prompt structure
89
+
90
+ ### Breaking (Medium Severity)
91
+ - **Model change** -- may affect behavior, pricing, and capabilities
92
+ - **Role ordering change** -- may affect model behavior
93
+
94
+ ### Non-Breaking
95
+ - Added variable with a default value
96
+ - Added messages (extends the prompt)
97
+ - Content modifications within existing messages
98
+ - Metadata changes (except model)
99
+
100
+ ## Output Formats
101
+
102
+ ### Text (default)
103
+ Rich terminal output with color-coded diffs:
104
+
105
+ ```
106
+ Prompt Diff: new_prompt.yaml
107
+ old: a1b2c3d4e5f6 new: f6e5d4c3b2a1
108
+
109
+ BREAKING CHANGES (2):
110
+ HIGH [variable] Variable 'tone' was removed
111
+ MEDIUM [model] Model changed from 'gpt-4' to 'gpt-4o'
112
+
113
+ Token Delta:
114
+ 150 -> 165 (+15, +10.0%)
115
+
116
+ Messages (1 changed):
117
+ ~ system
118
+ System message content modified
119
+ ```
120
+
121
+ ### JSON
122
+ Structured JSON for programmatic consumption:
123
+
124
+ ```bash
125
+ promptdiff old.yaml new.yaml --format json
126
+ ```
127
+
128
+ ### Markdown
129
+ GitHub-flavored Markdown for PR comments:
130
+
131
+ ```bash
132
+ promptdiff old.yaml new.yaml --format markdown
133
+ ```
134
+
135
+ ## Python API
136
+
137
+ ```python
138
+ from promptdiff import diff_files, format_text, format_json
139
+
140
+ # Compare two files
141
+ result = diff_files("prompts/v1.yaml", "prompts/v2.yaml")
142
+
143
+ # Check for breaking changes
144
+ if result.is_breaking:
145
+ for bc in result.breaking_changes:
146
+ print(f"[{bc.severity}] {bc.description}")
147
+
148
+ # Get token delta
149
+ print(f"Tokens: {result.token_delta.old_total} -> {result.token_delta.new_total}")
150
+
151
+ # Format output
152
+ print(format_text(result))
153
+ ```
154
+
155
+ ## CLI Reference
156
+
157
+ ```
158
+ Usage: promptdiff [OPTIONS] FILE_A FILE_B
159
+
160
+ Arguments:
161
+ FILE_A Path to the old prompt file
162
+ FILE_B Path to the new prompt file
163
+
164
+ Options:
165
+ -f, --format [text|json|markdown] Output format (default: text)
166
+ --exit-on-breaking Exit with code 1 if breaking changes found
167
+ --token-detail Show per-message token breakdowns
168
+ -e, --encoding TEXT tiktoken encoding (default: cl100k_base)
169
+ -V, --version Show version and exit
170
+ --help Show this message and exit
171
+ ```
172
+
173
+ ## Supported File Formats
174
+
175
+ All formats supported by prompttools-core:
176
+ - YAML (`.yaml`, `.yml`)
177
+ - JSON (`.json`)
178
+ - Markdown (`.md`)
179
+ - Text (`.txt`)
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,153 @@
1
+ # promptdiff
2
+
3
+ Semantic diff for LLM prompt changes. Part of the [prompttools](https://github.com/scottconverse/prompttools) suite.
4
+
5
+ Unlike generic text diffing, promptdiff understands prompt structure: messages, variables, metadata, and token counts. It classifies changes as breaking or non-breaking and outputs structured reports suitable for CI/CD pipelines and GitHub PR comments.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install promptdiff-ai
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Compare two prompt files
17
+ promptdiff old_prompt.yaml new_prompt.yaml
18
+
19
+ # JSON output for CI pipelines
20
+ promptdiff old.yaml new.yaml --format json
21
+
22
+ # Markdown output for GitHub PR comments
23
+ promptdiff old.yaml new.yaml --format markdown
24
+
25
+ # Exit with code 1 if breaking changes are found (CI gate)
26
+ promptdiff old.yaml new.yaml --exit-on-breaking
27
+
28
+ # Show per-message token breakdowns
29
+ promptdiff old.yaml new.yaml --token-detail
30
+ ```
31
+
32
+ ## What It Detects
33
+
34
+ ### Message-Level Changes
35
+ - Added, removed, or modified messages
36
+ - Per-message token deltas
37
+ - Unified content diffs within modified messages
38
+
39
+ ### Variable Changes
40
+ - New variables (with or without defaults)
41
+ - Removed variables
42
+ - Modified default values
43
+
44
+ ### Metadata Changes
45
+ - Model changes
46
+ - Added/removed/modified metadata keys
47
+
48
+ ### Token Deltas
49
+ - Total token count comparison
50
+ - Percentage change
51
+ - Per-message breakdowns (with `--token-detail`)
52
+
53
+ ## Breaking Change Classification
54
+
55
+ ### Breaking (High Severity)
56
+ - **New required variable** -- a variable added without a default value; existing callers will fail
57
+ - **Removed variable** -- callers referencing this variable will break
58
+ - **Removed message** -- changes the prompt structure
59
+
60
+ ### Breaking (Medium Severity)
61
+ - **Model change** -- may affect behavior, pricing, and capabilities
62
+ - **Role ordering change** -- may affect model behavior
63
+
64
+ ### Non-Breaking
65
+ - Added variable with a default value
66
+ - Added messages (extends the prompt)
67
+ - Content modifications within existing messages
68
+ - Metadata changes (except model)
69
+
70
+ ## Output Formats
71
+
72
+ ### Text (default)
73
+ Rich terminal output with color-coded diffs:
74
+
75
+ ```
76
+ Prompt Diff: new_prompt.yaml
77
+ old: a1b2c3d4e5f6 new: f6e5d4c3b2a1
78
+
79
+ BREAKING CHANGES (2):
80
+ HIGH [variable] Variable 'tone' was removed
81
+ MEDIUM [model] Model changed from 'gpt-4' to 'gpt-4o'
82
+
83
+ Token Delta:
84
+ 150 -> 165 (+15, +10.0%)
85
+
86
+ Messages (1 changed):
87
+ ~ system
88
+ System message content modified
89
+ ```
90
+
91
+ ### JSON
92
+ Structured JSON for programmatic consumption:
93
+
94
+ ```bash
95
+ promptdiff old.yaml new.yaml --format json
96
+ ```
97
+
98
+ ### Markdown
99
+ GitHub-flavored Markdown for PR comments:
100
+
101
+ ```bash
102
+ promptdiff old.yaml new.yaml --format markdown
103
+ ```
104
+
105
+ ## Python API
106
+
107
+ ```python
108
+ from promptdiff import diff_files, format_text, format_json
109
+
110
+ # Compare two files
111
+ result = diff_files("prompts/v1.yaml", "prompts/v2.yaml")
112
+
113
+ # Check for breaking changes
114
+ if result.is_breaking:
115
+ for bc in result.breaking_changes:
116
+ print(f"[{bc.severity}] {bc.description}")
117
+
118
+ # Get token delta
119
+ print(f"Tokens: {result.token_delta.old_total} -> {result.token_delta.new_total}")
120
+
121
+ # Format output
122
+ print(format_text(result))
123
+ ```
124
+
125
+ ## CLI Reference
126
+
127
+ ```
128
+ Usage: promptdiff [OPTIONS] FILE_A FILE_B
129
+
130
+ Arguments:
131
+ FILE_A Path to the old prompt file
132
+ FILE_B Path to the new prompt file
133
+
134
+ Options:
135
+ -f, --format [text|json|markdown] Output format (default: text)
136
+ --exit-on-breaking Exit with code 1 if breaking changes found
137
+ --token-detail Show per-message token breakdowns
138
+ -e, --encoding TEXT tiktoken encoding (default: cl100k_base)
139
+ -V, --version Show version and exit
140
+ --help Show this message and exit
141
+ ```
142
+
143
+ ## Supported File Formats
144
+
145
+ All formats supported by prompttools-core:
146
+ - YAML (`.yaml`, `.yml`)
147
+ - JSON (`.json`)
148
+ - Markdown (`.md`)
149
+ - Text (`.txt`)
150
+
151
+ ## License
152
+
153
+ MIT
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "promptdiff-ai"
7
+ version = "1.0.0"
8
+ description = "Semantic diff for LLM prompt changes"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ {name = "Scott Converse"},
14
+ ]
15
+ keywords = ["llm", "prompt", "diff", "semantic", "ai", "breaking-changes"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Software Development :: Quality Assurance",
26
+ ]
27
+ dependencies = [
28
+ "prompttools-core-ai>=1.0,<2.0",
29
+ "typer[all]>=0.12",
30
+ "rich>=13.0",
31
+ ]
32
+
33
+ [project.scripts]
34
+ promptdiff = "promptdiff.cli:app"
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/scottconverse/promptdiff"
38
+ Repository = "https://github.com/scottconverse/promptdiff"
39
+ Issues = "https://github.com/scottconverse/promptdiff/issues"
40
+
41
+ [project.optional-dependencies]
42
+ dev = [
43
+ "pytest>=8.0",
44
+ "pytest-cov",
45
+ "ruff",
46
+ "mypy",
47
+ ]
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/promptdiff"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ addopts = "-v --tb=short"
55
+
56
+ [tool.ruff]
57
+ target-version = "py39"
58
+ line-length = 100
59
+
60
+ [tool.mypy]
61
+ python_version = "3.9"
62
+ warn_return_any = true
63
+ warn_unused_configs = true
@@ -0,0 +1,52 @@
1
+ """promptdiff: Semantic diff for LLM prompt changes.
2
+
3
+ Public API exports for convenience imports::
4
+
5
+ from promptdiff import diff_files, PromptDiff, format_text
6
+ """
7
+
8
+ from promptdiff.models import (
9
+ BreakingChange,
10
+ ChangeStatus,
11
+ MessageDiff,
12
+ MetadataDiff,
13
+ PromptDiff,
14
+ TokenDelta,
15
+ VariableDiff,
16
+ )
17
+ from promptdiff.differ import (
18
+ compute_token_delta,
19
+ diff_files,
20
+ diff_messages,
21
+ diff_metadata,
22
+ diff_variables,
23
+ )
24
+ from promptdiff.analyzer import analyze_breaking_changes
25
+ from promptdiff.reporter import format_json, format_markdown, format_text
26
+
27
+ __version__ = "1.0.0"
28
+
29
+ __all__ = [
30
+ # Models
31
+ "BreakingChange",
32
+ "ChangeStatus",
33
+ "MessageDiff",
34
+ "MetadataDiff",
35
+ "PromptDiff",
36
+ "TokenDelta",
37
+ "VariableDiff",
38
+ # Differ
39
+ "compute_token_delta",
40
+ "diff_files",
41
+ "diff_messages",
42
+ "diff_metadata",
43
+ "diff_variables",
44
+ # Analyzer
45
+ "analyze_breaking_changes",
46
+ # Reporter
47
+ "format_json",
48
+ "format_markdown",
49
+ "format_text",
50
+ # Version
51
+ "__version__",
52
+ ]
@@ -0,0 +1,163 @@
1
+ """Breaking change detection for promptdiff.
2
+
3
+ Analyzes a PromptDiff and classifies changes by severity.
4
+
5
+ Breaking changes:
6
+ - New required variable (no default) -- high severity
7
+ - Removed variable -- high severity
8
+ - Removed message -- high severity
9
+ - Changed role ordering -- medium severity
10
+ - Model change (in metadata) -- medium severity
11
+
12
+ Non-breaking changes:
13
+ - Added variable with default
14
+ - Added messages
15
+ - Content modifications
16
+ - Metadata changes (except model)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from promptdiff.models import (
22
+ BreakingChange,
23
+ ChangeStatus,
24
+ PromptDiff,
25
+ )
26
+
27
+
28
+ def analyze_breaking_changes(diff: PromptDiff) -> list[BreakingChange]:
29
+ """Analyze a diff for breaking changes.
30
+
31
+ Parameters
32
+ ----------
33
+ diff:
34
+ The prompt diff to analyze.
35
+
36
+ Returns
37
+ -------
38
+ list[BreakingChange]
39
+ All detected breaking changes sorted by severity.
40
+ """
41
+ changes: list[BreakingChange] = []
42
+ changes.extend(_check_variable_changes(diff))
43
+ changes.extend(_check_message_changes(diff))
44
+ changes.extend(_check_metadata_changes(diff))
45
+ changes.extend(_check_role_ordering(diff))
46
+
47
+ # Sort by severity: high first, then medium, then low.
48
+ severity_order = {"high": 0, "medium": 1, "low": 2}
49
+ changes.sort(key=lambda c: severity_order.get(c.severity, 3))
50
+
51
+ return changes
52
+
53
+
54
+ def _check_variable_changes(diff: PromptDiff) -> list[BreakingChange]:
55
+ """Detect breaking variable changes."""
56
+ changes: list[BreakingChange] = []
57
+
58
+ for vd in diff.variable_diffs:
59
+ if vd.status == ChangeStatus.REMOVED:
60
+ changes.append(
61
+ BreakingChange(
62
+ category="variable",
63
+ description=f"Variable '{vd.name}' was removed",
64
+ severity="high",
65
+ )
66
+ )
67
+ elif vd.status == ChangeStatus.ADDED and vd.is_breaking:
68
+ changes.append(
69
+ BreakingChange(
70
+ category="variable",
71
+ description=(
72
+ f"New required variable '{vd.name}' added without a default value"
73
+ ),
74
+ severity="high",
75
+ )
76
+ )
77
+
78
+ return changes
79
+
80
+
81
+ def _check_message_changes(diff: PromptDiff) -> list[BreakingChange]:
82
+ """Detect breaking message changes."""
83
+ changes: list[BreakingChange] = []
84
+
85
+ for md in diff.message_diffs:
86
+ if md.status == ChangeStatus.REMOVED:
87
+ changes.append(
88
+ BreakingChange(
89
+ category="message",
90
+ description=f"{md.role.capitalize()} message was removed",
91
+ severity="high",
92
+ )
93
+ )
94
+
95
+ return changes
96
+
97
+
98
+ def _check_metadata_changes(diff: PromptDiff) -> list[BreakingChange]:
99
+ """Detect breaking metadata changes (model changes)."""
100
+ changes: list[BreakingChange] = []
101
+
102
+ for md in diff.metadata_diffs:
103
+ if md.key == "model" and md.status == ChangeStatus.MODIFIED:
104
+ changes.append(
105
+ BreakingChange(
106
+ category="model",
107
+ description=(
108
+ f"Model changed from '{md.old_value}' to '{md.new_value}'"
109
+ ),
110
+ severity="medium",
111
+ )
112
+ )
113
+ elif md.key == "model" and md.status == ChangeStatus.REMOVED:
114
+ changes.append(
115
+ BreakingChange(
116
+ category="model",
117
+ description="Model specification was removed",
118
+ severity="medium",
119
+ )
120
+ )
121
+
122
+ return changes
123
+
124
+
125
+ def _check_role_ordering(diff: PromptDiff) -> list[BreakingChange]:
126
+ """Detect role ordering changes.
127
+
128
+ If the sequence of roles (ignoring unchanged content) changes between
129
+ old and new, this is potentially breaking.
130
+ """
131
+ changes: list[BreakingChange] = []
132
+
133
+ # Reconstruct old and new role sequences from the message diffs.
134
+ old_roles: list[str] = []
135
+ new_roles: list[str] = []
136
+
137
+ for md in diff.message_diffs:
138
+ if md.status == ChangeStatus.REMOVED:
139
+ old_roles.append(md.role)
140
+ elif md.status == ChangeStatus.ADDED:
141
+ new_roles.append(md.role)
142
+ elif md.status in (ChangeStatus.MODIFIED, ChangeStatus.UNCHANGED):
143
+ old_roles.append(md.role)
144
+ new_roles.append(md.role)
145
+
146
+ if old_roles != new_roles and len(old_roles) > 0 and len(new_roles) > 0:
147
+ # Only flag if neither list is a subset scenario (pure additions/removals
148
+ # are already caught above). Check if the common elements changed order.
149
+ common_old = [r for r in old_roles if r in new_roles]
150
+ common_new = [r for r in new_roles if r in old_roles]
151
+ if common_old != common_new:
152
+ changes.append(
153
+ BreakingChange(
154
+ category="role",
155
+ description=(
156
+ f"Message role ordering changed: "
157
+ f"{' -> '.join(old_roles)} to {' -> '.join(new_roles)}"
158
+ ),
159
+ severity="medium",
160
+ )
161
+ )
162
+
163
+ return changes