markback 0.1.3__tar.gz → 0.1.5__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.
Files changed (56) hide show
  1. {markback-0.1.3 → markback-0.1.5}/.claude/settings.local.json +7 -1
  2. {markback-0.1.3 → markback-0.1.5}/.ishipped/card.md +1 -0
  3. {markback-0.1.3 → markback-0.1.5}/PKG-INFO +8 -1
  4. {markback-0.1.3 → markback-0.1.5}/README.md +7 -0
  5. markback-0.1.5/markback/cli.py +122 -0
  6. {markback-0.1.3 → markback-0.1.5}/pyproject.toml +3 -2
  7. markback-0.1.3/markback/cli.py +0 -435
  8. {markback-0.1.3 → markback-0.1.5}/.gitignore +0 -0
  9. {markback-0.1.3 → markback-0.1.5}/IMPLEMENTATION_NOTES.md +0 -0
  10. {markback-0.1.3 → markback-0.1.5}/LICENSE +0 -0
  11. {markback-0.1.3 → markback-0.1.5}/SPEC.md +0 -0
  12. {markback-0.1.3 → markback-0.1.5}/markback/__init__.py +0 -0
  13. {markback-0.1.3 → markback-0.1.5}/markback/config.py +0 -0
  14. {markback-0.1.3 → markback-0.1.5}/markback/linter.py +0 -0
  15. {markback-0.1.3 → markback-0.1.5}/markback/llm.py +0 -0
  16. {markback-0.1.3 → markback-0.1.5}/markback/parser.py +0 -0
  17. {markback-0.1.3 → markback-0.1.5}/markback/types.py +0 -0
  18. {markback-0.1.3 → markback-0.1.5}/markback/workflow.py +0 -0
  19. {markback-0.1.3 → markback-0.1.5}/markback/writer.py +0 -0
  20. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/LICENSE +0 -0
  21. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/README.md +0 -0
  22. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/package-lock.json +0 -0
  23. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/package.json +0 -0
  24. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/src/index.ts +0 -0
  25. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/src/linter.ts +0 -0
  26. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/src/parser.ts +0 -0
  27. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/src/types.ts +0 -0
  28. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/src/writer.ts +0 -0
  29. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/test/linter.test.js +0 -0
  30. {markback-0.1.3 → markback-0.1.5}/packages/markbackjs/tsconfig.json +0 -0
  31. {markback-0.1.3 → markback-0.1.5}/scripts/publish-npm.sh +0 -0
  32. {markback-0.1.3 → markback-0.1.5}/scripts/publish-pypi.sh +0 -0
  33. {markback-0.1.3 → markback-0.1.5}/scripts/publish.sh +0 -0
  34. {markback-0.1.3 → markback-0.1.5}/tests/__init__.py +0 -0
  35. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/compact_source.mb +0 -0
  36. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/errors/content_with_source.mb +0 -0
  37. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/errors/empty_feedback.mb +0 -0
  38. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/errors/malformed_uri.mb +0 -0
  39. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/errors/missing_feedback.mb +0 -0
  40. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/errors/multiple_feedback.mb +0 -0
  41. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/essay.label.txt +0 -0
  42. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/essay.txt +0 -0
  43. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/external_source.mb +0 -0
  44. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/freeform_feedback.mb +0 -0
  45. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/json_feedback.mb +0 -0
  46. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/label_list.mb +0 -0
  47. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/minimal.mb +0 -0
  48. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/multi_record.mb +0 -0
  49. {markback-0.1.3 → markback-0.1.5}/tests/fixtures/with_uri.mb +0 -0
  50. {markback-0.1.3 → markback-0.1.5}/tests/test_cli.py +0 -0
  51. {markback-0.1.3 → markback-0.1.5}/tests/test_config.py +0 -0
  52. {markback-0.1.3 → markback-0.1.5}/tests/test_linter.py +0 -0
  53. {markback-0.1.3 → markback-0.1.5}/tests/test_parser.py +0 -0
  54. {markback-0.1.3 → markback-0.1.5}/tests/test_types.py +0 -0
  55. {markback-0.1.3 → markback-0.1.5}/tests/test_workflow.py +0 -0
  56. {markback-0.1.3 → markback-0.1.5}/tests/test_writer.py +0 -0
@@ -11,7 +11,13 @@
11
11
  "Bash(python3 -m pytest:*)",
12
12
  "Bash(pip3 install:*)",
13
13
  "Bash(.venv/bin/python -m pytest:*)",
