Overview
This tool assembles Markdown from patterns, calls an LLM with a structured prompt, and writes a report as Markdown (and optionally PDF). It’s driven by EDN config, meaning you can change behavior by editing data, not code.
Why it’s cool:
- Configuration-as-data (EDN): composable, Git-friendly, reproducible.
- Clear pipeline: collect → call LLM → write → optional PDF and summary.
- Smart outputs: timestamped filenames avoid collisions with minimal friction.
- Extensible “agent” model: the LLM call is just a map merged into agent/call.
- Minimal code, lots of leverage (Pandoc, glob patterns, simple IO).
High-level data flow
flowchart TD
    A[Start] --> B[load-config (EDN path or map)]
    B --> C[mru/aggregate-md-from-patterns]
    C --> D[agent/call (merge model + {:system :pre :prompt})]
    D --> E[resolve-output-file]
    E --> F[Write main .md]
    F --> G{pdf?}
    G -- Yes --> H[Pandoc md->pdf]
    G -- No --> I[Skip]
    F --> J{summary?}
    J -- Yes --> K[agent/call for summary]
    K --> L[Write _summary.md]
    J -- No --> I
    H --> I[Done]
    L --> I
Code walkthrough (what each function does)
(ns margin-mania.reporting.compare-reviews
  (:require [clojure.edn :as edn]
            [pyjama.core :as agent]
            [margin-mania.reporting.utils :as mru]
            [pyjama.tools.pandoc]
            [clojure.java.io :as io])
  (:import (java.io File PushbackReader)
           (java.time LocalDateTime)
           (java.time.format DateTimeFormatter)))
- pyjama.core/agent: abstraction over the LLM call (- agent/call).
- mru/aggregate-md-from-patterns: globs files and concatenates Markdown (plus optional metadata).
- pyjama.tools.pandoc: converts Markdown to PDF.
load-config
(defn load-config [cfg]
  (cond
    (string? cfg)
    (with-open [r (io/reader cfg)]
      (edn/read (PushbackReader. r)))
    (map? cfg) cfg
    :else (throw (ex-info "Unsupported config type" {:given cfg}))))
- Accepts either a path to EDN or a pre-built map.
- Encourages configuration-as-data and REPL ergonomics.
Timestamp and output resolution
(defn ^:private timestamp []
  (.format (LocalDateTime/now)
           (DateTimeFormatter/ofPattern "yyyy-MM-dd_HH-mm-ss")))
(defn resolve-output-file
  "Return the actual File to write to.
   If out-file is a directory or has no extension, use <dir>/<yyyy-MM-dd_HH-mm-ss>.md."
  [out-file]
  (let [f (io/file out-file)
        as-dir? (or (.isDirectory f)
                    (not (re-find #"\.[^/\\]+$" (.getName f))))]
    (if as-dir?
      (io/file f (str (timestamp) ".md"))
      f)))
- If :out-fileis a directory or lacks an extension, it auto-generates a timestamped filename, e.g.2025-08-27_16-30-12.md.
Summary file helper
(defn ^:private summary-file
  "Given the primary output file, return the summary file: <same path> with `_summary.md`."
  ^File [^File final-file]
  (let [parent (.getParentFile final-file)
        name (.getName final-file)
        base (if (re-find #"\.md$" name)
               (subs name 0 (- (count name) 3))
               name)]
    (io/file parent (str base "_summary.md"))))
- Takes the main report path and returns the companion summary filename (e.g., report.md→report_summary.md).
The main engine: process-review
(defn process-review
  "If :summary true, performs a second LLM call over the first call's output and writes
   `<previous out-file>_summary.md`."
  [{:keys [patterns model out-file system pre pdf summary]}]
  (let [combined-md (mru/aggregate-md-from-patterns patterns)
        result-1 (agent/call
                   (merge model
                          {:system system
                           :pre    pre
                           :prompt [combined-md]}))
        final-file (resolve-output-file out-file)
        out-1-str (with-out-str (println result-1))]
    ;; write main result
    (io/make-parents final-file)
    (spit final-file out-1-str)
    ;; optional PDF for main result
    (when pdf
      (pyjama.tools.pandoc/md->pdf
        {:input  final-file
         :output (str final-file ".pdf")}))
    ;; optional summary step
    (when summary
      (let [sum-pre "Generated a short summary, (with title and points just like a ppt slide)  of %s"
            result-2 (agent/call
                       (merge model
                              {:system system
                               :pre    sum-pre
                               :prompt [out-1-str]}))
            sum-file (summary-file final-file)]
        (io/make-parents sum-file)
        (spit sum-file (with-out-str (println result-2)))))
    ;; return the path(s) for convenience
    {:out     (.getPath final-file)
     :summary (when summary (.getPath (summary-file final-file)))
     :pdf     (when pdf (str final-file ".pdf"))}))
- Aggregates input Markdown per :patterns, then calls the LLM once to produce the main report.
- Writes the main .md, and if:pdf true, renders a PDF via Pandoc.
- If :summary true, performs a second LLM call on the first output and writes..._summary.md.
- Returns a map of produced paths for convenience.
Notes: