Source code for curated_feed.export

"""Render a corpus file as compact Markdown for pasting into a chat session.

A collection day can run to several hundred items, so the output is deliberately
terse: two lines per item, numbered for reference and carrying the record `id` so
that picks can be traced back to the corpus.

The numbering is the same one `policy.py` puts in the prompt, and comes from the
same renderer. A chat session and a pipeline run therefore refer to an article by
the same number, which is what lets a response saved from one drive the other.
"""

from __future__ import annotations

import argparse
import sys
from collections.abc import Sequence
from pathlib import Path

from .config import Config, ConfigError, load_config
from .models import Candidate
from .policy import build_prompt, render_candidates
from .state import StateError, latest_corpus_path, read_corpus


[docs] def render_markdown( candidates: Sequence[Candidate], *, title: str, max_items: int | None = None ) -> str: """Numbered Markdown under a heading, one item per two lines.""" shown = candidates[:max_items] if max_items else candidates header = f"# {title} — {len(candidates)} items" if len(shown) != len(candidates): header += f" (showing first {len(shown)})" return f"{header}\n\n{render_candidates(shown, with_ids=True)}\n"
[docs] def render( candidates: Sequence[Candidate], *, config: Config, title: str, max_items: int | None = None, as_prompt: bool = False, prompt_path: Path | None = None, ) -> str: """The listing on its own, or the whole prompt the pipeline would send. The prompt form is what a chat session wants: the same policy documents and the same numbering the pipeline uses, so a response saved from that session can be fed straight back with `curate-score --provider file`. """ if as_prompt: shown = candidates[:max_items] if max_items else candidates prompt = build_prompt( shown, policy_path=config.paths.policy, prompt_path=prompt_path ) return prompt.as_text() + "\n" return render_markdown(candidates, title=title, max_items=max_items)
[docs] def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="curate-export", description="Render a corpus file as compact Markdown for review.", ) parser.add_argument( "corpus_file", nargs="?", type=Path, help="corpus file to render (default: the most recent one)", ) parser.add_argument("--config", type=Path, help="path to config.toml") parser.add_argument( "--corpus-dir", type=Path, help="directory holding corpus files" ) parser.add_argument("--max-items", type=int, help="render at most this many items") parser.add_argument("--out", type=Path, help="write to this file instead of stdout") parser.add_argument( "--prompt", action="store_true", help="emit the whole scoring prompt, policy documents included", ) parser.add_argument( "--scoring-prompt", type=Path, metavar="PATH", help="scoring prompt to use instead of the one shipped with the package", ) return parser
[docs] def resolve_corpus_file(args: argparse.Namespace, config: Config) -> Path: if args.corpus_file: path = Path(args.corpus_file) if not path.is_file(): raise StateError(f"corpus file not found: {path}") return path return latest_corpus_path(config.paths.corpus_dir)
[docs] def main(argv: Sequence[str] | None = None) -> int: args = build_parser().parse_args(argv) try: config = load_config(args.config).with_overrides( paths={"corpus_dir": args.corpus_dir} ) path = resolve_corpus_file(args, config) records = read_corpus(path) shown = records[: args.max_items] if args.max_items else records rendered = render( records, config=config, title=path.stem, max_items=args.max_items, as_prompt=args.prompt, prompt_path=args.scoring_prompt, ) except (ConfigError, StateError) as exc: print(f"error: {exc}", file=sys.stderr) return 2 if args.out: args.out.parent.mkdir(parents=True, exist_ok=True) args.out.write_text(rendered, encoding="utf-8") print( f"wrote {len(shown)} of {len(records)} item(s) to {args.out}", file=sys.stderr, ) else: sys.stdout.write(rendered) return 0
if __name__ == "__main__": # pragma: no cover raise SystemExit(main())