1use serde_json::{Map, Value, json};
10
11use crate::request::{Criteria, QType, Question};
12use crate::round::round_distribution;
13
14pub const LAYA_MODEL: &str = "laya-rl-agent";
16
17pub const TEMPERATURE_RANGE: (f64, f64) = (0.5, 5.0);
20
21#[must_use]
24pub fn clamp_temperature(t: f64) -> f64 {
25 if t.is_finite() { t.clamp(TEMPERATURE_RANGE.0, TEMPERATURE_RANGE.1) } else { 1.0 }
26}
27
28#[must_use]
30pub fn temperature_bucket(t: QType, k: usize) -> String {
31 let size = match k {
32 0..=2 => "2",
33 3..=5 => "3-5",
34 6..=10 => "6-10",
35 _ => "11+",
36 };
37 format!("{}:{size}", t.as_str())
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub struct Temperatures {
43 by_type: [f64; 3],
44 by_options: Vec<(String, f64)>,
45}
46
47impl Temperatures {
48 #[must_use]
50 pub fn new(by_type: [f64; 3], by_options: &[(String, f64)]) -> Self {
51 Self {
52 by_type: by_type.map(clamp_temperature),
53 by_options: by_options
54 .iter()
55 .map(|(k, t)| (k.clone(), clamp_temperature(*t)))
56 .collect(),
57 }
58 }
59
60 #[must_use]
62 pub fn get(&self, t: QType, k: usize) -> f64 {
63 let bucket = temperature_bucket(t, k);
64 self.by_options
65 .iter()
66 .find(|(b, _)| *b == bucket)
67 .map_or(self.by_type[t.index()], |(_, t)| *t)
68 }
69}
70
71impl Default for Temperatures {
72 fn default() -> Self {
73 Self::new([1.0; 3], &[])
74 }
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub enum Answer {
80 Choice {
82 choice: String,
84 probabilities: Vec<(String, f64)>,
86 confidence: f64,
88 act_probability: f64,
90 },
91 Score {
93 score: f64,
95 legend: Vec<Value>,
97 probabilities: Vec<f64>,
99 confidence: f64,
101 act_probability: f64,
103 },
104 Noul {
106 noul: f64,
108 confidence: f64,
110 act_probability: f64,
112 },
113}
114
115#[must_use]
117pub fn py_round(x: f64, digits: usize) -> f64 {
118 format!("{x:.digits$}").parse().unwrap_or(x)
119}
120
121impl Answer {
122 #[must_use]
125 pub fn answer_confidence(&self) -> f64 {
126 match self {
127 Answer::Choice { probabilities, .. } => {
128 probabilities.iter().map(|p| p.1).fold(0.0, f64::max).clamp(0.0, 1.0)
129 }
130 Answer::Score { probabilities, .. } => {
131 probabilities.iter().copied().fold(0.0, f64::max).clamp(0.0, 1.0)
132 }
133 Answer::Noul { confidence, .. } => *confidence,
134 }
135 }
136
137 #[must_use]
139 pub fn to_json(&self) -> Value {
140 let answer_confidence = py_round(self.answer_confidence(), 4);
141 let r = |x: f64| py_round(x, 4);
142 match self {
143 Answer::Choice { choice, probabilities, confidence, act_probability } => json!({
144 "type": "choice",
145 "choice": choice,
146 "probabilities": probabilities.iter().map(|(k, p)| (k.clone(), json!(r(*p)))).collect::<Map<_, _>>(),
147 "confidence": r(*confidence),
148 "answer_confidence": answer_confidence,
149 "action": {"act_probability": r(*act_probability)},
150 }),
151 Answer::Score { score, legend, probabilities, confidence, act_probability } => json!({
152 "type": "score",
153 "score": r(*score),
154 "legend": legend.iter().enumerate().map(|(i, v)| (i.to_string(), v.clone())).collect::<Map<_, _>>(),
155 "probabilities": probabilities.iter().enumerate().map(|(i, p)| (i.to_string(), json!(r(*p)))).collect::<Map<_, _>>(),
156 "confidence": r(*confidence),
157 "answer_confidence": answer_confidence,
158 "action": {"act_probability": r(*act_probability)},
159 }),
160 Answer::Noul { noul, confidence, act_probability } => json!({
161 "type": "noul",
162 "noul": r(*noul),
163 "confidence": r(*confidence),
164 "answer_confidence": answer_confidence,
165 "action": {"act_probability": r(*act_probability)},
166 }),
167 }
168 }
169}
170
171impl Answer {
172 #[must_use]
177 pub fn to_jev_json(&self, precision: Option<u32>, entropy: bool) -> Value {
178 let r = |x: f64| match precision {
179 Some(d) => py_round(x, d as usize),
180 None => f64::from(x as f32),
181 };
182 let dist = |p: &[f64]| -> Vec<f64> {
183 match precision {
184 Some(d) => round_distribution(p, d),
185 None => p.iter().map(|&x| f64::from(x as f32)).collect(),
186 }
187 };
188 match self {
189 Answer::Choice { choice, probabilities, confidence, .. } => {
190 let raw: Vec<f64> = probabilities.iter().map(|p| p.1).collect();
191 let conf = if entropy { *confidence } else { crate::confidence::choice_jev(&raw) };
192 json!({
193 "type": "choice",
194 "choice": choice,
195 "confidence": r(conf),
196 "probabilities": probabilities.iter().zip(dist(&raw)).map(|((k, _), p)| (k.clone(), json!(p))).collect::<Map<_, _>>(),
197 })
198 }
199 Answer::Score { score, legend, probabilities, confidence, .. } => {
200 let conf =
201 if entropy { *confidence } else { crate::confidence::score_jev(probabilities) };
202 json!({
203 "type": "score",
204 "score": r(*score),
205 "confidence": r(conf),
206 "legend": legend.iter().enumerate().map(|(i, v)| (i.to_string(), v.clone())).collect::<Map<_, _>>(),
207 "probabilities": dist(probabilities).into_iter().enumerate().map(|(i, p)| (i.to_string(), json!(p))).collect::<Map<_, _>>(),
208 })
209 }
210 Answer::Noul { noul, .. } => json!({"type": "noul", "noul": r(*noul)}),
211 }
212 }
213}
214
215#[derive(Debug, Clone, PartialEq)]
217pub struct Response {
218 pub model: String,
220 pub answers: Vec<(String, Answer)>,
222 pub input_tokens: usize,
224}
225
226impl Response {
227 #[must_use]
229 pub fn get(&self, id: &str) -> Option<&Answer> {
230 self.answers.iter().find(|(q, _)| q == id).map(|(_, a)| a)
231 }
232
233 #[must_use]
235 pub fn to_json(&self) -> Value {
236 let answers: Map<_, _> =
237 self.answers.iter().map(|(id, a)| (id.clone(), a.to_json())).collect();
238 json!({
239 "model": self.model,
240 "answers": answers,
241 "usage": {"input_tokens": self.input_tokens, "output_tokens": 0},
242 })
243 }
244}
245
246fn pairwise<T: Copy + Default + std::ops::Add<Output = T>>(x: &[T]) -> T {
248 let n = x.len();
249 if n < 8 {
250 return x.iter().fold(T::default(), |a, &b| a + b);
251 }
252 if n <= 128 {
253 let mut r = [x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7]];
254 let body = n - n % 8;
255 for c in x[8..body].as_chunks::<8>().0 {
256 for j in 0..8 {
257 r[j] = r[j] + c[j];
258 }
259 }
260 let mut s = ((r[0] + r[1]) + (r[2] + r[3])) + ((r[4] + r[5]) + (r[6] + r[7]));
261 for &v in &x[body..] {
262 s = s + v;
263 }
264 return s;
265 }
266 let half = n / 2 - (n / 2) % 8;
267 pairwise(&x[..half]) + pairwise(&x[half..])
268}
269
270#[allow(clippy::excessive_precision)]
275fn np_exp(x: f32) -> f32 {
276 const XMAX: f32 = 88.722_84;
277 const XMIN: f32 = -103.972_08;
278 const MAGIC: f32 = 12_582_912.0;
279 const P: [f32; 6] = [
280 9.999_999_999_980_870_924_916e-1,
281 7.257_664_613_233_124_478_488e-1,
282 2.473_615_434_895_520_810_817e-1,
283 5.114_512_081_637_298_353_406e-2,
284 6.757_896_990_527_504_603_057e-3,
285 5.082_762_527_590_693_718_096e-4,
286 ];
287 const Q: [f32; 3] = [1.0, -2.742_335_390_411_667_452_936e-1, 2.159_509_375_685_829_852_307e-2];
288 if x.is_nan() {
289 return x;
290 }
291 if x >= XMAX {
292 return f32::INFINITY;
293 }
294 if x <= XMIN {
295 return 0.0;
296 }
297 let q = (x * std::f32::consts::LOG2_E + MAGIC) - MAGIC;
298 let r = q.mul_add(-1.428_606_77e-6, q.mul_add(-6.931_457_52e-1, x));
299 let r = q.mul_add(0.0, r);
300 let num =
301 P[5].mul_add(r, P[4]).mul_add(r, P[3]).mul_add(r, P[2]).mul_add(r, P[1]).mul_add(r, P[0]);
302 let den = Q[2].mul_add(r, Q[1]).mul_add(r, Q[0]);
303 let poly = num / den;
304 let shift =
306 |poly: f32, q: f32| f32::from_bits(poly.to_bits().wrapping_add(((q as i32) << 23) as u32));
307 if q <= -125.0 {
308 let diff = (-(q + 125.0)) as u32;
309 shift(poly, -125.0) / (1u32 << diff) as f32
310 } else {
311 shift(poly, q)
312 }
313}
314
315#[allow(clippy::excessive_precision)]
319fn np_log(x: f32) -> f32 {
320 const P: [f32; 6] = [
321 0.0,
322 9.999_999_999_999_998_702_752e-1,
323 2.112_677_543_073_053_063_722,
324 1.480_000_633_576_506_585_156,
325 3.808_837_741_388_407_920_751e-1,
326 2.589_979_117_907_922_693_523e-2,
327 ];
328 const Q: [f32; 6] = [
329 1.0,
330 2.612_677_543_073_109_236_779,
331 2.453_006_071_784_736_363_091,
332 9.864_942_958_519_418_960_339e-1,
333 1.546_476_374_983_906_719_538e-1,
334 5.875_095_403_124_574_342_950e-3,
335 ];
336 if x.is_nan() || x < 0.0 {
337 return f32::NAN;
338 }
339 if x == 0.0 {
340 return f32::NEG_INFINITY;
341 }
342 if x.is_infinite() {
343 return x;
344 }
345 let (bits, bias) = if x < f32::MIN_POSITIVE {
346 ((x * f32::from_bits(0x7180_0000)).to_bits(), 100.0)
347 } else {
348 (x.to_bits(), 0.0)
349 };
350 let mut exponent = ((bits >> 23) as i32 - 126) as f32 - bias;
351 let mut m = f32::from_bits((bits & 0x7f_ffff) | (126 << 23));
352 if m <= std::f32::consts::FRAC_1_SQRT_2 {
353 m += m;
354 exponent -= 1.0;
355 }
356 let m = m - 1.0;
357 let num =
358 P[5].mul_add(m, P[4]).mul_add(m, P[3]).mul_add(m, P[2]).mul_add(m, P[1]).mul_add(m, P[0]);
359 let den =
360 Q[5].mul_add(m, Q[4]).mul_add(m, Q[3]).mul_add(m, Q[2]).mul_add(m, Q[1]).mul_add(m, Q[0]);
361 exponent.mul_add(std::f32::consts::LN_2, num / den)
362}
363
364fn softmax(logits: &[f32], t: f64) -> Vec<f32> {
366 let t = t as f32;
367 let z: Vec<f32> = logits.iter().map(|&l| l / t).collect();
368 let top = z.iter().copied().fold(f32::NEG_INFINITY, f32::max);
369 let e: Vec<f32> = z.iter().map(|&v| np_exp(v - top)).collect();
370 let s = pairwise(&e);
371 e.iter().map(|&v| v / s).collect()
372}
373
374fn entropy_confidence(p: &[f32]) -> f64 {
376 let k = p.len();
377 if k < 2 {
378 return 1.0;
379 }
380 let terms: Vec<f32> = p.iter().map(|&v| v * np_log(v.clamp(1e-12, 1.0))).collect();
381 let ent = -pairwise(&terms);
382 f64::from((1.0 - ent / (k as f64).ln() as f32).clamp(0.0, 1.0))
383}
384
385#[must_use]
387pub fn act_probability(act: [f32; 2]) -> f64 {
388 let top = act[0].max(act[1]);
389 let e = act.map(|v| (v - top).exp());
390 f64::from(e[0] * (1.0 / (e[0] + e[1])))
391}
392
393#[must_use]
399pub fn laya_answer(q: &Question, logits: &[f32], act: [f32; 2], temps: &Temperatures) -> Answer {
400 let k = q.criteria.len();
401 assert_eq!(logits.len(), k, "one logit per option");
402 let p = softmax(logits, temps.get(q.qtype, k));
403 let act_probability = act_probability(act);
404 match &q.criteria {
405 Criteria::Choice(opts) => {
406 let mut best = 0;
407 for (i, &v) in p.iter().enumerate() {
408 if v > p[best] {
409 best = i;
410 }
411 }
412 Answer::Choice {
413 choice: opts[best].label.clone(),
414 probabilities: opts
415 .iter()
416 .zip(&p)
417 .map(|(o, &v)| (o.label.clone(), f64::from(v)))
418 .collect(),
419 confidence: entropy_confidence(&p),
420 act_probability,
421 }
422 }
423 Criteria::Score(levels) => {
424 let weighted: Vec<f64> =
425 p.iter().enumerate().map(|(i, &v)| i as f64 * f64::from(v)).collect();
426 Answer::Score {
427 score: pairwise(&weighted),
428 legend: levels.clone(),
429 probabilities: p.iter().map(|&v| f64::from(v)).collect(),
430 confidence: entropy_confidence(&p),
431 act_probability,
432 }
433 }
434 Criteria::Noul { .. } => {
435 let yes = f64::from(p[1]);
436 Answer::Noul { noul: yes, confidence: yes.max(1.0 - yes), act_probability }
437 }
438 }
439}
440
441#[cfg(test)]
442#[allow(clippy::float_cmp)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn pairwise_order() {
449 let x = [1e8f32, 1.0, -1e8, 1.0, 0.5, 0.25, 3.0, 7.0, 1.0];
451 let lanes = ((1e8f32 + 1.0) + (-1e8 + 1.0)) + ((0.5 + 0.25) + (3.0 + 7.0)) + 1.0;
452 assert_eq!(pairwise(&x).to_bits(), lanes.to_bits());
453 }
454
455 #[test]
456 fn numpy_exp_log() {
457 for (x, want) in [(-0.0, 1.0f32), (-1.0, 0.367_879_43), (-20.0, 2.061_153_6e-9)] {
459 assert!((np_exp(x) - want).abs() <= want * 3e-7, "exp {x}");
460 }
461 assert_eq!(np_exp(0.0), 1.0);
462 assert_eq!(np_log(1.0), 0.0);
463 assert!((np_log(1e-12) - 1e-12f32.ln()).abs() < 1e-5);
464 assert!((np_log(0.3) - 0.3f32.ln()).abs() < 1e-6);
465 }
466
467 #[test]
468 fn rounding() {
469 assert_eq!(py_round(0.25, 1), 0.2);
470 assert_eq!(py_round(0.965_449_999, 4), 0.9654);
471 assert_eq!(py_round(1.0, 4), 1.0);
472 }
473
474 #[test]
475 fn buckets_and_clamp() {
476 let t = Temperatures::new([1.5, 1.2, 2.0], &[("choice:11+".into(), 0.1)]);
477 assert_eq!(t.get(QType::Choice, 12), 0.5);
478 assert_eq!(t.get(QType::Choice, 4), 1.5);
479 assert_eq!(temperature_bucket(QType::Score, 6), "score:6-10");
480 assert_eq!(clamp_temperature(f64::NAN), 1.0);
481 }
482
483 #[test]
484 fn jev_shape() {
485 let a = Answer::Choice {
486 choice: "b".into(),
487 probabilities: vec![("a".into(), 0.334), ("b".into(), 0.335), ("c".into(), 0.331)],
488 confidence: 0.1,
489 act_probability: 0.9,
490 };
491 let v = a.to_jev_json(Some(2), false);
492 let p = &v["probabilities"];
493 assert_eq!(
494 (p["a"].as_f64(), p["b"].as_f64(), p["c"].as_f64()),
495 (Some(0.33), Some(0.34), Some(0.33))
496 );
497 assert_eq!(v["choice"], "b");
498 assert!(v.get("action").is_none());
499 let s = Answer::Score {
500 score: 1.2345,
501 legend: vec![json!("lo"), json!("mid"), json!("hi")],
502 probabilities: vec![0.105, 0.555, 0.34],
503 confidence: 0.5,
504 act_probability: 1.0,
505 };
506 let v = s.to_jev_json(Some(1), false);
507 assert_eq!(v["score"], 1.2);
508 assert_eq!(v["legend"]["2"], "hi");
509 let n = Answer::Noul { noul: 0.876, confidence: 0.876, act_probability: 1.0 };
510 assert_eq!(n.to_jev_json(Some(2), false), json!({"type": "noul", "noul": 0.88}));
511 assert_eq!(n.to_jev_json(None, false)["noul"], f64::from(0.876f32));
512 }
513}