| """Score stage — two-layer WAM rubric for core papers (strong tier). |
| |
| Produces ``scores_json = {general, wam, weighted_total, rationale}``. The weighted total is |
| computed over *available* (non-"N/A") metrics using config weights; the top-4 WAM metrics are |
| weighted 2x by default so they dominate. Capped at ``constants.analyze_cap`` per run. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import sqlite3 |
|
|
| from wam.config import Config |
| from wam.llm import LLMClient |
| from wam.logging import get_logger |
| from wam.pipeline.schemas import ScoreCard |
| from wam.store import papers as ps |
|
|
| log = get_logger("pipeline.score") |
|
|
| SYSTEM = ("Score the paper on the two-layer WAM rubric. Use the profile's definitions and " |
| "scoring guidance. Output 0-10 per metric, or \"N/A\" when the paper does not address " |
| "a metric (do NOT guess). Be skeptical of self-reported numbers. Write the " |
| "rationale in English.\n\n" |
| "--- PROFILE ---\n{profile}") |
|
|
|
|
| def weighted_total(card: ScoreCard, cfg: Config) -> float: |
| gw = cfg.get("scoring.general_weights", {}) or {} |
| ww = cfg.get("scoring.wam_weights", {}) or {} |
| num = den = 0.0 |
| for metric, weight in gw.items(): |
| v = getattr(card.general, metric, "N/A") |
| if isinstance(v, int): |
| num += weight * v |
| den += weight |
| for metric, weight in ww.items(): |
| v = getattr(card.wam, metric, "N/A") |
| if isinstance(v, int): |
| num += weight * v |
| den += weight |
| return round(num / den, 2) if den else 0.0 |
|
|
|
|
| def run(cfg: Config, client: LLMClient, conn: sqlite3.Connection, |
| limit: int | None = None) -> int: |
| cap = limit or int(cfg.get("constants.analyze_cap", 40)) |
| system = SYSTEM.format(profile=cfg.profile_text()) |
| todo = ps.needs_score(conn, limit=cap) |
| log.info("scoring %d analyzed papers (cap=%d)", len(todo), cap) |
| done = 0 |
| for row in todo: |
| analysis = conn.execute("SELECT summary_json, analysis_json FROM papers WHERE id=?", |
| (row["id"],)).fetchone() |
| ctx = (f"Title: {row['title']}\n\nAbstract: {row['abstract'] or '(none)'}\n\n" |
| f"Summary: {analysis['summary_json']}\n\nAnalysis: {analysis['analysis_json']}") |
| try: |
| card = client.complete_json("score", system, ctx, ScoreCard, |
| label="score", max_tokens=5000) |
| except Exception as e: |
| log.warning("score failed for %s: %s", row["id"], e) |
| continue |
| payload = card.model_dump() |
| payload["weighted_total"] = weighted_total(card, cfg) |
| ps.set_scores(conn, row["id"], json.dumps(payload)) |
| done += 1 |
| if done % 10 == 0: |
| conn.commit() |
| conn.commit() |
| log.info("scored %d papers", done) |
| return done |
|
|