typed_dialogflow/
lib.rs

1//! # `typed-dialogflow`
2//!
3//! An easy-to-use typed [Google Dialogflow](https://dialogflow.cloud.google.com/) client.
4
5pub mod model;
6
7use gcp_auth::AuthenticationManager;
8use language_tags::LanguageTag;
9use model::DetectIntentResponse;
10use reqwest::Url;
11use serde::de::DeserializeOwned;
12use serde::Serialize;
13
14const SCOPES: &[&str] = &["https://www.googleapis.com/auth/dialogflow"];
15
16#[derive(Debug, thiserror::Error)]
17pub enum DialogflowError {
18    #[error("no GCP auth methods available: {0}")]
19    MissingAuth(#[from] gcp_auth::Error),
20
21    #[error("GCP token not available")]
22    TokenNotAvailable,
23
24    #[error("reqwest error: {0}")]
25    Reqwest(#[from] reqwest::Error),
26
27    #[error("cannot deserialize JSON response")]
28    ResponseNotDeserializable,
29}
30
31pub struct DetectIntentOptions {
32    language_code: LanguageTag,
33    geolocation: Option<(f32, f32)>,
34}
35
36impl Default for DetectIntentOptions {
37    fn default() -> Self {
38        Self {
39            language_code: LanguageTag::parse("en").unwrap(),
40            geolocation: None,
41        }
42    }
43}
44
45/// An authenticated Dialogflow client
46pub struct Dialogflow {
47    auth: AuthenticationManager,
48    client: reqwest::Client,
49    detect_intent_url: Url,
50    options: DetectIntentOptions,
51}
52
53impl Dialogflow {
54    /// Initializes Dialogflow
55    ///
56    /// Multiple strategies are used to authenticate the client, please refer to
57    /// [`gcp_auth`][gcp_auth::AuthenticationManager::new] for more information.
58    pub async fn new() -> Result<Self, DialogflowError> {
59        let auth = gcp_auth::AuthenticationManager::new().await?;
60        let project_id = auth.project_id().await?;
61
62        Ok(Self {
63            auth,
64            client: reqwest::Client::new(),
65            detect_intent_url: format!("https://dialogflow.googleapis.com/v2/projects/{project_id}/agent/sessions/dev:detectIntent").parse().unwrap(),
66            options: Default::default(),
67        })
68    }
69
70    pub fn with_detect_intent_options(
71        mut self,
72        detect_intent_options: DetectIntentOptions,
73    ) -> Self {
74        self.options = detect_intent_options;
75        self
76    }
77
78    /// Detects an intent and returns the result as [`serde`]-deserialized enum, `I`
79    ///
80    /// The enum must be shaped as follows:
81    ///   * The name of each enum variant should be the same as a Dialogflow intent name
82    ///   * The variants may only be, either:
83    ///       * A unit variant, for intents with no parameters
84    ///       * A struct variant, whose fields correspond to paramter names
85    ///
86    /// ## Enum definition example
87    ///
88    /// ```
89    /// #[derive(serde::Deserialize)]
90    /// #[serde(rename_all = "snake_case")]
91    /// enum Intent {
92    ///     Hello,
93    ///     Weather {
94    ///         location: String,
95    ///     },
96    ///     ThankYou,
97    /// }
98    /// ```
99    ///
100    /// In this case, the intents are named `hello`, `weather` and `thank_you`.
101    ///
102    /// ## Call example
103    ///
104    /// ```
105    /// # async fn f(dialogflow: typed_dialogflow::Dialogflow) {
106    /// #[derive(Debug, Eq, PartialEq, serde::Deserialize)]
107    /// #[serde(rename_all = "snake_case")]
108    /// enum Intent {
109    ///     Hello,
110    ///     Goodbye,
111    /// }
112    ///
113    /// let intent = dialogflow.detect_intent_serde::<Intent>("Hello !").await.unwrap();
114    ///
115    /// assert_eq!(intent, Intent::Hello);
116    /// # }
117    /// ```
118    ///
119    /// ## Unknown intent
120    ///
121    /// If the Dialogflow API cannot recognize an intent, this function will attempt to deserialize
122    /// a variant called `unknown` on your enum. This allows you to know that the text wasn't
123    /// recognized without having to deal with an [`Err`].
124    pub async fn detect_intent_serde<I: DeserializeOwned>(
125        &self,
126        text: &str,
127    ) -> Result<I, DialogflowError> {
128        #[derive(serde::Serialize)]
129        struct Request<'a> {
130            query_input: QueryInput<'a>,
131            query_params: QueryParams,
132        }
133
134        #[derive(serde::Serialize)]
135        #[serde(rename_all = "camelCase")]
136        struct QueryInput<'a> {
137            text: QueryInputText<'a>,
138        }
139
140        #[derive(serde::Serialize)]
141        #[serde(rename_all = "camelCase")]
142        struct QueryInputText<'a> {
143            language_code: &'a LanguageTag,
144            text: &'a str,
145        }
146
147        #[derive(serde::Serialize)]
148        #[serde(rename_all = "camelCase")]
149        struct QueryParams {
150            #[serde(skip_serializing_if = "Option::is_none")]
151            geo_location: Option<GeoLocation>,
152        }
153
154        #[derive(serde::Serialize)]
155        #[serde(rename_all = "camelCase")]
156        struct GeoLocation<F: Serialize = f32> {
157            latitude: F,
158            longitude: F,
159        }
160
161        let req = Request {
162            query_input: QueryInput {
163                text: QueryInputText {
164                    language_code: &self.options.language_code,
165                    text,
166                },
167            },
168            query_params: QueryParams {
169                geo_location: self.options.geolocation.map(|g| GeoLocation {
170                    latitude: g.0,
171                    longitude: g.1,
172                }),
173            },
174        };
175
176        let token = self
177            .auth
178            .get_token(SCOPES)
179            .await
180            .map_err(|_| DialogflowError::TokenNotAvailable)?;
181
182        let res = self
183            .client
184            .post(self.detect_intent_url.clone())
185            .header("Authorization", format!("Bearer {}", token.as_str()))
186            .json(&req)
187            .send()
188            .await?;
189
190        let res: DetectIntentResponse = res
191            .json()
192            .await
193            .map_err(|_| DialogflowError::ResponseNotDeserializable)?;
194
195        I::deserialize(res).map_err(|_| DialogflowError::ResponseNotDeserializable)
196    }
197}