Skip to main content

algocline_core/
query.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4/// Query identifier within a batch.
5///
6/// Use `single()` for alc.llm(), `batch(index)` for alc.llm_batch().
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct QueryId(String);
9
10impl QueryId {
11    /// For single alc.llm() calls.
12    pub fn single() -> Self {
13        Self("q-0".into())
14    }
15
16    /// For alc.llm_batch() with the given index.
17    pub fn batch(index: usize) -> Self {
18        Self(format!("q-{index}"))
19    }
20
21    /// Construct from an arbitrary string (e.g. MCP parameters).
22    pub fn parse(s: &str) -> Self {
23        Self(s.to_string())
24    }
25
26    pub fn as_str(&self) -> &str {
27        &self.0
28    }
29}
30
31impl fmt::Display for QueryId {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        self.0.fmt(f)
34    }
35}
36
37/// LLM request emitted during execution.
38/// Transport-agnostic (no channel, HTTP, or MCP Sampling details).
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct LlmQuery {
41    pub id: QueryId,
42    pub prompt: String,
43    pub system: Option<String>,
44    pub max_tokens: u32,
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn single_query_id() {
53        let id = QueryId::single();
54        assert_eq!(id.as_str(), "q-0");
55        assert_eq!(id.to_string(), "q-0");
56    }
57
58    #[test]
59    fn batch_query_ids_are_unique() {
60        let ids: Vec<QueryId> = (0..5).map(QueryId::batch).collect();
61        let set: std::collections::HashSet<&QueryId> = ids.iter().collect();
62        assert_eq!(set.len(), 5);
63        assert_eq!(ids[0].as_str(), "q-0");
64        assert_eq!(ids[3].as_str(), "q-3");
65    }
66
67    #[test]
68    fn single_equals_batch_zero() {
69        assert_eq!(QueryId::single(), QueryId::batch(0));
70    }
71
72    #[test]
73    fn parse_roundtrip() {
74        let id = QueryId::parse("q-42");
75        assert_eq!(id.as_str(), "q-42");
76        assert_eq!(id, QueryId::batch(42));
77    }
78
79    #[test]
80    fn parse_arbitrary() {
81        let id = QueryId::parse("custom-id");
82        assert_eq!(id.as_str(), "custom-id");
83    }
84
85    #[test]
86    fn query_id_roundtrip_json() {
87        let id = QueryId::batch(42);
88        let json = serde_json::to_string(&id).unwrap();
89        let restored: QueryId = serde_json::from_str(&json).unwrap();
90        assert_eq!(id, restored);
91    }
92
93    #[test]
94    fn llm_query_roundtrip_json() {
95        let query = LlmQuery {
96            id: QueryId::single(),
97            prompt: "test prompt".into(),
98            system: Some("system".into()),
99            max_tokens: 1024,
100        };
101        let json = serde_json::to_value(&query).unwrap();
102        let restored: LlmQuery = serde_json::from_value(json).unwrap();
103        assert_eq!(restored.id, query.id);
104        assert_eq!(restored.prompt, query.prompt);
105        assert_eq!(restored.system, query.system);
106        assert_eq!(restored.max_tokens, query.max_tokens);
107    }
108}
109
110#[cfg(test)]
111mod proptests {
112    use super::*;
113    use proptest::prelude::*;
114
115    proptest! {
116        #[test]
117        fn parse_roundtrip_arbitrary(s in "\\PC{1,100}") {
118            let id = QueryId::parse(&s);
119            prop_assert_eq!(id.as_str(), s.as_str());
120        }
121
122        #[test]
123        fn batch_roundtrip(index in 0usize..10_000) {
124            let id = QueryId::batch(index);
125            let expected = format!("q-{index}");
126            prop_assert_eq!(id.as_str(), expected.as_str());
127        }
128
129        #[test]
130        fn display_matches_as_str(s in "\\PC{1,100}") {
131            let id = QueryId::parse(&s);
132            prop_assert_eq!(id.to_string(), id.as_str().to_string());
133        }
134
135        #[test]
136        fn serde_roundtrip_arbitrary(s in "\\PC{1,100}") {
137            let id = QueryId::parse(&s);
138            let json = serde_json::to_string(&id).unwrap();
139            let restored: QueryId = serde_json::from_str(&json).unwrap();
140            prop_assert_eq!(id, restored);
141        }
142    }
143}