diogofpedrosa's picture
Update app.py
118b7fd verified
Raw
History Blame Contribute Delete
22.2 kB
import os
import email
from email import policy
from dotenv import load_dotenv
import openai
import json
import gradio as gr
load_dotenv()
client = openai.OpenAI(
api_key=os.environ.get("GROQ_API_KEY"),
base_url="https://api.groq.com/openai/v1"
)
CATEGORIES = [
"Reserva – Novos pedidos de reserva, alterações ou cancelamentos.",
"Check-in/Check-out – Informações sobre chegadas, saidas ou procedimentos.",
"Reclamação – Queixas ou problemas reportados por hóspedes.",
"Elogio – Feedback positivo ou agradecimentos de hóspedes.",
"Pagamento – Questões sobre faturação, pagamentos ou reembolsos.",
"Serviços – Pedidos de serviços adicionais (room service, spa, transfers, etc.).",
"Fornecedores – Comunicações com fornecedores de produtos ou serviços.",
"Eventos – Reservas ou informações sobre eventos, conferências ou casamentos.",
"RH – Questões de recursos humanos e gestão de pessoal.",
"Manutenção – Problemas técnicos ou pedidos de manutenção das instalações.",
"Marketing – Parcerias, promoções ou campanhas publicitárias.",
"Outros – Se nenhuma das anteriores se aplicar."
]
SYSTEM_PROMPT = '''
És um assistente especializado em processar e estruturar emails recebidos por um hotel. A tua tarefa é analisar o email e devolver uma resposta em **JSON** com cinco campos principais:
1. **classification**: A categoria do email (escolhida de uma lista fixa).
2. **TL;DR**: Um resumo muito curto (2-5 palavras).
3. **description**: Uma descrição clara e concisa do conteúdo do email.
4. **structured_data**: Um objeto com campos específicos relevantes para a categoria — preenche **apenas os campos que forem mencionados ou inferíveis com segurança no email**. Se não houver informação suficiente, deixa o campo ausente ou com valor `null`.
5. **automated_reply**: Uma resposta profissional e personalizada ao email recebido, adaptada à categoria e ao contexto específico.
### Lista de categorias e campos esperados em `structured_data`:
- **Reserva**:
```json
{
"guest_name": "string (nome completo do hóspede)",
"email": "string (email de contacto)",
"phone": "string (telefone de contacto)",
"número do CC ": "String",
"nif_cc": "string (NIF)",
"check_in_date": "YYYY-MM-DD",
"check_out_date": "YYYY-MM-DD",
"nights": "número (calculado automaticamente se possível)",
"room_type": "string (ex: 'duplo', 'suite', 'familiar')",
"guests": "número (total de pessoas)",
"adults": "número",
"children": "número",
"special_requests": "string (pedidos especiais, alergias, etc.)",
"meal_plan": "string (ex: 'pequeno-almoço', 'meia-pensão', 'pensão completa')",
"estimated_arrival_time": "HH:MM"
}
```
- **Check-in/Check-out**:
```json
{
"check_in_time": "HH:MM",
"check_out_time": "HH:MM",
"guest_name": "string",
"email": "string",
"phone": "string",
"reservation_id": "string",
"early_checkin": "boolean",
"late_checkout": "boolean",
"special_requests": "string"
}
```
- **Reclamação**:
```json
{
"guest_name": "string",
"reservation_id": "string",
"room_number": "string",
"issue_type": "string (ex: 'limpeza', 'barulho', 'equipamento', 'atendimento')",
"urgency": "Baixa/Média/Alta",
"affected_service": "string",
"date_of_incident": "YYYY-MM-DD",
"compensation_requested": "boolean"
}
```
- **Elogio**:
```json
{
"guest_name": "string",
"highlighted_service": "string",
"staff_member": "string",
"department": "string",
"reservation_id": "string"
}
```
- **Pagamento**:
```json
{
"guest_name": "string",
"invoice_number": "string",
"reservation_id": "string",
"amount": "string (ex: '€250.00')",
"payment_method": "string (ex: 'transferência', 'MB WAY', 'cartão')",
"payment_status": "string (ex: 'pago', 'pendente', 'reembolso')",
"due_date": "YYYY-MM-DD",
"nif": "string (para faturação)"
}
```
- **Serviços**:
```json
{
"requested_service": "string (ex: 'room service', 'spa', 'transfer', 'babysitting')",
"service_datetime": "YYYY-MM-DDTHH:MM",
"guest_name": "string",
"room_number": "string",
"location": "string",
"number_of_people": "número",
"special_requirements": "string"
}
```
- **Fornecedores**:
```json
{
"supplier_name": "string",
"contact_person": "string",
"product_or_service": "string",
"delivery_date": "YYYY-MM-DD",
"delivery_time": "HH:MM",
"order_number": "string",
"quantity": "string"
}
```
- **Eventos**:
```json
{
"event_type": "string (ex: 'casamento', 'conferência', 'aniversário')",
"event_date": "YYYY-MM-DD",
"event_time": "HH:MM",
"expected_guests": "número",
"contact_person": "string",
"email": "string",
"phone": "string",
"budget_range": "string",
"catering_required": "boolean",
"accommodation_required": "boolean",
"special_requirements": "string"
}
```
- **RH**:
```json
{
"topic": "string (ex: 'férias', 'folga', 'recrutamento', 'formação')",
"employee_name": "string",
"department": "string",
"start_date": "YYYY-MM-DD",
"end_date": "YYYY-MM-DD",
"urgency": "Baixa/Média/Alta"
}
```
- **Manutenção**:
```json
{
"location": "string (ex: 'quarto 312', 'receção', 'piscina')",
"issue_description": "string",
"issue_type": "string (ex: 'elétrico', 'canalização', 'ar condicionado', 'mobiliário')",
"urgency": "Baixa/Média/Alta",
"reported_by": "string",
"affects_guests": "boolean"
}
```
- **Marketing**:
```json
{
"campaign_name": "string",
"partner": "string",
"contact_person": "string",
"start_date": "YYYY-MM-DD",
"end_date": "YYYY-MM-DD",
"campaign_type": "string (ex: 'redes sociais', 'email marketing', 'parceria')",
"budget": "string"
}
```
- **Outros**:
```json
{
"topic": "string",
"requires_follow_up": "boolean"
}
```
### Instruções para `automated_reply`:
Regra Principal:
- **Deteta automaticamente o idioma do email original (português ou inglês)**.
- **Se o email estiver em inglês**, todos os campos de texto gerados devem estar **em português profissional**, o campo "automated_reply" deve vir em inglês adaptado culturalmente (ex: “Dear Mr. Smith”).
- **Se o email estiver em português**, mantém o português europeu formal e cordial.
- Usa sempre o nome do remetente, se disponível.
- **Reserva**:
- Se TODOS os campos obrigatórios estiverem presentes (nome, contacto, datas, tipo de quarto, NIF/CC): confirma a receção do pedido e informa que será processado.
- Se FALTAREM dados obrigatórios: solicita educadamente as informações em falta. Campos obrigatórios: `guest_name`, `email` ou `phone`, `nif_cc`, `check_in_date`, `check_out_date`, `room_type`, `guests`.
- **Check-in/Check-out**: Confirma o pedido e informa sobre políticas ou procedimentos relevantes.
- **Reclamação**: Pede desculpa pelo incómodo, assegura que a situação será analisada com prioridade, e agradece o feedback.
- **Elogio**: Agradece calorosamente o feedback positivo e transmite os elogios à equipa mencionada.
- **Pagamento**: Confirma receção do pedido e informa sobre os próximos passos ou envia documentação solicitada.
- **Serviços**: Confirma o pedido de serviço e informa disponibilidade ou solicita confirmação adicional.
- **Fornecedores**: Confirma receção da comunicação e próximos passos.
- **Eventos**: Agradece o interesse, solicita informações adicionais se necessário, ou confirma disponibilidade.
- **RH**: Confirma receção e informa sobre processo de aprovação.
- **Manutenção**: Confirma que a situação foi registada e será tratada com a devida urgência.
- **Marketing**: Agradece a proposta e indica interesse em explorar a parceria.
- **Outros**: Resposta genérica confirmando receção e indicando que será dado seguimento.
**Tom da resposta**: Profissional, cordial, empático, em português europeu. Usa o nome do hóspede/contacto quando disponível. Mantém um tom caloroso mas profissional.
### Instruções importantes:
- **NÃO inventes dados**. Só preenche campos se o email contiver informação clara ou se for uma inferência óbvia.
- Se não houver dados suficientes para um campo, **omite-o** ou usa `null`.
- **Mantém tudo em português europeu**.
- **Responde apenas com um objeto JSON válido**, sem explicações adicionais, markdown ou texto extra.
- A data atual é **3 de novembro de 2025** — podes usar isto para interpretar expressões como "amanhã", "na próxima semana", etc.
- Para reservas, se faltarem campos obrigatórios, a `automated_reply` DEVE solicitar essas informações específicas de forma clara e educada.
### Exemplo de resposta válida (Reserva completa):
```json
{
"classification": "Reserva",
"TL;DR": "Nova reserva",
"description": "Pedido de reserva de quarto duplo para duas pessoas de 15 a 17 de novembro com vista para o mar.",
"structured_data": {
"guest_name": "Miguel Ferreira",
"check_in_date": "2025-11-15",
"check_out_date": "2025-11-17",
"nights": 2,
"room_type": "duplo",
"guests": 2,
"special_requests": "vista para o mar"
},
"automated_reply": "Exmo. Sr. Miguel Ferreira,\n\nAgradecemos o seu contacto e interesse no nosso hotel.\n\nPara procedermos com a sua reserva de um quarto duplo com vista para o mar de 15 a 17 de novembro (2 noites), necessitamos de alguns dados adicionais:\n\n• Número de telefone ou email de contacto\n• Número do Cartão de Cidadão ou NIF (obrigatório para registo)\n• Hora prevista de chegada\n• Preferência de regime alimentar (apenas pequeno-almoço, meia-pensão, etc.)\n\nAssim que recebermos estas informações, teremos todo o prazer em confirmar a sua reserva.\n\nFicamos ao dispor para qualquer esclarecimento adicional.\n\nCom os melhores cumprimentos,\nEquipa de Reservas"
}
```
### Exemplo de resposta válida (Reclamação):
```json
{
"classification": "Reclamação",
"TL;DR": "Barulho noturno",
"description": "Hóspede reclama de barulho excessivo no corredor durante a madrugada no quarto 203.",
"structured_data": {
"guest_name": "Tiago Mendes",
"room_number": "203",
"issue_type": "barulho",
"urgency": "Média",
"affected_service": "descanso noturno"
},
"automated_reply": "Exmo. Sr. Tiago Mendes,\n\nLamentamos profundamente o incómodo causado durante a sua estadia no quarto 203.\n\nA situação que nos reporta relativamente ao barulho no corredor durante a madrugada é inaceitável e será imediatamente analisada pela nossa equipa de gestão. Estamos a tomar medidas para garantir que situações semelhantes não voltem a ocorrer.\n\nO seu conforto e descanso são a nossa prioridade máxima. Gostaríamos de contactá-lo pessoalmente para apresentar as nossas desculpas e encontrar uma solução adequada.\n\nAgradecemos por nos ter alertado para esta situação.\n\nCom os melhores cumprimentos,\nDireção do Hotel"
}
```
'''
def summarize_and_classify(email_body: str):
response = client.responses.create(
model="openai/gpt-oss-120b",
input=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": email_body}
]
)
return response.output_text.strip()
def classify_email(email_body: str):
if not email_body.strip():
return "Nenhuma entrada fornecida", "", "", ""
summary_text = summarize_and_classify(email_body)
try:
summary_json = json.loads(summary_text)
classification = summary_json.get("classification", "Classificação Manual")
tldr = summary_json.get("TL;DR", "N/A")
description = summary_json.get("description", "N/A")
json_output = json.dumps(summary_json.get("structured_data", {}), indent=2, ensure_ascii=False)
automatic_answer = summary_json.get("automated_reply", "N/A")
except json.JSONDecodeError:
classification = "⚠️ Erro ao analisar JSON"
tldr = ""
description = summary_text
json_output = ""
automatic_answer= ""
return classification, tldr, description, json_output, automatic_answer
def exemplo_email(tipo):
exemplos = {
"reserva": "Assunto: Pedido de reserva para o fim de semana\n\nBom dia,\nGostaria de reservar um quarto duplo para duas pessoas de 15 a 17 de novembro.\nPreferencialmente com vista para o mar.\nPodem confirmar a disponibilidade, por favor?\nCumprimentos,\nMiguel Ferreira",
"checkin": "Assunto: Pedido de check-out tardio\n\nBoa tarde,\nTenho uma reserva para amanhã (nº RES-2025-345).\nSerá possível fazer o check-out pelas 14h, em vez das 11h?\nObrigado,\nCarla Ramos",
"reclamacao": "Assunto: Reclamação sobre barulho durante a noite\n\nBoa noite,\nDurante a minha estadia no quarto 203, houve muito barulho no corredor até às 3h da manhã.\nPeço que revejam esta situação, pois foi bastante incómodo.\nCumprimentos,\nTiago Mendes",
"elogio": "Assunto: Agradecimento pela estadia maravilhosa\n\nOlá,\nGostaria de agradecer a toda a equipa pela simpatia e profissionalismo, especialmente à Sra. Joana da receção.\nO serviço foi impecável!\nCom os melhores cumprimentos,\nInês Oliveira",
"pagamento": "Assunto: Fatura pendente — reserva #2025-0021\n\nBoa tarde,\nPodem enviar-me a fatura referente à minha estadia de outubro?\nO pagamento foi feito por transferência bancária, no valor de €420,00.\nObrigada,\nRita Santos",
"servicos": "Assunto: Pedido de massagem e jantar no quarto\n\nBoa noite,\nEstou hospedado no quarto 507 e gostaria de agendar uma massagem para amanhã às 17h, e o jantar no quarto pelas 20h.\nCumprimentos,\nHugo Matos",
"fornecedores": "Assunto: Entrega de vinhos — Quinta da Serra\n\nBom dia,\nConfirmamos a entrega dos vinhos tintos reserva para dia 6 de novembro, pelas 10h.\nCumprimentos,\nAntónio Silva\nQuinta da Serra Lda.",
"eventos": "Assunto: Pedido de orçamento para casamento\n\nOlá,\nGostaríamos de saber se o vosso hotel organiza casamentos.\nPretendemos realizar o evento no dia 20 de junho de 2026, para cerca de 120 convidados.\nO contacto principal será a noiva, Marta Lopes.\nObrigada!",
"rh": "Assunto: Pedido de férias — dezembro\n\nBom dia,\nVenho solicitar férias de 16 a 30 de dezembro.\nAgradeço confirmação.\nCumprimentos,\nPaulo Costa\nDepartamento de Manutenção",
"manutencao": "Assunto: Ar condicionado avariado — quarto 312\n\nBoa tarde,\nO ar condicionado do quarto 312 não está a funcionar corretamente.\nEstá a emitir um ruído forte e não arrefece.\nAgradeço reparação urgente.\nObrigado,\nReceção",
"marketing": "Assunto: Proposta de parceria — Influenciadora de viagens\n\nOlá,\nSou a Carolina Sousa, criadora de conteúdo na área de viagens.\nGostaria de propor uma parceria para divulgação do vosso hotel nas minhas redes sociais durante novembro.\nPosso enviar proposta detalhada.\nObrigada,\nCarolina Sousa",
"outros": "Assunto: Pedido de informações gerais\n\nBoa tarde,\nGostaria de saber se o vosso hotel aceita animais de estimação e se existe estacionamento gratuito.\nCumprimentos,\nJoão Almeida",
}
return exemplos[tipo]
with gr.Blocks(theme=gr.themes.Ocean(), title="Classificador de Emails", css="""
.main-title {text-align: center;}
.subtitle {
text-align: center;
margin: 0 auto 0 auto;
font-size: 1.05em;
line-height: 1.6;
width: 80%;
color: var(--body-text-color);
}
.logo-image {
margin: 0 auto;
display: block;
}
.category-box {
display: inline-block;
padding: 6px 12px;
margin: 4px;
background-color: #f5f0ef;
border-radius: 12px;
font-size: 0.85em;
color: #8b7a77;
font-weight: 500;
border: 1px solid #8b7a77;
}
.category-container {
text-align: center;
flex-wrap: wrap;
display: flex;
justify-content: center;
margin-bottom: 2rem;
}
/* Custom button colors */
.primary, button.primary, .gradio-button.primary {
background: #8b7a77 !important;
background-color: #8b7a77 !important;
border-color: #8b7a77 !important;
color: white !important;
}
.primary:hover, button.primary:hover, .gradio-button.primary:hover {
background: #7a6966 !important;
background-color: #7a6966 !important;
border-color: #7a6966 !important;
}
.secondary, button.secondary, .gradio-button.secondary {
display: inline-block !important;
padding: 6px 12px !important;
margin: 4px !important;
background-color: #f5f0ef !important;
background: #f5f0ef !important;
border-radius: 12px !important;
font-size: 0.85em !important;
color: #8b7a77 !important;
font-weight: 500 !important;
border: 1px solid #8b7a77 !important;
}
/* Targeting all buttons to ensure color change */
button[variant="primary"] {
background: #8b7a77 !important;
background-color: #8b7a77 !important;
border-color: #8b7a77 !important;
}
button[variant="secondary"] {
background-color: #f5f0ef;
color: #8b7a77;
border: 1px solid #8b7a77;
}""") as demo:
logo = gr.Image(
value="logo-ap.png",
label="",
show_label=False,
show_download_button=False,
show_share_button=False,
show_fullscreen_button=False,
height=100,
container=False,
elem_classes="logo-image"
)
gr.Markdown("# 📧 Classificação e Resposta Automática a E-mails", elem_classes="main-title")
gr.Markdown(
"""
Este agente utiliza inteligência artificial para analisar, categorizar e propor respostas automáticas a emails recebidos por estabelecimentos hoteleiros. O sistema identifica com precisão o tipo de email, as entidades, fornece resumos executivos, classificando cada email nas seguintes categorias:
""",
elem_classes="subtitle"
)
categories_html = "".join([
f'<div class="category-box">{cat.split(" – ")[0]}</div>'
for cat in CATEGORIES
])
gr.HTML(f'<div class="category-container">{categories_html}</div>')
with gr.Row():
with gr.Column(scale=2):
gr.Markdown("### 📥 Email de Entrada")
email_input = gr.Textbox(
lines=8,
label="Conteúdo do Email",
placeholder="Cole aqui o conteúdo do email ou use um exemplo abaixo...",
show_label=False
)
gr.Markdown("### 🎯 Exemplos Rápidos")
with gr.Row():
btn_reserva = gr.Button("🏨 Reserva", variant="secondary", size="sm")
btn_checkin = gr.Button("🕓 Check-in/Out", variant="secondary", size="sm")
with gr.Row():
btn_reclamacao = gr.Button("😡 Reclamação", variant="secondary", size="sm")
btn_pagamento = gr.Button("💳 Pagamento", variant="secondary", size="sm")
with gr.Row():
btn_elogio = gr.Button("😊 Elogio", variant="secondary", size="sm")
btn_servicos = gr.Button("🛎️ Serviços", variant="secondary", size="sm")
with gr.Row():
btn_fornecedores = gr.Button("📦 Fornecedores", variant="secondary", size="sm")
btn_eventos = gr.Button("🎉 Eventos", variant="secondary", size="sm")
with gr.Row():
btn_rh = gr.Button("👥 RH", variant="secondary", size="sm")
btn_manutencao = gr.Button("🔧 Manutenção", variant="secondary", size="sm")
with gr.Row():
btn_marketing = gr.Button("📢 Marketing", variant="secondary", size="sm")
btn_outros = gr.Button("❓ Outros", variant="secondary", size="sm")
classify_btn = gr.Button("🔍 Classificar Email", variant="primary", size="lg")
with gr.Column(scale=3):
gr.Markdown("### 📊 Resultados da Classificação")
with gr.Row():
classification_output = gr.Textbox(
label="📌 Classificação",
lines=1,
interactive=False
)
tldr_output = gr.Textbox(
label="🎯 Tema",
lines=1,
interactive=False
)
description_output = gr.Textbox(
label="📝 Resumo",
lines=1,
max_lines=3,
interactive=True
)
json_output = gr.JSON(
label="Recolha de Entidades (JSON)"
)
automated_reply_output = gr.Textbox(
label="✉️ Resposta Automática Sugerida",
lines=2,
max_lines=40,
interactive=True
)
gr.HTML("""
<div style='
text-align:center;
padding:20px;
margin-top:30px;
font-size:0.95em;
color:#555;
border-top:1px solid #ddd;
'>
© 2025 Timestamp — Todos os direitos reservados
</div>
""")
# Button event handlers
for btn, tipo in [
(btn_reserva, "reserva"),
(btn_checkin, "checkin"),
(btn_reclamacao, "reclamacao"),
(btn_elogio, "elogio"),
(btn_pagamento, "pagamento"),
(btn_servicos, "servicos"),
(btn_fornecedores, "fornecedores"),
(btn_eventos, "eventos"),
(btn_rh, "rh"),
(btn_manutencao, "manutencao"),
(btn_marketing, "marketing"),
(btn_outros, "outros")
]:
btn.click(
lambda t=tipo: exemplo_email(t),
None,
email_input
)
classify_btn.click(
fn=classify_email,
inputs=email_input,
outputs=[classification_output, tldr_output, description_output, json_output, automated_reply_output]
)
if __name__ == "__main__":
demo.launch(share=True)