tuneprompt 1.0.0__py3-none-any.whl

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,15 @@
1
+ """PromptForge — local prompt quality scoring and optimization."""
2
+
3
+ from promptforge.analyzer import PromptAnalyzer
4
+ from promptforge.optimizer import PromptOptimizer
5
+ from promptforge.pipeline import PromptForge
6
+ from promptforge.scorer import PromptQualityScorer
7
+
8
+ __version__ = "1.0.0"
9
+ __all__ = [
10
+ "PromptAnalyzer",
11
+ "PromptForge",
12
+ "PromptOptimizer",
13
+ "PromptQualityScorer",
14
+ "__version__",
15
+ ]
@@ -0,0 +1,6 @@
1
+ """Package entrypoint: python -m promptforge"""
2
+
3
+ from promptforge.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from promptforge.scorer import PromptQualityScorer
6
+
7
+
8
+ class PromptAnalyzer:
9
+ """
10
+ Thin analysis helper (PRD `analyzer.py`).
11
+
12
+ Wraps the Phase-1 scorer and normalizes the structured contract used by
13
+ the optimizer and Phase-3 pipeline.
14
+ """
15
+
16
+ def __init__(self, scorer: PromptQualityScorer) -> None:
17
+ self.scorer = scorer
18
+
19
+ def analyze(self, prompt: str) -> dict[str, Any]:
20
+ result = self.scorer.analyze(prompt)
21
+ return {
22
+ "prompt": prompt,
23
+ "quality_score": result["quality_score"],
24
+ "dimensions": result["dimensions"],
25
+ "issues": result.get("issues", []),
26
+ "missing_information": result.get("missing_information", []),
27
+ }
promptforge/cli.py ADDED
@@ -0,0 +1,461 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def _read_prompt(prompt: str | None, file: str | None) -> str:
10
+ if file:
11
+ return Path(file).read_text(encoding="utf-8").strip()
12
+ if prompt == "-":
13
+ return sys.stdin.read().strip()
14
+ if prompt:
15
+ return prompt
16
+ raise SystemExit("Provide a prompt argument, --file, or '-' for stdin.")
17
+
18
+
19
+ def _cmd_version(_: argparse.Namespace) -> int:
20
+ from promptforge import __version__
21
+
22
+ print(__version__)
23
+ return 0
24
+
25
+
26
+ def _cmd_init(args: argparse.Namespace) -> int:
27
+ from promptforge.local_paths import (
28
+ ensure_dirs,
29
+ load_user_config,
30
+ save_user_config,
31
+ )
32
+
33
+ paths = ensure_dirs()
34
+ cfg = load_user_config()
35
+ cfg.setdefault("prefer_gpu", not args.cpu)
36
+ if args.quality_model:
37
+ cfg["quality_model_path"] = args.quality_model
38
+ if args.optimizer_model:
39
+ cfg["optimizer_model_path"] = args.optimizer_model
40
+ saved = save_user_config(cfg)
41
+ print("PromptForge home:", paths["home"])
42
+ print("Models dir: ", paths["models"])
43
+ print("Config: ", saved)
44
+ return 0
45
+
46
+
47
+ def _cmd_doctor(_: argparse.Namespace) -> int:
48
+ from promptforge.hub import doctor_report
49
+
50
+ report = doctor_report()
51
+ print("PromptForge doctor")
52
+ print("─" * 40)
53
+ for key, value in report.items():
54
+ print(f"{key:24s}: {value}")
55
+
56
+ ok = True
57
+ if not report["quality_model"]:
58
+ print("\nMissing quality model → run: promptforge download --quality")
59
+ ok = False
60
+ if not report["optimizer_model"]:
61
+ print("Missing optimizer model → run: promptforge download --optimizer")
62
+ ok = False
63
+ if report["prefer_gpu"] and not report["cuda_available"]:
64
+ print("\nWARNING: prefer_gpu=true but CUDA is unavailable (will use CPU).")
65
+ return 0 if ok else 1
66
+
67
+
68
+ def _cmd_download(args: argparse.Namespace) -> int:
69
+ from promptforge.hub import (
70
+ DEFAULT_OPTIMIZER_REPO,
71
+ DEFAULT_QUALITY_REPO,
72
+ download_defaults,
73
+ download_optimizer_model,
74
+ download_quality_model,
75
+ )
76
+
77
+ quality_repo = args.quality_repo or DEFAULT_QUALITY_REPO
78
+ optimizer_repo = args.optimizer_repo or DEFAULT_OPTIMIZER_REPO
79
+
80
+ if args.quality and not args.optimizer:
81
+ path = download_quality_model(repo_id=quality_repo, token=args.token)
82
+ print("Downloaded quality →", path)
83
+ return 0
84
+ if args.optimizer and not args.quality:
85
+ path = download_optimizer_model(repo_id=optimizer_repo, token=args.token)
86
+ print("Downloaded optimizer →", path)
87
+ return 0
88
+
89
+ paths = download_defaults(
90
+ quality_repo=quality_repo,
91
+ optimizer_repo=optimizer_repo,
92
+ token=args.token,
93
+ update_config=not args.no_config,
94
+ )
95
+ print("Downloaded quality →", paths["quality"])
96
+ print("Downloaded optimizer →", paths["optimizer"])
97
+ if not args.no_config:
98
+ print("Updated ~/.promptforge/config.yaml")
99
+ return 0
100
+
101
+
102
+ def _cmd_analyze(args: argparse.Namespace) -> int:
103
+ from promptforge import PromptForge
104
+
105
+ prompt = _read_prompt(args.prompt, args.file)
106
+ pf = PromptForge(
107
+ quality_model_path=args.model,
108
+ prefer_gpu=not args.cpu,
109
+ require_quality=True,
110
+ )
111
+ result = pf.analyze(prompt)
112
+ if args.json:
113
+ print(json.dumps(result, indent=2))
114
+ return 0
115
+
116
+ print(f"Prompt Quality: {result['quality_score']}/100\n")
117
+ print("Dimensions:")
118
+ for key, value in result["dimensions"].items():
119
+ print(f" {key:20s}: {value}")
120
+ if result["issues"]:
121
+ print("\nProblems:")
122
+ for issue in result["issues"]:
123
+ print(f" ✗ {issue}")
124
+ if result["missing_information"]:
125
+ print("\nMissing information:")
126
+ for item in result["missing_information"]:
127
+ print(f" - {item}")
128
+ print("\nRun:\n promptforge optimize \"...\"")
129
+ return 0
130
+
131
+
132
+ def _cmd_optimize(args: argparse.Namespace) -> int:
133
+ from promptforge import PromptForge
134
+
135
+ prompt = _read_prompt(args.prompt, args.file)
136
+ pf = PromptForge(
137
+ quality_model_path=args.quality_model,
138
+ optimizer_model_path=args.model,
139
+ prefer_gpu=not args.cpu,
140
+ require_optimizer=True,
141
+ )
142
+ analysis = None
143
+ if pf.scorer is not None and not args.no_analysis:
144
+ analysis = pf.analyze(prompt)
145
+
146
+ result = pf.optimize(
147
+ prompt,
148
+ analysis=analysis,
149
+ task_type=args.task_type,
150
+ use_scorer_analysis=False,
151
+ )
152
+ if args.json:
153
+ print(json.dumps(result, indent=2))
154
+ return 0
155
+
156
+ print("Optimized Prompt")
157
+ print("─" * 32)
158
+ print(result["optimized_prompt"])
159
+ return 0
160
+
161
+
162
+ def _cmd_run(args: argparse.Namespace) -> int:
163
+ from promptforge import PromptForge
164
+
165
+ prompt = _read_prompt(args.prompt, args.file)
166
+ pf = PromptForge(
167
+ quality_model_path=args.quality_model,
168
+ optimizer_model_path=args.optimizer_model,
169
+ prefer_gpu=not args.cpu,
170
+ require_quality=True,
171
+ require_optimizer=True,
172
+ )
173
+ result = pf.run(
174
+ prompt,
175
+ task_type=args.task_type,
176
+ rescore_optimized=not args.no_rescore,
177
+ )
178
+ if args.output:
179
+ pf.export_result(result, args.output)
180
+
181
+ if args.json:
182
+ print(json.dumps(result, indent=2))
183
+ return 0
184
+
185
+ before = result["before"]["quality_score"]
186
+ after = result["after"].get("quality_score", "n/a")
187
+ print(f"Prompt Quality: {before} -> {after} (delta {result['delta'].get('quality_score', 'n/a')})")
188
+ if result.get("used_fallback"):
189
+ model_val = result.get("model_validation") or {}
190
+ issues = model_val.get("issues") or []
191
+ print(
192
+ "\nNote: Model output failed validation; showing intent-preserving fallback."
193
+ )
194
+ if issues:
195
+ print(f"Rejection reasons: {', '.join(issues)}")
196
+ if model_val.get("instruction_preservation") is not None:
197
+ print(
198
+ f"Intent preservation: {model_val['instruction_preservation']:.2%} "
199
+ f"(min 8%)"
200
+ )
201
+ if model_val.get("repetitive"):
202
+ print("Detected repetitive output.")
203
+ raw = (result.get("raw_output") or "").strip()
204
+ if raw:
205
+ print("\nModel output (rejected):")
206
+ print("-" * 32)
207
+ preview = raw if len(raw) <= 600 else raw[:600] + "..."
208
+ print(preview)
209
+ print(
210
+ "\nFor real model rewrites, retrain: "
211
+ "python scripts/train_optimizer.py --require-gpu --fast --regenerate"
212
+ )
213
+ elif not result.get("trustworthy_improvement"):
214
+ print("\nWarning: Score delta may not reflect a trustworthy improvement.")
215
+ if result["before"].get("issues"):
216
+ print("\nProblems:")
217
+ for issue in result["before"]["issues"]:
218
+ print(f" - {issue}")
219
+ print("\nChanges:")
220
+ for change in result.get("changes", []):
221
+ print(f" + {change}")
222
+ print("\nOptimized Prompt")
223
+ print("-" * 32)
224
+ print(result["optimized_prompt"])
225
+ return 0
226
+
227
+
228
+ def _cmd_eval(args: argparse.Namespace) -> int:
229
+ from promptforge import PromptForge
230
+ from promptforge.evaluation import (
231
+ run_downstream_proxy_eval,
232
+ run_pipeline_evaluation,
233
+ save_eval_report,
234
+ )
235
+
236
+ pf = PromptForge(
237
+ quality_model_path=args.quality_model,
238
+ optimizer_model_path=args.optimizer_model,
239
+ prefer_gpu=not args.cpu,
240
+ require_quality=True,
241
+ require_optimizer=True,
242
+ )
243
+ report = run_pipeline_evaluation(pf, task_type=args.task_type)
244
+ downstream = run_downstream_proxy_eval(pf, task_type=args.task_type)
245
+ payload = {"pipeline": report, "downstream_proxy": downstream}
246
+
247
+ out = args.output or "outputs/phase3_eval/pipeline_report.json"
248
+ save_eval_report(payload, out)
249
+ print(json.dumps(payload["pipeline"]["summary"], indent=2))
250
+ print("\nDownstream proxy:")
251
+ print(json.dumps(payload["downstream_proxy"]["summary"], indent=2))
252
+ print(f"\nWrote {out}")
253
+ return 0
254
+
255
+
256
+ def _cmd_space(args: argparse.Namespace) -> int:
257
+ import importlib.util
258
+ from pathlib import Path
259
+
260
+ from promptforge.local_paths import resolve_model_path
261
+
262
+ app_path = Path(__file__).resolve().parents[2] / "demo" / "app.py"
263
+ if not app_path.exists():
264
+ app_path = Path.cwd() / "demo" / "app.py"
265
+ spec = importlib.util.spec_from_file_location("promptforge_demo_app", app_path)
266
+ if spec is None or spec.loader is None:
267
+ raise FileNotFoundError(f"Could not load Gradio app at {app_path}")
268
+ mod = importlib.util.module_from_spec(spec)
269
+ spec.loader.exec_module(mod)
270
+
271
+ quality = resolve_model_path(args.quality_model, kind="quality")
272
+ optimizer = resolve_model_path(args.optimizer_model, kind="optimizer")
273
+ if not quality or not optimizer:
274
+ raise SystemExit("Models missing. Run: promptforge download")
275
+
276
+ demo = mod.build_demo(
277
+ quality_model_path=quality,
278
+ optimizer_model_path=optimizer,
279
+ prefer_gpu=not args.cpu,
280
+ )
281
+ demo.launch(server_name=args.host, server_port=args.port, share=args.share)
282
+ return 0
283
+
284
+
285
+ def _cmd_train_quality(args: argparse.Namespace) -> int:
286
+ from promptforge.config import QualityScorerConfig
287
+ from promptforge.training.train_quality import train_quality_scorer
288
+
289
+ config = (
290
+ QualityScorerConfig.from_yaml(args.config)
291
+ if args.config
292
+ else QualityScorerConfig()
293
+ )
294
+ if args.num_examples is not None:
295
+ config.num_examples = args.num_examples
296
+ if args.output_dir:
297
+ config.output_dir = args.output_dir
298
+ if args.final_model_dir:
299
+ config.final_model_dir = args.final_model_dir
300
+ if args.dataset_path:
301
+ config.dataset_path = args.dataset_path
302
+ if args.epochs is not None:
303
+ config.num_train_epochs = args.epochs
304
+ if args.batch_size is not None:
305
+ config.per_device_train_batch_size = args.batch_size
306
+ if args.cpu:
307
+ config.prefer_gpu = False
308
+ config.use_fp16 = False
309
+ config.use_bf16 = False
310
+
311
+ train_quality_scorer(config, regenerate_dataset=args.regenerate)
312
+ return 0
313
+
314
+
315
+ def _cmd_train_optimizer(args: argparse.Namespace) -> int:
316
+ from promptforge.config import OptimizerConfig
317
+ from promptforge.training.train_optimizer import train_optimizer
318
+
319
+ config = (
320
+ OptimizerConfig.from_yaml(args.config)
321
+ if args.config
322
+ else OptimizerConfig()
323
+ )
324
+ if args.num_examples is not None:
325
+ config.num_examples = args.num_examples
326
+ if args.output_dir:
327
+ config.output_dir = args.output_dir
328
+ if args.final_model_dir:
329
+ config.final_model_dir = args.final_model_dir
330
+ if args.dataset_path:
331
+ config.dataset_path = args.dataset_path
332
+ if args.epochs is not None:
333
+ config.num_train_epochs = args.epochs
334
+ if args.batch_size is not None:
335
+ config.per_device_train_batch_size = args.batch_size
336
+ if args.base_model:
337
+ config.base_model_name = args.base_model
338
+ if args.cpu:
339
+ config.prefer_gpu = False
340
+ config.use_fp16 = False
341
+ config.use_bf16 = False
342
+ if args.load_in_4bit:
343
+ config.load_in_4bit = True
344
+
345
+ train_optimizer(config, regenerate_dataset=args.regenerate)
346
+ return 0
347
+
348
+
349
+ def build_parser() -> argparse.ArgumentParser:
350
+ parser = argparse.ArgumentParser(
351
+ prog="promptforge",
352
+ description="PromptForge — local prompt quality scoring & optimization",
353
+ )
354
+ sub = parser.add_subparsers(dest="command", required=True)
355
+
356
+ ver = sub.add_parser("version", help="Print package version")
357
+ ver.set_defaults(func=_cmd_version)
358
+
359
+ init = sub.add_parser("init", help="Create ~/.promptforge config + models dirs")
360
+ init.add_argument("--quality-model", type=str, default=None)
361
+ init.add_argument("--optimizer-model", type=str, default=None)
362
+ init.add_argument("--cpu", action="store_true")
363
+ init.set_defaults(func=_cmd_init)
364
+
365
+ doctor = sub.add_parser("doctor", help="Check local install, GPU, and model paths")
366
+ doctor.set_defaults(func=_cmd_doctor)
367
+
368
+ dl = sub.add_parser("download", help="Download models from Hugging Face Hub")
369
+ dl.add_argument("--quality", action="store_true", help="Download quality model only")
370
+ dl.add_argument("--optimizer", action="store_true", help="Download optimizer only")
371
+ dl.add_argument("--quality-repo", type=str, default=None)
372
+ dl.add_argument("--optimizer-repo", type=str, default=None)
373
+ dl.add_argument("--token", type=str, default=None)
374
+ dl.add_argument("--no-config", action="store_true")
375
+ dl.set_defaults(func=_cmd_download)
376
+
377
+ analyze = sub.add_parser("analyze", help="Score a prompt")
378
+ analyze.add_argument("prompt", nargs="?", default=None)
379
+ analyze.add_argument("--file", type=str, default=None)
380
+ analyze.add_argument("--model", type=str, default=None)
381
+ analyze.add_argument("--json", action="store_true")
382
+ analyze.add_argument("--cpu", action="store_true")
383
+ analyze.set_defaults(func=_cmd_analyze)
384
+
385
+ optimize = sub.add_parser("optimize", help="Optimize a prompt")
386
+ optimize.add_argument("prompt", nargs="?", default=None)
387
+ optimize.add_argument("--file", type=str, default=None)
388
+ optimize.add_argument("--model", type=str, default=None)
389
+ optimize.add_argument("--quality-model", type=str, default=None)
390
+ optimize.add_argument("--task-type", type=str, default="general")
391
+ optimize.add_argument("--no-analysis", action="store_true")
392
+ optimize.add_argument("--json", action="store_true")
393
+ optimize.add_argument("--cpu", action="store_true")
394
+ optimize.set_defaults(func=_cmd_optimize)
395
+
396
+ run = sub.add_parser("run", help="Analyze + optimize + compare")
397
+ run.add_argument("prompt", nargs="?", default=None)
398
+ run.add_argument("--file", type=str, default=None)
399
+ run.add_argument("--quality-model", type=str, default=None)
400
+ run.add_argument("--optimizer-model", type=str, default=None)
401
+ run.add_argument("--task-type", type=str, default="general")
402
+ run.add_argument("--no-rescore", action="store_true")
403
+ run.add_argument("--output", type=str, default=None)
404
+ run.add_argument("--json", action="store_true")
405
+ run.add_argument("--cpu", action="store_true")
406
+ run.set_defaults(func=_cmd_run)
407
+
408
+ ev = sub.add_parser("eval", help="Run evaluation reports")
409
+ ev.add_argument("--quality-model", type=str, default=None)
410
+ ev.add_argument("--optimizer-model", type=str, default=None)
411
+ ev.add_argument("--task-type", type=str, default="general")
412
+ ev.add_argument("--output", type=str, default="outputs/phase3_eval/pipeline_report.json")
413
+ ev.add_argument("--cpu", action="store_true")
414
+ ev.set_defaults(func=_cmd_eval)
415
+
416
+ space = sub.add_parser("space", help="Launch Gradio demo")
417
+ space.add_argument("--quality-model", type=str, default=None)
418
+ space.add_argument("--optimizer-model", type=str, default=None)
419
+ space.add_argument("--host", type=str, default="0.0.0.0")
420
+ space.add_argument("--port", type=int, default=7860)
421
+ space.add_argument("--share", action="store_true")
422
+ space.add_argument("--cpu", action="store_true")
423
+ space.set_defaults(func=_cmd_space)
424
+
425
+ train_q = sub.add_parser("train-quality", help="Train Phase-1 quality scorer")
426
+ train_q.add_argument("--config", type=str, default="configs/quality_scorer.yaml")
427
+ train_q.add_argument("--num-examples", type=int, default=None)
428
+ train_q.add_argument("--epochs", type=int, default=None)
429
+ train_q.add_argument("--batch-size", type=int, default=None)
430
+ train_q.add_argument("--output-dir", type=str, default=None)
431
+ train_q.add_argument("--final-model-dir", type=str, default=None)
432
+ train_q.add_argument("--dataset-path", type=str, default=None)
433
+ train_q.add_argument("--regenerate", action="store_true")
434
+ train_q.add_argument("--cpu", action="store_true")
435
+ train_q.set_defaults(func=_cmd_train_quality)
436
+
437
+ train_o = sub.add_parser("train-optimizer", help="Train Phase-2 LoRA optimizer")
438
+ train_o.add_argument("--config", type=str, default="configs/optimizer.yaml")
439
+ train_o.add_argument("--num-examples", type=int, default=None)
440
+ train_o.add_argument("--epochs", type=int, default=None)
441
+ train_o.add_argument("--batch-size", type=int, default=None)
442
+ train_o.add_argument("--base-model", type=str, default=None)
443
+ train_o.add_argument("--output-dir", type=str, default=None)
444
+ train_o.add_argument("--final-model-dir", type=str, default=None)
445
+ train_o.add_argument("--dataset-path", type=str, default=None)
446
+ train_o.add_argument("--regenerate", action="store_true")
447
+ train_o.add_argument("--load-in-4bit", action="store_true")
448
+ train_o.add_argument("--cpu", action="store_true")
449
+ train_o.set_defaults(func=_cmd_train_optimizer)
450
+
451
+ return parser
452
+
453
+
454
+ def main(argv: list[str] | None = None) -> int:
455
+ parser = build_parser()
456
+ args = parser.parse_args(argv)
457
+ return int(args.func(args))
458
+
459
+
460
+ if __name__ == "__main__":
461
+ sys.exit(main())
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def diff_dimensions(
7
+ before: dict[str, float],
8
+ after: dict[str, float],
9
+ ) -> dict[str, float]:
10
+ keys = sorted(set(before) | set(after))
11
+ return {
12
+ key: round(float(after.get(key, 0.0) - before.get(key, 0.0)), 2)
13
+ for key in keys
14
+ }
15
+
16
+
17
+ def infer_changes(
18
+ original: str,
19
+ optimized: str,
20
+ before: dict[str, Any] | None = None,
21
+ after: dict[str, Any] | None = None,
22
+ ) -> list[str]:
23
+ """Heuristic change summary for before/after UI."""
24
+ changes: list[str] = []
25
+ original_l = original.lower()
26
+ optimized_l = optimized.lower()
27
+
28
+ markers = [
29
+ ("audience", ["for ", "targeting", "audience", "developers", "users"]),
30
+ ("objective", ["build", "create", "write", "implement", "produce"]),
31
+ ("constraints", ["requirements", "constraints", "must", "use "]),
32
+ ("output format", ["return", "output", "json", "markdown", "numbered"]),
33
+ ("technical requirements", ["python", "react", "fastapi", "typescript", "postgresql"]),
34
+ ("structure", ["1.", "2.", "-", "requirements:"]),
35
+ ("context", ["this is for", "university", "production", "startup", "prototype"]),
36
+ ]
37
+
38
+ for label, needles in markers:
39
+ if any(n in optimized_l for n in needles) and not any(n in original_l for n in needles):
40
+ changes.append(f"Added {label}")
41
+
42
+ if len(optimized.split()) > len(original.split()) * 1.8:
43
+ changes.append("Expanded specificity")
44
+
45
+ if before and after:
46
+ b_dims = before.get("dimensions", {})
47
+ a_dims = after.get("dimensions", {})
48
+ for key, delta in diff_dimensions(b_dims, a_dims).items():
49
+ if delta >= 15:
50
+ changes.append(f"Improved {key} (+{delta})")
51
+
52
+ # Deduplicate while preserving order
53
+ seen: set[str] = set()
54
+ unique: list[str] = []
55
+ for item in changes:
56
+ if item not in seen:
57
+ seen.add(item)
58
+ unique.append(item)
59
+ return unique or ["Rewrote prompt for clarity and completeness"]
60
+
61
+
62
+ def build_comparison_payload(
63
+ original_prompt: str,
64
+ optimized_prompt: str,
65
+ before_analysis: dict[str, Any],
66
+ after_analysis: dict[str, Any] | None = None,
67
+ task_type: str = "general",
68
+ ) -> dict[str, Any]:
69
+ after_analysis = after_analysis or {}
70
+ before_score = float(before_analysis.get("quality_score", 0.0))
71
+ after_score = float(after_analysis.get("quality_score", before_score))
72
+ before_dims = before_analysis.get("dimensions", {})
73
+ after_dims = after_analysis.get("dimensions", {})
74
+
75
+ return {
76
+ "original_prompt": original_prompt,
77
+ "optimized_prompt": optimized_prompt,
78
+ "task_type": task_type,
79
+ "before": {
80
+ "quality_score": before_score,
81
+ "dimensions": before_dims,
82
+ "issues": before_analysis.get("issues", []),
83
+ "missing_information": before_analysis.get("missing_information", []),
84
+ },
85
+ "after": {
86
+ "quality_score": after_score,
87
+ "dimensions": after_dims,
88
+ "issues": after_analysis.get("issues", []),
89
+ "missing_information": after_analysis.get("missing_information", []),
90
+ },
91
+ "delta": {
92
+ "quality_score": round(after_score - before_score, 2),
93
+ "dimensions": diff_dimensions(before_dims, after_dims),
94
+ },
95
+ "changes": infer_changes(
96
+ original_prompt,
97
+ optimized_prompt,
98
+ before_analysis,
99
+ after_analysis,
100
+ ),
101
+ }