Add first weather text support; refactoring
parent
56332ca0f3
commit
99021f3994
@ -0,0 +1,144 @@
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Hermes types
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HermesIntent {
|
||||
input: String,
|
||||
#[serde(rename = "rawInput")]
|
||||
input_raw: String,
|
||||
intent: HermesIntentClassification,
|
||||
#[serde(rename = "sessionId")]
|
||||
session_id: String,
|
||||
#[serde(rename = "siteId")]
|
||||
site_id: String,
|
||||
slots: Vec<HermesSlot>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HermesIntentClassification {
|
||||
#[serde(rename = "confidenceScore")]
|
||||
confidence: f32,
|
||||
#[serde(rename = "intentName")]
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HermesSlot {
|
||||
confidence: f32,
|
||||
entity: String,
|
||||
#[serde(rename = "slotName")]
|
||||
name: String,
|
||||
value: HermesSlotValue,
|
||||
#[serde(rename = "rawValue")]
|
||||
value_raw: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HermesSlotValue {
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct HermesResponse {
|
||||
#[serde(rename = "sessionId")]
|
||||
session_id: String,
|
||||
text: Option<String>,
|
||||
}
|
||||
|
||||
// Native types
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Intent {
|
||||
pub name: String,
|
||||
pub input: String,
|
||||
pub input_raw: String,
|
||||
pub confidence: f32,
|
||||
pub session_id: String,
|
||||
pub site_id: String,
|
||||
pub slots: HashMap<String, Slot>,
|
||||
}
|
||||
|
||||
impl From<HermesIntent> for Intent {
|
||||
fn from(hermes_intent: HermesIntent) -> Self {
|
||||
let slots = hermes_intent
|
||||
.slots
|
||||
.into_iter()
|
||||
.map(|hermes_slot| (hermes_slot.name.clone(), Slot::from(hermes_slot)))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
name: hermes_intent.intent.name,
|
||||
input: hermes_intent.input,
|
||||
input_raw: hermes_intent.input_raw,
|
||||
confidence: hermes_intent.intent.confidence,
|
||||
session_id: hermes_intent.session_id,
|
||||
site_id: hermes_intent.site_id,
|
||||
slots,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Slot {
|
||||
pub confidence: f32,
|
||||
pub entity: String,
|
||||
pub value: String,
|
||||
pub value_raw: String,
|
||||
}
|
||||
|
||||
impl From<HermesSlot> for Slot {
|
||||
fn from(slot_data: HermesSlot) -> Self {
|
||||
Self {
|
||||
confidence: slot_data.confidence,
|
||||
entity: slot_data.entity,
|
||||
value: slot_data.value.value,
|
||||
value_raw: slot_data.value_raw,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Intent {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let intent_data = HermesIntent::deserialize(deserializer)?;
|
||||
Ok(Intent::from(intent_data))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
pub session_id: String,
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn new(session_id: String) -> Self {
|
||||
let text = None;
|
||||
Self { session_id, text }
|
||||
}
|
||||
|
||||
pub fn with_text(mut self, text: impl Into<String>) -> Self {
|
||||
self.text = Some(text.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response> for Vec<u8> {
|
||||
fn from(response: Response) -> Vec<u8> {
|
||||
let hermes_response = HermesResponse::from(response);
|
||||
serde_json::to_vec(&hermes_response).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response> for HermesResponse {
|
||||
fn from(response: Response) -> Self {
|
||||
HermesResponse {
|
||||
session_id: response.session_id,
|
||||
text: response.text,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,144 +1,111 @@
|
||||
#![warn(clippy::all)]
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use rumqttc::{AsyncClient, Event, MqttOptions, Packet, QoS};
|
||||
use std::error::Error;
|
||||
|
||||
// Hermes protocol types
|
||||
use crate::hermes::{Intent, Response};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HermesIntent {
|
||||
input: String,
|
||||
#[serde(rename = "rawInput")]
|
||||
input_raw: String,
|
||||
intent: HermesIntentClassification,
|
||||
#[serde(rename = "sessionId")]
|
||||
session_id: String,
|
||||
#[serde(rename = "siteId")]
|
||||
site_id: String,
|
||||
slots: Vec<HermesSlot>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HermesIntentClassification {
|
||||
#[serde(rename = "confidenceScore")]
|
||||
confidence: f32,
|
||||
#[serde(rename = "intentName")]
|
||||
name: String,
|
||||
}
|
||||
pub mod hermes;
|
||||
pub mod weather;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HermesSlot {
|
||||
confidence: f32,
|
||||
entity: String,
|
||||
#[serde(rename = "slotName")]
|
||||
name: String,
|
||||
value: HermesSlotValue,
|
||||
#[serde(rename = "rawValue")]
|
||||
value_raw: String,
|
||||
}
|
||||
// Main context
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HermesSlotValue {
|
||||
value: String,
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Context {
|
||||
pub(crate) openweather_api_key: String,
|
||||
pub(crate) mqtt_host: String,
|
||||
pub(crate) mqtt_port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct HermesResponse {
|
||||
#[serde(rename = "sessionId")]
|
||||
session_id: String,
|
||||
text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Intent {
|
||||
pub name: String,
|
||||
pub input: String,
|
||||
pub input_raw: String,
|
||||
pub confidence: f32,
|
||||
pub session_id: String,
|
||||
pub site_id: String,
|
||||
pub slots: HashMap<String, Slot>,
|
||||
}
|
||||
impl Context {
|
||||
pub fn new() -> Result<Self, Box<dyn Error + 'static>> {
|
||||
use std::env;
|
||||
|
||||
impl From<HermesIntent> for Intent {
|
||||
fn from(hermes_intent: HermesIntent) -> Self {
|
||||
let slots = hermes_intent
|
||||
.slots
|
||||
.into_iter()
|
||||
.map(|hermes_slot| (hermes_slot.name.clone(), Slot::from(hermes_slot)))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
name: hermes_intent.intent.name,
|
||||
input: hermes_intent.input,
|
||||
input_raw: hermes_intent.input_raw,
|
||||
confidence: hermes_intent.intent.confidence,
|
||||
session_id: hermes_intent.session_id,
|
||||
site_id: hermes_intent.site_id,
|
||||
slots,
|
||||
}
|
||||
Ok(Context {
|
||||
openweather_api_key: env::var("OPENWEATHER_API_KEY")?,
|
||||
mqtt_host: env::var("MQTT_HOST")?,
|
||||
mqtt_port: env::var("MQTT_PORT")?.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Slot {
|
||||
pub confidence: f32,
|
||||
pub entity: String,
|
||||
pub value: String,
|
||||
pub value_raw: String,
|
||||
}
|
||||
|
||||
impl From<HermesSlot> for Slot {
|
||||
fn from(slot_data: HermesSlot) -> Self {
|
||||
Self {
|
||||
confidence: slot_data.confidence,
|
||||
entity: slot_data.entity,
|
||||
value: slot_data.value.value,
|
||||
value_raw: slot_data.value_raw,
|
||||
// Handling
|
||||
|
||||
pub async fn handle(context: &mut Context, intent: Intent) -> Option<Response> {
|
||||
println!(
|
||||
">>> Detected intent: {} (confidence: {:.2})",
|
||||
intent.name, intent.confidence
|
||||
);
|
||||
|
||||
match intent.name.as_ref() {
|
||||
"test" => {
|
||||
let choice = intent.slots.get("choice").unwrap();
|
||||
Some(
|
||||
Response::new(intent.session_id)
|
||||
.with_text(format!("De test is geslaagd: {}", choice.value)),
|
||||
)
|
||||
}
|
||||
"WeatherForecastLocal" => Some(match weather::get_forecast(&context).await {
|
||||
Ok(description) => Response::new(intent.session_id).with_text(description),
|
||||
Err(e) => {
|
||||
println!("!!! Encountered weather API error: {}", e);
|
||||
Response::new(intent.session_id).with_text("Ik kon het weerbericht niet ophalen!")
|
||||
}
|
||||
}),
|
||||
_ => {
|
||||
println!("??? Ignoring unsupported intent: {:?}", intent.name);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Intent {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let intent_data = HermesIntent::deserialize(deserializer)?;
|
||||
Ok(Intent::from(intent_data))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
pub session_id: String,
|
||||
pub text: Option<String>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn new(session_id: String) -> Self {
|
||||
let text = None;
|
||||
Self { session_id, text }
|
||||
}
|
||||
|
||||
pub fn with_text(mut self, text: impl Into<String>) -> Self {
|
||||
self.text = Some(text.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response> for Vec<u8> {
|
||||
fn from(response: Response) -> Vec<u8> {
|
||||
let hermes_response = HermesResponse::from(response);
|
||||
serde_json::to_vec(&hermes_response).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response> for HermesResponse {
|
||||
fn from(response: Response) -> Self {
|
||||
HermesResponse {
|
||||
session_id: response.session_id,
|
||||
text: response.text,
|
||||
pub async fn event_loop(mut context: Context) -> Result<(), Box<dyn Error + 'static>> {
|
||||
let mut mqttoptions = MqttOptions::new(
|
||||
"rhasspy-skill-server",
|
||||
&context.mqtt_host,
|
||||
context.mqtt_port,
|
||||
);
|
||||
mqttoptions.set_keep_alive(10);
|
||||
|
||||
let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
|
||||
client.subscribe("hermes/intent/#", QoS::AtMostOnce).await?;
|
||||
|
||||
loop {
|
||||
let event = eventloop.poll().await?;
|
||||
if let Event::Incoming(Packet::Publish(published)) = event {
|
||||
let result: Result<Intent, _> = serde_json::from_slice(published.payload.as_ref());
|
||||
let response = match result {
|
||||
Ok(intent) => handle(&mut context, intent).await,
|
||||
Err(e) => {
|
||||
println!(
|
||||
"!!! Could not parse intent payload: {} at\n{:?}",
|
||||
e, published.payload
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(response) = response {
|
||||
let text = response.text.clone();
|
||||
let result = client
|
||||
.publish(
|
||||
"hermes/dialogueManager/endSession",
|
||||
QoS::AtMostOnce,
|
||||
false,
|
||||
response,
|
||||
)
|
||||
.await;
|
||||
match result {
|
||||
Ok(_) => {
|
||||
if let Some(text) = text {
|
||||
println!("<<< Reacted with a response: {}!", text);
|
||||
} else {
|
||||
println!("<<< Reacted with a silent response");
|
||||
}
|
||||
}
|
||||
Err(e) => println!("!!! Could not publish intent payload: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,68 +1,12 @@
|
||||
use rhasspy_skill_server::{Intent, Response};
|
||||
use rumqttc::{AsyncClient, Event, MqttOptions, Packet, QoS};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct Context;
|
||||
|
||||
async fn handle(_context: &mut Context, intent: Intent) -> Option<Response> {
|
||||
println!(
|
||||
">>> Detected intent: {} (confidence: {})",
|
||||
intent.name, intent.confidence
|
||||
);
|
||||
|
||||
match intent.name.as_ref() {
|
||||
"test" => {
|
||||
let choice = intent.slots.get("choice").unwrap();
|
||||
Some(
|
||||
Response::new(intent.session_id)
|
||||
.with_text(format!("De test is geslaagd: {}", choice.value)),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
println!("??? Ignoring unsupported intent: {:?}", intent.name);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
use rhasspy_skill_server::{event_loop, Context};
|
||||
use std::error::Error;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
|
||||
let mut mqttoptions = MqttOptions::new("rhasspy-skill-server", "void", 1883);
|
||||
mqttoptions.set_keep_alive(5);
|
||||
|
||||
let (client, mut eventloop) = AsyncClient::new(mqttoptions, 10);
|
||||
client.subscribe("hermes/intent/#", QoS::AtMostOnce).await?;
|
||||
async fn main() -> Result<(), Box<dyn Error + 'static>> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let mut context = Context::default();
|
||||
loop {
|
||||
let event = eventloop.poll().await?;
|
||||
if let Event::Incoming(Packet::Publish(published)) = event {
|
||||
let result: Result<Intent, _> = serde_json::from_slice(published.payload.as_ref());
|
||||
let response = match result {
|
||||
Ok(intent) => handle(&mut context, intent).await,
|
||||
Err(e) => {
|
||||
println!(
|
||||
"!!! Could not parse intent payload: {} at\n{:?}",
|
||||
e, published.payload
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let context = Context::new()?;
|
||||
event_loop(context).await?;
|
||||
|
||||
if let Some(response) = response {
|
||||
let result = client
|
||||
.publish(
|
||||
"hermes/dialogueManager/endSession",
|
||||
QoS::AtMostOnce,
|
||||
false,
|
||||
response,
|
||||
)
|
||||
.await;
|
||||
match result {
|
||||
Ok(_) => println!("<<< Reacted with a response!"),
|
||||
Err(e) => println!("!!! Could not publish intent payload: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -0,0 +1,94 @@
|
||||
use reqwest::{Client, Response, Url};
|
||||
use serde::Deserialize;
|
||||
use std::error::Error;
|
||||
|
||||
use crate::Context;
|
||||
|
||||
const LATITUDE: &'static str = "51.43";
|
||||
const LONGITUDE: &'static str = "5.47";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Current {
|
||||
weather: Vec<Weather>,
|
||||
main: Main,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Weather {
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Main {
|
||||
temp: f32,
|
||||
feels_like: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct OneCall {
|
||||
daily: Vec<Daily>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Daily {
|
||||
temp: Temp,
|
||||
weather: Vec<Weather>,
|
||||
pop: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Temp {
|
||||
day: f32,
|
||||
}
|
||||
|
||||
async fn request(context: &Context, url: &str) -> reqwest::Result<Response> {
|
||||
let client = Client::new();
|
||||
let mut url = Url::parse(url).unwrap();
|
||||
url.query_pairs_mut()
|
||||
.append_pair("appid", &context.openweather_api_key)
|
||||
.append_pair("lat", LATITUDE)
|
||||
.append_pair("lon", LONGITUDE)
|
||||
.append_pair("units", "metric")
|
||||
.append_pair("lang", "nl");
|
||||
client.get(url).send().await
|
||||
}
|
||||
|
||||
pub async fn get_weather(context: &Context) -> Result<String, Box<dyn Error + 'static>> {
|
||||
let url = "https://api.openweathermap.org/data/2.5/weather";
|
||||
let resp = request(&context, &url).await?;
|
||||
let cur_weather: Current = resp.json().await?;
|
||||
|
||||
let description = cur_weather.weather.first().unwrap().description.clone();
|
||||
let temperature = cur_weather.main.temp.round() as u32;
|
||||
let temperature_feel = cur_weather.main.feels_like.round() as u32;
|
||||
let weather = format!(
|
||||
"Het weer in Eindhoven is op het moment {description} en het is {temperature} graden \
|
||||
buiten en dat voelt als {temperature_feel} graden",
|
||||
description = description,
|
||||
temperature = temperature,
|
||||
temperature_feel = temperature_feel,
|
||||
);
|
||||
|
||||
Ok(weather)
|
||||
}
|
||||
|
||||
pub async fn get_forecast(context: &Context) -> Result<String, Box<dyn Error + 'static>> {
|
||||
let url = "https://api.openweathermap.org/data/2.5/onecall";
|
||||
let resp = request(&context, &url).await?;
|
||||
let weather_forecast: OneCall = resp.json().await?;
|
||||
|
||||
let today = weather_forecast.daily.first().unwrap();
|
||||
let description = today.weather.first().unwrap().description.clone();
|
||||
let temperature_day = today.temp.day.round() as u32;
|
||||
let rain_pct = (today.pop * 100.0).round() as u8;
|
||||
|
||||
let forecast = format!(
|
||||
"De weersverwachting van vandaag voor Eindhoven is {description} met een \
|
||||
middagtemperatuur van {temperature_day} graden en {rain_pct} procent kans op regen",
|
||||
description = description,
|
||||
temperature_day = temperature_day,
|
||||
rain_pct = rain_pct,
|
||||
);
|
||||
|
||||
Ok(forecast)
|
||||
}
|
Loading…
Reference in New Issue