feat(prisma): eligible --max-chars, deterministic unavailable-fulltext path, qwen silence fix (refs P43)
This commit is contained in:
@@ -168,6 +168,12 @@ def screen(
|
||||
def eligible(
|
||||
name: str = typer.Argument(..., help="Project slug."),
|
||||
limit: int = typer.Option(0, "--limit", "-n"),
|
||||
max_chars: int = typer.Option(
|
||||
0,
|
||||
"--max-chars",
|
||||
help="Cap per-item markdown (full text) at N chars so bounded-"
|
||||
"context local models keep the criteria in window. 0 = no cap.",
|
||||
),
|
||||
db: Path = typer.Option(_DEFAULT_ZOT, "--db"),
|
||||
storage: Path = typer.Option(_DEFAULT_STORAGE, "--storage"),
|
||||
hold: bool = typer.Option(True, "--hold/--no-hold"),
|
||||
@@ -188,6 +194,7 @@ def eligible(
|
||||
project,
|
||||
storage_dir=storage,
|
||||
limit=limit or None,
|
||||
max_fulltext_chars=max_chars or None,
|
||||
)
|
||||
|
||||
stats = _hold(_go, hold=hold)
|
||||
|
||||
@@ -28,10 +28,17 @@ if TYPE_CHECKING:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_SYSTEM = """\
|
||||
You are a systematic-review assistant running PRISMA stage 3
|
||||
(full-text eligibility). The paper's full text is attached; apply the
|
||||
criteria to the concrete methods, comparator, and outcomes reported.
|
||||
# One-sentence system message: longer system prompts make qwen models
|
||||
# served by Ollama return an EMPTY response (no text, no tool call,
|
||||
# finish=stop) — same quirk screen.py hit; the working guidance lives in
|
||||
# the cached first user message instead.
|
||||
_SYSTEM = "You are a systematic-review assistant running PRISMA stage 3 (full-text eligibility)."
|
||||
|
||||
_GUIDANCE = """\
|
||||
## Task
|
||||
|
||||
The paper's full text follows in the next message. Apply the criteria
|
||||
to the concrete methods, comparator, and outcomes reported.
|
||||
|
||||
Call `record_eligibility_decision` exactly once. Cite the page or
|
||||
section you relied on in `rationale`. If the full text is cut off
|
||||
@@ -47,7 +54,8 @@ def _build_call(project: Project, item_markdown: str) -> LLMCall:
|
||||
LLMMessage(
|
||||
role="user",
|
||||
content=(
|
||||
"## Inclusion / exclusion criteria\n\n"
|
||||
_GUIDANCE
|
||||
+ "\n## Inclusion / exclusion criteria\n\n"
|
||||
+ project.criteria
|
||||
+ "\n\n## Exclusion reason codebook\n\n"
|
||||
+ project.reasons
|
||||
@@ -92,6 +100,9 @@ def _queue(db: Db, project: str, limit: int | None) -> list[int]:
|
||||
return [r[0] for r in rows]
|
||||
|
||||
|
||||
_TRUNCATION_NOTE = "\n\n[... full text truncated to fit the model context ...]\n"
|
||||
|
||||
|
||||
def run(
|
||||
db: Db,
|
||||
provider: Provider,
|
||||
@@ -100,8 +111,16 @@ def run(
|
||||
storage_dir: Path,
|
||||
limit: int | None = None,
|
||||
progress: bool = True,
|
||||
max_fulltext_chars: int | None = None,
|
||||
) -> dict[str, int]:
|
||||
"""Run eligibility screening over the queue."""
|
||||
"""Run eligibility screening over the queue.
|
||||
|
||||
``max_fulltext_chars`` caps the per-item markdown so local models
|
||||
with bounded context windows see the criteria and the front of the
|
||||
paper rather than silently dropping the oldest tokens (Ollama
|
||||
truncates from the head, which would discard the cached criteria
|
||||
message before the paper's methods section).
|
||||
"""
|
||||
ids = _queue(db, project.name, limit)
|
||||
stats = {"assessed": 0, "include": 0, "exclude": 0, "uncertain": 0, "errors": 0}
|
||||
|
||||
@@ -109,6 +128,28 @@ def run(
|
||||
try:
|
||||
snap = load_item(db, zot_id, storage_dir)
|
||||
md = to_markdown(snap, include_fulltext=True)
|
||||
if "\n# Full text\n" not in md:
|
||||
# No retrievable full text — a deterministic PRISMA
|
||||
# outcome, not an LLM question. (Small no-fulltext
|
||||
# prompts also trip the qwen/Ollama empty-response
|
||||
# quirk, so this path must not touch the model.)
|
||||
apply_eligibility_decision(
|
||||
db,
|
||||
zot_id,
|
||||
project.name,
|
||||
{
|
||||
"decision": "uncertain",
|
||||
"reasons": ["unavailable-fulltext"],
|
||||
"rationale": "No full text retrieved by the fetch "
|
||||
"cascade; cannot assess eligibility.",
|
||||
},
|
||||
)
|
||||
db.commit()
|
||||
stats["uncertain"] += 1
|
||||
stats["assessed"] += 1
|
||||
continue
|
||||
if max_fulltext_chars and len(md) > max_fulltext_chars:
|
||||
md = md[:max_fulltext_chars] + _TRUNCATION_NOTE
|
||||
call = _build_call(project, md)
|
||||
result = provider.complete(call)
|
||||
if not result.tool_calls:
|
||||
|
||||
Reference in New Issue
Block a user