Skip to main content

hs_predict/session/
question.rs

1use serde::{Deserialize, Serialize};
2
3/// The logical step a question belongs to.
4///
5/// Stored in [`ClassificationSession`](super::ClassificationSession) alongside
6/// `current_question` so that `answer()` dispatches to the right state update
7/// without inspecting language-specific prompt text.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum QuestionStep {
11    /// Step 1 — main product identifier.
12    Identifier,
13    /// Step 2 — is the product a mixture?
14    IsMixture,
15    /// Step 3a-i — number of mixture components.
16    ComponentCount,
17    /// Step 3a-ii — CAS / name of the n-th mixture component.
18    ComponentIdentifier,
19    /// Step 3a-iii — weight fraction of the current mixture component.
20    ComponentFraction,
21    /// Step 3b-i — physical form (solid / powder / liquid / …).
22    PhysicalForm,
23    /// Step 3b-ii — solution concentration (only asked after `PhysicalForm::Solution`).
24    SolutionConcentration,
25    /// Step 4 — intended end-use.
26    IntendedUse,
27    /// Step 5 — organic or inorganic (only when SMILES is unavailable).
28    OrganicInorganic,
29    /// Step 6 — functional groups (only for organic compounds without SMILES).
30    FunctionalGroups,
31}
32
33/// セッションでユーザーに提示する質問
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(tag = "kind", rename_all = "snake_case")]
36pub enum Question {
37    /// 自由テキスト入力
38    Text {
39        prompt: String,
40        /// 入力例(省略可)
41        example: Option<String>,
42    },
43    /// 選択肢から1つ選ぶ
44    Choice {
45        prompt: String,
46        options: Vec<String>,
47    },
48    /// はい/いいえ
49    YesNo {
50        prompt: String,
51    },
52    /// 数値入力
53    Number {
54        prompt: String,
55        unit: String,
56        min: f64,
57        max: f64,
58    },
59    /// 複数選択(官能基などの場合)
60    MultiChoice {
61        prompt: String,
62        options: Vec<String>,
63        /// 「わからない」選択肢を含むか
64        include_unknown: bool,
65    },
66}
67
68impl Question {
69    pub fn prompt(&self) -> &str {
70        match self {
71            Question::Text { prompt, .. } => prompt,
72            Question::Choice { prompt, .. } => prompt,
73            Question::YesNo { prompt } => prompt,
74            Question::Number { prompt, .. } => prompt,
75            Question::MultiChoice { prompt, .. } => prompt,
76        }
77    }
78}
79
80/// ユーザーの回答
81///
82/// Serialized with adjacent tagging (`{ "kind": "text", "value": "..." }`)
83/// so that primitive-containing variants (Text, Choice, …) round-trip correctly.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
86pub enum Answer {
87    /// テキスト入力の回答
88    Text(String),
89    /// Choice の選択インデックス (0-based)
90    Choice(usize),
91    /// YesNo の回答
92    YesNo(bool),
93    /// 数値入力
94    Number(f64),
95    /// MultiChoice の選択インデックスリスト
96    MultiChoice(Vec<usize>),
97    /// スキップ(任意項目のみ)
98    Skip,
99}
100
101impl Answer {
102    pub fn kind_name(&self) -> &'static str {
103        match self {
104            Answer::Text(_) => "text",
105            Answer::Choice(_) => "choice",
106            Answer::YesNo(_) => "yes_no",
107            Answer::Number(_) => "number",
108            Answer::MultiChoice(_) => "multi_choice",
109            Answer::Skip => "skip",
110        }
111    }
112}
113
114/// 質問と回答のペア(セッション履歴用)
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct QAPair {
117    pub question: Question,
118    pub answer: Answer,
119}
120
121/// `answer()` メソッドの戻り値
122#[derive(Debug, Serialize)]
123#[serde(tag = "type")]
124pub enum SessionResult {
125    /// 次の質問が必要
126    NeedMoreInfo { next_question: Question },
127    /// 十分な情報が集まった — パイプラインへ渡す準備完了
128    Ready,
129    /// ルールエンジンでは決定不能 — LLM へ委譲が必要
130    RequiresLlm,
131}