import re from datetime import datetime, timedelta from typing import TYPE_CHECKING from app.services.orchestration.orchestrator_config import ( CANCEL_ORDER_REQUIRED_FIELDS, ORDER_REQUIRED_FIELDS, PENDING_ORDER_SELECTION_TTL_MINUTES, REVIEW_REQUIRED_FIELDS, ) if TYPE_CHECKING: from app.services.orchestration.orquestrador_service import OrquestradorService # essa classe é responsável por controlar qual o assunto está ativo na conversa, se existe fluxo aberto, se o usuário mandou dois pedidos ao mesmo tempo... class ConversationPolicy: def __init__(self, service: "OrquestradorService"): self.service = service def _save_context(self, user_id: int | None, context: dict | None) -> None: if user_id is None or not isinstance(context, dict): return self.service._save_user_context(user_id=user_id, context=context) def _decision_action(self, turn_decision: dict | None) -> str: return str((turn_decision or {}).get("action") or "").strip().lower() def _decision_intent(self, turn_decision: dict | None) -> str: return str((turn_decision or {}).get("intent") or "").strip().lower() def _decision_domain(self, turn_decision: dict | None) -> str: return str((turn_decision or {}).get("domain") or "").strip().lower() def _decision_selection_index(self, turn_decision: dict | None) -> int | None: value = (turn_decision or {}).get("selection_index") return value if isinstance(value, int) and value >= 0 else None # Essa função serve para reaproveitar informações já informadas antes, evitando pedir novamente ao usuário. def try_prefill_review_fields_from_memory(self, user_id: int | None, payload: dict) -> None: if user_id is None: return context = self.service._get_user_context(user_id) if not context: return memory = context.get("generic_memory", {}) if payload.get("placa") is None: plate = self.service.normalizer.normalize_plate(memory.get("placa")) if plate: payload["placa"] = plate # Essa função coloca um pedido na fila def queue_order(self, user_id: int | None, domain: str, order_message: str) -> None: self.queue_order_with_memory_seed( user_id=user_id, domain=domain, order_message=order_message, memory_seed=self.service._new_tab_memory(user_id=user_id), ) # Enfileira um próximo assunto para ser tratado depois, preservando dados úteis daquele pedido def queue_order_with_memory_seed( self, user_id: int | None, domain: str, order_message: str, memory_seed: dict | None = None, ) -> None: context = self.service._get_user_context(user_id) if not context or domain == "general": return queue = context.setdefault("order_queue", []) queue.append( { "domain": domain, "message": (order_message or "").strip(), "memory_seed": dict(memory_seed or self.service._new_tab_memory(user_id=user_id)), "created_at": datetime.utcnow().isoformat(), } ) self._save_context(user_id=user_id, context=context) # Transforma as entidades extraídas de um pedido em uma memória temporária pronta para usar quando esse pedido for processado. def build_order_memory_seed(self, user_id: int | None, order: dict | None = None) -> dict: seed = dict(self.service._new_tab_memory(user_id=user_id)) if not isinstance(order, dict): return seed entities = order.get("entities") if not isinstance(entities, dict): return seed generic_memory = self.service.normalizer.normalize_generic_fields(entities.get("generic_memory")) if generic_memory: seed.update(generic_memory) review_fields = self.service.normalizer.normalize_review_fields(entities.get("review_fields")) if review_fields.get("placa") and not seed.get("placa"): seed["placa"] = review_fields["placa"] order_fields = self.service.normalizer.normalize_order_fields(entities.get("order_fields")) if order_fields.get("cpf") and not seed.get("cpf"): seed["cpf"] = order_fields["cpf"] if order_fields.get("modelo_veiculo") and not seed.get("modelo_veiculo"): seed["modelo_veiculo"] = order_fields["modelo_veiculo"] return seed # Pega o próximo assunto pendente do usuário. def pop_next_order(self, user_id: int | None) -> dict | None: context = self.service._get_user_context(user_id) if not context: return None queue = context.setdefault("order_queue", []) if not queue: return None popped = queue.pop(0) self._save_context(user_id=user_id, context=context) return popped # Decide qual pedido deve ser processado agora, qual vai para fila, e se o usuário precisa escolher entre dois pedidos. def prepare_message_for_single_order( self, message: str, user_id: int | None, routing_plan: dict | None = None, ) -> tuple[str, str | None, str | None]: context = self.service._get_user_context(user_id) if not context: return message, None, None queue_notice = None active_domain = context.get("active_domain", "general") orders_raw = (routing_plan or {}).get("orders") if isinstance(routing_plan, dict) else None extracted_orders: list[dict] = [] if isinstance(orders_raw, list): for item in orders_raw: if not isinstance(item, dict): continue domain = str(item.get("domain") or "general").strip().lower() if domain not in {"review", "sales", "general"}: domain = "general" segment = str(item.get("message") or "").strip() if segment: extracted_orders.append( { "domain": domain, "message": segment, "entities": self.service._coerce_extraction_contract(item.get("entities")), } ) if not extracted_orders: extracted_orders = [{"domain": "general", "message": (message or "").strip()}] if ( len(extracted_orders) == 2 and all(order["domain"] != "general" for order in extracted_orders) and not self.has_open_flow(user_id=user_id, domain=active_domain) ): self.store_pending_order_selection(user_id=user_id, orders=extracted_orders) return message, None, self.render_order_selection_prompt(extracted_orders) if len(extracted_orders) <= 1: inferred = extracted_orders[0]["domain"] if ( inferred != "general" and inferred != active_domain and self.has_open_flow(user_id=user_id, domain=active_domain) ): self.queue_order(user_id=user_id, domain=inferred, order_message=message) queue_hint = self.render_queue_notice(1) prompt = self.render_open_flow_prompt(user_id=user_id, domain=active_domain) return message, None, f"{prompt}\n{queue_hint}" if queue_hint else prompt return message, None, None if self.has_open_flow(user_id=user_id, domain=active_domain): queued_count = 0 for queued in extracted_orders: if queued["domain"] != active_domain: self.queue_order_with_memory_seed( user_id=user_id, domain=queued["domain"], order_message=queued["message"], memory_seed=self.build_order_memory_seed(user_id=user_id, order=queued), ) queued_count += 1 queue_hint = self.render_queue_notice(queued_count) prompt = self.render_open_flow_prompt(user_id=user_id, domain=active_domain) return message, None, f"{prompt}\n{queue_hint}" if queue_hint else prompt first = extracted_orders[0] queued_count = 0 for queued in extracted_orders[1:]: self.queue_order_with_memory_seed( user_id=user_id, domain=queued["domain"], order_message=queued["message"], memory_seed=self.build_order_memory_seed(user_id=user_id, order=queued), ) queued_count += 1 context["active_domain"] = first["domain"] context["generic_memory"] = self.build_order_memory_seed(user_id=user_id, order=first) self._save_context(user_id=user_id, context=context) queue_notice = self.render_queue_notice(queued_count) return first["message"], queue_notice, None # Serve para concatenar mensagens auxiliares com a resposta principal. def compose_order_aware_response(self, response: str, queue_notice: str | None = None) -> str: lines = [] if queue_notice: lines.append(queue_notice) lines.append(response) return "\n".join(lines) # Armazena uma “decisão pendente” de qual pedido começar. def store_pending_order_selection(self, user_id: int | None, orders: list[dict]) -> None: context = self.service._get_user_context(user_id) if not context: return context["pending_order_selection"] = { "orders": [ { "domain": order["domain"], "message": order["message"], "memory_seed": self.build_order_memory_seed(user_id=user_id, order=order), } for order in orders[:2] ], "expires_at": datetime.utcnow() + timedelta(minutes=PENDING_ORDER_SELECTION_TTL_MINUTES), } self._save_context(user_id=user_id, context=context) # Cria o texto de escolha para o usuário. def render_order_selection_prompt(self, orders: list[dict]) -> str: if len(orders) < 2: return "Qual das acoes voce quer iniciar primeiro?" first_label = self.describe_order_selection_option(orders[0]) second_label = self.describe_order_selection_option(orders[1]) return ( "Identifiquei duas acoes na sua mensagem:\n" f"1. {first_label}\n" f"2. {second_label}\n" "Qual delas voce quer iniciar primeiro? Se for indiferente, eu escolho." ) # Formata o rótulo do pedido para exibição. def describe_order_selection_option(self, order: dict) -> str: domain = str(order.get("domain") or "general") message = str(order.get("message") or "").strip() domain_prefix = { "review": "Revisao", "sales": "Venda", "general": "Atendimento", }.get(domain, "Atendimento") return f"{domain_prefix}: {message}" # É um helper simples para busca de palavras-chave def contains_any_term(self, text: str, terms: set[str]) -> bool: return any(term in text for term in terms) # Limpa a mensagem para facilitar a detecção de escolha def strip_choice_message(self, text: str) -> str: cleaned = (text or "").strip() while cleaned and cleaned[-1] in ".!?,;:": cleaned = cleaned[:-1].rstrip() return cleaned # Se o usuário disser algo como: "esquece tudo e quero agendar revisão" a função remove a parte de reset e devolve só o novo pedido. def remove_order_selection_reset_prefix(self, message: str) -> str: raw = (message or "").strip() normalized = self.service.normalizer.normalize_text(raw) prefixes = ("esqueca tudo agora", "esqueca tudo", "esquece tudo agora", "esquece tudo") for prefix in prefixes: if normalized.startswith(prefix): return raw[len(prefix):].lstrip(" ,.:;-") return raw # Detecta intenção de abandonar o contexto atual def is_order_selection_reset_message(self, message: str) -> bool: normalized = self.service.normalizer.normalize_text(message).strip() reset_terms = { "esqueca tudo", "esqueca tudo agora", "esquece tudo", "esquece tudo agora", "ignora isso", "ignore isso", "deixa isso", "deixa pra la", "deixa para la", "novo assunto", "muda de assunto", "vamos comecar de novo", "comecar de novo", "reiniciar", "resetar conversa", } return self.contains_any_term(normalized, reset_terms) # Ajuda a perceber quando o usuário talvez tenha mudado de assunto sem responder à pergunta de escolha def looks_like_fresh_operational_request(self, message: str, turn_decision: dict | None = None) -> bool: decision_domain = self._decision_domain(turn_decision) decision_intent = self._decision_intent(turn_decision) if decision_domain in {"review", "sales"} or decision_intent not in {"", "general"}: return True normalized = self.service.normalizer.normalize_text(message).strip() if len(normalized) < 15: return False operational_terms = { "agendar", "revisao", "cancelar", "pedido", "comprar", "compra", "carro", "veiculo", "remarcar", "tambem", } return self.contains_any_term(normalized, operational_terms) # Distingue um comando global explicito de cancelamento do fluxo atual de um texto livre # que deve ser consumido como dado do rascunho aberto. def is_explicit_flow_cancel_message(self, message: str) -> bool: normalized = self.service.normalizer.normalize_text(message).strip() explicit_terms = { "cancelar fluxo", "cancela o fluxo", "cancelar esse fluxo", "cancela esse fluxo", "cancelar fluxo atual", "cancela o fluxo atual", "encerrar fluxo", "encerrar esse fluxo", "parar fluxo", "parar esse fluxo", "abandonar fluxo", "abandonar esse fluxo", "desistir desse fluxo", "desistir deste fluxo", "desistir dessa compra", "desistir desta compra", } return normalized in explicit_terms # Evita que frases como "desisti" sejam tratadas como comando global quando o sistema # esta justamente esperando o motivo do cancelamento. def should_defer_flow_cancellation_control(self, message: str, user_id: int | None) -> bool: if user_id is None or self.is_explicit_flow_cancel_message(message): return False pending_cancel_order = self.service.state.get_entry("pending_cancel_order_drafts", user_id, expire=True) if pending_cancel_order: payload = pending_cancel_order.get("payload", {}) if payload.get("numero_pedido") and not payload.get("motivo"): free_text = str(message or "").strip() if len(free_text) >= 4: return True pending_review_management = self.service.state.get_entry("pending_review_management_drafts", user_id, expire=True) if pending_review_management: payload = pending_review_management.get("payload", {}) action = pending_review_management.get("action", "cancel") if action == "cancel" and payload.get("protocolo") and not payload.get("motivo"): free_text = str(message or "").strip() if len(free_text) >= 4 and not self.service._is_affirmative_message(free_text): return True return False # Interpreta a resposta do usuário na etapa de seleção. def detect_selected_order_index( self, message: str, orders: list[dict], turn_decision: dict | None = None, ) -> tuple[int | None, bool]: selection_index = self._decision_selection_index(turn_decision) if selection_index is not None and 0 <= selection_index < len(orders): return selection_index, False normalized = self.strip_choice_message(self.service.normalizer.normalize_text(message)) indifferent_tokens = { "tanto faz", "indiferente", "qualquer um", "qualquer uma", "voce escolhe", "pode escolher", "fica a seu criterio", } if normalized in indifferent_tokens: return 0, True if normalized in {"1", "primeiro", "primeira", "opcao 1", "acao 1", "pedido 1"}: return 0, False if normalized in {"2", "segundo", "segunda", "opcao 2", "acao 2", "pedido 2"}: return 1, False decision_domain = self._decision_domain(turn_decision) if len(orders) >= 2 and decision_domain in {"review", "sales"}: matches = [index for index, order in enumerate(orders) if order.get("domain") == decision_domain] if len(matches) == 1: return matches[0], False review_matches = [index for index, order in enumerate(orders) if order.get("domain") == "review"] sales_matches = [index for index, order in enumerate(orders) if order.get("domain") == "sales"] has_review_signal = self.contains_any_term(normalized, {"revisao", "agendamento", "agendar", "remarcar", "pos venda"}) has_sales_signal = self.contains_any_term(normalized, {"venda", "compra", "comprar", "pedido", "cancelamento", "cancelar", "carro", "veiculo"}) if len(review_matches) == 1 and has_review_signal and not has_sales_signal: return review_matches[0], False if len(sales_matches) == 1 and has_sales_signal and not has_review_signal: return sales_matches[0], False return None, False # É a função que efetivamente trata a resposta do usuário quando você perguntou “qual pedido quer fazer primeiro?”. async def try_resolve_pending_order_selection( self, message: str, user_id: int | None, turn_decision: dict | None = None, ) -> str | None: context = self.service._get_user_context(user_id) if not context: return None pending = context.get("pending_order_selection") if not isinstance(pending, dict): return None if pending.get("expires_at") and pending["expires_at"] < datetime.utcnow(): context["pending_order_selection"] = None self._save_context(user_id=user_id, context=context) return None orders = pending.get("orders") or [] if len(orders) < 2: context["pending_order_selection"] = None self._save_context(user_id=user_id, context=context) return None decision_action = self._decision_action(turn_decision) if decision_action == "clear_context" or self.is_order_selection_reset_message(message): self.service._clear_user_conversation_state(user_id=user_id) cleaned_message = self.remove_order_selection_reset_prefix(message) if not cleaned_message: return "Tudo bem. Limpei o contexto atual. Pode me dizer o que voce quer fazer agora?" return await self.service.handle_message(cleaned_message, user_id=user_id) selected_index, auto_selected = self.detect_selected_order_index( message=message, orders=orders, turn_decision=turn_decision, ) if selected_index is None: if self.looks_like_fresh_operational_request(message, turn_decision=turn_decision): context["pending_order_selection"] = None self._save_context(user_id=user_id, context=context) return None return self.render_order_selection_prompt(orders) selected_order = orders[selected_index] remaining_order = orders[1 - selected_index] context["pending_order_selection"] = None self.queue_order_with_memory_seed( user_id=user_id, domain=remaining_order["domain"], order_message=remaining_order["message"], memory_seed=remaining_order.get("memory_seed"), ) intro = ( f"Vou escolher e comecar por: {self.describe_order_selection_option(selected_order)}" if auto_selected else f"Perfeito. Vou comecar por: {self.describe_order_selection_option(selected_order)}" ) selected_memory = dict(selected_order.get("memory_seed") or {}) if selected_memory: context["generic_memory"] = selected_memory self._save_context(user_id=user_id, context=context) next_response = await self.service.handle_message(str(selected_order.get("message") or ""), user_id=user_id) return f"{intro}\n{next_response}" # Cria o aviso de fila. def render_queue_notice(self, queued_count: int) -> str | None: if queued_count <= 0: return None if queued_count == 1: return "Anotei mais 1 pedido e sigo nele quando voce disser 'continuar'." return f"Anotei mais {queued_count} pedidos e sigo neles conforme voce for dizendo 'continuar'." # Mostra ao usuário o que falta concluir no fluxo atual antes de mudar de assunto. def render_open_flow_prompt(self, user_id: int | None, domain: str) -> str: if domain == "review" and user_id is not None: draft = self.service.state.get_entry("pending_review_drafts", user_id, expire=True) if draft: missing = [field for field in REVIEW_REQUIRED_FIELDS if field not in draft.get("payload", {})] if missing: return self.service._render_missing_review_fields_prompt(missing) management_draft = self.service.state.get_entry("pending_review_management_drafts", user_id, expire=True) if management_draft: action = management_draft.get("action", "cancel") payload = management_draft.get("payload", {}) if action == "reschedule": missing = [field for field in ("protocolo", "nova_data_hora") if field not in payload] if missing: return self.service._render_missing_review_reschedule_fields_prompt(missing) else: missing = [field for field in ("protocolo",) if field not in payload] if missing: return self.service._render_missing_review_cancel_fields_prompt(missing) if self.service.state.get_entry("pending_review_confirmations", user_id, expire=True): return "Antes de mudar de assunto, me confirme se podemos concluir seu agendamento de revisao." if self.service.state.get_entry("pending_review_reuse_confirmations", user_id, expire=True): return self.service._render_review_reuse_question() if domain == "sales" and user_id is not None: draft = self.service.state.get_entry("pending_order_drafts", user_id, expire=True) if draft: missing = [field for field in ORDER_REQUIRED_FIELDS if field not in draft.get("payload", {})] if missing: return self.service._render_missing_order_fields_prompt(missing) cancel_draft = self.service.state.get_entry("pending_cancel_order_drafts", user_id, expire=True) if cancel_draft: missing = [field for field in CANCEL_ORDER_REQUIRED_FIELDS if field not in cancel_draft.get("payload", {})] if missing: return self.service._render_missing_cancel_order_fields_prompt(missing) return "Vamos concluir este assunto primeiro e ja sigo com o proximo em seguida." # Cria a introdução para mudar de assunto de forma natural. def build_next_order_transition(self, domain: str) -> str: if domain == "sales": return "Agora, sobre a compra do veiculo:" if domain == "review": return "Agora, sobre o agendamento da revisao:" return "Agora, sobre o proximo assunto:" # Quando um fluxo termina, ela prepara a passagem para o próximo pedido da fila. async def maybe_auto_advance_next_order(self, base_response: str, user_id: int | None) -> str: context = self.service._get_user_context(user_id) if not context: return base_response if context.get("pending_switch"): return base_response active_domain = context.get("active_domain", "general") if self.has_open_flow(user_id=user_id, domain=active_domain): return base_response next_order = self.pop_next_order(user_id=user_id) if not next_order: return base_response context["pending_switch"] = { "source_domain": context.get("active_domain", "general"), "target_domain": next_order["domain"], "queued_message": next_order["message"], "memory_seed": dict(next_order.get("memory_seed") or self.service._new_tab_memory(user_id=user_id)), "expires_at": datetime.utcnow() + timedelta(minutes=15), } self._save_context(user_id=user_id, context=context) transition = self.build_next_order_transition(next_order["domain"]) return ( f"{base_response}\n\n" f"{transition}\n" "Tenho um proximo pedido na fila. Quando quiser, diga 'continuar' para eu seguir nele." ) # Converte intenções em um domínio principal de atendimento. def domain_from_intents(self, intents: dict | None) -> str: normalized = self.service.normalizer.normalize_intents(intents) review_score = ( int(normalized.get("review_schedule", False)) + int(normalized.get("review_list", False)) + int(normalized.get("review_cancel", False)) + int(normalized.get("review_reschedule", False)) ) sales_score = int(normalized.get("order_create", False)) + int(normalized.get("order_cancel", False)) if review_score > sales_score and review_score > 0: return "review" if sales_score > review_score and sales_score > 0: return "sales" return "general" # Detecta comandos de continuação. def is_context_switch_confirmation(self, message: str, turn_decision: dict | None = None) -> bool: if self._decision_action(turn_decision) in {"continue_queue", "cancel_active_flow", "clear_context", "discard_queue"}: return True if self._decision_domain(turn_decision) in {"review", "sales"}: return True return self.service._is_affirmative_message(message) or self.service._is_negative_message(message) # Executa o próximo pedido da fila quando o usuário disser “continuar”. def is_continue_queue_message(self, message: str, turn_decision: dict | None = None) -> bool: if self._decision_action(turn_decision) == "continue_queue" or self._decision_intent(turn_decision) == "queue_continue": return True normalized = self.service.normalizer.normalize_text(message).strip() normalized = re.sub(r"[.!?,;:]+$", "", normalized) return normalized in {"continuar", "pode continuar", "seguir", "pode seguir", "proximo", "segue"} # Executa o próximo pedido da fila quando o usuário disser “continuar”. async def try_continue_queued_order( self, message: str, user_id: int | None, turn_decision: dict | None = None, ) -> str | None: context = self.service._get_user_context(user_id) if not context: return None pending_switch = context.get("pending_switch") if not isinstance(pending_switch, dict): return None if pending_switch.get("expires_at") and pending_switch["expires_at"] < datetime.utcnow(): context["pending_switch"] = None self._save_context(user_id=user_id, context=context) return None queued_message = str(pending_switch.get("queued_message") or "").strip() if not queued_message: return None decision_action = self._decision_action(turn_decision) if self.service._is_negative_message(message) and decision_action != "continue_queue": context["pending_switch"] = None self._save_context(user_id=user_id, context=context) return "Tudo bem. Mantive o proximo pedido fora da fila por enquanto." if not ( self.is_continue_queue_message(message, turn_decision=turn_decision) or self.service._is_affirmative_message(message) ): return None target_domain = str(pending_switch.get("target_domain") or "general") memory_seed = dict(pending_switch.get("memory_seed") or {}) self.apply_domain_switch(user_id=user_id, target_domain=target_domain) refreshed = self.service._get_user_context(user_id) if refreshed is not None: refreshed["generic_memory"] = memory_seed self._save_context(user_id=user_id, context=refreshed) transition = self.build_next_order_transition(target_domain) next_response = await self.service.handle_message(queued_message, user_id=user_id) return f"{transition}\n{next_response}" # Diz se ainda existe algo pendente antes de encerrar aquele assunto. def has_open_flow(self, user_id: int | None, domain: str) -> bool: if user_id is None: return False if domain == "review": return bool( self.service.state.get_entry("pending_review_drafts", user_id, expire=True) or self.service.state.get_entry("pending_review_confirmations", user_id, expire=True) or self.service.state.get_entry("pending_review_management_drafts", user_id, expire=True) or self.service.state.get_entry("pending_review_reuse_confirmations", user_id, expire=True) ) if domain == "sales": return bool( self.service.state.get_entry("pending_order_drafts", user_id, expire=True) or self.service.state.get_entry("pending_cancel_order_drafts", user_id, expire=True) ) return False # Encerra o contexto anterior e troca oficialmente para o novo assunto. def apply_domain_switch(self, user_id: int | None, target_domain: str) -> None: context = self.service._get_user_context(user_id) if not context: return previous_domain = context.get("active_domain", "general") if previous_domain == "review": self.service._reset_pending_review_states(user_id=user_id) if previous_domain == "sales": self.service._reset_pending_order_states(user_id=user_id) context["active_domain"] = target_domain context["generic_memory"] = self.service._new_tab_memory(user_id=user_id) context["pending_order_selection"] = None context["pending_switch"] = None self._save_context(user_id=user_id, context=context) # Controla a confirmação de “você quer mesmo sair deste assunto e ir para outro?”. def handle_context_switch( self, message: str, user_id: int | None, target_domain_hint: str = "general", turn_decision: dict | None = None, ) -> str | None: context = self.service._get_user_context(user_id) if not context: return None pending_switch = context.get("pending_switch") if pending_switch: if pending_switch["expires_at"] < datetime.utcnow(): context["pending_switch"] = None self._save_context(user_id=user_id, context=context) elif self.is_context_switch_confirmation(message, turn_decision=turn_decision): if self.service._is_affirmative_message(message) or self._decision_domain(turn_decision) == pending_switch["target_domain"]: target_domain = pending_switch["target_domain"] self.apply_domain_switch(user_id=user_id, target_domain=target_domain) return self.render_context_switched_message(target_domain=target_domain) context["pending_switch"] = None self._save_context(user_id=user_id, context=context) return "Perfeito, vamos continuar no fluxo atual." pending_order_selection = context.get("pending_order_selection") if pending_order_selection and pending_order_selection.get("expires_at") < datetime.utcnow(): context["pending_order_selection"] = None self._save_context(user_id=user_id, context=context) current_domain = context.get("active_domain", "general") if target_domain_hint == "general" or target_domain_hint == current_domain: return None if not self.has_open_flow(user_id=user_id, domain=current_domain): return None context["pending_switch"] = { "source_domain": current_domain, "target_domain": target_domain_hint, "expires_at": datetime.utcnow() + timedelta(minutes=15), } self._save_context(user_id=user_id, context=context) return self.render_context_switch_confirmation(source_domain=current_domain, target_domain=target_domain_hint) # Marca qual domínio está ativo atualmente. def update_active_domain(self, user_id: int | None, domain_hint: str = "general") -> None: context = self.service._get_user_context(user_id) if context and domain_hint != "general": context["active_domain"] = domain_hint self._save_context(user_id=user_id, context=context) # Serve para exibir o nome do domínio em mensagens para o usuário. def domain_label(self, domain: str) -> str: labels = { "review": "agendamento de revisao", "sales": "compra de veiculo", "general": "atendimento geral", } return labels.get(domain, "atendimento") # É o prompt de confirmação da troca. def render_context_switch_confirmation(self, source_domain: str, target_domain: str) -> str: return ( f"Entendi que voce quer sair de {self.domain_label(source_domain)} " f"e ir para {self.domain_label(target_domain)}. Tem certeza?" ) #Mensagem exibida após a troca acontecer. def render_context_switched_message(self, target_domain: str) -> str: return f"Certo, contexto anterior encerrado. Vamos seguir com {self.domain_label(target_domain)}." # Serve para depuração, observabilidade ou até para alimentar outro componente com um resumo do estado atual. def build_context_summary(self, user_id: int | None) -> str: context = self.service._get_user_context(user_id) if not context: return "Contexto de conversa: sem contexto ativo." summary = [f"Contexto de conversa ativo: {self.domain_label(context.get('active_domain', 'general'))}."] memory = context.get("generic_memory", {}) if memory: summary.append(f"Memoria generica temporaria: {memory}.") selected_vehicle = context.get("selected_vehicle") if isinstance(selected_vehicle, dict) and selected_vehicle.get("modelo"): summary.append(f"Veiculo selecionado para compra: {selected_vehicle.get('modelo')}.") stock_results = context.get("last_stock_results") or [] if isinstance(stock_results, list) and stock_results: summary.append(f"Ultima consulta de estoque com {len(stock_results)} opcao(oes) disponivel(is).") order_queue = context.get("order_queue", []) if order_queue: summary.append(f"Fila de pedidos pendentes: {len(order_queue)}.") return " ".join(summary)