sokuto_runtime/dag/
interpolate.rs1use indexmap::IndexMap;
7use sokuto_core::schema::{Criteria, Question, QuestionType};
8use std::fmt::Write;
9
10#[derive(Debug, Clone)]
14pub struct NodeContextValue {
15 pub value: String,
17 pub confidence: f64,
19}
20
21impl NodeContextValue {
22 pub fn new(value: impl Into<String>, confidence: f64) -> Self {
24 Self {
25 value: value.into(),
26 confidence,
27 }
28 }
29}
30
31pub fn interpolate_prompt(
41 template: &str,
42 context: &IndexMap<String, NodeContextValue>,
43 output: &mut String,
44) {
45 output.clear();
46 output.reserve(template.len() + 64);
47 let mut cursor = 0;
48
49 while let Some(open) = template[cursor..].find('{') {
50 let open_idx = cursor + open;
51 output.push_str(&template[cursor..open_idx]);
52
53 if let Some(close) = template[open_idx..].find('}') {
54 let close_idx = open_idx + close;
55 let key = &template[open_idx + 1..close_idx];
56
57 if let Some((node_id, prop)) = key.split_once('.') {
58 if let Some(ctx_val) = context.get(node_id) {
59 match prop {
60 "value" | "answer" => output.push_str(&ctx_val.value),
61 "confidence" => {
62 let _ = write!(output, "{:.3}", ctx_val.confidence);
63 }
64 _ => output.push_str("null"),
65 }
66 } else {
67 output.push_str(&template[open_idx..=close_idx]);
69 }
70 } else if let Some(ctx_val) = context.get(key) {
71 output.push_str(&ctx_val.value);
72 } else {
73 output.push_str(&template[open_idx..=close_idx]);
74 }
75 cursor = close_idx + 1;
76 } else {
77 break;
78 }
79 }
80 output.push_str(&template[cursor..]);
81}
82
83pub fn interpolate_question(
92 question: &Question,
93 context: &IndexMap<String, NodeContextValue>,
94) -> Question {
95 if context.is_empty() {
96 return question.clone();
97 }
98
99 let mut scratch = String::with_capacity(question.instructions.len() + 64);
100 interpolate_prompt(&question.instructions, context, &mut scratch);
101 let interpolated_instructions = scratch.clone();
102
103 let interpolated_criteria = match &question.criteria {
104 Some(Criteria::Map(map)) => {
105 let mut new_map = IndexMap::with_capacity(map.len());
106 for (k, v) in map {
107 interpolate_prompt(v, context, &mut scratch);
108 new_map.insert(k.clone(), scratch.clone());
109 }
110 Some(Criteria::Map(new_map))
111 }
112 Some(Criteria::List(list)) => {
113 let mut new_list = Vec::with_capacity(list.len());
114 for item in list {
115 interpolate_prompt(item, context, &mut scratch);
116 new_list.push(scratch.clone());
117 }
118 Some(Criteria::List(new_list))
119 }
120 Some(Criteria::None) | None => None,
121 };
122
123 match question.question_type {
124 QuestionType::Choice => Question {
125 question_type: QuestionType::Choice,
126 instructions: interpolated_instructions,
127 criteria: interpolated_criteria,
128 },
129 QuestionType::Score => Question {
130 question_type: QuestionType::Score,
131 instructions: interpolated_instructions,
132 criteria: interpolated_criteria,
133 },
134 QuestionType::Noul => Question {
135 question_type: QuestionType::Noul,
136 instructions: interpolated_instructions,
137 criteria: None,
138 },
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_interpolate_prompt_basic() {
148 let mut context = IndexMap::new();
149 context.insert(
150 "node_a".to_string(),
151 NodeContextValue::new("approved", 0.9523),
152 );
153 context.insert("node_b".to_string(), NodeContextValue::new("true", 0.880));
154
155 let template = "先行判定は {node_a.value} (確信度: {node_a.confidence}) であり、フラグ={node_b.answer} です。";
156 let mut output = String::new();
157 interpolate_prompt(template, &context, &mut output);
158
159 assert_eq!(
160 output,
161 "先行判定は approved (確信度: 0.952) であり、フラグ=true です。"
162 );
163 }
164
165 #[test]
166 fn test_interpolate_prompt_unresolved_intact() {
167 let context = IndexMap::new();
168 let template = "未定義変数は {unknown.value} のままです。";
169 let mut output = String::new();
170 interpolate_prompt(template, &context, &mut output);
171
172 assert_eq!(output, "未定義変数は {unknown.value} のままです。");
173 }
174
175 #[test]
176 fn test_interpolate_question() {
177 let mut context = IndexMap::new();
178 context.insert("user_rank".to_string(), NodeContextValue::new("VIP", 0.99));
179
180 let mut criteria_map = IndexMap::new();
181 criteria_map.insert(
182 "free".to_string(),
183 "{user_rank.value}会員は手数料無料".to_string(),
184 );
185 criteria_map.insert("paid".to_string(), "通常手数料が発生".to_string());
186
187 let q = Question::new_choice(
188 "会員ランク {user_rank.value} に対する手数料を判定してください。".to_string(),
189 criteria_map,
190 );
191
192 let interpolated = interpolate_question(&q, &context);
193 assert_eq!(
194 interpolated.instructions,
195 "会員ランク VIP に対する手数料を判定してください。"
196 );
197 if let Some(Criteria::Map(m)) = &interpolated.criteria {
198 assert_eq!(m.get("free").unwrap(), "VIP会員は手数料無料");
199 } else {
200 panic!("Criteria が Map である必要があります。");
201 }
202 }
203}