sokuto_runtime/tokenizer/
prompt.rs1use sokuto_core::contract::model_spec::TOKEN_OPTION_MARKER;
7use sokuto_core::schema::{Criteria, Question, QuestionType};
8
9use crate::error::{Result, RuntimeError};
10
11pub const PREFIX_STATE: &str = "State: ";
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct FormattedPrompt {
17 pub prefix: &'static str,
19 pub suffix: String,
21 pub option_keys: Vec<String>,
23}
24
25pub fn format_prompt(question: &Question) -> Result<FormattedPrompt> {
35 match question.question_type {
36 QuestionType::Choice => {
37 let criteria = question.criteria.as_ref().ok_or_else(|| {
38 RuntimeError::InvalidQuestion(
39 "Choice 型には Criteria::Map が必須です。".to_string(),
40 )
41 })?;
42
43 let map = match criteria {
44 Criteria::Map(map) => map,
45 _ => {
46 return Err(RuntimeError::InvalidQuestion(
47 "Choice 型には Criteria::Map が指定される必要があります。".to_string(),
48 ));
49 }
50 };
51
52 let count = map.len();
53 if count == 0 || count > 255 {
54 return Err(RuntimeError::InvalidQuestion(format!(
55 "Choice 型の候補数は 1〜255 件である必要がありますが、{count} 件指定されました。"
56 )));
57 }
58
59 let mut option_keys = Vec::with_capacity(count);
60 let mut criteria_parts = Vec::with_capacity(count);
61
62 for (key, desc) in map {
63 option_keys.push(key.clone());
64 criteria_parts.push(format!("{TOKEN_OPTION_MARKER} {desc}"));
65 }
66
67 let criteria_text = criteria_parts.join(" ");
68 let suffix = format!(
69 "\nInstructions: {}\nCriteria: {}",
70 question.instructions, criteria_text
71 );
72
73 Ok(FormattedPrompt {
74 prefix: PREFIX_STATE,
75 suffix,
76 option_keys,
77 })
78 }
79 QuestionType::Score => {
80 let criteria = question.criteria.as_ref().ok_or_else(|| {
81 RuntimeError::InvalidQuestion(
82 "Score 型には Criteria::List が必須です。".to_string(),
83 )
84 })?;
85
86 let list = match criteria {
87 Criteria::List(list) => list,
88 _ => {
89 return Err(RuntimeError::InvalidQuestion(
90 "Score 型には Criteria::List が指定される必要があります。".to_string(),
91 ));
92 }
93 };
94
95 let count = list.len();
96 if !(2..=10).contains(&count) {
97 return Err(RuntimeError::InvalidQuestion(format!(
98 "Score 型の段階数は 2〜10 段階である必要がありますが、{count} 段階指定されました。"
99 )));
100 }
101
102 let mut option_keys = Vec::with_capacity(count);
103 let mut criteria_parts = Vec::with_capacity(count);
104
105 for (i, desc) in list.iter().enumerate() {
106 option_keys.push(i.to_string());
107 criteria_parts.push(format!("{TOKEN_OPTION_MARKER} {desc}"));
108 }
109
110 let criteria_text = criteria_parts.join(" ");
111 let suffix = format!(
112 "\nInstructions: {}\nCriteria: {}",
113 question.instructions, criteria_text
114 );
115
116 Ok(FormattedPrompt {
117 prefix: PREFIX_STATE,
118 suffix,
119 option_keys,
120 })
121 }
122 QuestionType::Noul => {
123 let option_keys = vec!["true".to_string(), "false".to_string()];
124 let criteria_text =
125 format!("{TOKEN_OPTION_MARKER} 真 (True) {TOKEN_OPTION_MARKER} 偽 (False)");
126 let suffix = format!(
127 "\nInstructions: {}\nCriteria: {}",
128 question.instructions, criteria_text
129 );
130
131 Ok(FormattedPrompt {
132 prefix: PREFIX_STATE,
133 suffix,
134 option_keys,
135 })
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use indexmap::IndexMap;
144
145 #[test]
146 fn test_format_prompt_choice() {
147 let mut criteria = IndexMap::new();
148 criteria.insert("activate".to_string(), "カードの有効化".to_string());
149 criteria.insert("status".to_string(), "配送状況の確認".to_string());
150
151 let question = Question::new_choice("意図を分類せよ。", criteria);
152 let formatted = format_prompt(&question).unwrap();
153
154 assert_eq!(formatted.prefix, "State: ");
155 assert_eq!(formatted.option_keys, vec!["activate", "status"]);
156 assert!(formatted.suffix.contains("Instructions: 意図を分類せよ。"));
157 assert!(
158 formatted
159 .suffix
160 .contains("Criteria: [OP] カードの有効化 [OP] 配送状況の確認")
161 );
162 }
163
164 #[test]
165 fn test_format_prompt_score() {
166 let criteria = vec![
167 "極めて不満".to_string(),
168 "不満".to_string(),
169 "満足".to_string(),
170 "大変満足".to_string(),
171 ];
172 let question = Question::new_score("顧客満足度を評価せよ。", criteria);
173 let formatted = format_prompt(&question).unwrap();
174
175 assert_eq!(formatted.prefix, "State: ");
176 assert_eq!(formatted.option_keys, vec!["0", "1", "2", "3"]);
177 assert!(
178 formatted
179 .suffix
180 .contains("Instructions: 顧客満足度を評価せよ。")
181 );
182 assert!(
183 formatted
184 .suffix
185 .contains("Criteria: [OP] 極めて不満 [OP] 不満 [OP] 満足 [OP] 大変満足")
186 );
187 }
188
189 #[test]
190 fn test_format_prompt_noul() {
191 let question = Question::new_noul("この言明は妥当であるか判定せよ。");
192 let formatted = format_prompt(&question).unwrap();
193
194 assert_eq!(formatted.prefix, "State: ");
195 assert_eq!(formatted.option_keys, vec!["true", "false"]);
196 assert!(
197 formatted
198 .suffix
199 .contains("Instructions: この言明は妥当であるか判定せよ。")
200 );
201 assert!(
202 formatted
203 .suffix
204 .contains("Criteria: [OP] 真 (True) [OP] 偽 (False)")
205 );
206 }
207
208 #[test]
209 fn test_format_prompt_invalid_choice_empty() {
210 let question = Question::new_choice("空の候補", IndexMap::new());
211 assert!(format_prompt(&question).is_err());
212 }
213
214 #[test]
215 fn test_format_prompt_invalid_score_count() {
216 let question = Question::new_score("1段階のみ", vec!["評価".to_string()]);
217 assert!(format_prompt(&question).is_err());
218 }
219}