1. Introdução
Você está assistindo TV, mas não consegue encontrar o controle remoto, ou talvez não queira acessar cada canal de TV para descobrir se há algo legal na televisão? Vamos perguntar ao Google Assistente o que está passando na TV! Neste laboratório, você criará uma ação simples usando o Dialogflow e aprenderá a integrá-lo ao Google Assistente.
A ordem das tarefas simula uma experiência de desenvolvedor de nuvem:
- criar um agente do Dialogflow v2
- Criar uma entidade personalizada
- Criar intents
- Configurar um webhook com o Firebase Functions
- testar o chatbot
- Ativar a integração com o Google Assistente
O que você criará
Vamos criar um agente de chatbot interativo para um guia de TV para o Google Assistente. Você pode perguntar ao guia de TV o que está no ar em um determinado canal. Por exemplo: "O que está no MTV?" A ação do guia de TV informa o que está em reprodução no momento e o que está no próximo. |
O que você vai aprender
- Como criar um chatbot com o Dialogflow v2
- Como criar entidades personalizadas com o Dialogflow
- Como criar uma conversa linear com o Dialogflow
- Como configurar fulfillments do webhook com o Dialogflow e o Firebase Functions
- Como levar seu aplicativo para o Google Assistente com o Actions on Google
Pré-requisitos
- Você vai precisar de uma identidade do Google / endereço do Gmail para criar um agente do Dialogflow.
- Não é necessário ter conhecimento básico de JavaScript, mas isso pode ser útil caso você queira alterar o código de fulfillment do webhook.
2. Etapas da configuração
Ativar a Atividade na Web no seu navegador
- Clique em: http://myaccount.google.com/activitycontrols
- Verifique se os recursos Web e A atividade no app está ativada:
Crie um agente do Dialogflow
- Na barra à esquerda, abaixo do logotipo, selecione Criar novo agente. Se você já tiver agentes, clique no menu suspenso primeiro.
- Especifique um nome de agente:
your-name-tvguide
(use seu próprio nome)
- Como idioma padrão, escolha: English - en
- Como fuso horário padrão, escolha o mais próximo de você.
- Clique em Criar.
Configure o Dialogflow
- Clique no ícone de engrenagem no menu à esquerda, ao lado do nome do projeto.
- Digite a seguinte descrição do agente: My TV Guide
- Role para baixo até Log Settings e deslize os dois interruptores para "Registrar as interações do Dialogflow" e para registrar todas as interações no Google Cloud Stackdriver. Precisaremos disso mais tarde, caso queiramos depurar nossa ação.
- Clique em Salvar.
Configurar o Actions on Google
- No painel à direita, clique no link Google Assistente em Saiba como ele funciona no Google Assistente.
Isso vai abrir: http://console.actions.google.com
Se você nunca usou o Actions on Google, precisa preencher este formulário:
- Tente abrir sua ação no simulador** clicando no nome do projeto.**
- Selecione Testar na barra de menus.
- Verifique se o simulador está definido como English e clique em Talk to my test-app.
A ação vai cumprimentá-lo, com a intent padrão básica do Dialogflow. Isso significa que a configuração da integração com a Action no Google funcionou.
3. Entidades personalizadas
Entidades são objetos em que seu app ou dispositivo realiza ações. Pense nisso como parâmetros / variáveis. No nosso Guia de TV, perguntaremos: "O que está passando no MTV". MTV é a entidade e a variável. Também posso pedir outros canais, como: "National Geographic" ou "Comedy Central". A entidade coletada será usada como um parâmetro na minha solicitação para o serviço da Web da API TV Guide.
Veja mais informações sobre as entidades do Dialogflow.
Como criar a entidade do canal
- No Console do Dialogflow, clique no item de menu: Entities
- Clique em Criar entidade.
- Nome da entidade:
channel
(use apenas letras minúsculas) - Transmita um nome para o canal. Alguns canais vão precisar de um sinônimo caso o Google Assistente entenda outra coisa. É possível usar a tecla Tab para inserir teclas enquanto você digita. Insira o número do canal como um valor de referência. E os nomes dos canais como sinônimos, como:
1 - 1, Net 1, Net Station 1
5**.** Alterne para o modo **Raw Edit** clicando no botão de menu ao lado do botão azul "Save".
- Copiar e cole as outras entidades no formato CSV:
"2","2","Net 2, Net Station 2"
"3","3","Net 3, Net Station 3"
"4","4","RTL 4"
"5","5","Movie Channel"
"6","6","Sports Channel"
"7","7","Comedy Central"
"8","8","Cartoon Network"
"9","9","National Geographic"
"10","10","MTV"
- Clique em Salvar.
4. Intents
O Dialogflow usa intents para categorizar as intenções de um usuário. As intents têm frases de treinamento, que são exemplos do que um usuário pode dizer ao agente. Por exemplo, um usuário que quer saber o que está passando na TV pode perguntar: "O que está passando na TV hoje?", "O que está tocando no momento?" ou simplesmente diga "tvguide".
Quando um usuário escreve ou diz algo, chamado de expressão do usuário, o Dialogflow faz a correspondência entre essa expressão e a melhor intent do seu agente. Essa correspondência também é conhecida como classificação de intent.
Confira mais informações sobre intents do Dialogflow.
Como modificar a intent de boas-vindas padrão
Quando você cria um novo agente do Dialogflow, duas intents padrão são criadas automaticamente. A Default Welcome Intent é o primeiro fluxo que você acessa quando inicia uma conversa com o agente. A Default Fallback Intent é o fluxo exibido quando o agente não entende você ou não consegue associar uma intent ao que você acabou de dizer.
- Clique em Intent de boas-vindas padrão.
No caso do Google Assistente, ele será iniciado automaticamente com a intent de boas-vindas padrão. Isso ocorre porque o Dialogflow está detectando o evento de boas-vindas. No entanto, também é possível invocar a intent dizendo uma das frases de treinamento inseridas.
Esta é a mensagem de boas-vindas para a intent de boas-vindas padrão:
Usuário | Agente |
"Ok Google, fale com seu-nome-tvguide." | "Olá, sou o agente do TV Guide. Posso dizer o que está tocando em um canal de TV. Por exemplo, você pode me perguntar: o que está passando no MTV." |
- Role para baixo até Respostas.
- Limpar todas as respostas de texto.
- Crie uma nova resposta de texto com a seguinte saudação:
Welcome, I am the TV Guide agent. I can tell you what's currently playing on a TV channel. For example, you can ask me: What's on MTV?
- Clique em Salvar.
Criar uma intent de teste temporária
Para fins de teste, vamos criar uma intent de teste temporária para testar o webhook mais tarde.
- Clique novamente no item de menu Intents.
- Clique em Create Intent (Criar intent).
- Insira o nome da intent:
Test Intent
(use o "T" e o "I" maiúsculos. - Se você soletrar a intent de forma diferente, o serviço de back-end não funcionará.
- Clique em Adicionar frases de treinamento
Test my agent
Test intent
- Clique em Fulfillment > Ativar fulfillment
Desta vez, não estamos fixando a resposta no código. A resposta virá de uma função do Cloud.
- Mude a posição da chave Enable Webhook call for this intent.
- Clique em Salvar.
Crie a intenção do canal
A intenção do canal contém esta parte da conversa:
Usuário | Agente |
"O que está passando no Comedy Central?" | ""No momento, no Comedy Central, a partir das 18h, os Simpsons estão passando. Depois, às 19h, o Family Guy vai começar." |
- Clique novamente no item de menu Intents.
- Clique em Create Intent (Criar intent).
- Insira o nome da intent:
Channel Intent
(use o "T" e o "I" maiúsculos. - Se você soletrar a intent de forma diferente, o serviço de back-end não funcionará. - Clique em Adicionar frases de treinamento e adicione o seguinte:
What's on MTV?
What's playing on Comedy Central?
What show will start at 8 PM on National Geographic?
What is currently on TV?
What is airing now.
Anything airing on Net Station 1 right now?
What can I watch at 7 PM?
What's on channel MTV?
What's on TV?
Please give me the tv guide.
Tell me what is on television.
What's on Comedy Central from 10 AM?
What will be on tv at noon?
Anything on National Geographic?
TV Guide
- Role para baixo até Ação e parâmetros.
Observe que @channel & Entidades @sys.time conhecidas pelo Dialogflow. Posteriormente no webhook, o nome e o valor dos parâmetros serão enviados ao serviço da Web. Exemplo:
channel=8
time=2020-01-29T19:00:00+01:00
- Marcar o canne como necessário
Quando você estiver conversando com o agente do TV Guide, sempre precisará preencher o nome do parâmetro do slot channel. Se o nome do canal não for mencionado no início da conversa, o Dialogflow fará mais perguntas até preencher todos os slots de parâmetros.
Como comando, digite:
For which TV channel do you want to hear the tv guide information?
In which TV channel are you interested?
- Não defina o parâmetro time conforme necessário.
O horário é opcional. Quando nenhum horário for especificado, o serviço da Web retornará o horário atual.
- Clique em Fulfillment
Desta vez, não estamos fixando a resposta no código. A resposta virá da função do Cloud. Assim, mude a chave Enable Webhook call for this intent (Ativar chamada de webhook para esta intent).
- Clique em Salvar.
5. Fulfillment de webhook
Se o agente precisar de mais do que respostas de intent estáticas, será necessário usar o fulfillment para conectar o serviço da Web ao agente. Conectar seu serviço da Web permite que você realize ações com base em expressões de usuário e envie respostas dinâmicas para o usuário. Por exemplo, se um usuário quiser receber a programação de TV para MTV, seu serviço da Web pode verificar seu banco de dados e responder ao usuário, a programação da MTV.
- Clique em Fulfillment no menu principal.
- Ative a opção Editor in-line.
Para testes e implementação simples do webhook, use o editor in-line. Ele usa o Cloud Functions para Firebase sem servidor.
- Clique na guia index.js no editor e copie e cole este trecho de código JavaScript para Node.js:
'use strict';
process.env.DEBUG = 'dialogflow:debug';
const {
dialogflow,
BasicCard,
Button,
Image,
List
} = require('actions-on-google');
const functions = require('firebase-functions');
const moment = require('moment');
const TVGUIDE_WEBSERVICE = 'https://tvguide-e4s5ds5dsa-ew.a.run.app/channel';
const { WebhookClient } = require('dialogflow-fulfillment');
var spokenText = '';
var results = null;
/* When the Test Intent gets invoked. */
function testHandler(agent) {
let spokenText = 'This is a test message, when you see this, it means your webhook fulfillment worked!';
if (agent.requestSource === agent.ACTIONS_ON_GOOGLE) {
let conv = agent.conv();
conv.ask(spokenText);
conv.ask(new BasicCard({
title: `Test Message`,
subTitle: `Dialogflow Test`,
image: new Image({
url: 'https://dummyimage.com/600x400/000/fff',
alt: 'Image alternate text',
}),
text: spokenText,
buttons: new Button({
title: 'This is a button',
url: 'https://assistant.google.com/',
}),
}));
// Add Actions on Google library responses to your agent's response
agent.add(conv);
} else {
agent.add(spokenText);
}
}
/* When the Channel Intent gets invoked. */
function channelHandler(agent) {
var jsonResponse = `{"ID":10,"Listings":[{"Title":"Catfish Marathon","Date":"2018-07-13","Time":"11:00:00"},{"Title":"Videoclips","Date":"2018-07-13","Time":"12:00:00"},{"Title":"Pimp my ride","Date":"2018-07-13","Time":"12:30:00"},{"Title":"Jersey Shore","Date":"2018-07-13","Time":"13:00:00"},{"Title":"Jersey Shore","Date":"2018-07-13","Time":"13:30:00"},{"Title":"Daria","Date":"2018-07-13","Time":"13:45:00"},{"Title":"The Real World","Date":"2018-07-13","Time":"14:00:00"},{"Title":"The Osbournes","Date":"2018-07-13","Time":"15:00:00"},{"Title":"Teenwolf","Date":"2018-07-13","Time":"16:00:00"},{"Title":"MTV Unplugged","Date":"2018-07-13","Time":"16:30:00"},{"Title":"Rupauls Drag Race","Date":"2018-07-13","Time":"17:30:00"},{"Title":"Ridiculousness","Date":"2018-07-13","Time":"18:00:00"},{"Title":"Punk'd","Date":"2018-07-13","Time":"19:00:00"},{"Title":"Jersey Shore","Date":"2018-07-13","Time":"20:00:00"},{"Title":"MTV Awards","Date":"2018-07-13","Time":"20:30:00"},{"Title":"Beavis & Butthead","Date":"2018-07-13","Time":"22:00:00"}],"Name":"MTV"}`;
var results = JSON.parse(jsonResponse);
var listItems = {};
spokenText = getSpeech(results);
for (var i = 0; i < results['Listings'].length; i++) {
listItems[`SELECT_${i}`] = {
title: `${getSpokenTime(results['Listings'][i]['Time'])} - ${results['Listings'][i]['Title']}`,
description: `Channel: ${results['Name']}`
}
}
if (agent.requestSource === agent.ACTIONS_ON_GOOGLE) {
let conv = agent.conv();
conv.ask(spokenText);
conv.ask(new List({
title: 'TV Guide',
items: listItems
}));
// Add Actions on Google library responses to your agent's response
agent.add(conv);
} else {
agent.add(spokenText);
}
}
/**
* Return a text string to be spoken out by the Google Assistant
* @param {object} JSON tv results
*/
var getSpeech = function(tvresults) {
let s = "";
if(tvresults['Listings'][0]) {
let channelName = tvresults['Name'];
let currentlyPlayingTime = getSpokenTime(tvresults['Listings'][0]['Time']);
let laterPlayingTime = getSpokenTime(tvresults['Listings'][1]['Time']);
s = `On ${channelName} from ${currentlyPlayingTime}, ${tvresults['Listings'][0]['Title']} is playing.
Afterwards at ${laterPlayingTime}, ${tvresults['Listings'][1]['Title']} will start.`
}
return s;
}
/**
* Return a natural spoken time
* @param {string} time in 'HH:mm:ss' format
* @returns {string} spoken time (like 8 30 pm i.s.o. 20:00:00)
*/
var getSpokenTime = function(time){
let datetime = moment(time, 'HH:mm:ss');
let min = moment(datetime).format('m');
let hour = moment(datetime).format('h');
let partOfTheDay = moment(datetime).format('a');
if (min == '0') {
min = '';
}
return `${hour} ${min} ${partOfTheDay}`;
};
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
var agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
let channelInput = request.body.queryResult.parameters.channel;
let requestedTime = request.body.queryResult.parameters.time;
let url = `${TVGUIDE_WEBSERVICE}/${channelInput}`;
var intentMap = new Map();
intentMap.set('Test Intent', testHandler);
intentMap.set('Channel Intent', channelHandler);
agent.handleRequest(intentMap);
});
- Clique na guia package.json no editor e copie e cole este código JSON que importa todas as bibliotecas do Gerenciador de pacotes do Node.js (NPM):
{
"name": "tvGuideFulfillment",
"description": "Requesting TV Guide information from a web service.",
"version": "1.0.0",
"private": true,
"license": "Apache Version 2.0",
"author": "Google Inc.",
"engines": {
"node": "8"
},
"scripts": {
"start": "firebase serve --only functions:dialogflowFirebaseFulfillment",
"deploy": "firebase deploy --only functions:dialogflowFirebaseFulfillment"
},
"dependencies": {
"actions-on-google": "^2.2.0",
"firebase-admin": "^5.13.1",
"firebase-functions": "^2.0.2",
"request": "^2.85.0",
"request-promise": "^4.2.5",
"moment" : "^2.24.0",
"dialogflow-fulfillment": "^0.6.1"
}
}
- Clique no botão Implantar. Isso vai levar um tempo, porque ele está implantando a função sem servidor. Na parte de baixo da tela, você vai ver um pop-up informando seu status.
- Vamos testar o webhook para ver se o código funciona. No simulador do lado direito, digite:
Test my agent.
Quando tudo estiver correto, você verá a mensagem "Esta é uma mensagem de teste".
- Teste a intenção do canal e faça a pergunta:
What's on MTV?
Quando tudo estiver correto, você verá:
"No MTV das 16h30, o MTV Unplugged está tocando. Depois, às 17h30, o Rupauls Drag Race vai começar."
Etapas opcionais: Firebase
Ao testar com um canal diferente, você vai perceber que os resultados de TV são os mesmos. Isso ocorre porque a função do Cloud ainda não está buscando de um servidor da Web real.
Para isso, é preciso estabelecer uma conexão de rede de saída.
Caso queira testar o aplicativo com um serviço da Web, faça upgrade do seu plano do Firebase para o Blaze. Observação: essas etapas são opcionais. Você também pode seguir para as próximas etapas deste laboratório para continuar testando seu aplicativo no Actions on Google.
- Acesse o console do Firebase: https://console.firebase.google.com
- Na parte inferior da tela, pressione o botão Fazer upgrade.
Selecione o plano Blaze no pop-up.
- Agora que sabemos que o webhook funciona, podemos continuar e substituir o código de
index.js
pelo abaixo. Isso garante que você possa solicitar informações do guia de TV pelo serviço da Web:
'use strict';
process.env.DEBUG = 'dialogflow:debug';
const {
dialogflow,
BasicCard,
Button,
Image,
List
} = require('actions-on-google');
const functions = require('firebase-functions');
const moment = require('moment');
const { WebhookClient } = require('dialogflow-fulfillment');
const rp = require('request-promise');
const TVGUIDE_WEBSERVICE = 'https://tvguide-e4s5ds5dsa-ew.a.run.app/channel';
var spokenText = '';
var results = null;
/* When the Test Intent gets invoked. */
function testHandler(agent) {
let spokenText = 'This is a test message, when you see this, it means your webhook fulfillment worked!';
if (agent.requestSource === agent.ACTIONS_ON_GOOGLE) {
let conv = agent.conv();
conv.ask(spokenText);
conv.ask(new BasicCard({
title: `Test Message`,
subTitle: `Dialogflow Test`,
image: new Image({
url: 'https://dummyimage.com/600x400/000/fff',
alt: 'Image alternate text',
}),
text: spokenText,
buttons: new Button({
title: 'This is a button',
url: 'https://assistant.google.com/',
}),
}));
// Add Actions on Google library responses to your agent's response
agent.add(conv);
} else {
agent.add(spokenText);
}
}
/* When the Channel Intent gets invoked. */
function channelHandler(agent) {
var listItems = {};
spokenText = getSpeech(results);
for (var i = 0; i < results['Listings'].length; i++) {
listItems[`SELECT_${i}`] = {
title: `${getSpokenTime(results['Listings'][i]['Time'])} - ${results['Listings'][i]['Title']}`,
description: `Channel: ${results['Name']}`
}
}
if (agent.requestSource === agent.ACTIONS_ON_GOOGLE) {
let conv = agent.conv();
conv.ask(spokenText);
conv.ask(new List({
title: 'TV Guide',
items: listItems
}));
// Add Actions on Google library responses to your agent's response
agent.add(conv);
} else {
agent.add(spokenText);
}
}
/**
* Return a text string to be spoken out by the Google Assistant
* @param {object} JSON tv results
*/
var getSpeech = function(tvresults) {
let s = "";
if(tvresults && tvresults['Listings'][0]) {
let channelName = tvresults['Name'];
let currentlyPlayingTime = getSpokenTime(tvresults['Listings'][0]['Time']);
let laterPlayingTime = getSpokenTime(tvresults['Listings'][1]['Time']);
s = `On ${channelName} from ${currentlyPlayingTime}, ${tvresults['Listings'][0]['Title']} is playing.
Afterwards at ${laterPlayingTime}, ${tvresults['Listings'][1]['Title']} will start.`
}
return s;
}
/**
* Return a natural spoken time
* @param {string} time in 'HH:mm:ss' format
* @returns {string} spoken time (like 8 30 pm i.s.o. 20:00:00)
*/
var getSpokenTime = function(time){
let datetime = moment(time, 'HH:mm:ss');
let min = moment(datetime).format('m');
let hour = moment(datetime).format('h');
let partOfTheDay = moment(datetime).format('a');
if (min == '0') {
min = '';
}
return `${hour} ${min} ${partOfTheDay}`;
};
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
var agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
let channelInput = request.body.queryResult.parameters.channel;
let requestedTime = request.body.queryResult.parameters.time;
let url = `${TVGUIDE_WEBSERVICE}/${channelInput}`;
if (requestedTime) {
console.log(requestedTime);
let offsetMin = moment().utcOffset(requestedTime)._offset;
console.log(offsetMin);
let time = moment(requestedTime).utc().add(offsetMin,'m').format('HH:mm:ss');
url = `${TVGUIDE_WEBSERVICE}/${channelInput}/${time}`;
}
console.log(url);
var options = {
uri: encodeURI(url),
json: true
};
// request promise calls an URL and returns the JSON response.
rp(options)
.then(function(tvresults) {
console.log(tvresults);
// the JSON response, will need to be formatted in 'spoken' text strings.
spokenText = getSpeech(tvresults);
results = tvresults;
})
.catch(function (err) {
console.error(err);
})
.finally(function(){
// kick start the Dialogflow app
// based on an intent match, execute
var intentMap = new Map();
intentMap.set('Test Intent', testHandler);
intentMap.set('Channel Intent', channelHandler);
agent.handleRequest(intentMap);
});
});
6. Ações no Google
O Actions on Google é uma plataforma de desenvolvimento para o Google Assistente. Ela permite o desenvolvimento terceirizado de "ações", miniaplicativos do Google Assistente que oferecem mais funcionalidades.
Você precisa invocar uma ação do Google pedindo para o Google abrir ou falar com um app.
Isso abrirá sua ação, mudará a voz, e você deixará o campo "nativo". escopo do Google Assistente. Isso significa que tudo o que você perguntar ao agente a partir deste ponto precisa ser criado por você. Não será possível pedir informações sobre a previsão do tempo ao Google de repente, se você quiser. primeiro é necessário deixar (fechar) o escopo da ação (seu app).
Testar sua ação no simulador do Google Assistente
Vamos testar a seguinte conversa:
Usuário | Google Assistente |
"Ok Google, fale com your-name-tv-guide." | "Claro. Vou buscar your-name-tv-guide." |
Usuário | Seu-Nome-Agente-TV-Guide |
- | "Olá, eu sou o guia de TV..." |
Testar meu agente | "Esta é uma mensagem de teste. Quando você a encontrar, significa que o fulfillment do webhook funcionou." |
O que tem no MTV? | O MTV Unplugged está tocando no MTV das 16h30. Depois, às 17h30, o Rupauls Drag Race vai começar. |
- Voltar para o simulador do Google Assistente
Abra: https://console.actions.google.com
- Clique no ícone de microfone e pergunte o seguinte:
Talk to my test agent
Test my agent
O Google Assistente vai responder com o seguinte:
- Agora vamos perguntar:
What's on Comedy Central?
Isso retornará:
No momento, no Comedy Central, a partir das 18h, The Simpsons está passando. Depois, às 19h, o programa Family Guy vai começar.
7. Parabéns
Você criou sua primeira ação do Google Assistente com o Dialogflow. Muito bem!
Como você deve ter notado, a ação estava sendo executada no modo de teste, que está vinculado à sua Conta do Google. Faça login no dispositivo Nest ou no app Google Assistente no smartphone iOS ou Android com a mesma conta. Você também pode testar sua ação.
Agora esta é uma demonstração de workshop. No entanto, ao criar aplicativos para o Google Assistente de verdade, você pode enviar a ação para aprovação. Leia este guia para mais informações.
O que vimos
- Como criar um chatbot com o Dialogflow v2
- Como criar entidades personalizadas com o Dialogflow
- Como criar uma conversa linear com o Dialogflow
- Como configurar fulfillments do webhook com o Dialogflow e o Firebase Functions
- Como levar seu aplicativo para o Google Assistente com o Actions on Google
Qual é a próxima etapa?
Gostou do codelab? Confira esses laboratórios incríveis!
Integre este codelab ao Google Chat para continuar:
Criar um guia de TV do Google Chat com o G Suite e o Dialogflow
- Criar ações para o Google Assistente com o Dialogflow (nível 1)
- Criar ações para o Google Assistente com o Dialogflow (nível 2)
- Criar ações para o Google Assistente com o Dialogflow (nível 3)
- Como entender o fulfillment integrando o Dialogflow ao Google Agenda
- Integrar a API Google Cloud Vision ao Dialogflow