Data Formats¶
This page is the canonical schema reference for every file bsllmner-mk2 reads or writes.
BioSample Input (bs_entries)¶
A list of BioSample entries. Accepted as either a JSON array or JSONL (one object per line). Each entry must contain a top-level accession. bsllmner2 logs a warning and skips an entry that has no accession.
[
{
"accession": "SAMEA00000001",
"characteristics": {
"title": [{"text": "HeLa cell RNA-seq"}],
"cell line": [{"text": "HeLa"}],
"organism": [{"text": "Homo sapiens"}]
}
}
]
JSONL:
{"accession": "SAMEA00000001", "characteristics": {...}}
{"accession": "SAMN00000002", "BioSample": {...}}
LLM input normalisation¶
construct_llm_input_json() turns each entry into a flat JSON object that maps an attribute name to its value. That object is the only part of the entry that reaches the LLM. Three rules hold for every input format:
- Every key that
bsllmner2/filter_keys.jsonlists is dropped. - A value is a string. If one attribute name carries two or more different values in the same entry, the value is a list of strings instead. Repeated identical values collapse into one.
- The object does not carry the
accession. The pipeline reads theaccessionfrom the raw entry.
| Input format | Detection | Source of the attributes |
|---|---|---|
| EBI | the top-level characteristics is a dict |
each characteristics key, with the text of the first list element as the value |
| NCBI | the entry has an object under a top-level BioSample key, or the entry itself has Attributes or Description |
Attributes.Attribute[], plus three keys that come from Description |
| other | neither of the above | the entry itself, minus the filtered top-level keys |
In the EBI format, a characteristics value that is not a non-empty list of objects holding a text key is dropped.
In the NCBI format, the attribute name is Attribute[].attribute_name and the value is Attribute[].content. The normalisation ignores harmonized_name and display_name, because every name that filter_keys.json lists appears as an attribute_name. An attribute whose attribute_name or content is not a non-empty string is dropped. Attributes.Attribute is a list when the entry holds two or more attributes, a single object when it holds one, and Attributes itself can be absent or null.
An NCBI entry nests the record under a BioSample key when it comes from the ddbj-search-converter JSONL, and keeps the record at the top level when it comes from the BioSample Bulk API. Both nestings produce the same object. Besides the attributes, three keys come from Description:
| Output key | Source |
|---|---|
title |
Description.Title |
organism |
Description.Organism.OrganismName, or Description.Organism.taxonomy_name when OrganismName is absent |
description |
Description.Comment.Paragraph |
Every other key of an NCBI entry is dropped: access, publication_date, last_update, submission_date, id, accession, Ids, Owner, Models, Package, Links, Status, and Description.SampleName. Description.SampleName repeats the sample_name attribute whenever it is present, so dropping it loses no value.
For example, this NCBI entry:
{
"BioSample": {
"access": "public",
"last_update": "2021-04-19T22:11:48.000+09:00",
"Ids": {"Id": {"namespace": "BioSample", "is_primary": "1", "content": "SAMN00000002"}},
"Description": {
"Title": "ChIP-seq of HeLa",
"Organism": {"taxonomy_id": "9606", "OrganismName": "Homo sapiens"},
"Comment": {"Paragraph": "HeLa cells from a 31 years old donor"}
},
"Owner": {"Name": {"content": "Example University"}},
"Models": {"Model": {"content": "Generic"}},
"Attributes": {
"Attribute": [
{"attribute_name": "cell line", "content": "HeLa", "harmonized_name": "cell_line", "display_name": "cell line"},
{"attribute_name": "External Id", "content": "SAMN00000002"},
{"attribute_name": "tissue", "content": "cervix"},
{"attribute_name": "tissue", "content": "epithelium"}
]
}
},
"accession": "SAMN00000002"
}
reaches the LLM as:
{
"cell line": "HeLa",
"tissue": ["cervix", "epithelium"],
"title": "ChIP-seq of HeLa",
"organism": "Homo sapiens",
"description": "HeLa cells from a 31 years old donor"
}
External Id is absent from the result because filter_keys.json lists it.
Mapping TSV (for evaluation)¶
A TSV with the header row below. Only mapping answer ID (human-curated) is used as the gold standard by bsllmner2_select --mapping; the other columns are informational.
| Column | Description |
|---|---|
BioSample ID |
BioSample accession. |
Experiment type |
Experiment type. |
extraction answer |
Auxiliary annotation. |
mapping answer ID |
Ground-truth ontology term ID. |
mapping answer label |
Ground-truth label. |
BioSample ID Experiment type extraction answer mapping answer ID mapping answer label
SAMN00000001 RNA-seq HeLa CVCL_0030 HeLa
SAMN00000002 RNA-seq HEK293 CVCL_0045 HEK293
Evaluation compares predicted term_id against mapping answer ID per accession for the cell_line field. See bsllmner2/pipeline.evaluate_select_output() and Benchmarking for how the metrics are computed and read.
ExtractResult¶
Written to bsllmner2-results/extract/{run_name}.json.
{
"entries": [
{
"accession": "SAMN00000001",
"extracted": { "cell_line": "HeLa" },
"raw_output": "{\"cell_line\": \"HeLa\"}",
"llm_timing": {
"total_duration": 1000000000,
"load_duration": 100000000,
"eval_count": 50,
"eval_duration": 500000000,
"prompt_eval_count": 100
}
}
],
"run_metadata": {
"run_name": "llama3.1:70b_20250101_120000",
"model": "llama3.1:70b",
"thinking": false,
"start_time": "2025-01-01T12:00:00Z",
"end_time": "2025-01-01T12:10:00Z",
"status": "completed",
"processing_time_sec": 600.0,
"total_entries": 1
},
"performance": null,
"errors": []
}
Schema¶
| Path | Type | Description |
|---|---|---|
entries[].accession |
string |
BioSample accession. |
entries[].extracted |
dict \| null |
Parsed JSON value from the LLM. List-shaped responses are normalised to null at the LLM boundary (a WARNING is logged with the accession, and raw_output preserves the original substring for inspection). Legacy result files containing extracted: [...] are coerced to null on load for backward compatibility. |
entries[].raw_output |
string \| null |
The last JSON substring extracted from the raw response text. |
entries[].llm_timing |
LlmTimingFields |
Per-call timing (nanoseconds). See below. |
run_metadata.run_name |
string |
Run identifier. |
run_metadata.model |
string |
Ollama model. |
run_metadata.thinking |
bool |
Whether thinking mode was on. |
run_metadata.start_time |
datetime |
ISO 8601 UTC. |
run_metadata.end_time |
datetime \| null |
ISO 8601 UTC. |
run_metadata.status |
"running" \| "completed" \| "failed" \| "interrupted" |
Run status. interrupted is emitted when the run was cancelled via Ctrl-C (KeyboardInterrupt); the partial result file is still dumped so resume works on the next invocation. |
run_metadata.processing_time_sec |
float \| null |
end_time - start_time in seconds. |
run_metadata.total_entries |
int \| null |
Number of entries in the result. |
performance |
PerformanceSummary \| null |
See PerformanceSummary. |
errors |
list[ErrorLog] |
Captured errors. |
LlmTimingFields¶
Subset of the Ollama ChatResponse timing fields, in nanoseconds.
| Field | Type | Description |
|---|---|---|
total_duration |
int |
Total duration (ns). |
load_duration |
int |
Model load duration (ns). |
eval_count |
int |
Tokens generated. |
eval_duration |
int |
Generation duration (ns). |
prompt_eval_count |
int |
Prompt tokens. |
SelectResult¶
Written to bsllmner2-results/select/select_{run_name}.json.
{
"entries": [
{
"extract": {
"accession": "SAMN00000001",
"extracted": { "cell_line": "HeLa", "tissue": "cervix" },
"raw_output": "{\"cell_line\": \"HeLa\", \"tissue\": \"cervix\"}",
"llm_timing": { "total_duration": 2000000000, "load_duration": 100000000, "eval_count": 80, "eval_duration": 1000000000, "prompt_eval_count": 250 }
},
"search_results": {
"cell_line": {
"HeLa": [
{
"term_uri": "http://purl.obolibrary.org/obo/CVCL_0030",
"term_id": "CVCL:0030",
"prop_uri": "http://www.w3.org/2000/01/rdf-schema#label",
"value": "HeLa",
"label": "HeLa",
"exact_match": true,
"text2term_score": null,
"reasoning": "Exact match on rdfs:label",
"comments": ["Disease: Cervical adenocarcinoma"],
"definitions": null
}
]
}
},
"text2term_results": {},
"select_timings": {
"cell_line": {
"HeLa": { "total_duration": 500000000, "load_duration": 0, "eval_count": 20, "eval_duration": 200000000, "prompt_eval_count": 50 }
}
},
"results": {
"cell_line": [
{
"value": "HeLa",
"term_id": "CVCL:0030",
"term_uri": "http://purl.obolibrary.org/obo/CVCL_0030",
"label": "HeLa",
"exact_match": true,
"reasoning": "Exact match on rdfs:label"
}
]
}
}
],
"run_metadata": { "...": "as in ExtractResult" },
"evaluation": null,
"performance": null,
"errors": []
}
Schema¶
| Path | Type | Description |
|---|---|---|
entries[].extract |
ExtractEntry |
Embedded extract result for this entry. |
entries[].search_results |
dict[str, dict[str, list[SearchResult]]] |
Stage 2a word-combination candidates, keyed by field name then by extracted value. See SearchResult. |
entries[].text2term_results |
dict[str, dict[str, list[SearchResult]]] |
Stage 2b text2term candidates, keyed by field name then by extracted value. See SearchResult. |
entries[].select_timings |
dict[str, dict[str, LlmTimingFields]] |
Per (field, value) Stage 3 LLM call timing. |
entries[].results |
dict[str, list[ResolvedValue]] |
Final per-field selections. See ResolvedValue. |
run_metadata |
RunMetadata |
Run identification and status. See ExtractResult. |
evaluation |
EvaluationMetrics \| null |
Set when --mapping is supplied. accuracy, precision, recall, f1 are stored as 0-1 ratios (not percentages). |
performance |
PerformanceSummary \| null |
Per-batch timing summary. Always populated in normal runs; only null when the run aborted before any batch executed. |
errors |
list[ErrorLog] |
Captured errors (Bsllmner2Error / per-entry LLM failures). Empty list when no errors occurred. |
SearchResult¶
One candidate returned by Stage 2 ontology search (Stage 2a word-combination index and Stage 2b text2term). Stored under entries[].search_results[field][value][] and entries[].text2term_results[field][value][].
| Field | Type | Description |
|---|---|---|
term_uri |
string |
Ontology term URI. |
term_id |
string |
Normalised term ID (e.g. CVCL:0030). |
prop_uri |
string \| null |
URI of the property that matched the value (e.g. http://www.w3.org/2000/01/rdf-schema#label). |
value |
string |
Property value that produced the match. |
label |
string \| null |
Preferred label of the term (rdfs:label / skos:prefLabel). |
exact_match |
bool |
true when the query and value are equal after NFKC normalisation. |
text2term_score |
float \| null |
text2term similarity score. null for Stage 2a hits. |
reasoning |
string \| null |
Human-readable provenance: "Exact match on <prop>" for Stage 2a exact hits, "text2term score: ..." for Stage 2b hits, otherwise null. |
definitions |
list[str] \| null |
obo:IAO_0000115 definitions from the OWL. Surfaced to the Stage 3 LLM as context only. |
comments |
list[str] \| null |
rdfs:comment values. Populated mainly by ChEBI; see Ontology Preparation. |
ResolvedValue¶
One final per-field pick. Stored under entries[].results[field][].
| Field | Type | Description |
|---|---|---|
value |
string |
The extracted value. |
term_id |
string \| null |
Picked ontology term ID. |
term_uri |
string \| null |
Picked ontology term URI. |
label |
string \| null |
Picked term label. |
exact_match |
bool \| null |
Whether the pick came from an exact word-combination match. |
reasoning |
string \| null |
Stage 3 LLM reasoning, or "Exact match on <prop>" for Stage 2a picks, or "text2term score: ..." for Stage 2b picks. |
SelectConfig¶
{
"fields": {
"cell_line": {
"ontology_file": "ontology/cellosaurus_human.owl",
"prompt_description": "Cell line is a group of cells that are genetically identical...",
"value_type": "string"
},
"drug": {
"ontology_file": "ontology/chebi_subset.owl",
"prompt_description": "Drug is a chemical or biological substance...",
"value_type": "array"
}
}
}
| Property | Type | Default | Description |
|---|---|---|---|
fields |
dict[str, SelectConfigField] |
(required) | Field name to field config. |
fields[name].ontology_file |
string \| null |
null |
Path to an OWL or TSV/CSV. null skips Stage 2/3 mapping and returns the extracted value as-is. |
fields[name].prompt_description |
string \| null |
null |
Description injected into the Stage 1 prompt. |
fields[name].value_type |
"string" \| "array" |
"string" |
"array" extracts a list of values for the field. |
Ontology files are built per Ontology Preparation. See Select Mode for usage notes.
Prompt YAML¶
- role: system
content: |-
You are a smart curator of biological data
- role: user
content: |-
I will input JSON formatted metadata of a sample...
Here is the input metadata:
role must be "system", "user", or "assistant". At runtime, the entry JSON is appended to the last message's content.
Format JSON Schema¶
Passed to Ollama's format= parameter.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"cell_line": { "type": ["string", "null"] }
},
"required": ["cell_line"],
"additionalProperties": true
}
In Select mode, the schema is synthesised from the select config by build_extract_schema_for_select(). For value_type: "array", the property becomes {"type": ["array", "null"], "items": {"type": "string"}}. The generated schema sets additionalProperties: false.
PerformanceSummary¶
Embedded in the performance field of ExtractResult and SelectResult. There is no separate benchmark file -- all timing data lives inside the result JSON.
| Path | Type | Description |
|---|---|---|
performance.total_input_entries |
int |
Total input entries. |
performance.completed_count |
int |
Entries that completed processing. |
performance.total_wall_sec |
float \| null |
Total wall-clock time (seconds). |
performance.stage_timings[] |
list[StageTimings] |
Per-batch stage breakdown. |
performance.ner_llm_timing |
LlmTimingSummary \| null |
Aggregated NER LLM timing. |
performance.select_llm_timing |
LlmTimingSummary \| null |
Aggregated Stage 3 LLM timing (Select only). |
performance.disk_io |
DiskIoTimings |
Disk I/O timings (Select only). |
Accuracy metrics live in SelectResult.evaluation, not in PerformanceSummary.
LlmTimingSummary¶
| Field | Description |
|---|---|
call_count |
Number of LLM calls. |
total_duration_sec |
Sum of total_duration across calls. |
mean_latency_sec |
Mean of (total_duration - load_duration). |
p50/p95/p99_latency_sec |
Latency percentiles. |
mean_tokens_per_sec |
Mean eval_count / eval_duration. |
p50/p95_tokens_per_sec |
tokens/sec percentiles. |
mean_load_duration_sec |
Mean model load time. |
max_load_duration_sec |
Max model load time. |
total_prompt_tokens |
Sum of prompt_eval_count. |
total_eval_tokens |
Sum of eval_count. |
StageTimings¶
One entry per processed batch.
| Field | Type | Description |
|---|---|---|
batch_idx |
int |
Zero-based batch index. |
batch_size |
int |
Entries in this batch. |
ner_sec |
float \| null |
Stage 1 NER wall-clock time. |
ontology_search_sec |
float \| null |
Stage 2a word-combination search time. |
text2term_sec |
float \| null |
Stage 2b text2term.map_terms() time. |
llm_select_sec |
float \| null |
Stage 3 LLM selection time (max-across-fields under asyncio.gather). |
resume_write_sec |
float \| null |
Per-batch resume-file write time. |
DiskIoTimings¶
Run-wide. Each list grows by one entry per operation, so len(list) is the operation count.
| Field | Type | Description |
|---|---|---|
index_cache_load_sec |
list[float] |
OntologyIndex cache load time (cache hit). |
index_cache_save_sec |
list[float] |
OntologyIndex cache save time (after rebuild). |
index_build_from_file_sec |
list[float] |
OntologyIndex rebuild from OWL/TSV (cache miss). |
text2term_cache_build_sec |
list[float] |
text2term.cache_ontology() build time (first run per OWL). |
text2term_cache_load_sec |
list[float] |
text2term.cache_exists() check time (cache-hit path). |
resume_write_sec |
list[float] |
Per-batch resume write time. |
For interpretation guidance see Benchmarking.