Skip to main content

rosetta_aisp_llm/
claude.rs

1//! Claude SDK Fallback
2//!
3//! Uses claude-agent-sdk-rs for LLM-based AISP conversion
4//! when deterministic Rosetta mappings have low confidence.
5
6use 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
11/// Generate symbol reference grouped by category
12fn 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
28/// System prompt for AISP conversion
29fn 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
97/// Create user prompt with context
98fn 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
125/// Claude SDK fallback provider
126///
127/// Uses Claude models via the claude-agent-sdk-rs crate to convert
128/// prose to AISP when deterministic conversion has low confidence.
129pub struct ClaudeFallback {
130    model: String,
131}
132
133impl Default for ClaudeFallback {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl ClaudeFallback {
140    /// Create new Claude fallback with default model (sonnet)
141    pub fn new() -> Self {
142        Self {
143            model: "sonnet".to_string(),
144        }
145    }
146
147    /// Create with specific model
148    pub fn with_model(model: impl Into<String>) -> Self {
149        Self {
150            model: model.into(),
151        }
152    }
153
154    /// Use haiku for simple/fast conversions
155    pub fn haiku() -> Self {
156        Self::with_model("haiku")
157    }
158
159    /// Use sonnet for balanced conversions
160    pub fn sonnet() -> Self {
161        Self::with_model("sonnet")
162    }
163
164    /// Use opus for complex conversions
165    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        // Configure minimal Claude instance
184        let options = ClaudeAgentOptions::builder()
185            .model(&self.model)
186            .system_prompt(system_prompt())
187            .max_turns(1) // Single turn for conversion
188            .permission_mode(PermissionMode::BypassPermissions)
189            .tools(Vec::<String>::new()) // No tools needed
190            .build();
191
192        let messages = query(&user_prompt, Some(options)).await?;
193
194        // Extract text response
195        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                        // Rough token estimate from cost
210                        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        // Check if Claude Code CLI is available
227        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}