1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//! The wire contract of one extraction call: the request body sent to Ollama,
//! and the JSON Schemas sent inside it.
//!
//! Split out of `extract.rs` when constraining decoding (#1944) pushed that
//! file past its frozen budget. The seam is how ONE backend is asked: Ollama's
//! request envelope and the schemas that constrain its decoding. It is not
//! everything that leaves the process — the prompts leave too, and stay in
//! `extract.rs` beside the parser, because they are what this crate wants said
//! to any backend: the OpenAI-compatible extractor sends the same prompts, in
//! an envelope `crate::openai` builds, and no schema.
//!
//! Each schema still sits beside nothing else, which is the point — it is one
//! contract stated twice, once for the sampler and once for the reader, and
//! `every_required_key_of_the_schema_is_a_key_the_parser_reads` is what keeps
//! the two from drifting.
use super::MAX_GENERATION_TOKENS;
/// The body of one `/api/generate` call, built where a test can read it.
///
/// Split out of [`super::OllamaExtractor`]'s `generate` so the wire contract can be
/// asserted without a live model: a `format` that silently stopped being sent
/// would restore the exact defect this shape exists to close, and no
/// stub-backed extraction test would notice.
#[cfg(feature = "extractor-http")]
pub(super) fn generate_body(
model: &str,
prompt: &str,
schema: &serde_json::Value,
) -> serde_json::Value {
serde_json::json!({
"model": model,
"prompt": prompt,
"stream": false,
"think": false,
// The same contract the prompt states, restated as a grammar the
// sampler cannot leave. Structure only: it makes replies parsable, it
// does not make them true, and the prompt stays the place that says
// what to extract.
"format": schema,
// Extraction models are large — the one this crate documents as an
// example is 21.9 GB — so an unload between calls is the dominant
// cost, not the generation. Shares the embedder's knob so one setting
// governs every Ollama call the daemon makes.
"keep_alive": crate::embedder::keep_alive(),
"options": { "temperature": 0, "num_predict": MAX_GENERATION_TOKENS },
})
}
/// One `{"fact": …, "entities": […]}` item, as a JSON Schema.
///
/// Kept beside [`super::RawFact`], which is what parses it back: the two are one
/// contract stated twice, once for the sampler and once for the reader, and
/// they must be changed together.
#[cfg(feature = "extractor-http")]
pub(super) fn fact_item_schema() -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"fact": { "type": "string" },
"entities": { "type": "array", "items": { "type": "string" } },
},
"required": ["fact", "entities"],
})
}
/// The JSON Schema for [`super::build_prompt`]'s reply: a bare array of facts.
#[cfg(feature = "extractor-http")]
pub(super) fn fact_list_schema() -> serde_json::Value {
serde_json::json!({ "type": "array", "items": fact_item_schema() })
}
/// The JSON Schema for [`super::build_graph_prompt`]'s reply, mirroring
/// [`super::RawExtraction`].
///
/// `value` keeps the `number` arm the prompt insists on ("Emit numbers as JSON
/// NUMBERS, never strings"): constraining it to `string` would silently undo
/// what the prompt spends a line asking for.
#[cfg(feature = "extractor-http")]
pub(super) fn extraction_schema() -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"facts": { "type": "array", "items": fact_item_schema() },
"relations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"subject": { "type": "string" },
"predicate": { "type": "string" },
"object": { "type": "string" },
},
"required": ["subject", "predicate", "object"],
},
},
"attributes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"entity": { "type": "string" },
"key": { "type": "string" },
"value": { "type": ["string", "number", "boolean"] },
},
"required": ["entity", "key", "value"],
},
},
},
"required": ["facts", "relations", "attributes"],
})
}