| 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> |
| """) |
| |
| 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) |