llm_toolkit/
intent.rs

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