Skip to main content

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}