import re from datetime import datetime, timedelta from app.core.time_utils import utc_now from fastapi import HTTPException from app.services.orchestration.orchestrator_config import ( PENDING_REVIEW_DRAFT_TTL_MINUTES, REVIEW_REQUIRED_FIELDS, ) from app.services.flows.review_flow_support import ReviewFlowStateSupport # Esse mixin concentra os fluxos incrementais de revisao e pos-venda. class ReviewFlowMixin: @property def _review_flow_state_support(self) -> ReviewFlowStateSupport: support = getattr(self, "__review_flow_state_support", None) if support is None: support = ReviewFlowStateSupport(self) setattr(self, "__review_flow_state_support", support) return support def _review_now(self) -> datetime: return self._review_flow_state_support.review_now() def _get_review_flow_snapshot(self, user_id: int | None, snapshot_key: str) -> dict | None: return self._review_flow_state_support.get_flow_snapshot( user_id=user_id, snapshot_key=snapshot_key, ) def _set_review_flow_snapshot( self, user_id: int | None, snapshot_key: str, value: dict | None, *, active_task: str | None = None, ) -> None: self._review_flow_state_support.set_flow_snapshot( user_id=user_id, snapshot_key=snapshot_key, value=value, active_task=active_task, ) def _get_review_flow_entry(self, bucket: str, user_id: int | None, snapshot_key: str) -> dict | None: return self._review_flow_state_support.get_flow_entry( bucket=bucket, user_id=user_id, snapshot_key=snapshot_key, ) def _set_review_flow_entry( self, bucket: str, user_id: int | None, snapshot_key: str, value: dict, *, active_task: str | None = None, ) -> None: self._review_flow_state_support.set_flow_entry( bucket=bucket, user_id=user_id, snapshot_key=snapshot_key, value=value, active_task=active_task, ) def _pop_review_flow_entry( self, bucket: str, user_id: int | None, snapshot_key: str, *, active_task: str | None = None, ) -> dict | None: return self._review_flow_state_support.pop_flow_entry( bucket=bucket, user_id=user_id, snapshot_key=snapshot_key, active_task=active_task, ) def _decision_intent(self, turn_decision: dict | None) -> str: return str((turn_decision or {}).get("intent") or "").strip().lower() def _log_review_flow_source( self, source: str, payload: dict | None = None, missing_fields: list[str] | None = None, ) -> None: self._review_flow_state_support.log_review_flow_source( source=source, payload=payload, missing_fields=missing_fields, ) def _active_domain(self, user_id: int | None) -> str: return self._review_flow_state_support.active_domain(user_id=user_id) def _clean_review_model_candidate(self, raw_model: str | None) -> str | None: text = str(raw_model or "").strip(" ,.;:-") if not text: return None text = re.sub(r"\s+", " ", text) text = re.sub(r"\be\b$", "", text).strip(" ,.;:-") if not text: return None stop_terms = { "amanha", "hoje", "revisao", "agendar", "marcar", "cancelar", "pedido", "sim", "nao", "ok", "pode", } lowered = text.lower() if lowered in stop_terms: return None if any(term in lowered for term in {"agendar revisao", "marcar revisao", "cancelar revisao"}): return None if not re.search(r"[a-z]", lowered): return None if len(text.split()) > 4: return None return text.title() def _extract_review_model_from_message(self, normalized_message: str) -> str | None: explicit_match = re.search( r"(?:modelo do meu carro (?:e|eh)?|meu carro (?:e|eh)?|carro (?:e|eh)?|veiculo (?:e|eh)?)\s+([a-z0-9][a-z0-9\s-]{1,30})", normalized_message, flags=re.IGNORECASE, ) if explicit_match: raw_model = explicit_match.group(1) raw_model = re.split(r"\b(?:ele e|ele eh|ano|placa|km|quilometragem|data|amanha|hoje)\b", raw_model, maxsplit=1)[0] return self._clean_review_model_candidate(raw_model) has_year = bool(re.search(r"(? None: if not isinstance(payload, dict): return normalized_message = self._normalize_text(message).strip() if "placa" not in payload: for token in str(message or "").split(): normalized_plate = self.normalizer.normalize_plate(token) if normalized_plate: payload["placa"] = normalized_plate break if "data_hora" not in payload: normalized_datetime = self.normalizer.normalize_review_datetime_text( message, now_provider=self._review_now, ) if normalized_datetime and normalized_datetime != str(message or "").strip(): payload["data_hora"] = normalized_datetime if "km" not in payload: km_match = re.search(r"(? str | None: text = self.normalizer.normalize_datetime_connector(message) patterns = ( r"(?\d{1,2})[/-](?P\d{1,2})[/-](?P\d{4})(?!\s+\d{1,2}:\d{2})(?!\d)", r"(?\d{4})[/-](?P\d{1,2})[/-](?P\d{1,2})(?!\s+\d{1,2}:\d{2})(?!\d)", ) for pattern in patterns: match = re.search(pattern, str(text or "")) if not match: continue parts = match.groupdict() return f"{int(parts['day']):02d}/{int(parts['month']):02d}/{int(parts['year']):04d}" normalized_text = self._normalize_text(message).strip() if self.normalizer.extract_hhmm_from_text(message): return None if "hoje" in normalized_text: return self._review_now().strftime("%d/%m/%Y") if "amanha" in normalized_text: return (self._review_now() + timedelta(days=1)).strftime("%d/%m/%Y") return None def _merge_review_base_date_with_time(self, message: str, payload: dict) -> None: if not isinstance(payload, dict): return if payload.get("data_hora") or not payload.get("data_hora_base"): return time_text = self.normalizer.extract_hhmm_from_text(message) if not time_text: return payload["data_hora"] = f"{payload['data_hora_base']} {time_text}" payload.pop("data_hora_base", None) def _store_review_base_date_from_message(self, message: str, payload: dict) -> None: if not isinstance(payload, dict): return if payload.get("data_hora") or payload.get("data_hora_base"): return date_only = self._extract_review_date_only_text(message) if not date_only: return payload["data_hora_base"] = date_only def _extract_review_management_datetime_from_message(self, message: str) -> str | None: return self.normalizer.normalize_review_datetime_text( message, now_provider=self._review_now, ) def _merge_review_management_base_date_with_time(self, message: str, payload: dict) -> None: if not isinstance(payload, dict): return if payload.get("nova_data_hora") or not payload.get("nova_data_hora_base"): return time_text = self.normalizer.extract_hhmm_from_text(message) if not time_text: return payload["nova_data_hora"] = f"{payload['nova_data_hora_base']} {time_text}" payload.pop("nova_data_hora_base", None) def _store_review_management_base_date_from_message(self, message: str, payload: dict) -> None: if not isinstance(payload, dict): return if payload.get("nova_data_hora") or payload.get("nova_data_hora_base"): return date_only = self._extract_review_date_only_text(message) if not date_only: return payload["nova_data_hora_base"] = date_only def _is_review_temporal_follow_up(self, message: str, payload: dict | None) -> bool: if not isinstance(payload, dict): return False if payload.get("data_hora"): return False has_time = bool(self.normalizer.extract_hhmm_from_text(message)) if has_time and payload.get("data_hora_base"): return True has_date_only = bool(self._extract_review_date_only_text(message)) if has_date_only and not payload.get("data_hora"): return True return False def _infer_review_management_action( self, message: str, extracted_fields: dict | None = None, ) -> str | None: normalized_message = self._normalize_text(message).strip() management_fields = self._normalize_review_management_fields(extracted_fields) has_protocol = bool(management_fields.get("protocolo") or self._extract_review_protocol_from_text(message)) if any(term in normalized_message for term in {"agendamento", "agendamentos"}) and any( term in normalized_message for term in {"listar", "liste", "mostrar", "mostre", "ver", "consultar"} ): return "list" if not has_protocol: return None if any(term in normalized_message for term in {"remarcar", "reagendar", "alterar data", "mudar data", "trocar data"}): return "reschedule" if any(term in normalized_message for term in {"cancelar", "cancelamento", "desmarcar"}): return "cancel" return None def _extract_review_cancel_reason_from_message(self, message: str) -> str | None: raw_message = str(message or "").strip() if len(raw_message) < 4: return None patterns = ( r"\bporque\b", r"\bpois\b", r"\bpor conta de\b", r"\bmotivo(?: do cancelamento)?\b\s*[:\-]?", r"\bja que\b", ) for pattern in patterns: match = re.search(pattern, raw_message, flags=re.IGNORECASE) if not match: continue reason = raw_message[match.end():].strip(" ,.;:-") if len(reason) >= 4: return reason return None def _should_bootstrap_review_from_active_context(self, message: str, payload: dict | None = None) -> bool: normalized_message = self._normalize_text(message).strip() normalized_payload = payload if isinstance(payload, dict) else {} if normalized_payload: return True explicit_review_terms = { "agendar revisao", "marcar revisao", "nova revisao", "revisao agora", "revisao", } return any(term in normalized_message for term in explicit_review_terms) def _is_explicit_review_reuse_request(self, message: str) -> bool: normalized_message = self._normalize_text(message).strip() reuse_terms = { "reutilizar", "reaproveitar", "usar de novo", "usar novamente", "mesmo carro", "ultimo carro", "ultimo veiculo", "ultimo veículo", } if not any(term in normalized_message for term in reuse_terms): return False return any(term in normalized_message for term in {"carro", "veiculo", "veículo", "informacoes", "dados"}) async def _try_handle_review_management( self, message: str, user_id: int | None, extracted_fields: dict | None = None, intents: dict | None = None, turn_decision: dict | None = None, ) -> str | None: if user_id is None: return None normalized_intents = self._normalize_intents(intents) draft = self._get_review_flow_entry("pending_review_management_drafts", user_id, "review_management") schedule_draft = self._get_review_flow_entry("pending_review_drafts", user_id, "review_schedule") pending_reuse = self._get_review_flow_entry( "pending_review_reuse_confirmations", user_id, "review_reuse_confirmation", ) decision_intent = self._decision_intent(turn_decision) inferred_action = self._infer_review_management_action(message=message, extracted_fields=extracted_fields) normalized_fields = self._normalize_review_management_fields(extracted_fields) protocol_in_message = normalized_fields.get("protocolo") or self._extract_review_protocol_from_text(message) open_schedule_context = bool(schedule_draft or pending_reuse) has_list_intent = ( decision_intent == "review_list" or normalized_intents.get("review_list", False) or inferred_action == "list" ) has_cancel_intent = ( decision_intent == "review_cancel" or normalized_intents.get("review_cancel", False) or inferred_action == "cancel" ) has_reschedule_intent = ( decision_intent == "review_reschedule" or normalized_intents.get("review_reschedule", False) or inferred_action == "reschedule" ) if open_schedule_context and not protocol_in_message and inferred_action is None: return None if (decision_intent == "review_schedule" or normalized_intents.get("review_schedule", False)) and inferred_action is None: if draft is not None: self._pop_review_flow_entry( "pending_review_management_drafts", user_id, "review_management", active_task="review_management", ) draft = None return None if has_list_intent: self._reset_pending_review_states(user_id=user_id) try: tool_result = await self.tool_executor.execute( "listar_agendamentos_revisao", {"limite": 100}, user_id=user_id, ) except HTTPException as exc: return self._http_exception_detail(exc) return self._fallback_format_tool_result("listar_agendamentos_revisao", tool_result) if not has_cancel_intent and not has_reschedule_intent and draft is None: return None if draft is None: action = "reschedule" if has_reschedule_intent else "cancel" draft = { "action": action, "payload": {}, "expires_at": utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES), } else: if has_reschedule_intent: draft["action"] = "reschedule" elif has_cancel_intent: draft["action"] = "cancel" extracted = self._normalize_review_management_fields(extracted_fields) if "protocolo" not in extracted: inferred_protocol = self._extract_review_protocol_from_text(message) if inferred_protocol: extracted["protocolo"] = inferred_protocol action = draft.get("action", "cancel") current_protocol = extracted.get("protocolo") or draft["payload"].get("protocolo") if action == "reschedule" and "nova_data_hora" not in extracted: normalized_new_datetime = self._extract_review_management_datetime_from_message(message) if normalized_new_datetime: extracted["nova_data_hora"] = normalized_new_datetime if action == "cancel" and "motivo" not in extracted and current_protocol: inferred_reason = self._extract_review_cancel_reason_from_message(message) if inferred_reason: extracted["motivo"] = inferred_reason elif not has_cancel_intent: free_text = str(message or "").strip() if free_text and len(free_text) >= 4 and not self._is_affirmative_message(free_text): extracted["motivo"] = free_text draft["payload"].update(extracted) if action == "reschedule": self._merge_review_management_base_date_with_time(message=message, payload=draft["payload"]) if "nova_data_hora" not in draft["payload"]: self._store_review_management_base_date_from_message(message=message, payload=draft["payload"]) draft["expires_at"] = utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES) self._set_review_flow_entry( "pending_review_management_drafts", user_id, "review_management", draft, active_task="review_management", ) if action == "reschedule": missing = [field for field in ("protocolo", "nova_data_hora") if field not in draft["payload"]] if missing: if missing == ["nova_data_hora"] and draft["payload"].get("nova_data_hora_base"): return ( f"Perfeito. Tenho a data {draft['payload']['nova_data_hora_base']}. " "Agora me informe o horario desejado para a revisao." ) return self._render_missing_review_reschedule_fields_prompt(missing) try: tool_result = await self.tool_executor.execute( "editar_data_revisao", { "protocolo": draft["payload"]["protocolo"], "nova_data_hora": draft["payload"]["nova_data_hora"], }, user_id=user_id, ) except HTTPException as exc: error = self.tool_executor.coerce_http_error(exc) if error.get("retryable") and error.get("field"): draft["payload"].pop(str(error["field"]), None) draft["expires_at"] = utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES) self._set_review_flow_entry( "pending_review_management_drafts", user_id, "review_management", draft, active_task="review_management", ) return self._http_exception_detail(exc) self._pop_review_flow_entry( "pending_review_management_drafts", user_id, "review_management", active_task="review_management", ) return self._fallback_format_tool_result("editar_data_revisao", tool_result) missing = [field for field in ("protocolo",) if field not in draft["payload"]] if missing: return self._render_missing_review_cancel_fields_prompt(missing) try: tool_result = await self.tool_executor.execute( "cancelar_agendamento_revisao", { "protocolo": draft["payload"]["protocolo"], "motivo": draft["payload"].get("motivo"), }, user_id=user_id, ) except HTTPException as exc: error = self.tool_executor.coerce_http_error(exc) if error.get("retryable") and error.get("field"): draft["payload"].pop(str(error["field"]), None) draft["expires_at"] = utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES) self._set_review_flow_entry( "pending_review_management_drafts", user_id, "review_management", draft, active_task="review_management", ) return self._http_exception_detail(exc) self._pop_review_flow_entry( "pending_review_management_drafts", user_id, "review_management", active_task="review_management", ) return self._fallback_format_tool_result("cancelar_agendamento_revisao", tool_result) def _render_missing_review_fields_prompt(self, missing_fields: list[str], payload: dict | None = None) -> str: labels = { "placa": "a placa do veiculo", "data_hora": "a data e hora desejada para a revisao", "modelo": "o modelo do veiculo", "ano": "o ano do veiculo", "km": "a quilometragem atual (km)", "revisao_previa_concessionaria": "se ja fez revisao na concessionaria (sim/nao)", } if isinstance(payload, dict) and payload.get("data_hora_base") and "data_hora" in missing_fields: itens = ["- o horario desejado para a revisao"] itens.extend(f"- {labels[field]}" for field in missing_fields if field != "data_hora") return ( f"Perfeito. Tenho a data {payload['data_hora_base']}. " "Para agendar sua revisao, ainda preciso dos dados abaixo:\n" + "\n".join(itens) ) itens = [f"- {labels[field]}" for field in missing_fields] return "Para agendar sua revisao, preciso dos dados abaixo:\n" + "\n".join(itens) def _render_missing_review_cancel_fields_prompt(self, missing_fields: list[str]) -> str: labels = { "protocolo": "o protocolo da revisao (ex.: REV-20260310-ABC12345)", } itens = [f"- {labels[field]}" for field in missing_fields] return "Para cancelar o agendamento de revisao, preciso dos dados abaixo:\n" + "\n".join(itens) def _render_missing_review_reschedule_fields_prompt(self, missing_fields: list[str]) -> str: labels = { "protocolo": "o protocolo da revisao (ex.: REV-20260310-ABC12345)", "nova_data_hora": "a nova data e hora desejada para a revisao", } itens = [f"- {labels[field]}" for field in missing_fields] return "Para remarcar sua revisao, preciso dos dados abaixo:\n" + "\n".join(itens) def _render_review_reuse_question(self, payload: dict | None = None) -> str: package = payload if isinstance(payload, dict) else {} plate = str(package.get("placa") or "").strip() model = str(package.get("modelo") or "").strip() vehicle_label = "" if plate and model: vehicle_label = f" do ultimo veiculo ({model}, placa {plate})" elif plate: vehicle_label = f" do ultimo veiculo (placa {plate})" elif model: vehicle_label = f" do ultimo veiculo ({model})" return ( f"Posso reutilizar os dados{vehicle_label} e voce me passa so a nova data/hora da revisao? " "(sim/nao)" ) def _store_last_review_package(self, user_id: int | None, payload: dict | None) -> None: self._review_flow_state_support.store_last_review_package( user_id=user_id, payload=payload, ) def _get_last_review_package(self, user_id: int | None) -> dict | None: return self._review_flow_state_support.get_last_review_package(user_id=user_id) async def _try_collect_and_schedule_review( self, message: str, user_id: int | None, extracted_fields: dict | None = None, intents: dict | None = None, turn_decision: dict | None = None, ) -> str | None: if user_id is None: return None normalized_intents = self._normalize_intents(intents) decision_intent = self._decision_intent(turn_decision) has_intent = decision_intent == "review_schedule" or normalized_intents.get("review_schedule", False) has_management_intent = ( decision_intent in {"review_list", "review_cancel", "review_reschedule"} or normalized_intents.get("review_list", False) or normalized_intents.get("review_cancel", False) or normalized_intents.get("review_reschedule", False) ) if self._infer_review_management_action(message=message, extracted_fields=extracted_fields): return None if has_management_intent: self._pop_review_flow_entry( "pending_review_drafts", user_id, "review_schedule", active_task="review_schedule", ) self._pop_review_flow_entry( "pending_review_reuse_confirmations", user_id, "review_reuse_confirmation", ) return None draft = self._get_review_flow_entry("pending_review_drafts", user_id, "review_schedule") extracted = self._normalize_review_fields(extracted_fields) pending_reuse = self._get_review_flow_entry( "pending_review_reuse_confirmations", user_id, "review_reuse_confirmation", ) pending_confirmation = self._get_review_flow_entry( "pending_review_confirmations", user_id, "review_confirmation", ) active_review_context = self._active_domain(user_id) == "review" review_flow_source = "draft" if draft else None if has_intent and draft is None and pending_confirmation and not self._is_affirmative_message(message): self._pop_review_flow_entry( "pending_review_confirmations", user_id, "review_confirmation", ) pending_confirmation = None if pending_reuse: should_reuse = False date_only = self._extract_review_date_only_text(message) has_explicit_time = bool(self.normalizer.extract_hhmm_from_text(message)) if date_only and not has_explicit_time: extracted.pop("data_hora", None) if self._is_negative_message(message): self._pop_review_flow_entry( "pending_review_reuse_confirmations", user_id, "review_reuse_confirmation", ) pending_reuse = None if not extracted: draft = { "payload": {}, "expires_at": utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES), } self._set_review_flow_entry( "pending_review_drafts", user_id, "review_schedule", draft, active_task="review_schedule", ) self._log_review_flow_source( source="last_review_package", payload=draft["payload"], missing_fields=list(REVIEW_REQUIRED_FIELDS), ) return self._render_missing_review_fields_prompt(list(REVIEW_REQUIRED_FIELDS)) elif self._is_affirmative_message(message) or "data_hora" in extracted: should_reuse = True elif date_only: should_reuse = True else: self._log_review_flow_source(source="last_review_package", payload=pending_reuse.get("payload")) return self._render_review_reuse_question(pending_reuse.get("payload")) if should_reuse: seed_payload = dict(pending_reuse.get("payload") or {}) if draft is None: draft = { "payload": seed_payload, "expires_at": utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES), } else: for key, value in seed_payload.items(): draft["payload"].setdefault(key, value) self._pop_review_flow_entry( "pending_review_reuse_confirmations", user_id, "review_reuse_confirmation", ) review_flow_source = "last_review_package" if date_only and not extracted.get("data_hora"): draft["payload"]["data_hora_base"] = date_only self._set_review_flow_entry( "pending_review_drafts", user_id, "review_schedule", draft, active_task="review_schedule", ) self._log_review_flow_source( source=review_flow_source, payload=draft["payload"], missing_fields=["data_hora"], ) return f"Perfeito. Tenho a data {date_only}. Agora me informe o horario desejado para a revisao." if "data_hora" not in extracted: self._set_review_flow_entry( "pending_review_drafts", user_id, "review_schedule", draft, active_task="review_schedule", ) self._log_review_flow_source(source=review_flow_source, payload=draft["payload"], missing_fields=["data_hora"]) return "Perfeito. Me informe apenas a data e hora desejada para a revisao." last_package = self._get_last_review_package(user_id=user_id) explicit_reuse_request = self._is_explicit_review_reuse_request(message) active_context_reuse_request = ( active_review_context and draft is None and self._should_bootstrap_review_from_active_context(message=message, payload=extracted) ) should_offer_reuse = bool(last_package) and not pending_reuse and ( (has_intent and draft is None) or explicit_reuse_request or active_context_reuse_request ) if should_offer_reuse: self._set_review_flow_entry( "pending_review_reuse_confirmations", user_id, "review_reuse_confirmation", { "payload": last_package, "expires_at": utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES), }, ) self._log_review_flow_source(source="last_review_package", payload=last_package) return self._render_review_reuse_question(last_package) if ( draft and not has_intent and ( decision_intent in {"order_create", "order_cancel"} or normalized_intents.get("order_create", False) or normalized_intents.get("order_cancel", False) ) and not extracted ): if not self._is_review_temporal_follow_up(message=message, payload=draft.get("payload")): self._pop_review_flow_entry( "pending_review_drafts", user_id, "review_schedule", active_task="review_schedule", ) return None bootstrap_payload = dict(extracted) self._supplement_review_fields_from_message(message=message, payload=bootstrap_payload) self._try_prefill_review_fields_from_memory(user_id=user_id, payload=bootstrap_payload) should_bootstrap_from_context = ( active_review_context and self._should_bootstrap_review_from_active_context(message=message, payload=bootstrap_payload) ) if not has_intent and draft is None and not should_bootstrap_from_context: return None if draft is None: # Cria um draft com TTL para permitir coleta do agendamento # em varias mensagens sem perder o progresso. review_flow_source = "active_domain_fallback" if should_bootstrap_from_context and not has_intent else "intent_bootstrap" draft = { "payload": dict(bootstrap_payload), "expires_at": utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES), } draft["payload"].update(extracted) self._merge_review_base_date_with_time(message=message, payload=draft["payload"]) self._supplement_review_fields_from_message(message=message, payload=draft["payload"]) self._merge_review_base_date_with_time(message=message, payload=draft["payload"]) self._store_review_base_date_from_message(message=message, payload=draft["payload"]) self._merge_review_base_date_with_time(message=message, payload=draft["payload"]) self._try_prefill_review_fields_from_memory(user_id=user_id, payload=draft["payload"]) if ( "revisao_previa_concessionaria" not in draft["payload"] and draft["payload"] and not extracted ): if self._is_affirmative_message(message): draft["payload"]["revisao_previa_concessionaria"] = True elif self._is_negative_message(message): draft["payload"]["revisao_previa_concessionaria"] = False draft["expires_at"] = utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES) self._set_review_flow_entry( "pending_review_drafts", user_id, "review_schedule", draft, active_task="review_schedule", ) missing = [field for field in REVIEW_REQUIRED_FIELDS if field not in draft["payload"]] if missing: self._log_review_flow_source(source=review_flow_source or "draft", payload=draft["payload"], missing_fields=missing) if missing == ["data_hora"] and draft["payload"].get("data_hora_base"): return ( f"Perfeito. Tenho a data {draft['payload']['data_hora_base']}. " "Agora me informe o horario desejado para a revisao." ) return self._render_missing_review_fields_prompt(missing, payload=draft["payload"]) try: tool_result = await self.tool_executor.execute( "agendar_revisao", draft["payload"], user_id=user_id, ) except HTTPException as exc: error = self.tool_executor.coerce_http_error(exc) self._capture_review_confirmation_suggestion( tool_name="agendar_revisao", arguments=draft["payload"], exc=exc, user_id=user_id, ) if error.get("retryable") and error.get("field"): draft["payload"].pop(str(error["field"]), None) draft["expires_at"] = utc_now() + timedelta(minutes=PENDING_REVIEW_DRAFT_TTL_MINUTES) self._set_review_flow_entry( "pending_review_drafts", user_id, "review_schedule", draft, active_task="review_schedule", ) self._log_review_flow_source(source=review_flow_source or "draft", payload=draft["payload"]) return self._http_exception_detail(exc) self._pop_review_flow_entry( "pending_review_drafts", user_id, "review_schedule", active_task="review_schedule", ) self._store_last_review_package(user_id=user_id, payload=draft["payload"]) self._log_review_flow_source(source=review_flow_source or "draft", payload=draft["payload"]) if hasattr(self, "_capture_successful_tool_side_effects"): self._capture_successful_tool_side_effects( tool_name="agendar_revisao", arguments=draft["payload"], tool_result=tool_result, user_id=user_id, ) return self._fallback_format_tool_result("agendar_revisao", tool_result)