14
- "Bash(chmod:*)"
14
+ "Bash(chmod:*)",
15
+ "Bash(grep:*)",
16
+ "Bash(wc:*)",
17
+ "Bash(ls:*)",
18
+ "Bash(pip install:*)",
19
+ "Bash(PYTHONPATH=/src/markback EDITOR=cat python3:*)",
20
+ "Bash(PYTHONPATH=/src/markback python3:*)"
15
21
  ]
16
22
  }
17
23
  }
@@ -2,6 +2,7 @@
2
2
  title: "MarkBack"
3
3
  summary: "Human-writable format for pairing content with labels and feedback."
4
4
  shipped: 2026-01-04
5
+ theme: forest
5
6
  tags: [data-annotation, machine-learning, cli, python, typescript]
6
7
  links:
7
8
  - label: "markback.org"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markback
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: A compact, human-writable format for storing content paired with feedback/labels
5
5
  Project-URL: Homepage, https://github.com/dandriscoll/markback
6
6
  Project-URL: Repository, https://github.com/dandriscoll/markback
@@ -95,10 +95,17 @@ if result.has_errors:
95
95
 
96
96
  ## CLI Usage
97
97
 
98
+ The CLI is available via two commands:
99
+ - `markback` - Full command name
100
+ - `mb` - Convenient shorthand (works on all platforms including Windows)
101
+
102
+ Both commands are functionally identical. Examples below use `markback`, but you can substitute `mb` anywhere.
103
+
98
104
  ### Initialize configuration
99
105
 
100
106
  ```bash
101
107
  markback init
108
+ # or: mb init
102
109
  ```
103
110
 
104
111
  Creates a `.env` file with all configuration options.
@@ -60,10 +60,17 @@ if result.has_errors:
60
60
 
61
61
  ## CLI Usage
62
62
 
63
+ The CLI is available via two commands:
64
+ - `markback` - Full command name
65
+ - `mb` - Convenient shorthand (works on all platforms including Windows)
66
+
67
+ Both commands are functionally identical. Examples below use `markback`, but you can substitute `mb` anywhere.
68
+
63
69
  ### Initialize configuration
64
70
 
65
71
  ```bash
66
72
  markback init
73
+ # or: mb init
67
74
  ```
68
75
 
69
76
  Creates a `.env` file with all configuration options.
