1pub 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
45pub struct Dialogflow {
47 auth: AuthenticationManager,
48 client: reqwest::Client,
49 detect_intent_url: Url,
50 options: DetectIntentOptions,
51}
52
53impl Dialogflow {
54 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 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}