1use crate::{Answer, AnswerValue, Boolean, Choice, Error, ErrorKind, Question, Score, State};
3use indexmap::IndexMap;
4use serde_json::{Map, Value, json};
5#[derive(Clone, Debug)]
7pub struct WireRequest {
8 pub state: State,
10 pub questions: IndexMap<String, Question>,
12 pub request_id: Option<String>,
14}
15fn invalid(path: &str, message: impl Into<String>) -> Error {
16 Error::new(ErrorKind::InvalidRequest, message).with_path(path)
17}
18fn no_unknown(object: &Map<String, Value>, allowed: &[&str], path: &str) -> Result<(), Error> {
19 if let Some(key) = object
20 .keys()
21 .filter(|k| !allowed.contains(&k.as_str()))
22 .min()
23 {
24 return Err(invalid(
25 &if path.is_empty() {
26 key.clone()
27 } else {
28 format!("{path}.{key}")
29 },
30 "unknown field",
31 ));
32 }
33 Ok(())
34}
35pub fn decode_request(request: Value) -> Result<WireRequest, Error> {
42 let object = request
43 .as_object()
44 .ok_or_else(|| invalid("", "the request must be a JSON object"))?;
45 no_unknown(
46 object,
47 &["schema_version", "state", "questions", "request_id"],
48 "",
49 )?;
50 if object
51 .get("schema_version")
52 .is_some_and(|v| v.as_str() != Some("1"))
53 {
54 return Err(invalid("schema_version", "only \"1\" is supported"));
55 }
56 let value = object
57 .get("state")
58 .ok_or_else(|| invalid("state", "required"))?;
59 let state = State::try_from(value.clone())
60 .map_err(|_| invalid("state", "must be text, a JSON object, or an array"))?;
61 let request_id = optional_string(object.get("request_id"), "request_id")?;
62 let specs = object
63 .get("questions")
64 .and_then(Value::as_array)
65 .filter(|v| !v.is_empty())
66 .ok_or_else(|| invalid("questions", "must be a non-empty array"))?;
67 let mut questions = IndexMap::new();
68 for (i, spec) in specs.iter().enumerate() {
69 let path = format!("questions[{i}]");
70 let (name, question) = decode_question(spec, &path)?;
71 if questions.contains_key(&name) {
72 return Err(invalid(
73 &format!("{path}.name"),
74 format!("{} is used twice", python_repr(&name)),
75 ));
76 }
77 questions.insert(name, question);
78 }
79 Ok(WireRequest {
80 state,
81 questions,
82 request_id,
83 })
84}
85fn optional_string(value: Option<&Value>, path: &str) -> Result<Option<String>, Error> {
86 match value {
87 None | Some(Value::Null) => Ok(None),
88 Some(Value::String(s)) => Ok(Some(s.clone())),
89 _ => Err(invalid(path, "must be a string")),
90 }
91}
92fn decode_question(spec: &Value, path: &str) -> Result<(String, Question), Error> {
93 let obj = spec
94 .as_object()
95 .ok_or_else(|| invalid(path, "must be a JSON object"))?;
96 let kind = obj.get("type").and_then(Value::as_str).unwrap_or("");
97 let mut allowed = vec!["name", "type", "instructions", "min_confidence"];
98 match kind {
99 "boolean" => allowed.extend(["true", "false"]),
100 "choice" => allowed.push("options"),
101 "score" => allowed.push("levels"),
102 _ => {
103 return Err(invalid(
104 &format!("{path}.type"),
105 "must be 'boolean', 'choice', or 'score'",
106 ));
107 }
108 }
109 no_unknown(obj, &allowed, path)?;
110 let name = obj
111 .get("name")
112 .and_then(Value::as_str)
113 .filter(|s| !s.is_empty())
114 .ok_or_else(|| invalid(&format!("{path}.name"), "must be a non-empty string"))?;
115 let descriptions = if kind == "boolean" {
117 (
118 optional_string(obj.get("true"), &format!("{path}.true"))?,
119 optional_string(obj.get("false"), &format!("{path}.false"))?,
120 )
121 } else {
122 (None, None)
123 };
124 let options = if kind == "choice" {
125 Some(decode_options(obj.get("options"), path)?)
126 } else {
127 None
128 };
129 let levels = if kind == "score" {
130 let levels = obj
131 .get("levels")
132 .and_then(Value::as_array)
133 .ok_or_else(|| invalid(&format!("{path}.levels"), "must be an array of strings"))?;
134 Some(levels)
135 } else {
136 None
137 };
138 let instructions = obj
139 .get("instructions")
140 .and_then(Value::as_str)
141 .filter(|s| !crate::question::is_blank(s))
142 .ok_or_else(|| invalid(path, "instructions must be a non-empty string"))?;
143 let threshold = match obj.get("min_confidence") {
144 None => 0.0,
145 Some(v) => v
146 .as_f64()
147 .filter(|n| n.is_finite() && (0.0..=1.0).contains(n))
148 .ok_or_else(|| invalid(path, "min_confidence must be a number from 0 to 1"))?,
149 };
150 let question = match kind {
151 "boolean" => Boolean::new(instructions)?
152 .with_descriptions(descriptions.0, descriptions.1)
153 .with_min_confidence(threshold)?
154 .into(),
155 "choice" => {
156 let options = options.unwrap_or_default();
157 if options.len() < 2 {
158 return Err(invalid(
159 path,
160 format!("need at least 2 options, got {}", options.len()),
161 ));
162 }
163 Choice::new(instructions, options)?
164 .with_min_confidence(threshold)?
165 .into()
166 }
167 _ => {
168 let levels = levels.map(Vec::as_slice).unwrap_or(&[]);
169 if levels.len() < 2 {
170 return Err(invalid(
171 path,
172 format!("need at least 2 levels, got {}", levels.len()),
173 ));
174 }
175 let levels: Vec<String> = levels
176 .iter()
177 .map(|v| {
178 v.as_str()
179 .filter(|s| !s.is_empty())
180 .map(str::to_owned)
181 .ok_or_else(|| invalid(path, "levels must be non-empty strings"))
182 })
183 .collect::<Result<_, _>>()?;
184 if levels
185 .iter()
186 .collect::<std::collections::HashSet<_>>()
187 .len()
188 != levels.len()
189 {
190 return Err(invalid(path, "levels must be unique"));
191 }
192 Score::new(instructions, levels)?
193 .with_min_confidence(threshold)?
194 .into()
195 }
196 };
197 Ok((name.to_owned(), question))
198}
199fn decode_options(
200 value: Option<&Value>,
201 path: &str,
202) -> Result<IndexMap<String, Option<String>>, Error> {
203 let options = value
204 .and_then(Value::as_array)
205 .ok_or_else(|| invalid(&format!("{path}.options"), "must be an array"))?;
206 let mut result = IndexMap::new();
207 for (i, option) in options.iter().enumerate() {
208 let where_ = format!("{path}.options[{i}]");
209 let (name, description) = match option {
210 Value::String(s) => (Some(s.as_str()), None),
211 Value::Object(obj) => {
212 no_unknown(obj, &["name", "description"], &where_)?;
213 let description =
214 optional_string(obj.get("description"), &format!("{where_}.description"))?;
215 (obj.get("name").and_then(Value::as_str), description)
216 }
217 _ => {
218 return Err(invalid(
219 &where_,
220 "must be a name or {\"name\", \"description\"}",
221 ));
222 }
223 };
224 let name = name
225 .filter(|s| !s.is_empty())
226 .ok_or_else(|| invalid(&where_, "needs a non-empty name"))?;
227 if result.contains_key(name) {
228 return Err(invalid(
229 &where_,
230 format!("option {} is used twice", python_repr(name)),
231 ));
232 }
233 result.insert(name.to_owned(), description);
234 }
235 Ok(result)
236}
237fn python_repr(s: &str) -> String {
238 let quote = if s.contains('\'') && !s.contains('"') {
239 '"'
240 } else {
241 '\''
242 };
243 let mut out = String::new();
244 out.push(quote);
245 for c in s.chars() {
246 match c {
247 '\\' => out.push_str("\\\\"),
248 '\n' => out.push_str("\\n"),
249 '\r' => out.push_str("\\r"),
250 '\t' => out.push_str("\\t"),
251 c if c == quote => {
252 out.push('\\');
253 out.push(c);
254 }
255 c if c.is_control() => {
256 let n = c as u32;
257 if n < 256 {
258 out.push_str(&format!("\\x{n:02x}"));
259 } else {
260 out.push_str(&format!("\\u{n:04x}"));
261 }
262 }
263 c => out.push(c),
264 }
265 }
266 out.push(quote);
267 out
268}
269fn significant(value: f64) -> Result<Value, Error> {
270 let rounded = format!("{value:.3e}")
271 .parse::<f64>()
272 .map_err(|_| Error::new(ErrorKind::Runtime, "decimal conversion failed"))?;
273 serde_json::Number::from_f64(rounded)
274 .map(Value::Number)
275 .ok_or_else(|| Error::new(ErrorKind::InvalidResponse, "wire numbers must be finite"))
276}
277pub fn encode_response(
287 request: &WireRequest,
288 answers: &IndexMap<String, Answer>,
289) -> Result<Value, Error> {
290 if answers.is_empty()
291 || answers.len() != request.questions.len()
292 || request
293 .questions
294 .keys()
295 .any(|name| !answers.contains_key(name))
296 {
297 return Err(Error::new(
298 ErrorKind::InvalidResponse,
299 "wire answers must cover exactly the requested questions",
300 ));
301 }
302 let mut response = Map::new();
303 if let Some(id) = &request.request_id {
304 response.insert("request_id".into(), json!(id));
305 }
306 let mut entries = Map::new();
307 for (name, answer) in answers {
308 let question = &request.questions[name];
309 if matches!(question, Question::Box(_)) {
310 return Err(Error::new(
311 ErrorKind::Unsupported,
312 "wire schema 1 does not support Box",
313 ));
314 }
315 let value = match answer.value() {
316 None => Value::Null,
317 Some(AnswerValue::Boolean(v)) => json!(v),
318 Some(AnswerValue::Choice(v)) => json!(v),
319 Some(AnswerValue::Score(v)) => significant(*v)?,
320 Some(AnswerValue::Box(_)) => {
321 return Err(Error::new(
322 ErrorKind::Unsupported,
323 "wire schema 1 does not support Box",
324 ));
325 }
326 };
327 answer.revalidate(question)?;
329 let probabilities: Map<String, Value> = answer
330 .probabilities()
331 .iter()
332 .map(|(k, v)| Ok((k.clone(), significant(*v)?)))
333 .collect::<Result<_, Error>>()?;
334 entries.insert(name.clone(),json!({"type":question.kind(),"value":value,"decided":answer.decided(),"confidence":significant(answer.confidence())?,"probabilities":probabilities}));
335 }
336 response.insert("answers".into(), Value::Object(entries));
337 let call=answers.first().and_then(|(_,a)|a.call()).map(|c|json!({"provider":c.provider,"model":c.model,"input_tokens":c.input_tokens,"output_tokens":c.output_tokens})).unwrap_or(Value::Null);
338 response.insert("call".into(), call);
339 Ok(Value::Object(response))
340}
341pub fn encode_error(error: &Error, request_id: Option<&str>) -> Value {
343 let code = match error.kind() {
344 ErrorKind::Timeout => "timeout",
345 ErrorKind::InvalidRequest
346 | ErrorKind::InvalidQuestion
347 | ErrorKind::InvalidState
348 | ErrorKind::InvalidArgument => "invalid_request",
349 _ => "provider_error",
350 };
351 let mut response = Map::new();
352 if let Some(id) = request_id {
353 response.insert("request_id".into(), json!(id));
354 }
355 response.insert(
356 "error".into(),
357 json!({"code":code,"message":error.to_string()}),
358 );
359 Value::Object(response)
360}