rosetta_aisp_llm/
claude.rs1use crate::provider::{LlmProvider, LlmResult};
7use anyhow::Result;
8use async_trait::async_trait;
9use rosetta_aisp::{get_all_categories, symbol_to_prose, symbols_by_category, ConversionTier};
10
11fn symbol_ref_grouped() -> String {
13 let mut output = String::new();
14 let categories = get_all_categories();
15
16 for category in categories {
17 output.push_str(&format!("\n### {}\n", category.to_uppercase()));
18 let symbols = symbols_by_category(category);
19 for symbol in symbols {
20 if let Some(pattern) = symbol_to_prose(symbol) {
21 output.push_str(&format!("- {}: {}\n", symbol, pattern));
22 }
23 }
24 }
25 output
26}
27
28fn system_prompt() -> String {
30 let symbol_ref = symbol_ref_grouped();
31
32 format!(
33 r#"You are an AISP (AI Symbolic Programming) conversion specialist.
34
35Convert natural language prose to AISP 5.1 symbolic notation using these rules:
36
37## Symbol Reference (Rosetta Stone)
38{symbol_ref}
39
40## Output Format by Tier
41
42### Minimal Tier
43Direct symbol substitution only. Example:
44Input: "Define x as 5"
45Output: x≜5
46
47### Standard Tier
48Include header block with metadata:
49```
50𝔸5.1.[name]@[date]
51γ≔[name]
52
53⟦Λ:Funcs⟧{{
54 [symbol conversion]
55}}
56
57⟦Ε⟧⟨δ≜0.70;τ≜◊⁺⟩
58```
59
60### Full Tier
61Complete AISP document with all blocks:
62```
63𝔸5.1.[name]@[date]
64γ≔[name].definitions
65ρ≔⟨[name],types,rules⟩
66
67⟦Ω:Meta⟧{{
68 domain≜[name]
69 version≜1.0.0
70 ∀D∈AISP:Ambig(D)<0.02
71}}
72
73⟦Σ:Types⟧{{
74 [inferred types]
75}}
76
77⟦Γ:Rules⟧{{
78 [inferred rules]
79}}
80
81⟦Λ:Funcs⟧{{
82 [symbol conversion]
83}}
84
85⟦Ε⟧⟨δ≜0.82;φ≜100;τ≜◊⁺⁺;⊢valid;∎⟩
86```
87
88## Rules
891. Output ONLY the AISP notation - no explanations
902. Preserve semantic meaning precisely
913. Use appropriate Unicode symbols from the reference
924. For ambiguous phrases, choose the most logical interpretation
935. Never hallucinate symbols not in the reference"#
94 )
95}
96
97fn create_user_prompt(
99 prose: &str,
100 tier: ConversionTier,
101 unmapped: &[String],
102 partial_output: Option<&str>,
103) -> String {
104 let mut prompt = format!(
105 r#"Convert this prose to AISP ({} tier):
106
107"{}""#,
108 tier, prose
109 );
110
111 if !unmapped.is_empty() {
112 prompt.push_str(&format!(
113 "\n\nNote: These phrases couldn't be mapped deterministically: {}",
114 unmapped.join(", ")
115 ));
116 }
117
118 if let Some(partial) = partial_output {
119 prompt.push_str(&format!("\n\nPartial conversion attempt:\n{}", partial));
120 }
121
122 prompt
123}
124
125pub struct ClaudeFallback {
130 model: String,
131}
132
133impl Default for ClaudeFallback {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl ClaudeFallback {
140 pub fn new() -> Self {
142 Self {
143 model: "sonnet".to_string(),
144 }
145 }
146
147 pub fn with_model(model: impl Into<String>) -> Self {
149 Self {
150 model: model.into(),
151 }
152 }
153
154 pub fn haiku() -> Self {
156 Self::with_model("haiku")
157 }
158
159 pub fn sonnet() -> Self {
161 Self::with_model("sonnet")
162 }
163
164 pub fn opus() -> Self {
166 Self::with_model("opus")
167 }
168}
169
170#[async_trait]
171impl LlmProvider for ClaudeFallback {
172 async fn convert(
173 &self,
174 prose: &str,
175 tier: ConversionTier,
176 unmapped: &[String],
177 partial_output: Option<&str>,
178 ) -> Result<LlmResult> {
179 use claude_agent_sdk_rs::{query, ClaudeAgentOptions, ContentBlock, Message, PermissionMode};
180
181 let user_prompt = create_user_prompt(prose, tier, unmapped, partial_output);
182
183 let options = ClaudeAgentOptions::builder()
185 .model(&self.model)
186 .system_prompt(system_prompt())
187 .max_turns(1) .permission_mode(PermissionMode::BypassPermissions)
189 .tools(Vec::<String>::new()) .build();
191
192 let messages = query(&user_prompt, Some(options)).await?;
193
194 let mut output = String::new();
196 let mut tokens_used = None;
197
198 for message in messages {
199 match message {
200 Message::Assistant(msg) => {
201 for block in msg.message.content {
202 if let ContentBlock::Text(text) = block {
203 output.push_str(&text.text);
204 }
205 }
206 }
207 Message::Result(result) => {
208 if let Some(cost) = result.total_cost_usd {
209 tokens_used = Some((cost * 100000.0) as usize);
211 }
212 }
213 _ => {}
214 }
215 }
216
217 Ok(LlmResult {
218 output: output.trim().to_string(),
219 provider: "claude".to_string(),
220 model: self.model.clone(),
221 tokens_used,
222 })
223 }
224
225 async fn is_available(&self) -> bool {
226 std::process::Command::new("claude")
228 .arg("--version")
229 .output()
230 .is_ok()
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn test_system_prompt_generation() {
240 let prompt = system_prompt();
241 assert!(prompt.contains("AISP"));
242 assert!(prompt.contains("Rosetta Stone"));
243 }
244
245 #[test]
246 fn test_user_prompt_minimal() {
247 let prompt = create_user_prompt("Define x as 5", ConversionTier::Minimal, &[], None);
248 assert!(prompt.contains("Define x as 5"));
249 assert!(prompt.contains("minimal"));
250 }
251
252 #[test]
253 fn test_user_prompt_with_unmapped() {
254 let prompt = create_user_prompt(
255 "Define x as 5",
256 ConversionTier::Standard,
257 &["foo".to_string(), "bar".to_string()],
258 None,
259 );
260 assert!(prompt.contains("foo"));
261 assert!(prompt.contains("bar"));
262 }
263}