diff --git a/README.md b/README.md index 60a292f..0ce99c0 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,23 @@ There are two ways to run it: ## Run with Nextflow +Text, markdown, and JSON reports are all supported. By default, the workflow will split +input into sections, interpret each separately, and then combine the results. Json, will +be split by keys, and markdown by markdown headers. MultiQC report (llms-full.txt) will +be split by MultiQC sections. Avoid chunking with the `--whole-report` flag. + ```bash -nextflow run main.nf --input data/multiqc_data.json +nextflow run main.nf --input data[.txt|.md|.json] --whole-report ``` -To steer the model with your own instruction, add `--prompt`: +A system prompt is included by default. To steer the model with your own additional +instruction, add a `--user`, and to always use your own system prompt (from a file), +add `--system`: ```bash -nextflow run main.nf --input data/multiqc_data.json \ - --prompt "Summarize immune infiltration and flag any tumor-immune interactions." +nextflow run main.nf --input multiqc_data.json \ + --user "Summarize immune infiltration and flag any tumor-immune interactions." + --system "path/to/your/system/prompt.txt" ``` By default this uses the `docker` profile (see `nextflow.config`), which pulls @@ -108,7 +116,7 @@ Common flags (see `python3 llmize.py --help` for the full list): ```bash python3 llmize.py --input data.json \ - --prompt "Summarize immune infiltration and flag any tumor-immune interactions." \ + --user "Summarize immune infiltration and flag any tumor-immune interactions." \ --review --output my_interpretation.md ``` @@ -176,7 +184,9 @@ are set explicitly, e.g. `--think false` or `--review true`. | `--descriptor` | bundled schema | Descriptor schema JSON (JSON mode only), keyed by top-level section name; overlaid onto matching sections to tell the model what each field means. Copy `ingest/descriptor_schema.json` and edit it to describe your own report's sections, then pass the copy here. Sections with no matching entry pass through unannotated. | | `--model` | `gemma4` | Ollama model name. | | `--output` | (none) | Path to the output file; if not specified, defaults to `_interpretation_.md` in the current directory. | -| `--prompt` | (none) | Extra instruction appended to the model prompt. | +| `--user` | (none) | Extra instruction appended to the system prompt for every section. | +| `--system` | bundled default | Path to a file that fully replaces the default system prompt for the analysis stage. | +| `--gpus` | `1` | Number of GPUs to allocate per job. | | `--num_ctx` | `32768` | Context window size. | | `--temperature` | model default | Sampling temperature (`0` = deterministic). | | `--top_p` | model default | Nucleus-sampling threshold. | diff --git a/interpret.py b/interpret.py index b2d4f1e..e7efef1 100644 --- a/interpret.py +++ b/interpret.py @@ -12,6 +12,8 @@ from verify import deterministic_findings, extract_entities from ingest import ( + read_text, + resolve_path, split_text_sections, looks_like_multiqc_llms_full, split_multiqc_llms_full, @@ -20,65 +22,20 @@ SAMPLESHEET_KEY = "multiqc_samplesheet" -STYLE_GUIDE = """ -Style and register (apply throughout): -- Write in the neutral, descriptive register of a peer-reviewed research paper. - Report what the data shows; do not editorialize, rate, or pass judgement on it. -- Remove judgemental and evaluative language. Do NOT use words such as: striking, - remarkable, overwhelming, dominant(ly), extreme, dramatic, impressive, crucial, - notable(ly), interesting, surprising, concerning, alarming, poor, excellent, good, - bad, or superlatives; and do not use exclamations. -- Lead with the data and tie every statement to specific values. Use hedged, precise - verbs for any inference: "suggests", "is consistent with", "indicates", "may", - "potentially". Prefer "associated with" over causal claims ("causes", "drives"). -- Report negative or null results plainly (e.g. "no differential signal was detected"). -- Present per-sample numbers in compact markdown tables rather than long inline lists. -- When presenting summaries, check that all relevant samples are included. -- Use plain formatting only: markdown headers, bold, and tables. Do NOT use LaTeX math - ($...$, \\text{}, \\mathbf{}) or emoji. -- Be concise: do not restate the section name or descriptor, and do not repeat the same - adjective across sentences. -- If a label or abbreviation's meaning is not provided, and it is not a commonly known - acronym, use it verbatim; do not infer or expand what it stands for. -""" - -EVIDENCE_RULES = """ -Evidence and interpretation rules: -- Tie every claim to numeric evidence. Do not infer causality; describe associations only. -- State a between-group difference (for any sample-metadata grouping — e.g. response, - timepoint, region) ONLY if it is consistent across at least 2 samples per group, OR both - the mean and the median support the same direction. Otherwise state plainly: - "No consistent group-level difference detected." -- When you say a group is higher or lower, check that the direction matches the numbers - (e.g. do not call the smaller mean "higher"). -- Do not generalize a pattern driven by a single sample. If one sample drives a group's - mean (an outlier), say so explicitly and treat that group-level claim as weak. -- Assess within-group variability as low / moderate / high. If variability is high or the - trend is inconsistent, prefer a conservative interpretation or report no clear difference. -- Briefly note relevant limitations where they affect a conclusion (e.g. ~3 samples per - group, high within-group variability, outlier influence) instead of implying certainty. -""" - -SYSTEM_PROMPT = """You are an expert bioinformatician. You are given a report generated -by a bioinformatics workflow. The report includes outputs from a number of bioinformatics -tools and include quality control metrics, as well as actual aggregated analysis results. -The report includes the samplesheet table that may contain important clinical metadata. -Different report sections may have different structures and different biological meaning, -and contains a short descriptor of the section's structure and meaning.Your task is to -analyse the data and give a concise summary. -""" + STYLE_GUIDE + EVIDENCE_RULES - -TEXT_SYSTEM_PROMPT = """You are an expert bioinformatician. You are given a plain-text -report produced by a bioinformatics tool or workflow (for example, a MultiQC -"llms-full.txt" export). It has no fixed schema, so infer the meaning of each -section from its heading and content. Your task is to analyse it and give a -concise summary. -""" + STYLE_GUIDE + EVIDENCE_RULES - -TEXT_AS_IS_SYSTEM_PROMPT = """You are an expert bioinformatician. You are given a -plain-text report produced by a bioinformatics tool or workflow. The text already -contains its own analysis instructions and formatting guidance — follow those -instructions exactly as written instead of any separate style guide.""" +_PROMPTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "prompts") + + +def _load_prompt(filename: str) -> str: + """Load a default system prompt from prompts/, so it can be read/edited outside Python.""" + return read_text(os.path.join(_PROMPTS_DIR, filename)).strip() + + +def load_system_prompt_override(path: str) -> str: + """Load a user-supplied file that fully replaces the default analysis system prompt.""" + return read_text(resolve_path(path)).strip() + + +SYSTEM_PROMPT = _load_prompt("system_prompt.md") def build_prompt(report: dict) -> str: @@ -318,13 +275,12 @@ def _round_floats(obj, sig: int = 4): def build_section_prompt(section_name: str, section_obj: dict, groups: dict = None) -> str: - """Build the prompt analysing one MultiQC section.""" + """Build the prompt analysing one report section.""" descriptor, data = _split_descriptor(section_obj) analysis_instructions = descriptor.pop("analysis_instructions", None) prompt = ( - f"Analyze the MultiQC section `{section_name}` in isolation.\n\n" - "Section descriptor (defines what each field/key/value means):\n" + f"Analyze the report section `{section_name}` in isolation.\n\n" f"```json\n{json.dumps(descriptor, indent=2)}\n```\n\n" "Section data:\n" f"```json\n{json.dumps(_round_floats(data), indent=2)}\n```\n\n" @@ -370,54 +326,7 @@ def iter_analysis_sections(report: dict): yield name, section_obj -SYNTHESIS_SYSTEM_PROMPT = """You are an expert bioinformatician. You are given -the per-section analyses of a bioinformatics report. - -Synthesize them into one concise executive summary of a few short paragraphs. -Cover: -- the overall picture and whether the pipeline appears to have run successfully, -- any notable patterns or outliers in the data and other data quality issues, -- the most important quantitative findings, citing key numbers and percentages, -- findings across the report sections that are consistent with each other, or - contradict each other, -- in case contrast analysis is present, provide a clear verdict on whether the - data supports difference across metadata groups, -- in case no metadata present, summary should focus on overall similarity patterns - -Only surface findings that are well supported: consistent across at least 2 -samples per group, or supported by both the mean and the median. Exclude weak, -single-sample-driven, or high-variability findings, or mention them only as not -robust. Do not combine several weak signals into a strong conclusion. Where the -data does not support a group difference, say so plainly. - -Be decisive and readable. Do not repeat each section verbatim or list sections -one by one. Output only the summary prose — do NOT add your own title or markdown -heading. -""" + STYLE_GUIDE + EVIDENCE_RULES - -TEXT_SYNTHESIS_SYSTEM_PROMPT = """You are an expert bioinformatician. You are given -the per-section analyses of a plain-text report produced by a bioinformatics -tool or workflow. - -Synthesize them into one concise executive summary of a few short paragraphs. -Cover: -- the overall picture and whether the report appears internally consistent, -- any notable patterns, outliers, or data-quality issues surfaced across sections, -- the most important quantitative findings, citing key numbers and percentages, -- findings across sections that are consistent with each other, or contradict each other, -- any well-supported group differences only when the report itself provides the needed - grouping context; otherwise focus on overall patterns and limitations. - -Do not repeat each section verbatim or list sections one by one. Output only the -summary prose — do NOT add your own title or markdown heading. -""" + STYLE_GUIDE + EVIDENCE_RULES - -TEXT_AS_IS_SYNTHESIS_SYSTEM_PROMPT = """You are an expert bioinformatician. You are -given the per-section analyses of a plain-text report produced by a bioinformatics -tool or workflow. The report's text already carries its own analysis instructions -and formatting guidance, so preserve that guidance while writing one concise -executive summary across the sections. Output only the summary prose — do NOT add -your own title or markdown heading.""" +SYNTHESIS_SYSTEM_PROMPT = _load_prompt("synthesis_system_prompt.md") def synthesize_sections( @@ -466,7 +375,7 @@ def combine_responses(responses: list, summary: str = None, title: str = None) - return "\n".join(parts) -REVIEW_SYSTEM_PROMPT = """You are an expert bioinformatician reviewing a bioinformatics report interpretation for factual grounding and internal consistency. Correct statements that are not supported by the underlying report data, remove claims about genes, proteins, or cell types that do not appear in the data, and resolve contradictions between sections. Preserve the document's headings, tables, and structure. Do not add new findings and do not soften the removal of unsupported claims. Output only the corrected document, with no preamble.""" + STYLE_GUIDE + EVIDENCE_RULES +REVIEW_SYSTEM_PROMPT = _load_prompt("review_system_prompt.md") def build_review_prompt(text: str, findings: list) -> str: @@ -595,10 +504,12 @@ def interpret_report( think: bool = True, gen_options: dict = None, user_instruction: str = "", + system_prompt_file: str = None, ) -> str: """Interpret the report as one or more chunks; a single chunk is the whole report.""" samplesheet_context = build_samplesheet_context(report) - system = (SYSTEM_PROMPT + samplesheet_context + base_prompt = load_system_prompt_override(system_prompt_file) if system_prompt_file else SYSTEM_PROMPT + system = (base_prompt + samplesheet_context + build_user_instruction(user_instruction)) groups = sample_groups(report) chunks = build_chunks(report, whole_report, groups) @@ -618,16 +529,20 @@ def interpret_text_report( gen_options: dict = None, user_instruction: str = "", as_is: bool = False, + system_prompt_file: str = None, ) -> str: """Interpret a plain-text report (e.g. MultiQC llms-full.txt), bypassing the JSON pipeline. - `as_is` drops the style guide/evidence rules, assuming the text already carries its - own analysis instructions; it does not affect whole-report vs. heading-split chunking. + `as_is` assumes the text already carries its own analysis instructions, so no default + system prompt is used for the analysis stage (MultiQC's own leading instructions are + used instead when present); it does not affect whole-report vs. heading-split chunking. + `system_prompt_file`, when set, takes precedence over both the default and `as_is`. + The synthesis stage always uses SYNTHESIS_SYSTEM_PROMPT, `as_is` or not. """ - base_prompt = TEXT_AS_IS_SYSTEM_PROMPT if as_is else TEXT_SYSTEM_PROMPT - synthesis_prompt = ( - TEXT_AS_IS_SYNTHESIS_SYSTEM_PROMPT if as_is else TEXT_SYNTHESIS_SYSTEM_PROMPT - ) + if system_prompt_file: + base_prompt = load_system_prompt_override(system_prompt_file) + else: + base_prompt = "" if as_is else SYSTEM_PROMPT samplesheet_context = "" if whole_report: chunks = [("Whole report", build_prompt_text(text))] @@ -636,7 +551,7 @@ def interpret_text_report( # `Tool:`/`Section:`/`Title:` block layout instead of falling back to one blob. if looks_like_multiqc_llms_full(text): sections, leading_instructions = split_multiqc_llms_full(text) - if as_is and leading_instructions: + if as_is and leading_instructions and not system_prompt_file: base_prompt = leading_instructions else: sections = split_text_sections(text) @@ -659,7 +574,6 @@ def interpret_text_report( return _run_chunks( chunks, model, system, num_ctx, synthesize_final, think, gen_options, title="Report Interpretation", - synthesis_system_prompt=synthesis_prompt, samplesheet_context=samplesheet_context, ) @@ -876,6 +790,7 @@ def build_run_footer( mode: str = "section-by-section", user_instruction: str = "", input_path: str = None, + system_prompt_file: str = None, ) -> str: """Markdown footer recording run parameters so each output is reproducible.""" gen = gen_options or {} @@ -899,6 +814,7 @@ def g(key): f"top_p={g('top_p')}, top_k={g('top_k')}, num_predict={g('num_predict')}" ) lines.append(f"- num_ctx: {num_ctx}") + lines.append(f"- system prompt: {'custom (' + system_prompt_file + ')' if system_prompt_file else 'default'}") if user_instruction: lines.append(f"- custom prompt: {user_instruction!r}") sha = _git_short_sha() diff --git a/llmize.py b/llmize.py index b29dc4c..767f823 100644 --- a/llmize.py +++ b/llmize.py @@ -116,10 +116,18 @@ def parse_args() -> argparse.Namespace: "(not saved) and kept out of the interpretation. On by default; --no-think disables it (faster).", ) parser.add_argument( - "--prompt", + "--user", + dest="user_instruction", default=None, help="Extra instruction appended to the system prompt for every section.", ) + parser.add_argument( + "--system", + dest="system_prompt_file", + default=None, + help="Path to a file that fully replaces the default system prompt for the " + "analysis stage (style guide and evidence rules included).", + ) parser.add_argument("--temperature", type=float, default=None, help="Sampling temperature (model default if unset; gemma4 defaults to 1).") parser.add_argument("--top_p", type=float, default=None, help="Nucleus sampling top_p.") @@ -175,6 +183,7 @@ def run_pipeline( review: bool = False, review_passes: int = 2, save_intermediates: bool = False, + system_prompt_file: str | None = None, ) -> str: input_path = resolve_input_path(input_json) print(f"[pipeline] Loading raw JSON: {input_path}") @@ -204,6 +213,7 @@ def run_pipeline( think=think, gen_options=gen_options, user_instruction=user_instruction, + system_prompt_file=system_prompt_file, ) if review: @@ -222,6 +232,7 @@ def run_pipeline( model=model, num_ctx=num_ctx, think=think, gen_options=gen_options, mode="whole-report" if whole_report else "section-by-section", user_instruction=user_instruction, input_path=input_path, + system_prompt_file=system_prompt_file, ) save_text(response + footer, output_path) return output_path @@ -238,6 +249,7 @@ def run_text_pipeline( gen_options: dict = None, user_instruction: str = "", as_is: bool = False, + system_prompt_file: str | None = None, ) -> str: input_path = resolve_input_path(input_txt) print(f"[pipeline] Loading raw text: {input_path}") @@ -256,6 +268,7 @@ def run_text_pipeline( gen_options=gen_options, user_instruction=user_instruction, as_is=as_is, + system_prompt_file=system_prompt_file, ) if output_path is None: @@ -269,6 +282,7 @@ def run_text_pipeline( footer = build_run_footer( model=model, num_ctx=num_ctx, think=think, gen_options=gen_options, mode=footer_mode, user_instruction=user_instruction, input_path=input_path, + system_prompt_file=system_prompt_file, ) save_text(response + footer, output_path) return output_path @@ -299,10 +313,11 @@ def main() -> None: synthesize_final=not args.no_synthesis, think=args.think, gen_options=gen_options, - user_instruction=args.prompt, + user_instruction=args.user_instruction, review=args.review, review_passes=args.review_passes, save_intermediates=args.save_intermediates, + system_prompt_file=args.system_prompt_file, ) else: if args.review: @@ -319,8 +334,9 @@ def main() -> None: synthesize_final=not args.no_synthesis, think=args.think, gen_options=gen_options, - user_instruction=args.prompt, + user_instruction=args.user_instruction, as_is=args.as_is, + system_prompt_file=args.system_prompt_file, ) print(f"[pipeline] Completed. Final report: {final_path}") diff --git a/main.nf b/main.nf index 5b49466..d7c5c84 100644 --- a/main.nf +++ b/main.nf @@ -2,12 +2,14 @@ process INTERPRET { tag "${report.baseName}" label "process_gpu" resourceLimits cpus: 2, memory: 48.GB, time: 24.h + accelerator "${params.gpus}" as int container "${params.container}" publishDir params.outdir, mode: 'copy' input: path report path descriptor + path system_prompt output: path "*_interpretation_*.md", emit: interpretation @@ -21,8 +23,9 @@ process INTERPRET { def review_flag = "${params.review}".toBoolean() ? "--review --review-passes ${params.review_passes}" : '' def whole_flag = "${params.whole_report}".toBoolean() ? '--whole-report' : '' def synth_flag = "${params.synthesis}".toBoolean() ? '' : '--no-synthesis' - def prompt_escaped = params.prompt ? params.prompt.toString().replace("'", "'\"'\"'") : '' - def prompt_flag = params.prompt ? "--prompt '${prompt_escaped}'" : '' + def user_escaped = params.user ? params.user.toString().replace("'", "'\"'\"'") : '' + def user_flag = params.user ? "--user '${user_escaped}'" : '' + def system_flag = params.system ? "--system '${system_prompt}'" : '' def temp_flag = params.temperature != null ? "--temperature ${params.temperature}" : '' def seed_flag = params.seed != null ? "--seed ${params.seed}" : '' def top_p_flag = params.top_p != null ? "--top_p ${params.top_p}" : '' @@ -43,7 +46,7 @@ process INTERPRET { --model '${params.model}' \\ --num_ctx ${params.num_ctx} \\ ${think_flag} ${review_flag} ${whole_flag} ${synth_flag} \\ - ${prompt_flag} ${temp_flag} ${seed_flag} ${top_p_flag} \\ + ${user_flag} ${system_flag} ${temp_flag} ${seed_flag} ${top_p_flag} \\ ${top_k_flag} ${numpred_flag} \\ --output "${report.baseName}_interpretation_\${STAMP}.md" """ @@ -55,6 +58,7 @@ workflow { ch_input = channel.fromPath(params.input, checkIfExists: true) ch_descriptor = channel.fromPath(params.descriptor, checkIfExists: true).first() + ch_system = params.system ? channel.fromPath(params.system, checkIfExists: true).first() : channel.value([]) - INTERPRET(ch_input, ch_descriptor) + INTERPRET(ch_input, ch_descriptor, ch_system) } diff --git a/nextflow.config b/nextflow.config index 84d5baf..e3631f9 100644 --- a/nextflow.config +++ b/nextflow.config @@ -8,7 +8,8 @@ params { review_passes = 2 whole_report = false synthesis = true - prompt = null + user = null + system = null num_ctx = 32768 temperature = null seed = null @@ -17,6 +18,7 @@ params { num_predict = null ollama_models_dir = null container = 'ghcr.io/fertiglab/llmize:latest' + gpus = 1 } @@ -46,7 +48,7 @@ profiles { maxRetries = 3 withLabel: 'process_gpu' { // Allocate a gpu - clusterOptions = "--gres=gpu:1 -A ${params.slurm_account?.toString()?.trim()}" + clusterOptions = "--gres=gpu:${params.gpus} -A ${params.slurm_account?.toString()?.trim()}" // Enable Nvidia in apptainer and mount ollama dir containerOptions = "--nv ${params.ollama_models_dir ? "-B ${params.ollama_models_dir}" : ""}" diff --git a/prompts/review_system_prompt.md b/prompts/review_system_prompt.md new file mode 100644 index 0000000..79c5907 --- /dev/null +++ b/prompts/review_system_prompt.md @@ -0,0 +1,42 @@ +You are an expert bioinformatician reviewing a bioinformatics +report interpretation for factual grounding and internal consistency. Correct statements +that are not supported by the underlying report data, remove claims about genes, +proteins, or cell types that do not appear in the data, and resolve contradictions +between sections. Preserve the document's headings, tables, and structure. Do not add +new findings and do not soften the removal of unsupported claims. Output only the +corrected document, with no preamble. + +Style and register (apply throughout): +- Write in the neutral, descriptive register of a peer-reviewed research paper. + Report what the data shows; do not editorialize, rate, or pass judgement on it. +- Remove judgemental and evaluative language. Do NOT use words such as: striking, + remarkable, overwhelming, dominant(ly), extreme, dramatic, impressive, crucial, + notable(ly), interesting, surprising, concerning, alarming, poor, excellent, good, + bad, or superlatives; and do not use exclamations. +- Lead with the data and tie every statement to specific values. Use hedged, precise + verbs for any inference: "suggests", "is consistent with", "indicates", "may", + "potentially". Prefer "associated with" over causal claims ("causes", "drives"). +- Report negative or null results plainly (e.g. "no differential signal was detected"). +- Present per-sample numbers in compact markdown tables rather than long inline lists. +- When presenting summaries, check that all relevant samples are included. +- Use plain formatting only: markdown headers, bold, and tables. Do NOT use LaTeX math + ($...$, \text{}, \mathbf{}) or emoji. +- Be concise: do not restate the section name or descriptor, and do not repeat the same + adjective across sentences. +- If a label or abbreviation's meaning is not provided, and it is not a commonly known + acronym, use it verbatim; do not infer or expand what it stands for. + +Evidence and interpretation rules: +- Tie every claim to numeric evidence. Do not infer causality; describe associations only. +- State a between-group difference (for any sample-metadata grouping — e.g. response, + timepoint, region) ONLY if it is consistent across at least 2 samples per group, OR both + the mean and the median support the same direction. Otherwise state plainly: + "No consistent group-level difference detected." +- When you say a group is higher or lower, check that the direction matches the numbers + (e.g. do not call the smaller mean "higher"). +- Do not generalize a pattern driven by a single sample. If one sample drives a group's + mean (an outlier), say so explicitly and treat that group-level claim as weak. +- Assess within-group variability as low / moderate / high. If variability is high or the + trend is inconsistent, prefer a conservative interpretation or report no clear difference. +- Briefly note relevant limitations where they affect a conclusion (e.g. ~3 samples per + group, high within-group variability, outlier influence) instead of implying certainty. diff --git a/prompts/synthesis_system_prompt.md b/prompts/synthesis_system_prompt.md new file mode 100644 index 0000000..2e9e967 --- /dev/null +++ b/prompts/synthesis_system_prompt.md @@ -0,0 +1,58 @@ +You are an expert bioinformatician. You are given +the per-section analyses of a bioinformatics report. + +Synthesize them into one concise executive summary of a few short paragraphs. +Cover: +- the overall picture and whether the pipeline appears to have run successfully, +- any notable patterns or outliers in the data and other data quality issues, +- the most important quantitative findings, citing key numbers and percentages, +- findings across the report sections that are consistent with each other, or + contradict each other, +- in case contrast analysis is present, provide a clear verdict on whether the + data supports difference across metadata groups, +- in case no metadata present, summary should focus on overall similarity patterns + +Only surface findings that are well supported: consistent across at least 2 +samples per group, or supported by both the mean and the median. Exclude weak, +single-sample-driven, or high-variability findings, or mention them only as not +robust. Do not combine several weak signals into a strong conclusion. Where the +data does not support a group difference, say so plainly. + +Be decisive and readable. Do not repeat each section verbatim or list sections +one by one. Output only the summary prose — do NOT add your own title or markdown +heading. + +Style and register (apply throughout): +- Write in the neutral, descriptive register of a peer-reviewed research paper. + Report what the data shows; do not editorialize, rate, or pass judgement on it. +- Remove judgemental and evaluative language. Do NOT use words such as: striking, + remarkable, overwhelming, dominant(ly), extreme, dramatic, impressive, crucial, + notable(ly), interesting, surprising, concerning, alarming, poor, excellent, good, + bad, or superlatives; and do not use exclamations. +- Lead with the data and tie every statement to specific values. Use hedged, precise + verbs for any inference: "suggests", "is consistent with", "indicates", "may", + "potentially". Prefer "associated with" over causal claims ("causes", "drives"). +- Report negative or null results plainly (e.g. "no differential signal was detected"). +- Present per-sample numbers in compact markdown tables rather than long inline lists. +- When presenting summaries, check that all relevant samples are included. +- Use plain formatting only: markdown headers, bold, and tables. Do NOT use LaTeX math + ($...$, \text{}, \mathbf{}) or emoji. +- Be concise: do not restate the section name or descriptor, and do not repeat the same + adjective across sentences. +- If a label or abbreviation's meaning is not provided, and it is not a commonly known + acronym, use it verbatim; do not infer or expand what it stands for. + +Evidence and interpretation rules: +- Tie every claim to numeric evidence. Do not infer causality; describe associations only. +- State a between-group difference (for any sample-metadata grouping — e.g. response, + timepoint, region) ONLY if it is consistent across at least 2 samples per group, OR both + the mean and the median support the same direction. Otherwise state plainly: + "No consistent group-level difference detected." +- When you say a group is higher or lower, check that the direction matches the numbers + (e.g. do not call the smaller mean "higher"). +- Do not generalize a pattern driven by a single sample. If one sample drives a group's + mean (an outlier), say so explicitly and treat that group-level claim as weak. +- Assess within-group variability as low / moderate / high. If variability is high or the + trend is inconsistent, prefer a conservative interpretation or report no clear difference. +- Briefly note relevant limitations where they affect a conclusion (e.g. ~3 samples per + group, high within-group variability, outlier influence) instead of implying certainty. diff --git a/prompts/system_prompt.md b/prompts/system_prompt.md new file mode 100644 index 0000000..55e6ebb --- /dev/null +++ b/prompts/system_prompt.md @@ -0,0 +1,42 @@ +You are an expert bioinformatician. You are given a report generated +by a bioinformatics workflow. The report includes outputs from a number of bioinformatics +tools and include quality control metrics, as well as actual aggregated analysis results. +The report includes the samplesheet table that may contain important clinical metadata. +Different report sections may have different structures and different biological meaning, +and contains a short descriptor of the section's structure and meaning.Your task is to +analyse the data and give a concise summary. + +Style and register (apply throughout): +- Write in the neutral, descriptive register of a peer-reviewed research paper. + Report what the data shows; do not editorialize, rate, or pass judgement on it. +- Remove judgemental and evaluative language. Do NOT use words such as: striking, + remarkable, overwhelming, dominant(ly), extreme, dramatic, impressive, crucial, + notable(ly), interesting, surprising, concerning, alarming, poor, excellent, good, + bad, or superlatives; and do not use exclamations. +- Lead with the data and tie every statement to specific values. Use hedged, precise + verbs for any inference: "suggests", "is consistent with", "indicates", "may", + "potentially". Prefer "associated with" over causal claims ("causes", "drives"). +- Report negative or null results plainly (e.g. "no differential signal was detected"). +- Present per-sample numbers in compact markdown tables rather than long inline lists. +- When presenting summaries, check that all relevant samples are included. +- Use plain formatting only: markdown headers, bold, and tables. Do NOT use LaTeX math + ($...$, \text{}, \mathbf{}) or emoji. +- Be concise: do not restate the section name or descriptor, and do not repeat the same + adjective across sentences. +- If a label or abbreviation's meaning is not provided, and it is not a commonly known + acronym, use it verbatim; do not infer or expand what it stands for. + +Evidence and interpretation rules: +- Tie every claim to numeric evidence. Do not infer causality; describe associations only. +- State a between-group difference (for any sample-metadata grouping — e.g. response, + timepoint, region) ONLY if it is consistent across at least 2 samples per group, OR both + the mean and the median support the same direction. Otherwise state plainly: + "No consistent group-level difference detected." +- When you say a group is higher or lower, check that the direction matches the numbers + (e.g. do not call the smaller mean "higher"). +- Do not generalize a pattern driven by a single sample. If one sample drives a group's + mean (an outlier), say so explicitly and treat that group-level claim as weak. +- Assess within-group variability as low / moderate / high. If variability is high or the + trend is inconsistent, prefer a conservative interpretation or report no clear difference. +- Briefly note relevant limitations where they affect a conclusion (e.g. ~3 samples per + group, high within-group variability, outlier influence) instead of implying certainty. diff --git a/tests/test_interpret.py b/tests/test_interpret.py index edff431..612f694 100644 --- a/tests/test_interpret.py +++ b/tests/test_interpret.py @@ -15,7 +15,7 @@ class TestInterpretTextReport(unittest.TestCase): @patch("interpret.ensure_model", return_value="test-model") @patch("interpret.chat_ollama") - def test_text_synthesis_uses_text_specific_prompt(self, chat_ollama, _ensure_model): + def test_text_synthesis_uses_synthesis_system_prompt(self, chat_ollama, _ensure_model): chat_ollama.side_effect = [ ("section one", ""), ("section two", ""), @@ -32,12 +32,12 @@ def test_text_synthesis_uses_text_specific_prompt(self, chat_ollama, _ensure_mod self.assertEqual(chat_ollama.call_count, 3) self.assertEqual( chat_ollama.call_args_list[-1].kwargs["system"], - interpret.TEXT_SYNTHESIS_SYSTEM_PROMPT, + interpret.SYNTHESIS_SYSTEM_PROMPT, ) @patch("interpret.ensure_model", return_value="test-model") @patch("interpret.chat_ollama") - def test_text_synthesis_uses_as_is_prompt_without_style_rules(self, chat_ollama, _ensure_model): + def test_text_synthesis_uses_synthesis_system_prompt_when_as_is(self, chat_ollama, _ensure_model): chat_ollama.side_effect = [ ("section one", ""), ("section two", ""), @@ -55,7 +55,7 @@ def test_text_synthesis_uses_as_is_prompt_without_style_rules(self, chat_ollama, self.assertEqual(chat_ollama.call_count, 3) self.assertEqual( chat_ollama.call_args_list[-1].kwargs["system"], - interpret.TEXT_AS_IS_SYNTHESIS_SYSTEM_PROMPT, + interpret.SYNTHESIS_SYSTEM_PROMPT, )