aft/commands/semantic_search/
paging.rs1use std::fmt;
2use std::ops::Range;
3
4use serde_json::Value;
5
6use super::blocks::{
7 BlockBuildError, BlockBuilder, BlockReply, PageRequest, MAX_BLOCK_DEPTH, MAX_OFFSET,
8 MAX_PUBLIC_TOP_K,
9};
10use super::blocks::{StabilityUnit, BLOCK_DEPTHS};
11
12pub const DEFAULT_TOP_K: usize = 10;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct ValidatedPageRequest {
16 offset: usize,
17 top_k: usize,
18}
19
20impl ValidatedPageRequest {
21 pub fn offset(self) -> usize {
22 self.offset
23 }
24
25 pub fn top_k(self) -> usize {
26 self.top_k
27 }
28
29 pub fn interval_end(self) -> u64 {
30 (self.offset as u64).saturating_add(self.top_k as u64)
31 }
32
33 fn as_block_request(self) -> PageRequest {
34 PageRequest {
35 offset: self.offset,
36 top_k: self.top_k,
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct PagingValidationError {
43 field: &'static str,
44 message: String,
45}
46
47impl PagingValidationError {
48 pub const fn code(&self) -> &'static str {
49 "invalid_request"
50 }
51
52 pub const fn field(&self) -> &'static str {
53 self.field
54 }
55}
56
57impl fmt::Display for PagingValidationError {
58 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59 formatter.write_str(&self.message)
60 }
61}
62
63impl std::error::Error for PagingValidationError {}
64
65#[derive(Debug)]
66pub enum PagingError {
67 InvalidRequest(PagingValidationError),
68 Build(BlockBuildError),
69}
70
71impl fmt::Display for PagingError {
72 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::InvalidRequest(error) => error.fmt(formatter),
75 Self::Build(error) => error.fmt(formatter),
76 }
77 }
78}
79
80impl std::error::Error for PagingError {}
81
82impl From<PagingValidationError> for PagingError {
83 fn from(error: PagingValidationError) -> Self {
84 Self::InvalidRequest(error)
85 }
86}
87
88impl From<BlockBuildError> for PagingError {
89 fn from(error: BlockBuildError) -> Self {
90 Self::Build(error)
91 }
92}
93
94fn invalid(field: &'static str, message: impl Into<String>) -> PagingValidationError {
95 PagingValidationError {
96 field,
97 message: message.into(),
98 }
99}
100
101fn parse_integer(
102 value: Option<&Value>,
103 field: &'static str,
104 default: usize,
105 minimum: usize,
106 maximum: usize,
107 maximum_name: &'static str,
108) -> Result<usize, PagingValidationError> {
109 let Some(value) = value else {
110 return Ok(default);
111 };
112 let Some(integer) = value.as_i64() else {
113 return Err(invalid(
114 field,
115 format!("{field} must be an integer between {minimum} and {maximum_name} ({maximum})"),
116 ));
117 };
118 if integer < minimum as i64 || integer > maximum as i64 {
119 return Err(invalid(
120 field,
121 format!("{field} must be between {minimum} and {maximum_name} ({maximum})"),
122 ));
123 }
124 Ok(integer as usize)
125}
126
127pub fn parse_public_page_request(
129 params: &Value,
130) -> Result<ValidatedPageRequest, PagingValidationError> {
131 let offset = parse_integer(
132 params.get("offset"),
133 "offset",
134 0,
135 0,
136 MAX_OFFSET,
137 "MAX_OFFSET",
138 )?;
139 let top_k_value = match (params.get("topK"), params.get("top_k")) {
140 (Some(_), Some(_)) => {
141 return Err(invalid("topK", "topK and top_k cannot both be supplied"));
142 }
143 (Some(value), None) | (None, Some(value)) => Some(value),
144 (None, None) => None,
145 };
146 let top_k = parse_integer(
147 top_k_value,
148 "topK",
149 DEFAULT_TOP_K,
150 1,
151 MAX_PUBLIC_TOP_K,
152 "MAX_TOP_K",
153 )?;
154 Ok(ValidatedPageRequest { offset, top_k })
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum StopState {
159 S1MoreAtDepth,
160 S2Exhausted,
161 S3DepthCap,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct StopConditions {
166 pub interval_satisfied: bool,
167 pub lanes_exhausted: bool,
168 pub at_depth_cap: bool,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub struct UnreachableLoopExit;
173
174impl fmt::Display for UnreachableLoopExit {
175 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 formatter.write_str(
177 "the escalation loop cannot exit below the depth cap while its interval is unsatisfied and candidates remain",
178 )
179 }
180}
181
182impl std::error::Error for UnreachableLoopExit {}
183
184pub fn select_stop_state(conditions: StopConditions) -> Result<StopState, UnreachableLoopExit> {
187 if conditions.lanes_exhausted {
188 return Ok(StopState::S2Exhausted);
189 }
190 if conditions.at_depth_cap {
191 return Ok(StopState::S3DepthCap);
192 }
193 if conditions.interval_satisfied {
194 return Ok(StopState::S1MoreAtDepth);
195 }
196 Err(UnreachableLoopExit)
197}
198
199#[derive(Debug, Clone, PartialEq)]
200pub struct SearchPage {
201 pub reply: BlockReply,
202 pub stop_state: StopState,
203 pub stability_void: bool,
204}
205
206impl SearchPage {
207 pub fn shown(&self) -> usize {
208 self.reply.page.len()
209 }
210
211 pub fn total_at_stop(&self) -> usize {
212 self.reply.canonical_list.len()
213 }
214}
215
216pub fn serve_public_page(
217 builder: &BlockBuilder,
218 request: ValidatedPageRequest,
219) -> Result<SearchPage, PagingError> {
220 let interval_end = request.interval_end();
221 let reply = builder.build_for_request(request.as_block_request())?;
222 let stop_state = select_stop_state(StopConditions {
223 interval_satisfied: (reply.canonical_list.len() as u64) >= interval_end,
224 lanes_exhausted: reply.lanes_exhausted,
225 at_depth_cap: reply.retrieval_depth == MAX_BLOCK_DEPTH,
226 })
227 .expect("BlockBuilder can only return at a valid escalation-loop exit");
228 Ok(SearchPage {
229 reply,
230 stop_state,
231 stability_void: false,
232 })
233}
234
235#[derive(Debug, Clone, PartialEq)]
237pub struct ReferenceList {
238 pub stability_units: Vec<StabilityUnit>,
239 pub retrieval_depth: usize,
240 pub depth_tier: usize,
241 pub lanes_exhausted: bool,
242}
243
244pub(crate) fn build_l(
247 builder: &BlockBuilder,
248 interval: Range<u64>,
249) -> Result<ReferenceList, BlockBuildError> {
250 assert_eq!(interval.start, 0, "build_l accepts only canonical prefixes");
251 let target = interval.end;
252 let mut tier = BLOCK_DEPTHS
253 .iter()
254 .position(|depth| (*depth as u64) >= target)
255 .unwrap_or(BLOCK_DEPTHS.len() - 1);
256
257 loop {
258 let reply = builder.build_at_depth(BLOCK_DEPTHS[tier])?;
259 let enough = (reply.canonical_list.len() as u64) >= target;
260 if enough || reply.lanes_exhausted || tier + 1 == BLOCK_DEPTHS.len() {
261 return Ok(ReferenceList {
262 stability_units: reply
263 .canonical_list
264 .stability_units()
265 .into_iter()
266 .take(target.min(usize::MAX as u64) as usize)
267 .collect(),
268 retrieval_depth: reply.retrieval_depth,
269 depth_tier: reply.depth_tier,
270 lanes_exhausted: reply.lanes_exhausted,
271 });
272 }
273 tier += 1;
274 }
275}
276
277pub fn build_reference_list(
279 builder: &BlockBuilder,
280 interval: Range<u64>,
281) -> Result<ReferenceList, BlockBuildError> {
282 build_l(builder, interval)
283}