llm_toolkit/
intent.rs

1//! Traits and implementations for extracting structured intents from LLM responses.
2
3pub mod frame;
4
5use self::frame::IntentFrame;
6use std::str::FromStr;
7use thiserror::Error;
8
9/// An error type for intent extraction failures.
10#[derive(Debug, Error)]
11pub enum IntentError {
12    #[error("Extraction failed: Tag '{tag}' not found in response")]
13    TagNotFound { tag: String },
14
15    #[error("Parsing failed: Could not parse '{value}' into a valid intent")]
16    ParseFailed { value: String },
17
18    #[error(transparent)]
19    Other(#[from] anyhow::Error),
20}
21
22/// Alias for IntentError for backward compatibility with macro-generated code.
23pub type IntentExtractionError = IntentError;
24
25/// Parse error for intent extraction
26#[derive(Debug, Error)]
27pub enum ParseError {
28    #[error("Failed to parse intent: {0}")]
29    ParseError(String),
30}
31
32/// Helper function for extracting intents from LLM responses using XML-style tags.
33/// This is used by the `define_intent` macro.
34pub fn extract_intent_from_response<T>(
35    response: &str,
36    tag: &str,
37) -> Result<T, IntentExtractionError>
38where
39    T: FromStr,
40    T::Err: std::fmt::Display,
41{
42    use crate::extract::FlexibleExtractor;
43    use crate::extract::core::ContentExtractor;
44
45    let extractor = FlexibleExtractor::new();
46    let extracted_str =
47        extractor
48            .extract_tagged(response, tag)
49            .ok_or_else(|| IntentError::TagNotFound {
50                tag: tag.to_string(),
51            })?;
52
53    T::from_str(&extracted_str).map_err(|e| IntentError::ParseFailed {
54        value: format!("{}: {}", extracted_str, e),
55    })
56}
57
58/// A generic trait for extracting a structured intent of type `T` from a string response.
59///
60/// Type `T` is typically an enum representing the possible intents.
61pub trait IntentExtractor<T>
62where
63    T: FromStr,
64{
65    /// Extracts and parses an intent from the given text.
66    fn extract_intent(&self, text: &str) -> Result<T, IntentError>;
67}
68
69/// A classic, prompt-based implementation of `IntentExtractor`.
70///
71/// This extractor uses `FlexibleExtractor` to find content within a specific
72/// XML-like tag (e.g., `<intent>...<intent>`) and then parses that content
73/// into the target intent type `T`.
74#[deprecated(
75    since = "0.8.0",
76    note = "Please use `IntentFrame` instead for better safety and clarity."
77)]
78pub struct PromptBasedExtractor {
79    frame: IntentFrame,
80}
81
82#[allow(deprecated)]
83impl PromptBasedExtractor {
84    /// Creates a new extractor that looks for the specified tag.
85    pub fn new(tag: &str) -> Self {
86        Self {
87            frame: IntentFrame::new(tag, tag),
88        }
89    }
90}
91
92#[allow(deprecated)]
93impl<T> IntentExtractor<T> for PromptBasedExtractor
94where
95    T: FromStr,
96{
97    fn extract_intent(&self, text: &str) -> Result<T, IntentError> {
98        self.frame.extract_intent(text)
99    }
100}
101
102#[cfg(test)]
103#[allow(deprecated)]
104mod tests {
105    use super::*;
106
107    #[derive(Debug, PartialEq)]
108    enum TestIntent {
109        Login,
110        Logout,
111    }
112
113    impl FromStr for TestIntent {
114        type Err = String;
115
116        fn from_str(s: &str) -> Result<Self, Self::Err> {
117            match s {
118                "Login" => Ok(TestIntent::Login),
119                "Logout" => Ok(TestIntent::Logout),
120                _ => Err(format!("Unknown intent: {}", s)),
121            }
122        }
123    }
124
125    #[test]
126    fn test_extract_intent_success() {
127        let extractor = PromptBasedExtractor::new("intent");
128        let text = "<intent>Login</intent>";
129        let result: Result<TestIntent, _> = IntentExtractor::extract_intent(&extractor, text);
130        assert_eq!(result.unwrap(), TestIntent::Login);
131    }
132
133    #[test]
134    fn test_extract_intent_tag_not_found() {
135        let extractor = PromptBasedExtractor::new("intent");
136        let text = "No intent tag here";
137        let result: Result<TestIntent, _> = IntentExtractor::extract_intent(&extractor, text);
138
139        match result {
140            Err(IntentError::TagNotFound { tag }) => {
141                assert_eq!(tag, "intent");
142            }
143            _ => panic!("Expected TagNotFound error"),
144        }
145    }
146
147    #[test]
148    fn test_extract_intent_parse_failed() {
149        let extractor = PromptBasedExtractor::new("intent");
150        let text = "<intent>Invalid</intent>";
151        let result: Result<TestIntent, _> = IntentExtractor::extract_intent(&extractor, text);
152
153        match result {
154            Err(IntentError::ParseFailed { value }) => {
155                assert_eq!(value, "Invalid");
156            }
157            _ => panic!("Expected ParseFailed error"),
158        }
159    }
160}