@@ -0,0 +1,122 @@
1
+ """MarkBack command-line interface."""
2
+
3
+ import glob
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Annotated
10
+
11
+ import typer
12
+ from rich.console import Console
13
+
14
+ from .writer import write_file
15
+
16
+ err_console = Console(stderr=True)
17
+
18
+
19
+ def get_mb_path(target: Path) -> Path:
20
+ """Get the .mb file path for a target file."""
21
+ return target.with_suffix(target.suffix + ".mb")
22
+
23
+
24
+ def get_feedback_path(directory: Path) -> Path:
25
+ """Get a non-colliding feedback.mb path in the given directory."""
26
+ candidate = directory / "feedback.mb"
27
+ if not candidate.exists():
28
+ return candidate
29
+ counter = 1
30
+ while True:
31
+ candidate = directory / f"feedback-{counter}.mb"
32
+ if not candidate.exists():
33
+ return candidate
34
+ counter += 1
35
+
36
+
37
+ def collect_glob(pattern: str) -> None:
38
+ """Expand a glob pattern and write all matches into a single .mb file."""
39
+ from .types import Record, SourceRef
40
+
41
+ matches = sorted(glob.glob(pattern))
42
+ if not matches:
43
+ err_console.print(f"[red]No files match pattern: {pattern}[/red]")
44
+ raise typer.Exit(1)
45
+
46
+ records = [Record(source=SourceRef(f), feedback="") for f in matches]
47
+
48
+ # Determine output directory from the pattern's parent, defaulting to cwd
49
+ pattern_parent = Path(pattern).parent
50
+ output_dir = pattern_parent if pattern_parent != Path("") else Path(".")
51
+ output_path = get_feedback_path(output_dir)
52
+
53
+ write_file(output_path, records)
54
+ err_console.print(f"Created {output_path} with {len(records)} source(s)")
55
+ open_editor(output_path)
56
+
57
+
58
+ def open_editor(path: Path) -> None:
59
+ """Open the file in the user's editor."""
60
+ editor = os.environ.get("EDITOR") or os.environ.get("VISUAL")
61
+
62
+ if editor:
63
+ subprocess.run([editor, str(path)])
64
+ elif sys.platform == "win32":
65
+ os.startfile(path)
66
+ elif sys.platform == "darwin":
67
+ subprocess.run(["open", str(path)])
68
+ else:
69
+ # Try xdg-open first, then fall back to common editors
70
+ if shutil.which("xdg-open"):
71
+ result = subprocess.run(["xdg-open", str(path)], capture_output=True)
72
+ if result.returncode == 0:
73
+ return
74
+ for fallback in ("nano", "vi"):
75
+ if shutil.which(fallback):
76
+ subprocess.run([fallback, str(path)])
77
+ return
78
+ err_console.print(
79
+ f"[red]No editor found. Set the EDITOR environment variable, e.g.:[/red]\n"
80
+ f" export EDITOR=nano"
81
+ )
82
+ raise typer.Exit(1)
83
+
84
+
85
+ def main(
86
+ target: Annotated[
87
+ str,
88
+ typer.Argument(help="Target file or glob pattern to manage feedback for"),
89
+ ],
90
+ ):
91
+ """MarkBack: Create or view feedback for a target file."""
92
+ # Check if target is a glob pattern
93
+ if any(c in target for c in ('*', '?', '[')):
94
+ collect_glob(target)
95
+ return
96
+
97
+ target = Path(target)
98
+ mb_path = get_mb_path(target)
99
+
100
+ if not mb_path.exists():
101
+ # Create new .mb file
102
+ if not target.exists():
103
+ err_console.print(f"[red]Target file not found: {target}[/red]")
104
+ raise typer.Exit(1)
105
+
106
+ from .types import Record
107
+ record = Record(
108
+ source=target,
109
+ feedback="",
110
+ )
111
+ write_file(mb_path, [record])
112
+
113
+ open_editor(mb_path)
114
+
115
+
116
+ def cli():
117
+ """Entry point for the CLI."""
118
+ typer.run(main)
119
+
120
+
121
+ if __name__ == "__main__":
122
+ cli()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "markback"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "A compact, human-writable format for storing content paired with feedback/labels"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -48,7 +48,8 @@ dev = [
48
48
  ]
49
49
 
50
50
  [project.scripts]
51
- markback = "markback.cli:app"
51
+ markback = "markback.cli:cli"
52
+ mb = "markback.cli:cli"
52
53
 
53
54
  [tool.pytest.ini_options]
54
55
  testpaths = ["tests"]
