katago_analysis/engine/response.rs
1use serde::Deserialize;
2use serde_json::{Map, Value};
3
4use crate::{Model, Player};
5
6/// A response from the analysis engine.
7#[derive(Debug, Clone, Deserialize)]
8#[serde(try_from = "Value")]
9#[expect(
10 clippy::large_enum_variant,
11 reason = "Boxing AnalysisResponse would be inconvenient, and very little would be gained"
12)]
13pub enum Response {
14 /// The result of analyzing a position.
15 Analyze(AnalysisResponse),
16
17 /// Indicates that analysis was terminated before analyzing the specified position.
18 NoResults {
19 /// The request ID.
20 id: String,
21
22 /// The position index, where 0 is the position before the first move.
23 turn_number: usize,
24 },
25
26 /// KataGo's version information.
27 QueryVersion {
28 /// The request ID.
29 id: String,
30
31 /// A string indicating the most recent KataGo release version that this version is a descendant of,
32 /// such as `"1.6.1"`.
33 version: String,
34
35 /// The precise git hash this KataGo version was compiled from, or the string `"<omitted>"` if KataGo was
36 /// compiled separately from its repo or without Git support.
37 git_hash: String,
38 },
39
40 /// Indicates that the cache was cleared.
41 ClearCache {
42 /// The request ID.
43 id: String,
44 },
45
46 /// Acknowledgement of a terminate request. The engine will proceed to send [`NoResults`][Response::NoResults] or
47 /// partial [`Analyze`][Response::Analyze] responses for each position after they have been terminated.
48 Terminate {
49 /// The request ID.
50 id: String,
51
52 /// The ID of the request being terminated.
53 terminate_id: String,
54
55 /// The positions being terminated, if specified in the request.
56 #[serde(default)]
57 turn_numbers: Option<Vec<usize>>,
58 },
59
60 /// Acknowledgement of a request to terminate all analyses. The engine will proceed to send
61 /// [`NoResults`][Response::NoResults] or partial [`Analyze`][Response::Analyze] responses for each position
62 /// after they have been terminated.
63 TerminateAll {
64 /// The request ID.
65 id: String,
66
67 /// The positions being terminated, if specified in the request.
68 #[serde(default)]
69 turn_numbers: Option<Vec<usize>>,
70 },
71
72 /// Information about the currently loaded neural network models.
73 QueryModels {
74 /// The request ID.
75 id: String,
76
77 /// A list of available models.
78 models: Vec<Model>,
79 },
80
81 /// An error with no known associated request.
82 GeneralError {
83 /// The error message.
84 error: String,
85 },
86
87 /// An error in processing a request.
88 FieldError {
89 /// The request ID.
90 id: String,
91
92 /// The error message.
93 error: String,
94
95 /// The request field which is the source of the error.
96 field: String,
97 },
98
99 /// A warning in processing a request. The engine will still generate analysis responses for the request.
100 FieldWarning {
101 /// The request ID.
102 id: String,
103
104 /// The warning message.
105 warning: String,
106
107 /// The request field which is the source of the warning.
108 field: String,
109 },
110}
111
112impl TryFrom<Value> for Response {
113 type Error = String;
114
115 fn try_from(value: Value) -> Result<Self, Self::Error> {
116 fn try_parse_field_error(map: &Map<String, Value>) -> Option<Response> {
117 let error = map.get("error")?.as_str()?;
118 let field = map.get("field")?.as_str()?;
119 let id = map.get("id")?.as_str()?;
120 Some(Response::FieldError {
121 id: id.to_string(),
122 error: error.to_string(),
123 field: field.to_string(),
124 })
125 }
126
127 fn try_parse_general_error(map: &Map<String, Value>) -> Option<Response> {
128 let error = map.get("error")?.as_str()?;
129 Some(Response::GeneralError {
130 error: error.to_string(),
131 })
132 }
133
134 fn try_parse_field_warning(map: &Map<String, Value>) -> Option<Response> {
135 let warning = map.get("warning")?.as_str()?;
136 let field = map.get("field")?.as_str()?;
137 let id = map.get("id")?.as_str()?;
138 Some(Response::FieldWarning {
139 id: id.to_string(),
140 warning: warning.to_string(),
141 field: field.to_string(),
142 })
143 }
144
145 fn try_parse_query_version(map: &Map<String, Value>) -> Option<Response> {
146 let action = map.get("action")?.as_str()?;
147 if action != "query_version" {
148 return None;
149 }
150 let id = map.get("id")?.as_str()?;
151 let version = map.get("version")?.as_str()?;
152 let git_hash = map.get("git_hash")?.as_str()?;
153 Some(Response::QueryVersion {
154 id: id.to_string(),
155 version: version.to_string(),
156 git_hash: git_hash.to_string(),
157 })
158 }
159
160 fn try_parse_clear_cache(map: &Map<String, Value>) -> Option<Response> {
161 let action = map.get("action")?.as_str()?;
162 if action != "clear_cache" {
163 return None;
164 }
165 let id = map.get("id")?.as_str()?;
166 Some(Response::ClearCache { id: id.to_string() })
167 }
168
169 fn try_parse_no_results(map: &Map<String, Value>) -> Option<Response> {
170 map.get("noResults")?;
171 let id = map.get("id")?.as_str()?;
172 let turn_number = map.get("turnNumber")?.as_u64()? as usize;
173 Some(Response::NoResults {
174 id: id.to_string(),
175 turn_number,
176 })
177 }
178
179 fn try_parse_terminate(map: &Map<String, Value>) -> Option<Response> {
180 let action = map.get("action")?.as_str()?;
181 if action != "terminate" {
182 return None;
183 }
184 let id = map.get("id")?.as_str()?;
185 let terminate_id = map.get("terminateId")?.as_str()?;
186 let turn_numbers = map
187 .get("turnNumbers")
188 .and_then(|v| serde_json::from_value(v.clone()).ok());
189 Some(Response::Terminate {
190 id: id.to_string(),
191 terminate_id: terminate_id.to_string(),
192 turn_numbers,
193 })
194 }
195
196 fn try_parse_terminate_all(map: &Map<String, Value>) -> Option<Response> {
197 let action = map.get("action")?.as_str()?;
198 if action != "terminate_all" {
199 return None;
200 }
201 let id = map.get("id")?.as_str()?;
202 let turn_numbers = map
203 .get("turnNumbers")
204 .and_then(|v| serde_json::from_value(v.clone()).ok());
205 Some(Response::TerminateAll {
206 id: id.to_string(),
207 turn_numbers,
208 })
209 }
210
211 fn try_parse_query_models(map: &Map<String, Value>) -> Option<Response> {
212 let action = map.get("action")?.as_str()?;
213 if action != "query_models" {
214 return None;
215 }
216 let id = map.get("id")?.as_str()?;
217 let models = map.get("models")?;
218 Some(Response::QueryModels {
219 id: id.to_string(),
220 models: serde_json::from_value(models.clone()).ok()?,
221 })
222 }
223
224 fn try_parse_analysis(value: Value) -> Option<Response> {
225 serde_json::from_value(value).ok().map(Response::Analyze)
226 }
227
228 let map = value.as_object().ok_or("expected object")?;
229 try_parse_field_error(map)
230 .or_else(|| try_parse_general_error(map))
231 .or_else(|| try_parse_field_warning(map))
232 .or_else(|| try_parse_query_version(map))
233 .or_else(|| try_parse_clear_cache(map))
234 .or_else(|| try_parse_no_results(map))
235 .or_else(|| try_parse_terminate(map))
236 .or_else(|| try_parse_terminate_all(map))
237 .or_else(|| try_parse_query_models(map))
238 .or_else(|| try_parse_analysis(value))
239 .ok_or("unrecognized response format".to_string())
240 }
241}
242
243/// The result of analyzing a position.
244#[derive(Debug, Clone, Deserialize)]
245#[serde(rename_all = "camelCase")]
246pub struct AnalysisResponse {
247 /// The request ID.
248 pub id: String,
249
250 /// Whether this is a partial analysis result. `false` indicates no more responses will be sent.
251 pub is_during_search: bool,
252
253 /// The position index, where 0 is the position before the first move.
254 pub turn_number: usize,
255
256 /// The list of moves the engine considered.
257 pub move_infos: Vec<MoveInfo>,
258
259 /// Information about the root position.
260 pub root_info: RootInfo,
261
262 /// The ownership prediction, in row-major order.
263 pub ownership: Option<Vec<f64>>,
264
265 /// The standard deviation of the ownership prediction, in row-major order.
266 pub ownership_stdev: Option<Vec<f64>>,
267
268 /// The policy prediction, in row-major order with the pass move at the end.
269 pub policy: Option<Vec<f64>>,
270
271 /// The humanSL policy prediction, in row-major order with the pass move at the end.
272 pub human_policy: Option<Vec<f64>>,
273}
274
275/// The result of analyzing a candidate move.
276#[derive(Debug, Clone, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct MoveInfo {
279 /// The move location in GTP format (`"A1"`, `"pass"`, etc.). This corresponds to the `move` field in KataGo's
280 /// response.
281 #[serde(rename = "move")]
282 pub mv: String,
283
284 /// The number of visits invested in this move.
285 pub visits: u32,
286
287 /// The number of visits the root "wants" to invest in this move.
288 pub edge_visits: u32,
289
290 /// The winrate, in the range [0, 1].
291 pub winrate: f64,
292
293 /// The predicted number of points that the current side is leading by.
294 pub score_lead: f64,
295
296 /// The predicted standard deviation of the score lead.
297 pub score_stdev: f64,
298
299 /// The predicted score at the end of the game after selfplay.
300 pub score_selfplay: f64,
301
302 /// The policy prior of this move.
303 pub prior: f64,
304
305 /// The predicted probability that the game will have a void result.
306 pub no_result_value: Option<f64>,
307
308 /// The humanSL policy prior of this move.
309 pub human_prior: Option<f64>,
310
311 /// The utility of this move.
312 pub utility: f64,
313
314 /// The LCB of this move's winrate.
315 pub lcb: f64,
316
317 /// The LCB of this move's utility.
318 pub utility_lcb: f64,
319
320 /// The total weight of this move's visits.
321 pub weight: f64,
322
323 /// The total weight of the visits the root "wants" to invest in this move.
324 pub edge_weight: f64,
325
326 /// The relative ranking of this move, where 0 is best.
327 pub order: usize,
328
329 /// The value used to determine the move ranking.
330 pub play_selection_value: f64,
331
332 /// If present, indicates the move that was actually searched to get the evaluation of this move.
333 pub is_symmetry_of: Option<String>,
334
335 /// The principal variation for this move.
336 pub pv: Vec<String>,
337
338 /// The number of visits invested in each position in the principal variation.
339 pub pv_visits: Option<Vec<u32>>,
340
341 /// The number of visits invested in each move in the principal variation.
342 pub pv_edge_visits: Option<Vec<u32>>,
343
344 /// The ownership prediction, in row-major order.
345 pub ownership: Option<Vec<f64>>,
346
347 /// The standard deviation of the ownership prediction, in row-major order.
348 pub ownership_stdev: Option<Vec<f64>>,
349}
350
351/// The result of analyzing the root position.
352#[derive(Debug, Clone, Deserialize)]
353#[serde(rename_all = "camelCase")]
354pub struct RootInfo {
355 /// The winrate, in the range [0, 1].
356 pub winrate: f64,
357
358 /// The predicted number of points that the current side is leading by.
359 pub score_lead: f64,
360
361 /// The predicted score at the end of the game after selfplay.
362 pub score_selfplay: f64,
363
364 /// The utility.
365 pub utility: f64,
366
367 /// The number of visits received.
368 pub visits: u32,
369
370 /// The hash of this position.
371 pub this_hash: String,
372
373 /// The hash of this position that is invariant under board symmetries.
374 pub sym_hash: String,
375
376 /// The player to move.
377 pub current_player: Player,
378
379 /// The winrate prediction from the neural network.
380 pub raw_winrate: f64,
381
382 /// The score lead prediction from the neural network.
383 pub raw_lead: f64,
384
385 /// The selfplay score prediction from the neural network.
386 pub raw_score_selfplay: f64,
387
388 /// The selfplay score standard deviation prediction from the neural network.
389 pub raw_score_selfplay_stdev: f64,
390
391 /// The void result probability prediction from the neural network.
392 pub raw_no_result_prob: f64,
393
394 /// The short-term winrate uncertainty prediction from the neural network.
395 pub raw_st_wr_error: f64,
396
397 /// The short-term score uncertainty prediction from the neural network.
398 pub raw_st_score_error: f64,
399
400 /// A measure of how much meaningful game is left until the winner is known, predicted by the neural network.
401 pub raw_var_time_left: f64,
402
403 /// The winrate prediction from the humanSL neural network.
404 pub human_winrate: Option<f64>,
405
406 /// The score prediction from the humanSL neural network.
407 pub human_score_mean: Option<f64>,
408
409 /// The score standard deviation prediction from the humanSL neural network.
410 pub human_score_stdev: Option<f64>,
411
412 /// The short-term winrate uncertainty prediction from the humanSL neural network.
413 pub human_st_wr_error: Option<f64>,
414
415 /// The short-term score uncertainty prediction from the humanSL neural network.
416 pub human_st_score_error: Option<f64>,
417}