1515from quorabust .persist import load_classifier
1616
1717_REQUIRED_COLUMNS = {"question1" , "question2" , "is_duplicate" }
18+ _METADATA_KEYS = [
19+ "feature_backend" ,
20+ "feature_schema" ,
21+ "n_train" ,
22+ "n_eval" ,
23+ "seed" ,
24+ "quorabust_version" ,
25+ "git_revision" ,
26+ "csv_sha256" ,
27+ ]
1828
1929
2030def _package_version () -> str :
@@ -53,6 +63,18 @@ def _markdown_table(headers: list[str], rows: list[list[Any]]) -> str:
5363 return "\n " .join (out )
5464
5565
66+ def _metadata_from_meta (meta : dict [str , Any ]) -> dict [str , Any ]:
67+ return {k : meta [k ] for k in _METADATA_KEYS if k in meta }
68+
69+
70+ def _persisted_metrics_from_meta (meta : dict [str , Any ]) -> dict [str , float ]:
71+ return {
72+ k .removeprefix ("eval_" ): float (v )
73+ for k , v in sorted (meta .items ())
74+ if k .startswith ("eval_" ) and isinstance (v , int | float )
75+ }
76+
77+
5678def evaluate_holdout (
5779 builder : Any ,
5880 clf : Any ,
@@ -100,22 +122,8 @@ def render_model_card(
100122 ["generated_by" , f"Quorabust { _package_version ()} " ],
101123 ]
102124
103- metadata_keys = [
104- "feature_backend" ,
105- "feature_schema" ,
106- "n_train" ,
107- "n_eval" ,
108- "seed" ,
109- "quorabust_version" ,
110- "git_revision" ,
111- "csv_sha256" ,
112- ]
113- metadata_rows = [[k , meta [k ]] for k in metadata_keys if k in meta ]
114- persisted_metric_rows = [
115- [k .removeprefix ("eval_" ), meta [k ]]
116- for k in sorted (meta )
117- if k .startswith ("eval_" ) and isinstance (meta [k ], int | float )
118- ]
125+ metadata_rows = [[k , v ] for k , v in _metadata_from_meta (meta ).items ()]
126+ persisted_metric_rows = [[k , v ] for k , v in _persisted_metrics_from_meta (meta ).items ()]
119127
120128 parts = [
121129 "# Quorabust Model Card" ,
@@ -200,9 +208,56 @@ def render_model_card(
200208 return "\n " .join (parts )
201209
202210
211+ def build_report_payload (
212+ * ,
213+ artifact : str ,
214+ meta : dict [str , Any ],
215+ holdout_metrics : dict [str , Any ] | None = None ,
216+ ) -> dict [str , Any ]:
217+ """Build a machine-readable report payload for CI and model comparisons."""
218+ payload : dict [str , Any ] = {
219+ "artifact" : artifact ,
220+ "generated_by" : f"Quorabust { _package_version ()} " ,
221+ "intended_use" : (
222+ "Scores pairs of short natural-language questions and returns the probability "
223+ "that the pair is semantically duplicate."
224+ ),
225+ "training_metadata" : _metadata_from_meta (meta ),
226+ "persisted_evaluation" : _persisted_metrics_from_meta (meta ),
227+ "serving_contract" : {
228+ "predict" : "POST /predict" ,
229+ "metrics" : "GET /metrics" ,
230+ "input" : {"question1" : "list[str]" , "question2" : "list[str]" },
231+ "output" : {"proba_duplicate" : "list[float]" },
232+ },
233+ "caveats" : [
234+ "Performance depends on the training data distribution and threshold." ,
235+ "Re-run on a current holdout set before comparing artifacts." ,
236+ ],
237+ }
238+ if holdout_metrics is not None :
239+ payload ["holdout_evaluation" ] = {
240+ k : v
241+ for k , v in holdout_metrics .items ()
242+ if k not in {"tn" , "fp" , "fn" , "tp" }
243+ }
244+ payload ["confusion_matrix" ] = {
245+ "labels" : ["not_duplicate" , "duplicate" ],
246+ "actual_0" : {
247+ "predicted_0" : holdout_metrics ["tn" ],
248+ "predicted_1" : holdout_metrics ["fp" ],
249+ },
250+ "actual_1" : {
251+ "predicted_0" : holdout_metrics ["fn" ],
252+ "predicted_1" : holdout_metrics ["tp" ],
253+ },
254+ }
255+ return payload
256+
257+
203258def main (argv : list [str ] | None = None ) -> int :
204259 parser = argparse .ArgumentParser (
205- description = "Generate a Markdown model card for a Quorabust artifact." ,
260+ description = "Generate a model report for a Quorabust artifact." ,
206261 )
207262 parser .add_argument ("--model" , type = Path , required = True , help = "Saved .pkl artifact" )
208263 parser .add_argument (
@@ -222,7 +277,13 @@ def main(argv: list[str] | None = None) -> int:
222277 default = None ,
223278 help = "Public artifact label to print instead of the local model path" ,
224279 )
225- parser .add_argument ("--out" , type = Path , default = None , help = "Write Markdown here" )
280+ parser .add_argument (
281+ "--format" ,
282+ choices = ["markdown" , "json" ],
283+ default = "markdown" ,
284+ help = "Report format to print or write" ,
285+ )
286+ parser .add_argument ("--out" , type = Path , default = None , help = "Write report here" )
226287 args = parser .parse_args (argv )
227288
228289 if not args .model .is_file ():
@@ -249,16 +310,28 @@ def main(argv: list[str] | None = None) -> int:
249310 print (str (exc ), file = sys .stderr )
250311 return 1
251312
252- card = render_model_card (
253- artifact = args .artifact_label or str (args .model .resolve ()),
254- meta = meta ,
255- holdout_metrics = holdout_metrics ,
256- )
313+ artifact = args .artifact_label or str (args .model .resolve ())
314+ if args .format == "json" :
315+ report = json .dumps (
316+ build_report_payload (
317+ artifact = artifact ,
318+ meta = meta ,
319+ holdout_metrics = holdout_metrics ,
320+ ),
321+ indent = 2 ,
322+ sort_keys = True ,
323+ )
324+ else :
325+ report = render_model_card (
326+ artifact = artifact ,
327+ meta = meta ,
328+ holdout_metrics = holdout_metrics ,
329+ )
257330 if args .out is None :
258- print (card )
331+ print (report )
259332 else :
260333 args .out .parent .mkdir (parents = True , exist_ok = True )
261- args .out .write_text (card , encoding = "utf-8" )
334+ args .out .write_text (report + " \n " , encoding = "utf-8" )
262335 print (f"wrote { args .out .resolve ()} " )
263336 return 0
264337
0 commit comments