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-file
is 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: