The last stage. Takes what gr_extract() found and writes it up section by
section, against an outline fixed in advance, with every section citing the
rows it rests on.
Usage
gr_synthesise(
extraction,
protocol = NULL,
outline = NULL,
question = NULL,
client = NULL,
model = NULL,
max_section_tokens = 1200L,
temperature = NULL,
include_unclear = FALSE,
cite_style = c("auto", "marker", "author-year", "numeric"),
bib = NULL,
style = NULL,
coherence = FALSE,
references = TRUE,
claims = NULL,
gaps = NULL,
trace = NULL
)Arguments
- extraction
A
gr_extractionfromgr_extract(), or a data frame shaped like its$table.- protocol
A
gr_protocol(); itsoutlineandquestionare used unless you give them directly.- outline
The sections, as a named character vector: names are headings, values say what that section has to cover. As
gr_protocol().- question
The review question, for framing.
- client
A
gr_client.- model, max_section_tokens, temperature
Overrides for the writing calls.
- include_unclear
Write from rows whose extraction was incomplete. Off by default: a row with nothing in it contributes nothing but its own absence, and the count of skipped rows is reported either way.
- cite_style
How citations appear in the finished prose.
"auto"(the default) names the studies when the table can name all of them and uses markers when it cannot."author-year"asks for names and warns if they cannot be produced;"numeric"gives(1, 2);"marker"leaves[study 1]as written.The model always writes
[study N], whatever this is set to, and the rendering happens afterwards from the table. That is deliberate: a marker can be checked exactly against the rows that exist, whereas verifying an author-year string would mean matching a name the model wrote against a name in the table, and near-misses (Smith for Smyth, 2019 for 2018) are both the errors that matter and the ones fuzzy matching forgives. A rendered citation is therefore a fact about the extraction rather than something the model asserted.$sections$text_markedand$text_markedkeep the marker form so the check can be re-run on the published prose.- bib
Which columns carry bibliographic identity, as a named list of
citation,authors,year,title,venue,doi. Omitted, the conventional names are looked for, which is whygr_protocols("bibliography")works without configuration. The most reliable route is acitationfield asked for during extraction: parsing an arbitrary author list is a heuristic, and where it cannot be done confidently the run falls back to markers rather than printing a name that may be wrong.- style
A register instruction appended to the writing prompts, such as
"formal academic; hedge claims; past tense for findings". It governs how sections are written, never what they may say: the rules about citing every claim and inventing nothing hold whatever voice is asked for.- coherence
Revision passes over the finished draft:
TRUEfor all three,FALSEfor none, or any of"structure"(reorder and merge),"cut"(remove repetition) and"register"(polish sentences) by name. Each pass is forbidden from doing the others' job, and each is discarded (with$draftkept) if it changed the citations, arrived truncated, or strengthened a claim. Off by default: each is a call, and each is a chance for a model to touch finished prose.- references
Append a
## Referencessection built from the studies the finished text actually cites. Alphabetical under"author-year", numbered by study otherwise: the list is labelled by whatever the prose uses to point into it.- claims
A
gr_claims()result. With it each section argues that section's claims and sees only the studies those claims rest on, rather than being handed every study and writing a paragraph per row. Needs anoutlinefromgr_outline(), which carries the assignment.- gaps
A
gr_gaps()result, or lines of text. Given to the closing sectiongr_outline()named, with an instruction to state those gaps and no others.- trace
A
gr_trace()to fold this write-up's accounting into, as ingr_read_many(). It is a parent, not this stage's counter:$traceis still the write-up's own, sogr_options(max_calls =)bounds the write-up rather than being spent by the screening that came before it.
Value
An object of class gr_synthesis:
textThe whole write-up, as markdown, citations rendered and the reference list appended.
text_markedThe same document with
[study N]markers intact: what the citation check ran on.draftThe write-up before the coherence pass, for comparison.
referencesThe reference list, or
NULL.cite_styleThe style actually used, which is not always the one asked for.
coherenceOne row per revision pass, or
NULLif none ran:pass(which of"structure","cut","register"),ran,kept,lostandadded(citations, if the revision changed them), andreasonfor anything discarded.sectionsOne row per section:
section,brief,text,n_cited,n_unknown,partial.citationsEvery citation, resolved to the row it points at, in long form:
section,study,document,document_id.studiesThe rows that were written from, with the
studynumber each was cited by.traceAs
gr_extract().
Which rows are used
Rows that were never read (status "failed" or "skipped") are left out,
and so are duplicates: a study counted twice is the error this whole
pipeline exists to avoid, and gr_read_many() has already marked them. The
number left out is reported by print() and is in $skipped.
What it costs
One call per section when the table fits one prompt, which is the usual case: a hundred rows of a ten-field schema is a few thousand tokens. A table too large for one prompt is written in batches and merged, so a section costs batches + merges instead. Either way the cost is per section, not per document: the expensive reading has already happened.
Examples
fields <- gr_fields(design = "The study design",
n = gr_field("Participants", type = "integer"))
cl <- gr_mock_client(function(messages, params) {
seen <- paste(vapply(messages, function(m) as.character(m$content), character(1)),
collapse = " ")
if (grepl("<studies>", seen, fixed = TRUE)) "One randomised trial of 120 people [study 1]."
else '{"design":"randomised trial","n":120,
"design__quote":"We ran a randomised trial.",
"n__quote":"We enrolled 120 people."}'
})
f <- tempfile(fileext = ".txt")
writeLines("We ran a randomised trial. We enrolled 120 people.", f)
x <- gr_extract(f, fields, client = cl)
#> [1/1] file1dcd1821ed84.txt
#> Extracting 'file1dcd1821ed84.txt' with the 'txt' extractor.
#> Ingested 1 block(s), ~18 tokens (0 chars removed by cleaning).
#> Segmenting with 'structural' (cap 900 tokens, overlap 90).
#> Reading with 'extract' (all|N+conflicts|none) over 1 chunk(s).
s <- gr_synthesise(x, question = "Does it work?",
outline = c("Included studies" = "How many, of what design"),
client = cl)
#> [1/1] Included studies
s$sections[, c("section", "n_cited", "n_unknown")]
#> section n_cited n_unknown
#> 1 Included studies 1 0