import logging from app.services.ai.llm_service import LLMService from app.services.orchestration.entity_normalizer import EntityNormalizer logger = logging.getLogger(__name__) class MessagePlanner: def __init__(self, llm: LLMService, normalizer: EntityNormalizer): self.llm = llm self.normalizer = normalizer async def extract_message_plan(self, message: str, user_id: int | None) -> dict: prompt = ( "Analise a mensagem e retorne APENAS JSON valido com roteamento e entidades por pedido.\n" "Sem markdown e sem texto extra.\n\n" "Formato:\n" "{\n" ' "orders": [\n' " {\n" ' "domain": "review|sales|general",\n' ' "message": "trecho literal do pedido",\n' ' "entities": {\n' ' "generic_memory": {"placa": null, "cpf": null, "orcamento_max": null, "perfil_veiculo": []},\n' ' "review_fields": {"placa": null, "data_hora": null, "modelo": null, "ano": null, "km": null, "revisao_previa_concessionaria": null},\n' ' "review_management_fields": {"protocolo": null, "nova_data_hora": null, "motivo": null},\n' ' "order_fields": {"cpf": null, "vehicle_id": null, "modelo_veiculo": null},\n' ' "cancel_order_fields": {"numero_pedido": null, "motivo": null},\n' ' "intents": {"review_schedule": false, "review_list": false, "review_cancel": false, "review_reschedule": false, "order_create": false, "order_cancel": false}\n' " }\n" " }\n" " ]\n" "}\n\n" "Regras:\n" "- Se houver mais de um pedido operacional, separe em itens distintos em ordem de aparicao.\n" "- Se nao houver pedido operacional, use domain='general' com a mensagem inteira.\n" "- Mantenha cada message curta e fiel ao texto do usuario.\n\n" f"Contexto: user_id={user_id if user_id is not None else 'anonimo'}\n" f"Mensagem do usuario: {message}" ) default = self.normalizer.empty_message_plan(message=message) try: result = await self.llm.generate_response(message=prompt, tools=[]) text = (result.get("response") or "").strip() payload = self.normalizer.parse_json_object(text) if not isinstance(payload, dict): logger.warning("Plano de mensagem invalido (nao JSON objeto). user_id=%s", user_id) return default return self.normalizer.coerce_message_plan(payload=payload, message=message) except Exception: logger.exception("Falha ao extrair plano da mensagem com LLM. user_id=%s", user_id) return default async def extract_routing(self, message: str, user_id: int | None) -> dict: plan = await self.extract_message_plan(message=message, user_id=user_id) return { "orders": [ { "domain": item.get("domain", "general"), "message": item.get("message", ""), } for item in plan.get("orders", []) ] } async def extract_entities(self, message: str, user_id: int | None) -> dict: user_context = f"user_id={user_id}" if user_id is not None else "user_id=anonimo" prompt = ( "Extraia entidades da mensagem do usuario e retorne APENAS JSON valido.\n" "Nao use markdown, nao adicione texto antes/depois, nao invente dados ausentes.\n" "Se nao houver valor, use null ou lista vazia.\n\n" "Formato obrigatorio:\n" "{\n" ' "generic_memory": {\n' ' "placa": null,\n' ' "cpf": null,\n' ' "orcamento_max": null,\n' ' "perfil_veiculo": []\n' " },\n" ' "review_fields": {\n' ' "placa": null,\n' ' "data_hora": null,\n' ' "modelo": null,\n' ' "ano": null,\n' ' "km": null,\n' ' "revisao_previa_concessionaria": null\n' " },\n" ' "review_management_fields": {\n' ' "protocolo": null,\n' ' "nova_data_hora": null,\n' ' "motivo": null\n' " },\n" ' "order_fields": {\n' ' "cpf": null,\n' ' "vehicle_id": null,\n' ' "modelo_veiculo": null\n' " },\n" ' "cancel_order_fields": {\n' ' "numero_pedido": null,\n' ' "motivo": null\n' " },\n" ' "intents": {\n' ' "review_schedule": false,\n' ' "review_list": false,\n' ' "review_cancel": false,\n' ' "review_reschedule": false,\n' ' "order_create": false,\n' ' "order_cancel": false\n' " }\n" "}\n\n" f"Contexto: {user_context}\n" f"Mensagem do usuario: {message}" ) default = self.normalizer.empty_extraction_payload() try: result = await self.llm.generate_response(message=prompt, tools=[]) text = (result.get("response") or "").strip() if not text: logger.warning("Extracao vazia do LLM. user_id=%s", user_id) return default payload = self.normalizer.parse_json_object(text) if not isinstance(payload, dict): logger.warning("Extracao invalida (nao JSON objeto). user_id=%s", user_id) return default return self.normalize_extraction_payload(payload) except Exception: logger.exception("Falha ao extrair entidades com LLM. user_id=%s", user_id) return default def resolve_entities_for_message_plan(self, message_plan: dict, routed_message: str) -> dict: default = self.normalizer.empty_extraction_payload() if not isinstance(message_plan, dict): return default target = (routed_message or "").strip() raw_orders = message_plan.get("orders") if not isinstance(raw_orders, list): return default for item in raw_orders: if not isinstance(item, dict): continue segment = str(item.get("message") or "").strip() if segment != target: continue return self.normalize_extraction_payload(item.get("entities")) return default def normalize_extraction_payload(self, payload) -> dict: coerced = self.normalizer.coerce_extraction_contract(payload) return { "generic_memory": self.normalizer.normalize_generic_fields(coerced.get("generic_memory")), "review_fields": self.normalizer.normalize_review_fields(coerced.get("review_fields")), "review_management_fields": self.normalizer.normalize_review_management_fields(coerced.get("review_management_fields")), "order_fields": self.normalizer.normalize_order_fields(coerced.get("order_fields")), "cancel_order_fields": self.normalizer.normalize_cancel_order_fields(coerced.get("cancel_order_fields")), "intents": self.normalizer.normalize_intents(coerced.get("intents")), }