@@ -1,435 +0,0 @@
1
- """MarkBack command-line interface."""
2
-
3
- import json
4
- import sys
5
- from pathlib import Path
6
- from typing import Annotated, Optional
7
-
8
- import typer
9
- from rich.console import Console
10
- from rich.table import Table
11
-
12
- from .config import Config, init_env, load_config, validate_config
13
- from .linter import format_diagnostics, lint_files, summarize_results
14
- from .parser import parse_file, parse_string
15
- from .types import Severity, parse_feedback
16
- from .writer import OutputMode, normalize_file, write_file, write_records_multi
17
-
18
- app = typer.Typer(
19
- name="markback",
20
- help="MarkBack: A compact format for content + feedback",
21
- no_args_is_help=True,
22
- )
23
-
24
- console = Console()
25
- err_console = Console(stderr=True)
26
-
27
-
28
- @app.command()
29
- def init(
30
- path: Annotated[
31
- Path,
32
- typer.Argument(help="Path to create .env file"),
33
- ] = Path(".env"),
34
- force: Annotated[
35
- bool,
36
- typer.Option("--force", "-f", help="Overwrite existing file"),
37
- ] = False,
38
- ):
39
- """Initialize a .env configuration file."""
40
- if init_env(path, force=force):
41
- console.print(f"[green]Created {path}[/green]")
42
- else:
43
- console.print(f"[yellow]{path} already exists. Use --force to overwrite.[/yellow]")
44
- raise typer.Exit(1)
45
-
46
-
47
- @app.command()
48
- def lint(
49
- paths: Annotated[
50
- list[Path],
51
- typer.Argument(help="Files or directories to lint"),
52
- ],
53
- output_json: Annotated[
54
- bool,
55
- typer.Option("--json", "-j", help="Output as JSON"),
56
- ] = False,
57
- no_source_check: Annotated[
58
- bool,
59
- typer.Option("--no-source-check", help="Skip checking if @source files exist"),
60
- ] = False,
61
- no_canonical_check: Annotated[
62
- bool,
63
- typer.Option("--no-canonical-check", help="Skip canonical format check"),
64
- ] = False,
65
- ):
66
- """Lint MarkBack files."""
67
- results = lint_files(
68
- paths,
69
- check_sources=not no_source_check,
70
- check_canonical=not no_canonical_check,
71
- )
72
-
73
- summary = summarize_results(results)
74
-
75
- # Collect all diagnostics
76
- all_diagnostics = []
77
- for result in results:
78
- all_diagnostics.extend(result.diagnostics)
79
-
80
- if output_json:
81
- output = {
82
- "summary": summary,
83
- "diagnostics": [d.to_dict() for d in all_diagnostics],
84
- }
85
- console.print(json.dumps(output, indent=2))
86
- else:
87
- # Print diagnostics
88
- for d in all_diagnostics:
89
- if d.severity == Severity.ERROR:
90
- err_console.print(f"[red]{d}[/red]")
91
- else:
92
- err_console.print(f"[yellow]{d}[/yellow]")
93
-
94
- # Print summary
95
- console.print()
96
- console.print(f"Files: {summary['files']}")
97
- console.print(f"Records: {summary['records']}")
98
- console.print(f"Errors: {summary['errors']}")
99
- console.print(f"Warnings: {summary['warnings']}")
100
-
101
- # Exit with error code if there were errors
102
- if summary["errors"] > 0:
103
- raise typer.Exit(1)
104
-
105
-
106
- @app.command()
107
- def normalize(
108
- input_path: Annotated[
109
- Path,
110
- typer.Argument(help="Input MarkBack file"),
111
- ],
112
- output_path: Annotated[
113
- Optional[Path],
114
- typer.Argument(help="Output file (omit for in-place)"),
115
- ] = None,
116
- in_place: Annotated[
117
- bool,
118
- typer.Option("--in-place", "-i", help="Modify input file in place"),
119
- ] = False,
120
- ):
121
- """Normalize a MarkBack file to canonical format."""
122
- try:
123
- content = normalize_file(
124
- input_path,
125
- output_path=output_path,
126
- in_place=in_place or (output_path is None),
127
- )
128
-
129
- if output_path:
130
- console.print(f"[green]Wrote {output_path}[/green]")
131
- elif in_place:
132
- console.print(f"[green]Normalized {input_path}[/green]")
133
- else:
134
- console.print(content)
135
-
136
- except ValueError as e:
137
- err_console.print(f"[red]Error: {e}[/red]")
138
- raise typer.Exit(1)
139
-
140
-
141
- @app.command("list")
142
- def list_records(
143
- paths: Annotated[
144
- list[Path],
145
- typer.Argument(help="Files or directories to list"),
146
- ],
147
- output_json: Annotated[
148
- bool,
149
- typer.Option("--json", "-j", help="Output as JSON"),
150
- ] = False,
151
- ):
152
- """List records in MarkBack files."""
153
- all_records = []
154
-
155
- for path in paths:
156
- if path.is_dir():
157
- for mb_file in path.glob("**/*.mb"):
158
- result = parse_file(mb_file)
159
- for record in result.records:
160
- all_records.append((mb_file, record))
161
- else:
162
- result = parse_file(path)
163
- for record in result.records:
164
- all_records.append((path, record))
165
-
166
- if output_json:
167
- output = []
168
- for file_path, record in all_records:
169
- output.append({
170
- "file": str(file_path),
171
- "uri": record.uri,
172
- "source": str(record.source) if record.source else None,
173
- "feedback": record.feedback,
174
- "has_content": record.has_inline_content(),
175
- })
176
- console.print(json.dumps(output, indent=2))
177
- else:
178
- table = Table(show_header=True)
179
- table.add_column("URI", style="cyan")
180
- table.add_column("Source", style="green")
181
- table.add_column("Feedback", style="white")
182
-
183
- for file_path, record in all_records:
184
- uri = record.uri or "-"
185
- source = str(record.source) if record.source else "-"
186
- # Truncate feedback for display
187
- feedback = record.feedback
188
- if len(feedback) > 50:
189
- feedback = feedback[:47] + "..."
190
-
191
- table.add_row(uri, source, feedback)
192
-
193
- console.print(table)
194
- console.print(f"\nTotal: {len(all_records)} records")
195
-
196
-
197
- @app.command()
198
- def convert(
199
- input_path: Annotated[
200
- Path,
201
- typer.Argument(help="Input MarkBack file or directory"),
202
- ],
203
- output_path: Annotated[
204
- Path,
205
- typer.Argument(help="Output file or directory"),
206
- ],
207
- to: Annotated[
208
- str,
209
- typer.Option("--to", "-t", help="Output format: single, multi, compact, paired"),
210
- ] = "multi",
211
- ):
212
- """Convert between MarkBack storage modes."""
213
- # Parse input
214
- if input_path.is_dir():
215
- from .parser import parse_directory
216
- result = parse_directory(input_path)
217
- else:
218
- result = parse_file(input_path)
219
-
220
- if result.has_errors:
221
- err_console.print("[red]Cannot convert file with errors[/red]")
222
- for d in result.diagnostics:
223
- if d.severity == Severity.ERROR:
224
- err_console.print(f"[red]{d}[/red]")
225
- raise typer.Exit(1)
226
-
227
- records = result.records
228
-
229
- # Convert to output format
230
- mode_map = {
231
- "single": OutputMode.SINGLE,
232
- "multi": OutputMode.MULTI,
233
- "compact": OutputMode.COMPACT,
234
- "paired": OutputMode.PAIRED,
235
- }
236
-
237
- if to not in mode_map:
238
- err_console.print(f"[red]Unknown format: {to}. Use: single, multi, compact, paired[/red]")
239
- raise typer.Exit(1)
240
-
241
- mode = mode_map[to]
242
-
243
- if mode == OutputMode.PAIRED:
244
- # Paired mode: create output directory with label files
245
- output_path.mkdir(parents=True, exist_ok=True)
246
- from .writer import write_paired_files
247
- for i, record in enumerate(records):
248
- # Determine filename from URI or source or index
249
- if record.source:
250
- basename = Path(str(record.source)).stem
251
- elif record.uri:
252
- basename = record.uri.split("/")[-1].split(":")[-1]
253
- else:
254
- basename = f"record_{i:04d}"
255
-
256
- label_path = output_path / f"{basename}.label.txt"
257
- write_paired_files(label_path, None, record)
258
-
259
- console.print(f"[green]Wrote {len(records)} label files to {output_path}[/green]")
260
-
261
- elif mode == OutputMode.SINGLE:
262
- if len(records) != 1:
263
- err_console.print(f"[red]Single mode requires exactly 1 record, got {len(records)}[/red]")
264
- raise typer.Exit(1)
265
- write_file(output_path, records, mode=mode)
266
- console.print(f"[green]Wrote {output_path}[/green]")
267
-
268
- else:
269
- write_file(output_path, records, mode=mode)
270
- console.print(f"[green]Wrote {len(records)} records to {output_path}[/green]")
271
-
272
-
273
- # Workflow subcommand group
274
- workflow_app = typer.Typer(
275
- name="workflow",
276
- help="Editor/Operator LLM workflow commands",
277
- )
278
- app.add_typer(workflow_app, name="workflow")
279
-
280
-
281
- @workflow_app.command("run")
282
- def workflow_run(
283
- dataset: Annotated[
284
- Path,
285
- typer.Argument(help="Path to MarkBack dataset file or directory"),
286
- ],
287
- prompt: Annotated[
288
- str,
289
- typer.Option("--prompt", "-p", help="Initial prompt (or path to prompt file)"),
290
- ] = "",
291
- output: Annotated[
292
- Path,
293
- typer.Option("--output", "-o", help="Output file for results"),
294
- ] = Path("workflow_result.json"),
295
- env_file: Annotated[
296
- Optional[Path],
297
- typer.Option("--env", "-e", help="Path to .env file"),
298
- ] = None,
299
- ):
300
- """Run the editor/operator workflow on a dataset."""
301
- from .workflow import run_workflow, save_workflow_result
302
-
303
- # Load config
304
- config = load_config(env_file)
305
-
306
- # Validate config
307
- issues = validate_config(config)
308
- if issues:
309
- for issue in issues:
310
- err_console.print(f"[yellow]Config warning: {issue}[/yellow]")
311
-
312
- if config.editor is None or config.operator is None:
313
- err_console.print("[red]Editor and Operator LLMs must be configured in .env[/red]")
314
- raise typer.Exit(1)
315
-
316
- # Load initial prompt
317
- initial_prompt = prompt
318
- if prompt and Path(prompt).exists():
319
- initial_prompt = Path(prompt).read_text(encoding="utf-8")
320
-
321
- # Load dataset
322
- if dataset.is_dir():
323
- from .parser import parse_directory
324
- result = parse_directory(dataset)
325
- else:
326
- result = parse_file(dataset)
327
-
328
- if result.has_errors:
329
- err_console.print("[red]Dataset has errors:[/red]")
330
- for d in result.diagnostics:
331
- if d.severity == Severity.ERROR:
332
- err_console.print(f"[red]{d}[/red]")
333
- raise typer.Exit(1)
334
-
335
- if not result.records:
336
- err_console.print("[red]No records found in dataset[/red]")
337
- raise typer.Exit(1)
338
-
339
- console.print(f"Loaded {len(result.records)} records from {dataset}")
340
- console.print("Running workflow...")
341
-
342
- try:
343
- workflow_result = run_workflow(config, initial_prompt, result.records)
344
-
345
- # Save result
346
- output_file = save_workflow_result(workflow_result, output, config)
347
- console.print(f"[green]Results saved to {output_file}[/green]")
348
-
349
- # Print summary
350
- console.print("\n[bold]Workflow Results:[/bold]")
351
- console.print(f"Refined prompt length: {len(workflow_result.refined_prompt)} chars")
352
- console.print(f"Outputs generated: {len(workflow_result.outputs)}")
353
-
354
- eval_result = workflow_result.evaluation
355
- console.print(f"\n[bold]Evaluation:[/bold]")
356
- console.print(f"Total: {eval_result['total']}")
357
- console.print(f"Correct: {eval_result['correct']}")
358
- console.print(f"Incorrect: {eval_result['incorrect']}")
359
- console.print(f"Accuracy: {eval_result['accuracy']:.1%}")
360
-
361
- except Exception as e:
362
- err_console.print(f"[red]Workflow error: {e}[/red]")
363
- raise typer.Exit(1)
364
-
365
-
366
- @workflow_app.command("evaluate")
367
- def workflow_evaluate(
368
- results_file: Annotated[
369
- Path,
370
- typer.Argument(help="Path to workflow results JSON"),
371
- ],
372
- output_json: Annotated[
373
- bool,
374
- typer.Option("--json", "-j", help="Output as JSON"),
375
- ] = False,
376
- ):
377
- """Show evaluation details from a workflow run."""
378
- if not results_file.exists():
379
- err_console.print(f"[red]File not found: {results_file}[/red]")
380
- raise typer.Exit(1)
381
-
382
- data = json.loads(results_file.read_text(encoding="utf-8"))
383
- evaluation = data.get("evaluation", {})
384
-
385
- if output_json:
386
- console.print(json.dumps(evaluation, indent=2))
387
- else:
388
- console.print("[bold]Evaluation Summary:[/bold]")
389
- console.print(f"Total: {evaluation.get('total', 0)}")
390
- console.print(f"Correct: {evaluation.get('correct', 0)}")
391
- console.print(f"Incorrect: {evaluation.get('incorrect', 0)}")
392
- console.print(f"Accuracy: {evaluation.get('accuracy', 0):.1%}")
393
-
394
- details = evaluation.get("details", [])
395
- if details:
396
- console.print("\n[bold]Details:[/bold]")
397
- for d in details:
398
- status = "[green]PASS[/green]" if d.get("match") else "[red]FAIL[/red]"
399
- uri = d.get("uri") or f"record {d.get('record_idx')}"
400
- console.print(f" {status} {uri}: expected={d.get('expected_label')}")
401
-
402
-
403
- @workflow_app.command("prompt")
404
- def workflow_prompt(
405
- results_file: Annotated[
406
- Path,
407
- typer.Argument(help="Path to workflow results JSON"),
408
- ],
409
- output: Annotated[
410
- Optional[Path],
411
- typer.Option("--output", "-o", help="Save prompt to file"),
412
- ] = None,
413
- ):
414
- """Extract the refined prompt from a workflow run."""
415
- if not results_file.exists():
416
- err_console.print(f"[red]File not found: {results_file}[/red]")
417
- raise typer.Exit(1)
418
-
419
- data = json.loads(results_file.read_text(encoding="utf-8"))
420
- prompt = data.get("refined_prompt", "")
421
-
422
- if output:
423
- output.write_text(prompt, encoding="utf-8")
424
- console.print(f"[green]Saved prompt to {output}[/green]")
425
- else:
426
- console.print(prompt)
427
-
428
-
429
- def main():
430
- """Entry point for the CLI."""
431
- app()
432
-
433
-
434
- if __name__ == "__main__":
435
- main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes