mejoras en el modelo de clarificacion de productos
This commit is contained in:
@@ -1,153 +0,0 @@
|
|||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H1","location":"evolution.js:9","message":"parsed_webhook","data":{"ok":true,"reason":null,"has_text":true,"source":"sim"},"timestamp":1768629587289}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H2","location":"pipeline.js:128","message":"processMessage_enter","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","provider":"evolution","chat_id":"5432233230322@s.whatsapp.net","text_len":29},"timestamp":1768629587320}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H3","location":"pipeline.js:150","message":"conversation_state_loaded","data":{"prev_state":"IDLE","isStale":false,"state_updated_at":"2026-01-17T05:59:47.320Z","has_context":true},"timestamp":1768629587326}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":0,"heights":16,"applied":0,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629587333}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":0,"heights":0,"applied":0,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629587336}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":0,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":967,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629587336}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H6","location":"turnEngineV3.js:231","message":"nlu_input_built","data":{"text_len":29,"state":"IDLE","memory_len":31,"pending_clarification":false,"pending_item":false,"last_shown_options":0},"timestamp":1768629587339}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H10","location":"openai.js:196","message":"nlu_normalized_first","data":{"intent":"other","unit":"kg","selection":"null","needs_catalog":true},"timestamp":1768629590578}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H5","location":"turnEngineV3.js:235","message":"nlu_result","data":{"intent":"other","needsCatalog":true,"has_pending_clarification":false,"has_pending_item":false,"nlu_valid":true,"raw_len":303},"timestamp":1768629590580}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:152","message":"snapshot_counts","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","total_snapshot":780,"total_sellable":769,"query":"asado","limit":12},"timestamp":1768629590590}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:168","message":"snapshot_search_result","data":{"query":"asado","found":12,"sample_names":["ASADO VENTANA","Tapa de Asado Wagyu","Tapa De Asado (copia)"]},"timestamp":1768629590595}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H9","location":"catalogRetrieval.js:158","message":"catalog_sources","data":{"query":"asado","aliases_count":4,"snapshot_count":12,"snapshot_source":"snapshot"},"timestamp":1768629590596}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H4","location":"pipeline.js:198","message":"turn_v3_result","data":{"intent":"browse","next_state":"BROWSING","missing_fields":1,"actions_count":1},"timestamp":1768629590624}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":2,"heights":0,"applied":0,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629590646}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":2,"heights":2,"applied":2,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629590653}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":2,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":967,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629590653}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H1","location":"evolution.js:9","message":"parsed_webhook","data":{"ok":true,"reason":null,"has_text":true,"source":"sim"},"timestamp":1768629595476}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H2","location":"pipeline.js:128","message":"processMessage_enter","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","provider":"evolution","chat_id":"5432233230322@s.whatsapp.net","text_len":1},"timestamp":1768629595481}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H3","location":"pipeline.js:150","message":"conversation_state_loaded","data":{"prev_state":"BROWSING","isStale":false,"state_updated_at":"2026-01-17T05:59:50.622Z","has_context":true},"timestamp":1768629595485}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":2,"heights":2,"applied":2,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629595487}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":2,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":967,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629595487}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":2,"heights":2,"applied":2,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629595493}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H6","location":"turnEngineV3.js:231","message":"nlu_input_built","data":{"text_len":1,"state":"BROWSING","memory_len":162,"pending_clarification":true,"pending_item":false,"last_shown_options":10},"timestamp":1768629595493}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H11","location":"openai.js:129","message":"selection_inferred","data":{"inferred":true,"pending_item":false,"has_shown_options":true,"text":"6"},"timestamp":1768629598519}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H10","location":"openai.js:196","message":"nlu_normalized_first","data":{"intent":"other","unit":null,"selection":"set","needs_catalog":false},"timestamp":1768629598519}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H5","location":"turnEngineV3.js:235","message":"nlu_result","data":{"intent":"other","needsCatalog":false,"has_pending_clarification":true,"has_pending_item":false,"nlu_valid":true,"raw_len":359},"timestamp":1768629598519}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H12","location":"turnEngineV3.js:239","message":"pending_clarification_resolved","data":{"kind":"chosen","selection_type":"index","selection_value":"6","text_len":1},"timestamp":1768629598520}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H14","location":"turnEngineV3.js:171","message":"pending_item_display_unit","data":{"name":"Asado Premium","categories":["Carnes > Vacuna"],"display_unit":"kg"},"timestamp":1768629598521}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H4","location":"pipeline.js:198","message":"turn_v3_result","data":{"intent":"add_to_cart","next_state":"AWAITING_QUANTITY","missing_fields":1,"actions_count":0},"timestamp":1768629598521}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":4,"heights":2,"applied":2,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629598551}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":4,"heights":4,"applied":4,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629598553}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":4,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":967,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629598554}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H1","location":"evolution.js:9","message":"parsed_webhook","data":{"ok":true,"reason":null,"has_text":true,"source":"sim"},"timestamp":1768629603948}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H2","location":"pipeline.js:128","message":"processMessage_enter","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","provider":"evolution","chat_id":"5432233230322@s.whatsapp.net","text_len":6},"timestamp":1768629603951}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":4,"heights":4,"applied":4,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629603959}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":4,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":967,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629603960}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":4,"heights":4,"applied":4,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629603962}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H3","location":"pipeline.js:150","message":"conversation_state_loaded","data":{"prev_state":"AWAITING_QUANTITY","isStale":false,"state_updated_at":"2026-01-17T05:59:58.532Z","has_context":true},"timestamp":1768629603963}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H6","location":"turnEngineV3.js:231","message":"nlu_input_built","data":{"text_len":6,"state":"AWAITING_QUANTITY","memory_len":200,"pending_clarification":false,"pending_item":true,"last_shown_options":0},"timestamp":1768629603968}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H10","location":"openai.js:196","message":"nlu_normalized_first","data":{"intent":"other","unit":"kg","selection":"null","needs_catalog":true},"timestamp":1768629607572}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H12","location":"turnEngineV3.js:332","message":"pending_item_quantity","data":{"quantity_in":1,"unit_in":"kg","qty_resolved":1000,"text":"1 dije"},"timestamp":1768629607573}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H5","location":"turnEngineV3.js:235","message":"nlu_result","data":{"intent":"other","needsCatalog":true,"has_pending_clarification":false,"has_pending_item":true,"nlu_valid":true,"raw_len":317},"timestamp":1768629607573}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H4","location":"pipeline.js:198","message":"turn_v3_result","data":{"intent":"add_to_cart","next_state":"CART_ACTIVE","missing_fields":0,"actions_count":1},"timestamp":1768629607573}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":6,"heights":4,"applied":4,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629607595}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":6,"heights":6,"applied":6,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629607604}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":6,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":967,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629607604}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H1","location":"evolution.js:9","message":"parsed_webhook","data":{"ok":true,"reason":null,"has_text":true,"source":"sim"},"timestamp":1768629612932}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H2","location":"pipeline.js:128","message":"processMessage_enter","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","provider":"evolution","chat_id":"5432233230322@s.whatsapp.net","text_len":18},"timestamp":1768629612934}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":6,"heights":6,"applied":6,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629612940}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":6,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":967,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629612940}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":6,"heights":6,"applied":6,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629612946}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H3","location":"pipeline.js:150","message":"conversation_state_loaded","data":{"prev_state":"CART_ACTIVE","isStale":false,"state_updated_at":"2026-01-17T06:00:07.593Z","has_context":true},"timestamp":1768629612947}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H6","location":"turnEngineV3.js:231","message":"nlu_input_built","data":{"text_len":18,"state":"CART_ACTIVE","memory_len":117,"pending_clarification":false,"pending_item":false,"last_shown_options":0},"timestamp":1768629612951}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H10","location":"openai.js:196","message":"nlu_normalized_first","data":{"intent":"recommend","unit":null,"selection":"null","needs_catalog":true},"timestamp":1768629616398}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H5","location":"turnEngineV3.js:235","message":"nlu_result","data":{"intent":"recommend","needsCatalog":true,"has_pending_clarification":false,"has_pending_item":false,"nlu_valid":true,"raw_len":305},"timestamp":1768629616398}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H4","location":"pipeline.js:198","message":"turn_v3_result","data":{"intent":"recommend","next_state":"CART_ACTIVE","missing_fields":1,"actions_count":0},"timestamp":1768629616403}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":8,"heights":6,"applied":6,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629616430}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":8,"heights":8,"applied":8,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629616438}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":8,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":967,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629616438}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H1","location":"evolution.js:9","message":"parsed_webhook","data":{"ok":true,"reason":null,"has_text":true,"source":"sim"},"timestamp":1768629618573}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H2","location":"pipeline.js:128","message":"processMessage_enter","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","provider":"evolution","chat_id":"5432233230322@s.whatsapp.net","text_len":2},"timestamp":1768629618575}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H3","location":"pipeline.js:150","message":"conversation_state_loaded","data":{"prev_state":"CART_ACTIVE","isStale":false,"state_updated_at":"2026-01-17T06:00:16.415Z","has_context":true},"timestamp":1768629618578}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":8,"heights":8,"applied":8,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629618585}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H6","location":"turnEngineV3.js:231","message":"nlu_input_built","data":{"text_len":2,"state":"CART_ACTIVE","memory_len":111,"pending_clarification":false,"pending_item":false,"last_shown_options":0},"timestamp":1768629618584}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":8,"heights":8,"applied":8,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629618588}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":8,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":967,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629618588}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H10","location":"openai.js:196","message":"nlu_normalized_first","data":{"intent":"recommend","unit":null,"selection":"null","needs_catalog":true},"timestamp":1768629622528}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H5","location":"turnEngineV3.js:235","message":"nlu_result","data":{"intent":"recommend","needsCatalog":true,"has_pending_clarification":false,"has_pending_item":false,"nlu_valid":true,"raw_len":305},"timestamp":1768629622528}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:152","message":"snapshot_counts","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","total_snapshot":780,"total_sellable":769,"query":"provoleta","limit":9},"timestamp":1768629622538}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:168","message":"snapshot_search_result","data":{"query":"provoleta","found":7,"sample_names":["Quesos Provoletas de Vaca Santa Rosa","Quesos Provoletas de Vaca Formagge","Queso Provoleta de Cabra"]},"timestamp":1768629622543}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H9","location":"catalogRetrieval.js:158","message":"catalog_sources","data":{"query":"provoleta","aliases_count":0,"snapshot_count":7,"snapshot_source":"snapshot"},"timestamp":1768629622543}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:152","message":"snapshot_counts","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","total_snapshot":780,"total_sellable":769,"query":"chimichurri","limit":9},"timestamp":1768629622563}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:168","message":"snapshot_search_result","data":{"query":"chimichurri","found":6,"sample_names":["Finca la victoria (chimichurri, porotos, morrones, salsa criolla, berenjenas) - berenjena","Finca la victoria (chimichurri, porotos, morrones, salsa criolla, berenjenas) - morrones","Finca la victoria (chimichurri, porotos, morrones, salsa criolla, berenjenas) - porotos"]},"timestamp":1768629622566}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H9","location":"catalogRetrieval.js:158","message":"catalog_sources","data":{"query":"chimichurri","aliases_count":0,"snapshot_count":6,"snapshot_source":"snapshot"},"timestamp":1768629622567}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:152","message":"snapshot_counts","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","total_snapshot":780,"total_sellable":769,"query":"ensalada","limit":9},"timestamp":1768629622582}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:168","message":"snapshot_search_result","data":{"query":"ensalada","found":0,"sample_names":[]},"timestamp":1768629622586}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H9","location":"catalogRetrieval.js:158","message":"catalog_sources","data":{"query":"ensalada","aliases_count":0,"snapshot_count":0,"snapshot_source":"snapshot"},"timestamp":1768629622586}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:152","message":"snapshot_counts","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","total_snapshot":780,"total_sellable":769,"query":"pan","limit":9},"timestamp":1768629622591}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:168","message":"snapshot_search_result","data":{"query":"pan","found":9,"sample_names":["Panceta Bajo sodio Curada FETEADA","Panceta Bajo sodio Curada (copia)","PAN CASERO"]},"timestamp":1768629622595}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H9","location":"catalogRetrieval.js:158","message":"catalog_sources","data":{"query":"pan","aliases_count":1,"snapshot_count":9,"snapshot_source":"snapshot"},"timestamp":1768629622595}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:152","message":"snapshot_counts","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","total_snapshot":780,"total_sellable":769,"query":"vino tinto","limit":9},"timestamp":1768629622616}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:168","message":"snapshot_search_result","data":{"query":"vino tinto","found":0,"sample_names":[]},"timestamp":1768629622619}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H9","location":"catalogRetrieval.js:158","message":"catalog_sources","data":{"query":"vino tinto","aliases_count":0,"snapshot_count":0,"snapshot_source":"snapshot"},"timestamp":1768629622619}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:152","message":"snapshot_counts","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","total_snapshot":780,"total_sellable":769,"query":"malbec","limit":9},"timestamp":1768629622624}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H8","location":"wooSnapshot.js:168","message":"snapshot_search_result","data":{"query":"malbec","found":9,"sample_names":["VINO Bizzotto Reserva Malbec","VINO Peñon de Agrelo Malbec","VINO CASTORE MALBEC"]},"timestamp":1768629622628}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H9","location":"catalogRetrieval.js:158","message":"catalog_sources","data":{"query":"malbec","aliases_count":0,"snapshot_count":9,"snapshot_source":"snapshot"},"timestamp":1768629622629}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H4","location":"pipeline.js:198","message":"turn_v3_result","data":{"intent":"recommend","next_state":"CART_ACTIVE","missing_fields":1,"actions_count":1},"timestamp":1768629637044}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":10,"heights":8,"applied":8,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629637120}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":10,"heights":10,"applied":10,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629637121}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":10,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":998,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629637122}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H1","location":"evolution.js:9","message":"parsed_webhook","data":{"ok":true,"reason":null,"has_text":true,"source":"sim"},"timestamp":1768629663933}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H2","location":"pipeline.js:128","message":"processMessage_enter","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","provider":"evolution","chat_id":"5432233230322@s.whatsapp.net","text_len":36},"timestamp":1768629663936}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":10,"heights":10,"applied":10,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629663943}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":10,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":998,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629663944}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H3","location":"pipeline.js:150","message":"conversation_state_loaded","data":{"prev_state":"CART_ACTIVE","isStale":false,"state_updated_at":"2026-01-17T06:00:37.072Z","has_context":true},"timestamp":1768629663948}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H6","location":"turnEngineV3.js:231","message":"nlu_input_built","data":{"text_len":36,"state":"CART_ACTIVE","memory_len":213,"pending_clarification":true,"pending_item":false,"last_shown_options":10},"timestamp":1768629663954}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":10,"heights":10,"applied":10,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629663961}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H11","location":"openai.js:129","message":"selection_inferred","data":{"inferred":false,"pending_item":false,"has_shown_options":true,"text":"ok, agregame chimich"},"timestamp":1768629668306}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H10","location":"openai.js:196","message":"nlu_normalized_first","data":{"intent":"add_to_cart","unit":null,"selection":"null","needs_catalog":true},"timestamp":1768629668306}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H5","location":"turnEngineV3.js:235","message":"nlu_result","data":{"intent":"add_to_cart","needsCatalog":true,"has_pending_clarification":true,"has_pending_item":false,"nlu_valid":true,"raw_len":445},"timestamp":1768629668306}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H12","location":"turnEngineV3.js:239","message":"pending_clarification_resolved","data":{"kind":"ask","selection_type":null,"selection_value":null,"text_len":36},"timestamp":1768629668307}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H4","location":"pipeline.js:198","message":"turn_v3_result","data":{"intent":"browse","next_state":"CART_ACTIVE","missing_fields":1,"actions_count":1},"timestamp":1768629668307}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":12,"heights":10,"applied":10,"scroll_height":1408,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629668385}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":12,"heights":12,"applied":12,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629668390}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":12,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":1439,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629668390}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H1","location":"evolution.js:9","message":"parsed_webhook","data":{"ok":true,"reason":null,"has_text":true,"source":"sim"},"timestamp":1768629676646}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H2","location":"pipeline.js:128","message":"processMessage_enter","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","provider":"evolution","chat_id":"5432233230322@s.whatsapp.net","text_len":5},"timestamp":1768629676649}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H3","location":"pipeline.js:150","message":"conversation_state_loaded","data":{"prev_state":"CART_ACTIVE","isStale":false,"state_updated_at":"2026-01-17T06:01:08.342Z","has_context":true},"timestamp":1768629676652}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":12,"heights":12,"applied":12,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629676658}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":12,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":1439,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629676659}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H6","location":"turnEngineV3.js:231","message":"nlu_input_built","data":{"text_len":5,"state":"CART_ACTIVE","memory_len":305,"pending_clarification":true,"pending_item":false,"last_shown_options":10},"timestamp":1768629676661}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":12,"heights":12,"applied":12,"scroll_height":1390,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629676664}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H11","location":"openai.js:129","message":"selection_inferred","data":{"inferred":true,"pending_item":false,"has_shown_options":true,"text":"1 y 7"},"timestamp":1768629681604}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H5","location":"turnEngineV3.js:235","message":"nlu_result","data":{"intent":"add_to_cart","needsCatalog":true,"has_pending_clarification":true,"has_pending_item":false,"nlu_valid":true,"raw_len":370},"timestamp":1768629681605}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H10","location":"openai.js:196","message":"nlu_normalized_first","data":{"intent":"add_to_cart","unit":null,"selection":"set","needs_catalog":true},"timestamp":1768629681604}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H12","location":"turnEngineV3.js:239","message":"pending_clarification_resolved","data":{"kind":"chosen","selection_type":"index","selection_value":"1","text_len":5},"timestamp":1768629681605}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H14","location":"turnEngineV3.js:171","message":"pending_item_display_unit","data":{"name":"Chimichurri","categories":["Proveeduría > Sal pimienta y especias"],"display_unit":"unit"},"timestamp":1768629681605}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H4","location":"pipeline.js:198","message":"turn_v3_result","data":{"intent":"add_to_cart","next_state":"ERROR_RECOVERY","missing_fields":1,"actions_count":0},"timestamp":1768629681606}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":14,"heights":12,"applied":12,"scroll_height":1639,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629681638}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":14,"heights":14,"applied":14,"scroll_height":1618,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629681641}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":14,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":1439,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629681642}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H1","location":"evolution.js:9","message":"parsed_webhook","data":{"ok":true,"reason":null,"has_text":true,"source":"sim"},"timestamp":1768629697326}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H2","location":"pipeline.js:128","message":"processMessage_enter","data":{"tenantId":"eb71b9a7-9ccf-430e-9b25-951a0c589c0f","provider":"evolution","chat_id":"5432233230322@s.whatsapp.net","text_len":39},"timestamp":1768629697329}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H3","location":"pipeline.js:150","message":"conversation_state_loaded","data":{"prev_state":"ERROR_RECOVERY","isStale":false,"state_updated_at":"2026-01-17T06:01:21.616Z","has_context":true},"timestamp":1768629697340}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":14,"heights":14,"applied":14,"scroll_height":1618,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629697341}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":14,"heights":14,"applied":14,"scroll_height":1618,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629697342}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":14,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":1439,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629697342}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H6","location":"turnEngineV3.js:231","message":"nlu_input_built","data":{"text_len":39,"state":"ERROR_RECOVERY","memory_len":247,"pending_clarification":false,"pending_item":true,"last_shown_options":0},"timestamp":1768629697347}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H10","location":"openai.js:196","message":"nlu_normalized_first","data":{"intent":"other","unit":null,"selection":"null","needs_catalog":true},"timestamp":1768629702666}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H5","location":"turnEngineV3.js:235","message":"nlu_result","data":{"intent":"other","needsCatalog":true,"has_pending_clarification":false,"has_pending_item":true,"nlu_valid":true,"raw_len":513},"timestamp":1768629702666}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H12","location":"turnEngineV3.js:332","message":"pending_item_quantity","data":{"quantity_in":0,"unit_in":null,"qty_resolved":null,"text":"chimichurri una unid"},"timestamp":1768629702666}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H4","location":"pipeline.js:198","message":"turn_v3_result","data":{"intent":"add_to_cart","next_state":"AWAITING_QUANTITY","missing_fields":1,"actions_count":0},"timestamp":1768629702667}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":16,"heights":14,"applied":14,"scroll_height":1870,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629702691}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":16,"heights":16,"applied":16,"scroll_height":1850,"client_height":1390,"host_height":1516,"box_height":1513.5999755859375},"timestamp":1768629702693}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":16,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":1562,"client_height":967,"host_height":1093,"box_height":1090.5999755859375},"timestamp":1768629702693}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":0,"heights":14,"applied":0,"scroll_height":1926,"client_height":1926,"host_height":2074.300048828125,"box_height":2050.300048828125},"timestamp":1768630824934}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":14,"chat_id":"5491133230322@s.whatsapp.net","scroll_height":1926,"client_height":1926,"host_height":2073.5,"box_height":2049.5},"timestamp":1768630824934}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":14,"heights":14,"applied":14,"scroll_height":2362,"client_height":2362,"host_height":2510,"box_height":2486},"timestamp":1768630824939}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":12,"heights":14,"applied":0,"scroll_height":1522,"client_height":1522,"host_height":1669.75,"box_height":1645.75},"timestamp":1768630848988}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":12,"heights":12,"applied":12,"scroll_height":1925,"client_height":1925,"host_height":2073,"box_height":2049},"timestamp":1768630849001}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":12,"chat_id":"5421133230322@s.whatsapp.net","scroll_height":1924,"client_height":1924,"host_height":2072.199951171875,"box_height":2048.199951171875},"timestamp":1768630849002}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":12,"heights":16,"applied":0,"scroll_height":1841,"client_height":1841,"host_height":1988.7000732421875,"box_height":1964.7000732421875},"timestamp":1768630855088}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":16,"chat_id":"5492233230322@s.whatsapp.net","scroll_height":1840,"client_height":1840,"host_height":1987.9000244140625,"box_height":1963.9000244140625},"timestamp":1768630855089}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":16,"heights":16,"applied":16,"scroll_height":2389,"client_height":2389,"host_height":2537,"box_height":2513},"timestamp":1768630855093}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":16,"heights":16,"applied":0,"scroll_height":2096,"client_height":2096,"host_height":2243.800048828125,"box_height":2219.800048828125},"timestamp":1768630857542}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H16","location":"conversation-inspector.js:248","message":"apply_heights","data":{"rows":16,"heights":16,"applied":16,"scroll_height":2621,"client_height":2621,"host_height":2769,"box_height":2745},"timestamp":1768630857552}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":16,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":2620,"client_height":2620,"host_height":2768.199951171875,"box_height":2744.199951171875},"timestamp":1768630857552}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":14,"chat_id":"5491133230322@s.whatsapp.net","scroll_height":1366,"client_height":1136,"host_height":1284,"box_height":1260},"timestamp":1768631476665}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":12,"chat_id":"5421133230322@s.whatsapp.net","scroll_height":1555,"client_height":1156,"host_height":1304,"box_height":1280},"timestamp":1768631805322}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":12,"chat_id":"5421133230322@s.whatsapp.net","scroll_height":1555,"client_height":1156,"host_height":1304,"box_height":1280},"timestamp":1768632738993}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":16,"chat_id":"5432233230322@s.whatsapp.net","scroll_height":1868,"client_height":1196,"host_height":1344,"box_height":1320},"timestamp":1768633339517}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":16,"chat_id":"5492233230322@s.whatsapp.net","scroll_height":1674,"client_height":1046,"host_height":1194,"box_height":1170},"timestamp":1768633558760}
|
|
||||||
{"sessionId":"debug-session","runId":"pre-fix","hypothesisId":"H15","location":"run-timeline.js:180","message":"bubbles_layout","data":{"count":16,"chat_id":"5492233230322@s.whatsapp.net","scroll_height":1674,"client_height":1046,"host_height":1194,"box_height":1170},"timestamp":1768633644474}
|
|
||||||
15
db/migrations/20260118100000_reco_rules_product_ids.sql
Normal file
15
db/migrations/20260118100000_reco_rules_product_ids.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
-- migrate:up
|
||||||
|
-- Agregar columnas para asociación directa producto-producto
|
||||||
|
alter table product_reco_rules
|
||||||
|
add column if not exists trigger_product_ids integer[] not null default '{}',
|
||||||
|
add column if not exists recommended_product_ids integer[] not null default '{}';
|
||||||
|
|
||||||
|
-- Índice GIN para búsqueda rápida por trigger_product_ids
|
||||||
|
create index if not exists product_reco_rules_trigger_ids_idx
|
||||||
|
on product_reco_rules using gin (trigger_product_ids);
|
||||||
|
|
||||||
|
-- migrate:down
|
||||||
|
drop index if exists product_reco_rules_trigger_ids_idx;
|
||||||
|
alter table product_reco_rules
|
||||||
|
drop column if exists trigger_product_ids,
|
||||||
|
drop column if exists recommended_product_ids;
|
||||||
@@ -102,7 +102,7 @@ class AliasesCrud extends HTMLElement {
|
|||||||
|
|
||||||
async loadProducts() {
|
async loadProducts() {
|
||||||
try {
|
try {
|
||||||
const data = await api.products({ limit: 500 });
|
const data = await api.products({ limit: 2000 });
|
||||||
this.products = data.items || [];
|
this.products = data.items || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error loading products:", e);
|
console.error("Error loading products:", e);
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class ChatSimulator extends HTMLElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimistic: que aparezca en la columna izquierda al instante
|
// 1. Actualizar lista de conversaciones
|
||||||
emit("conversation:upsert", {
|
emit("conversation:upsert", {
|
||||||
chat_id: from,
|
chat_id: from,
|
||||||
from: pushName || "test_lucas",
|
from: pushName || "test_lucas",
|
||||||
@@ -143,9 +143,11 @@ class ChatSimulator extends HTMLElement {
|
|||||||
last_activity: new Date().toISOString(),
|
last_activity: new Date().toISOString(),
|
||||||
last_run_id: null,
|
last_run_id: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2. Seleccionar el chat (si es el mismo, no recarga - optimizado en run-timeline)
|
||||||
emit("ui:selectedChat", { chat_id: from });
|
emit("ui:selectedChat", { chat_id: from });
|
||||||
|
|
||||||
// Optimistic: mostrar burbuja del usuario inmediatamente
|
// 3. Mostrar burbuja optimista INMEDIATAMENTE
|
||||||
emit("message:optimistic", {
|
emit("message:optimistic", {
|
||||||
chat_id: from,
|
chat_id: from,
|
||||||
message_id: `optimistic-${Date.now()}`,
|
message_id: `optimistic-${Date.now()}`,
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ class ConversationInspector extends HTMLElement {
|
|||||||
this.shadowRoot.getElementById("step").onclick = () => this.step();
|
this.shadowRoot.getElementById("step").onclick = () => this.step();
|
||||||
|
|
||||||
this._unsubSel = on("ui:selectedChat", async ({ chat_id }) => {
|
this._unsubSel = on("ui:selectedChat", async ({ chat_id }) => {
|
||||||
|
// Si es el mismo chat, no recargar (para no borrar items optimistas)
|
||||||
|
if (this.chatId === chat_id) return;
|
||||||
this.chatId = chat_id;
|
this.chatId = chat_id;
|
||||||
await this.loadData();
|
await this.loadData();
|
||||||
});
|
});
|
||||||
@@ -87,6 +89,17 @@ class ConversationInspector extends HTMLElement {
|
|||||||
const messageId = message?.message_id || null;
|
const messageId = message?.message_id || null;
|
||||||
if (messageId) this.highlight(messageId);
|
if (messageId) this.highlight(messageId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for optimistic messages to add placeholder item
|
||||||
|
this._unsubOptimistic = on("message:optimistic", (msg) => {
|
||||||
|
if (!this.chatId) {
|
||||||
|
this.chatId = msg.chat_id;
|
||||||
|
this.shadowRoot.getElementById("chat").textContent = msg.chat_id;
|
||||||
|
this.shadowRoot.getElementById("meta").textContent = "Nueva conversación";
|
||||||
|
}
|
||||||
|
if (msg.chat_id !== this.chatId) return;
|
||||||
|
this.addOptimisticItem(msg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
@@ -95,6 +108,7 @@ class ConversationInspector extends HTMLElement {
|
|||||||
this._unsubLayout?.();
|
this._unsubLayout?.();
|
||||||
this._unsubScroll?.();
|
this._unsubScroll?.();
|
||||||
this._unsubSelectMessage?.();
|
this._unsubSelectMessage?.();
|
||||||
|
this._unsubOptimistic?.();
|
||||||
this.pause();
|
this.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +194,14 @@ class ConversationInspector extends HTMLElement {
|
|||||||
metaEl.textContent = `Inspector de ${this.messages.length} mensajes.`;
|
metaEl.textContent = `Inspector de ${this.messages.length} mensajes.`;
|
||||||
countEl.textContent = this.messages.length ? `${this.messages.length} filas` : "";
|
countEl.textContent = this.messages.length ? `${this.messages.length} filas` : "";
|
||||||
|
|
||||||
|
// Preserve optimistic items before clearing
|
||||||
|
const optimisticItems = [...list.querySelectorAll('.item[data-message-id^="optimistic-"]')];
|
||||||
|
|
||||||
|
// Obtener timestamps de mensajes IN del servidor para comparar
|
||||||
|
const serverInTimestamps = this.messages
|
||||||
|
.filter(m => m.direction === "in")
|
||||||
|
.map(m => new Date(m.ts).getTime());
|
||||||
|
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
this.rowMap.clear();
|
this.rowMap.clear();
|
||||||
this.rowOrder = [];
|
this.rowOrder = [];
|
||||||
@@ -196,7 +218,7 @@ class ConversationInspector extends HTMLElement {
|
|||||||
const intent = run?.llm_output?.intent || "—";
|
const intent = run?.llm_output?.intent || "—";
|
||||||
const nextState = run?.llm_output?.next_state || "—";
|
const nextState = run?.llm_output?.next_state || "—";
|
||||||
const prevState = row.nextRun?.prev_state || "—";
|
const prevState = row.nextRun?.prev_state || "—";
|
||||||
const basket = run?.llm_output?.basket_resolved?.items || [];
|
const basket = run?.llm_output?.full_basket?.items || run?.llm_output?.basket_resolved?.items || [];
|
||||||
const tools = this.toolSummary(run?.tools || []);
|
const tools = this.toolSummary(run?.tools || []);
|
||||||
|
|
||||||
const llmMeta = run?.llm_output?._llm || null;
|
const llmMeta = run?.llm_output?._llm || null;
|
||||||
@@ -237,6 +259,23 @@ class ConversationInspector extends HTMLElement {
|
|||||||
this.rowMap.set(msg.message_id, el);
|
this.rowMap.set(msg.message_id, el);
|
||||||
this.rowOrder.push(msg.message_id);
|
this.rowOrder.push(msg.message_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-add preserved optimistic items ONLY if no server message covers it
|
||||||
|
for (const optItem of optimisticItems) {
|
||||||
|
// Obtener timestamp del optimista (está en el ID: optimistic-{timestamp})
|
||||||
|
const msgId = optItem.dataset.messageId;
|
||||||
|
const optTs = parseInt(msgId.replace("optimistic-", ""), 10) || 0;
|
||||||
|
|
||||||
|
// Si hay un mensaje del servidor con timestamp cercano (10 seg), no re-agregar
|
||||||
|
const hasServerMatch = serverInTimestamps.some(ts => Math.abs(ts - optTs) < 10000);
|
||||||
|
if (hasServerMatch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.appendChild(optItem);
|
||||||
|
this.rowMap.set(msgId, optItem);
|
||||||
|
this.rowOrder.push(msgId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyHeights() {
|
applyHeights() {
|
||||||
@@ -302,6 +341,44 @@ class ConversationInspector extends HTMLElement {
|
|||||||
this.highlight(messageId);
|
this.highlight(messageId);
|
||||||
this._playIdx += 1;
|
this._playIdx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addOptimisticItem(msg) {
|
||||||
|
const list = this.shadowRoot.getElementById("list");
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
// Remove any existing optimistic item
|
||||||
|
const existing = list.querySelector(`.item[data-message-id^="optimistic-"]`);
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "item in";
|
||||||
|
el.dataset.messageId = msg.message_id;
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="kv">
|
||||||
|
<div class="k">IN</div>
|
||||||
|
<div class="v">${new Date(msg.ts).toLocaleString()}</div>
|
||||||
|
<div class="k">STATE</div>
|
||||||
|
<div class="v">—</div>
|
||||||
|
<div class="k">INTENT</div>
|
||||||
|
<div class="v">—</div>
|
||||||
|
<div class="k">NLU</div>
|
||||||
|
<div class="v">procesando...</div>
|
||||||
|
</div>
|
||||||
|
<div class="cart"><strong>Carrito:</strong> —</div>
|
||||||
|
<div class="chips"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
list.appendChild(el);
|
||||||
|
list.scrollTop = list.scrollHeight;
|
||||||
|
|
||||||
|
this.rowMap.set(msg.message_id, el);
|
||||||
|
this.rowOrder.push(msg.message_id);
|
||||||
|
|
||||||
|
// Apply min height
|
||||||
|
el.style.minHeight = "120px";
|
||||||
|
el.style.marginBottom = "12px";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("conversation-inspector", ConversationInspector);
|
customElements.define("conversation-inspector", ConversationInspector);
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ class ProductsCrud extends HTMLElement {
|
|||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: "open" });
|
this.attachShadow({ mode: "open" });
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.selected = null;
|
this.selectedItems = []; // Array de productos seleccionados
|
||||||
|
this.lastClickedIndex = -1; // Para Shift+Click
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.searchQuery = "";
|
this.searchQuery = "";
|
||||||
this.stockFilter = false;
|
this.stockFilter = false;
|
||||||
@@ -29,9 +30,10 @@ class ProductsCrud extends HTMLElement {
|
|||||||
button.secondary:hover { background:#2d3e52; }
|
button.secondary:hover { background:#2d3e52; }
|
||||||
|
|
||||||
.list { flex:1; overflow-y:auto; }
|
.list { flex:1; overflow-y:auto; }
|
||||||
.item { background:#0f1520; border:1px solid #253245; border-radius:8px; padding:12px; margin-bottom:8px; cursor:pointer; transition:all .15s; }
|
.item { background:#0f1520; border:1px solid #253245; border-radius:8px; padding:12px; margin-bottom:8px; cursor:pointer; transition:all .15s; user-select:none; }
|
||||||
.item:hover { border-color:#1f6feb; }
|
.item:hover { border-color:#1f6feb; }
|
||||||
.item.active { border-color:#1f6feb; background:#111b2a; }
|
.item.active { border-color:#1f6feb; background:#111b2a; }
|
||||||
|
.item.selected { border-color:#2ecc71; background:#0f2a1a; }
|
||||||
.item-name { font-weight:600; color:#e7eef7; margin-bottom:4px; }
|
.item-name { font-weight:600; color:#e7eef7; margin-bottom:4px; }
|
||||||
.item-meta { font-size:12px; color:#8aa0b5; }
|
.item-meta { font-size:12px; color:#8aa0b5; }
|
||||||
.item-price { color:#2ecc71; font-weight:600; }
|
.item-price { color:#2ecc71; font-weight:600; }
|
||||||
@@ -182,9 +184,15 @@ class ProductsCrud extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
for (const item of filteredItems) {
|
this._filteredItems = filteredItems; // Guardar referencia para Shift+Click
|
||||||
|
|
||||||
|
for (let i = 0; i < filteredItems.length; i++) {
|
||||||
|
const item = filteredItems[i];
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "item" + (this.selected?.woo_product_id === item.woo_product_id ? " active" : "");
|
const isSelected = this.selectedItems.some(s => s.woo_product_id === item.woo_product_id);
|
||||||
|
const isSingleSelected = isSelected && this.selectedItems.length === 1;
|
||||||
|
el.className = "item" + (isSelected ? " selected" : "") + (isSingleSelected ? " active" : "");
|
||||||
|
el.dataset.index = i;
|
||||||
|
|
||||||
const price = item.price != null ? `$${Number(item.price).toLocaleString()}` : "—";
|
const price = item.price != null ? `$${Number(item.price).toLocaleString()}` : "—";
|
||||||
const sku = item.sku || "—";
|
const sku = item.sku || "—";
|
||||||
@@ -193,8 +201,12 @@ class ProductsCrud extends HTMLElement {
|
|||||||
? `<span class="badge stock">En stock</span>`
|
? `<span class="badge stock">En stock</span>`
|
||||||
: `<span class="badge nostock">Sin stock</span>`;
|
: `<span class="badge nostock">Sin stock</span>`;
|
||||||
|
|
||||||
|
// Mostrar unidad actual si está definida
|
||||||
|
const unit = item.sell_unit || item.payload?._sell_unit_override;
|
||||||
|
const unitBadge = unit ? `<span class="badge" style="background:#1a3a5c;color:#7eb8e7;">${unit === 'unit' ? 'Unidad' : 'Kg'}</span>` : '';
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="item-name">${item.name || "Sin nombre"} ${stockBadge}</div>
|
<div class="item-name">${item.name || "Sin nombre"} ${stockBadge} ${unitBadge}</div>
|
||||||
<div class="item-meta">
|
<div class="item-meta">
|
||||||
<span class="item-price">${price}</span> ·
|
<span class="item-price">${price}</span> ·
|
||||||
SKU: ${sku} ·
|
SKU: ${sku} ·
|
||||||
@@ -202,30 +214,78 @@ class ProductsCrud extends HTMLElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
el.onclick = () => {
|
el.onclick = (e) => this.handleItemClick(e, item, i);
|
||||||
this.selected = item;
|
|
||||||
this.renderList();
|
|
||||||
this.renderDetail();
|
|
||||||
// Scroll detail panel to top
|
|
||||||
const detail = this.shadowRoot.getElementById("detail");
|
|
||||||
if (detail) detail.scrollTop = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
list.appendChild(el);
|
list.appendChild(el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleItemClick(e, item, index) {
|
||||||
|
if (e.shiftKey && this.lastClickedIndex >= 0) {
|
||||||
|
// Shift+Click: seleccionar rango
|
||||||
|
const start = Math.min(this.lastClickedIndex, index);
|
||||||
|
const end = Math.max(this.lastClickedIndex, index);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
const rangeItem = this._filteredItems[i];
|
||||||
|
if (!this.selectedItems.some(s => s.woo_product_id === rangeItem.woo_product_id)) {
|
||||||
|
this.selectedItems.push(rangeItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (e.ctrlKey || e.metaKey) {
|
||||||
|
// Ctrl+Click: toggle individual
|
||||||
|
const idx = this.selectedItems.findIndex(s => s.woo_product_id === item.woo_product_id);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.selectedItems.splice(idx, 1);
|
||||||
|
} else {
|
||||||
|
this.selectedItems.push(item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Click normal: selección única
|
||||||
|
this.selectedItems = [item];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastClickedIndex = index;
|
||||||
|
this.renderList();
|
||||||
|
this.renderDetail();
|
||||||
|
|
||||||
|
// Scroll detail panel to top
|
||||||
|
const detail = this.shadowRoot.getElementById("detail");
|
||||||
|
if (detail) detail.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
renderDetail() {
|
renderDetail() {
|
||||||
const detail = this.shadowRoot.getElementById("detail");
|
const detail = this.shadowRoot.getElementById("detail");
|
||||||
|
|
||||||
if (!this.selected) {
|
if (!this.selectedItems.length) {
|
||||||
detail.innerHTML = `<div class="detail-empty">Seleccioná un producto para ver detalles</div>`;
|
detail.innerHTML = `<div class="detail-empty">Seleccioná un producto para ver detalles</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const p = this.selected;
|
// Si hay múltiples seleccionados, mostrar vista de edición masiva
|
||||||
const categories = (p.payload?.categories || []).map(c => c.name).join(", ") || "—";
|
if (this.selectedItems.length > 1) {
|
||||||
const attributes = (p.payload?.attributes || []).map(a => `${a.name}: ${a.options?.join(", ")}`).join("; ") || "—";
|
this.renderMultiDetail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = this.selectedItems[0];
|
||||||
|
|
||||||
|
// Categorías: pueden estar en p.categories (string JSON) o p.payload.categories (array)
|
||||||
|
let categoriesArray = [];
|
||||||
|
if (p.categories) {
|
||||||
|
try {
|
||||||
|
const cats = typeof p.categories === 'string' ? JSON.parse(p.categories) : p.categories;
|
||||||
|
categoriesArray = Array.isArray(cats) ? cats.map(c => c.name || c) : [String(cats)];
|
||||||
|
} catch { categoriesArray = [String(p.categories)]; }
|
||||||
|
} else if (p.payload?.categories) {
|
||||||
|
categoriesArray = p.payload.categories.map(c => c.name || c);
|
||||||
|
}
|
||||||
|
const categoriesText = categoriesArray.join(", ");
|
||||||
|
|
||||||
|
const attributes = (p.attributes_normalized || p.payload?.attributes || []).map(a => `${a.name}: ${a.options?.join(", ")}`).join("; ") || "—";
|
||||||
|
|
||||||
|
// Determinar unidad actual (de payload o inferida)
|
||||||
|
const currentUnit = p.sell_unit || p.payload?._sell_unit_override || this.inferUnit(p);
|
||||||
|
|
||||||
detail.innerHTML = `
|
detail.innerHTML = `
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -245,13 +305,31 @@ class ProductsCrud extends HTMLElement {
|
|||||||
<div class="field-value">${p.price != null ? `$${Number(p.price).toLocaleString()} ${p.currency || ""}` : "—"}</div>
|
<div class="field-value">${p.price != null ? `$${Number(p.price).toLocaleString()} ${p.currency || ""}` : "—"}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Categorías</label>
|
<label>Unidad de venta</label>
|
||||||
<div class="field-value">${categories}</div>
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
|
<select id="sellUnit" style="background:#0f1520;color:#e7eef7;border:1px solid #253245;border-radius:8px;padding:8px 12px;font-size:13px;">
|
||||||
|
<option value="kg" ${currentUnit === "kg" ? "selected" : ""}>Por peso (kg)</option>
|
||||||
|
<option value="unit" ${currentUnit === "unit" ? "selected" : ""}>Por unidad</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px;color:#8aa0b5;margin-top:4px;">
|
||||||
|
Define si este producto se vende por peso o por unidad
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Categorías (separadas por coma)</label>
|
||||||
|
<input type="text" id="categoriesInput" value="${categoriesText}" placeholder="ej: Carnes, Vacuno, Premium" style="width:100%;" />
|
||||||
|
<div style="font-size:11px;color:#8aa0b5;margin-top:4px;">
|
||||||
|
Categorías del producto, separadas por coma
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Atributos</label>
|
<label>Atributos</label>
|
||||||
<div class="field-value">${attributes}</div>
|
<div class="field-value">${attributes}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<button id="saveProduct" style="width:100%;padding:10px;">Guardar cambios</button>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Última actualización</label>
|
<label>Última actualización</label>
|
||||||
<div class="field-value">${p.refreshed_at ? new Date(p.refreshed_at).toLocaleString() : "—"}</div>
|
<div class="field-value">${p.refreshed_at ? new Date(p.refreshed_at).toLocaleString() : "—"}</div>
|
||||||
@@ -261,6 +339,136 @@ class ProductsCrud extends HTMLElement {
|
|||||||
<div class="field-value json">${JSON.stringify(p.payload || {}, null, 2)}</div>
|
<div class="field-value json">${JSON.stringify(p.payload || {}, null, 2)}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Bind save button
|
||||||
|
this.shadowRoot.getElementById("saveProduct").onclick = () => this.saveProduct();
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveProduct() {
|
||||||
|
if (this.selectedItems.length !== 1) return;
|
||||||
|
|
||||||
|
const p = this.selectedItems[0];
|
||||||
|
const btn = this.shadowRoot.getElementById("saveProduct");
|
||||||
|
const sellUnitSelect = this.shadowRoot.getElementById("sellUnit");
|
||||||
|
const categoriesInput = this.shadowRoot.getElementById("categoriesInput");
|
||||||
|
|
||||||
|
const sell_unit = sellUnitSelect.value;
|
||||||
|
const categories = categoriesInput.value.split(",").map(s => s.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = "Guardando...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.updateProduct(p.woo_product_id, { sell_unit, categories });
|
||||||
|
|
||||||
|
// Actualizar localmente
|
||||||
|
p.sell_unit = sell_unit;
|
||||||
|
p.categories = JSON.stringify(categories.map(name => ({ name })));
|
||||||
|
const idx = this.items.findIndex(i => i.woo_product_id === p.woo_product_id);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.items[idx].sell_unit = sell_unit;
|
||||||
|
this.items[idx].categories = p.categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.textContent = "Guardado!";
|
||||||
|
this.renderList();
|
||||||
|
setTimeout(() => { btn.textContent = "Guardar cambios"; btn.disabled = false; }, 1500);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error saving product:", e);
|
||||||
|
alert("Error guardando: " + (e.message || e));
|
||||||
|
btn.textContent = "Guardar cambios";
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMultiDetail() {
|
||||||
|
const detail = this.shadowRoot.getElementById("detail");
|
||||||
|
const count = this.selectedItems.length;
|
||||||
|
const names = this.selectedItems.slice(0, 5).map(p => p.name).join(", ");
|
||||||
|
const moreText = count > 5 ? ` y ${count - 5} más...` : "";
|
||||||
|
|
||||||
|
detail.innerHTML = `
|
||||||
|
<div class="field">
|
||||||
|
<label>Productos seleccionados</label>
|
||||||
|
<div class="field-value" style="color:#2ecc71;font-weight:600;">${count} productos</div>
|
||||||
|
<div style="font-size:11px;color:#8aa0b5;margin-top:4px;">${names}${moreText}</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Unidad de venta (para todos)</label>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
|
<select id="sellUnit" style="background:#0f1520;color:#e7eef7;border:1px solid #253245;border-radius:8px;padding:8px 12px;font-size:13px;flex:1;">
|
||||||
|
<option value="kg">Por peso (kg)</option>
|
||||||
|
<option value="unit">Por unidad</option>
|
||||||
|
</select>
|
||||||
|
<button id="saveUnit" style="padding:8px 16px;">Guardar para ${count}</button>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px;color:#8aa0b5;margin-top:4px;">
|
||||||
|
Se aplicará a todos los productos seleccionados
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<button id="clearSelection" class="secondary" style="width:100%;">Limpiar selección</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.shadowRoot.getElementById("saveUnit").onclick = () => this.saveProductUnit();
|
||||||
|
this.shadowRoot.getElementById("clearSelection").onclick = () => {
|
||||||
|
this.selectedItems = [];
|
||||||
|
this.lastClickedIndex = -1;
|
||||||
|
this.renderList();
|
||||||
|
this.renderDetail();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inferUnit(p) {
|
||||||
|
const name = String(p.name || "").toLowerCase();
|
||||||
|
const cats = (p.payload?.categories || []).map(c => String(c.name || c.slug || "").toLowerCase());
|
||||||
|
const allText = name + " " + cats.join(" ");
|
||||||
|
|
||||||
|
// Productos que típicamente se venden por unidad
|
||||||
|
if (/chimichurri|provoleta|queso|pan|salsa|aderezo|condimento|especias?|vino|vinos|bebida|cerveza|gaseosa|whisky|ron|gin|vodka|fernet/i.test(allText)) {
|
||||||
|
return "unit";
|
||||||
|
}
|
||||||
|
return "kg";
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveProductUnit() {
|
||||||
|
if (!this.selectedItems.length) return;
|
||||||
|
|
||||||
|
const select = this.shadowRoot.getElementById("sellUnit");
|
||||||
|
const btn = this.shadowRoot.getElementById("saveUnit");
|
||||||
|
const unit = select.value;
|
||||||
|
const count = this.selectedItems.length;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = "Guardando...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// IDs de todos los productos seleccionados
|
||||||
|
const wooProductIds = this.selectedItems.map(p => p.woo_product_id);
|
||||||
|
|
||||||
|
// Un solo request para todos
|
||||||
|
await api.updateProductsUnit(wooProductIds, { sell_unit: unit });
|
||||||
|
|
||||||
|
// Actualizar localmente
|
||||||
|
for (const p of this.selectedItems) {
|
||||||
|
p.sell_unit = unit;
|
||||||
|
const idx = this.items.findIndex(i => i.woo_product_id === p.woo_product_id);
|
||||||
|
if (idx >= 0) this.items[idx].sell_unit = unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.textContent = `Guardado ${count}!`;
|
||||||
|
this.renderList(); // Actualizar badges en lista
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = count > 1 ? `Guardar para ${count}` : "Guardar";
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 1500);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error saving product unit:", e);
|
||||||
|
alert("Error guardando: " + (e.message || e));
|
||||||
|
btn.textContent = count > 1 ? `Guardar para ${count}` : "Guardar";
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,19 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
this.searchQuery = "";
|
this.searchQuery = "";
|
||||||
this.editMode = null; // 'create' | 'edit' | null
|
this.editMode = null; // 'create' | 'edit' | null
|
||||||
|
|
||||||
|
// Cache de productos para el selector
|
||||||
|
this.allProducts = [];
|
||||||
|
this.productsLoaded = false;
|
||||||
|
|
||||||
|
// Productos seleccionados en el formulario
|
||||||
|
this.selectedTriggerProducts = [];
|
||||||
|
this.selectedRecommendedProducts = [];
|
||||||
|
|
||||||
this.shadowRoot.innerHTML = `
|
this.shadowRoot.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
:host { display:block; height:100%; padding:16px; overflow:hidden; }
|
:host { display:block; height:100%; padding:16px; overflow:hidden; }
|
||||||
* { box-sizing:border-box; font-family:system-ui,Segoe UI,Roboto,Arial; }
|
* { box-sizing:border-box; font-family:system-ui,Segoe UI,Roboto,Arial; }
|
||||||
.container { display:grid; grid-template-columns:1fr 450px; gap:16px; height:100%; }
|
.container { display:grid; grid-template-columns:1fr 500px; gap:16px; height:100%; }
|
||||||
.panel { background:#121823; border:1px solid #1e2a3a; border-radius:10px; padding:16px; overflow:hidden; display:flex; flex-direction:column; }
|
.panel { background:#121823; border:1px solid #1e2a3a; border-radius:10px; padding:16px; overflow:hidden; display:flex; flex-direction:column; }
|
||||||
.panel-title { font-size:14px; font-weight:700; color:#8aa0b5; text-transform:uppercase; letter-spacing:.4px; margin-bottom:12px; }
|
.panel-title { font-size:14px; font-weight:700; color:#8aa0b5; text-transform:uppercase; letter-spacing:.4px; margin-bottom:12px; }
|
||||||
|
|
||||||
@@ -55,6 +63,38 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
|
|
||||||
.toggle { display:flex; align-items:center; gap:8px; cursor:pointer; }
|
.toggle { display:flex; align-items:center; gap:8px; cursor:pointer; }
|
||||||
.toggle input { width:auto; }
|
.toggle input { width:auto; }
|
||||||
|
|
||||||
|
/* Product selector styles */
|
||||||
|
.product-selector { position:relative; }
|
||||||
|
.product-search { margin-bottom:8px; }
|
||||||
|
.product-dropdown {
|
||||||
|
position:absolute; top:100%; left:0; right:0; z-index:100;
|
||||||
|
background:#0f1520; border:1px solid #253245; border-radius:8px;
|
||||||
|
max-height:200px; overflow-y:auto; display:none;
|
||||||
|
}
|
||||||
|
.product-dropdown.open { display:block; }
|
||||||
|
.product-option {
|
||||||
|
padding:8px 12px; cursor:pointer; font-size:13px; color:#e7eef7;
|
||||||
|
display:flex; justify-content:space-between; align-items:center;
|
||||||
|
}
|
||||||
|
.product-option:hover { background:#1a2535; }
|
||||||
|
.product-option.selected { background:#1a3a5c; }
|
||||||
|
.product-option .price { font-size:11px; color:#8aa0b5; }
|
||||||
|
|
||||||
|
.selected-products { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; min-height:30px; }
|
||||||
|
.product-chip {
|
||||||
|
display:inline-flex; align-items:center; gap:4px;
|
||||||
|
background:#253245; color:#e7eef7; padding:4px 8px 4px 12px;
|
||||||
|
border-radius:999px; font-size:12px;
|
||||||
|
}
|
||||||
|
.product-chip .remove {
|
||||||
|
cursor:pointer; width:16px; height:16px; border-radius:50%;
|
||||||
|
background:#e74c3c; color:#fff; font-size:10px;
|
||||||
|
display:flex; align-items:center; justify-content:center;
|
||||||
|
}
|
||||||
|
.product-chip .remove:hover { background:#c0392b; }
|
||||||
|
|
||||||
|
.empty-hint { color:#8aa0b5; font-size:12px; font-style:italic; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -72,7 +112,7 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-title" id="formTitle">Detalle</div>
|
<div class="panel-title" id="formTitle">Detalle</div>
|
||||||
<div class="form" id="form">
|
<div class="form" id="form">
|
||||||
<div class="form-empty">Seleccioná una regla o creá una nueva</div>
|
<div class="form-empty">Selecciona una regla o crea una nueva</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,6 +129,19 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
this.shadowRoot.getElementById("newBtn").onclick = () => this.showCreateForm();
|
this.shadowRoot.getElementById("newBtn").onclick = () => this.showCreateForm();
|
||||||
|
|
||||||
this.load();
|
this.load();
|
||||||
|
this.loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadProducts() {
|
||||||
|
if (this.productsLoaded) return;
|
||||||
|
try {
|
||||||
|
const data = await api.products({ limit: 2000 });
|
||||||
|
this.allProducts = (data.items || []).filter(p => p.stock_status === "instock");
|
||||||
|
this.productsLoaded = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error loading products:", e);
|
||||||
|
this.allProducts = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@@ -108,6 +161,11 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProductName(id) {
|
||||||
|
const p = this.allProducts.find(x => x.woo_product_id === id);
|
||||||
|
return p?.name || `Producto #${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
renderList() {
|
renderList() {
|
||||||
const list = this.shadowRoot.getElementById("list");
|
const list = this.shadowRoot.getElementById("list");
|
||||||
|
|
||||||
@@ -126,10 +184,15 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "item" + (this.selected?.id === item.id ? " active" : "");
|
el.className = "item" + (this.selected?.id === item.id ? " active" : "");
|
||||||
|
|
||||||
const trigger = item.trigger || {};
|
// Mostrar productos trigger
|
||||||
const keywords = (trigger.keywords || []).join(", ") || "—";
|
const triggerIds = item.trigger_product_ids || [];
|
||||||
const queries = (item.queries || []).slice(0, 3).join(", ");
|
const triggerNames = triggerIds.slice(0, 3).map(id => this.getProductName(id)).join(", ");
|
||||||
const hasMore = (item.queries || []).length > 3;
|
const triggerMore = triggerIds.length > 3 ? ` (+${triggerIds.length - 3})` : "";
|
||||||
|
|
||||||
|
// Mostrar productos recomendados
|
||||||
|
const recoIds = item.recommended_product_ids || [];
|
||||||
|
const recoNames = recoIds.slice(0, 3).map(id => this.getProductName(id)).join(", ");
|
||||||
|
const recoMore = recoIds.length > 3 ? ` (+${recoIds.length - 3})` : "";
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="item-key">
|
<div class="item-key">
|
||||||
@@ -137,13 +200,15 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
<span class="badge ${item.active ? "active" : "inactive"}">${item.active ? "Activa" : "Inactiva"}</span>
|
<span class="badge ${item.active ? "active" : "inactive"}">${item.active ? "Activa" : "Inactiva"}</span>
|
||||||
<span class="badge priority">P: ${item.priority}</span>
|
<span class="badge priority">P: ${item.priority}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-trigger">Keywords: ${keywords}</div>
|
<div class="item-trigger">Cuando piden: ${triggerNames || "—"}${triggerMore}</div>
|
||||||
<div class="item-queries">→ ${queries}${hasMore ? "..." : ""}</div>
|
<div class="item-queries">→ Recomendar: ${recoNames || "—"}${recoMore}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
el.onclick = () => {
|
el.onclick = () => {
|
||||||
this.selected = item;
|
this.selected = item;
|
||||||
this.editMode = "edit";
|
this.editMode = "edit";
|
||||||
|
this.selectedTriggerProducts = [...(item.trigger_product_ids || [])];
|
||||||
|
this.selectedRecommendedProducts = [...(item.recommended_product_ids || [])];
|
||||||
this.renderList();
|
this.renderList();
|
||||||
this.renderForm();
|
this.renderForm();
|
||||||
};
|
};
|
||||||
@@ -155,6 +220,8 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
showCreateForm() {
|
showCreateForm() {
|
||||||
this.selected = null;
|
this.selected = null;
|
||||||
this.editMode = "create";
|
this.editMode = "create";
|
||||||
|
this.selectedTriggerProducts = [];
|
||||||
|
this.selectedRecommendedProducts = [];
|
||||||
this.renderList();
|
this.renderList();
|
||||||
this.renderForm();
|
this.renderForm();
|
||||||
}
|
}
|
||||||
@@ -165,7 +232,7 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
|
|
||||||
if (!this.editMode) {
|
if (!this.editMode) {
|
||||||
title.textContent = "Detalle";
|
title.textContent = "Detalle";
|
||||||
form.innerHTML = `<div class="form-empty">Seleccioná una regla o creá una nueva</div>`;
|
form.innerHTML = `<div class="form-empty">Selecciona una regla o crea una nueva</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,31 +240,20 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
title.textContent = isCreate ? "Nueva Regla" : "Editar Regla";
|
title.textContent = isCreate ? "Nueva Regla" : "Editar Regla";
|
||||||
|
|
||||||
const rule_key = this.selected?.rule_key || "";
|
const rule_key = this.selected?.rule_key || "";
|
||||||
const trigger = this.selected?.trigger || {};
|
|
||||||
const queries = this.selected?.queries || [];
|
|
||||||
const ask_slots = this.selected?.ask_slots || [];
|
|
||||||
const active = this.selected?.active !== false;
|
const active = this.selected?.active !== false;
|
||||||
const priority = this.selected?.priority || 100;
|
const priority = this.selected?.priority || 100;
|
||||||
|
|
||||||
// Convert arrays to comma-separated strings for display
|
|
||||||
const triggerKeywords = (trigger.keywords || []).join(", ");
|
|
||||||
const queriesText = (queries || []).join(", ");
|
|
||||||
const askSlotsText = Array.isArray(ask_slots)
|
|
||||||
? ask_slots.map(s => typeof s === "string" ? s : s?.slot || s?.keyword || "").filter(Boolean).join(", ")
|
|
||||||
: "";
|
|
||||||
|
|
||||||
form.innerHTML = `
|
form.innerHTML = `
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Rule Key (identificador unico)</label>
|
<label>Nombre de la regla</label>
|
||||||
<input type="text" id="ruleKeyInput" value="${rule_key}" ${isCreate ? "" : "disabled"} placeholder="ej: asado_core, alcohol_yes" />
|
<input type="text" id="ruleKeyInput" value="${rule_key}" ${isCreate ? "" : "disabled"} placeholder="ej: asado_recos, vinos_con_carne" />
|
||||||
<div class="field-hint">Sin espacios, en minusculas con guiones bajos</div>
|
<div class="field-hint">Identificador unico, sin espacios</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-row">
|
<div class="field-row">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Prioridad</label>
|
<label>Prioridad</label>
|
||||||
<input type="number" id="priorityInput" value="${priority}" min="1" max="1000" />
|
<input type="number" id="priorityInput" value="${priority}" min="1" max="1000" />
|
||||||
<div class="field-hint">Mayor = primero</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Estado</label>
|
<label>Estado</label>
|
||||||
@@ -209,21 +265,23 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Trigger (palabras clave)</label>
|
<label>Cuando el cliente pide...</label>
|
||||||
<textarea id="triggerInput" placeholder="asado, parrilla, carne, bife...">${triggerKeywords}</textarea>
|
<div class="product-selector" id="triggerSelector">
|
||||||
<div class="field-hint">Palabras que activan esta regla, separadas por coma</div>
|
<input type="text" class="product-search" id="triggerSearch" placeholder="Buscar producto..." />
|
||||||
|
<div class="product-dropdown" id="triggerDropdown"></div>
|
||||||
|
<div class="selected-products" id="triggerSelected"></div>
|
||||||
|
</div>
|
||||||
|
<div class="field-hint">Productos que activan esta recomendacion</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Productos a recomendar</label>
|
<label>Recomendar estos productos...</label>
|
||||||
<textarea id="queriesInput" placeholder="provoleta, chimichurri, ensalada...">${queriesText}</textarea>
|
<div class="product-selector" id="recoSelector">
|
||||||
<div class="field-hint">Productos a buscar cuando se activa la regla, separados por coma</div>
|
<input type="text" class="product-search" id="recoSearch" placeholder="Buscar producto..." />
|
||||||
</div>
|
<div class="product-dropdown" id="recoDropdown"></div>
|
||||||
|
<div class="selected-products" id="recoSelected"></div>
|
||||||
<div class="field">
|
</div>
|
||||||
<label>Preguntar sobre... (opcional)</label>
|
<div class="field-hint">Productos a sugerir al cliente</div>
|
||||||
<textarea id="askSlotsInput" placeholder="achuras, cerdo, vino...">${askSlotsText}</textarea>
|
|
||||||
<div class="field-hint">El bot preguntara al usuario sobre estos temas de forma natural</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@@ -233,18 +291,114 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Setup event handlers
|
||||||
this.shadowRoot.getElementById("saveBtn").onclick = () => this.save();
|
this.shadowRoot.getElementById("saveBtn").onclick = () => this.save();
|
||||||
this.shadowRoot.getElementById("cancelBtn").onclick = () => this.cancel();
|
this.shadowRoot.getElementById("cancelBtn").onclick = () => this.cancel();
|
||||||
if (!isCreate) {
|
if (!isCreate) {
|
||||||
this.shadowRoot.getElementById("deleteBtn").onclick = () => this.delete();
|
this.shadowRoot.getElementById("deleteBtn").onclick = () => this.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup product selectors
|
||||||
|
this.setupProductSelector("trigger", this.selectedTriggerProducts);
|
||||||
|
this.setupProductSelector("reco", this.selectedRecommendedProducts);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseCommaSeparated(str) {
|
setupProductSelector(type, selectedIds) {
|
||||||
return String(str || "")
|
const searchInput = this.shadowRoot.getElementById(`${type}Search`);
|
||||||
.split(",")
|
const dropdown = this.shadowRoot.getElementById(`${type}Dropdown`);
|
||||||
.map(s => s.trim().toLowerCase())
|
const selectedContainer = this.shadowRoot.getElementById(`${type}Selected`);
|
||||||
.filter(Boolean);
|
|
||||||
|
const renderSelected = () => {
|
||||||
|
const ids = type === "trigger" ? this.selectedTriggerProducts : this.selectedRecommendedProducts;
|
||||||
|
if (!ids.length) {
|
||||||
|
selectedContainer.innerHTML = `<span class="empty-hint">Ningun producto seleccionado</span>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedContainer.innerHTML = ids.map(id => {
|
||||||
|
const name = this.getProductName(id);
|
||||||
|
return `<span class="product-chip" data-id="${id}">${name}<span class="remove">×</span></span>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
selectedContainer.querySelectorAll(".remove").forEach(btn => {
|
||||||
|
btn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const id = parseInt(btn.parentElement.dataset.id, 10);
|
||||||
|
if (type === "trigger") {
|
||||||
|
this.selectedTriggerProducts = this.selectedTriggerProducts.filter(x => x !== id);
|
||||||
|
} else {
|
||||||
|
this.selectedRecommendedProducts = this.selectedRecommendedProducts.filter(x => x !== id);
|
||||||
|
}
|
||||||
|
renderSelected();
|
||||||
|
renderDropdown(searchInput.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDropdown = (query) => {
|
||||||
|
const q = (query || "").toLowerCase().trim();
|
||||||
|
const selectedSet = new Set(type === "trigger" ? this.selectedTriggerProducts : this.selectedRecommendedProducts);
|
||||||
|
|
||||||
|
let filtered = this.allProducts;
|
||||||
|
if (q) {
|
||||||
|
filtered = filtered.filter(p => p.name.toLowerCase().includes(q));
|
||||||
|
}
|
||||||
|
filtered = filtered.slice(0, 50); // Limit for performance
|
||||||
|
|
||||||
|
if (!q && !filtered.length) {
|
||||||
|
dropdown.classList.remove("open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dropdown.innerHTML = filtered.map(p => {
|
||||||
|
const isSelected = selectedSet.has(p.woo_product_id);
|
||||||
|
return `
|
||||||
|
<div class="product-option ${isSelected ? "selected" : ""}" data-id="${p.woo_product_id}">
|
||||||
|
<span>${p.name}</span>
|
||||||
|
<span class="price">$${p.price || 0}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
dropdown.querySelectorAll(".product-option").forEach(opt => {
|
||||||
|
opt.onclick = () => {
|
||||||
|
const id = parseInt(opt.dataset.id, 10);
|
||||||
|
if (type === "trigger") {
|
||||||
|
if (!this.selectedTriggerProducts.includes(id)) {
|
||||||
|
this.selectedTriggerProducts.push(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!this.selectedRecommendedProducts.includes(id)) {
|
||||||
|
this.selectedRecommendedProducts.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchInput.value = "";
|
||||||
|
dropdown.classList.remove("open");
|
||||||
|
renderSelected();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.classList.add("open");
|
||||||
|
};
|
||||||
|
|
||||||
|
searchInput.oninput = () => {
|
||||||
|
clearTimeout(this[`_${type}Timer`]);
|
||||||
|
this[`_${type}Timer`] = setTimeout(() => renderDropdown(searchInput.value), 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
searchInput.onfocus = () => {
|
||||||
|
if (searchInput.value || this.allProducts.length) {
|
||||||
|
renderDropdown(searchInput.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close dropdown on outside click
|
||||||
|
document.addEventListener("click", (e) => {
|
||||||
|
if (!this.shadowRoot.getElementById(`${type}Selector`)?.contains(e.target)) {
|
||||||
|
dropdown.classList.remove("open");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
@@ -252,29 +406,30 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
const priority = parseInt(this.shadowRoot.getElementById("priorityInput").value, 10) || 100;
|
const priority = parseInt(this.shadowRoot.getElementById("priorityInput").value, 10) || 100;
|
||||||
const active = this.shadowRoot.getElementById("activeInput").checked;
|
const active = this.shadowRoot.getElementById("activeInput").checked;
|
||||||
|
|
||||||
// Parse comma-separated values into arrays
|
|
||||||
const triggerKeywords = this.parseCommaSeparated(this.shadowRoot.getElementById("triggerInput").value);
|
|
||||||
const queries = this.parseCommaSeparated(this.shadowRoot.getElementById("queriesInput").value);
|
|
||||||
const askSlotsKeywords = this.parseCommaSeparated(this.shadowRoot.getElementById("askSlotsInput").value);
|
|
||||||
|
|
||||||
if (!ruleKey) {
|
if (!ruleKey) {
|
||||||
alert("El rule_key es requerido");
|
alert("El nombre de la regla es requerido");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build trigger object with keywords array
|
if (!this.selectedTriggerProducts.length) {
|
||||||
const trigger = triggerKeywords.length > 0 ? { keywords: triggerKeywords } : {};
|
alert("Selecciona al menos un producto trigger");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Ask slots as simple array of keywords (LLM will formulate questions naturally)
|
if (!this.selectedRecommendedProducts.length) {
|
||||||
const ask_slots = askSlotsKeywords;
|
alert("Selecciona al menos un producto para recomendar");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
rule_key: ruleKey,
|
rule_key: ruleKey,
|
||||||
trigger,
|
trigger: {}, // Legacy field, keep empty
|
||||||
queries,
|
queries: [], // Legacy field, keep empty
|
||||||
ask_slots,
|
ask_slots: [],
|
||||||
active,
|
active,
|
||||||
priority,
|
priority,
|
||||||
|
trigger_product_ids: this.selectedTriggerProducts,
|
||||||
|
recommended_product_ids: this.selectedRecommendedProducts,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -312,6 +467,8 @@ class RecommendationsCrud extends HTMLElement {
|
|||||||
cancel() {
|
cancel() {
|
||||||
this.editMode = null;
|
this.editMode = null;
|
||||||
this.selected = null;
|
this.selected = null;
|
||||||
|
this.selectedTriggerProducts = [];
|
||||||
|
this.selectedRecommendedProducts = [];
|
||||||
this.renderList();
|
this.renderList();
|
||||||
this.renderForm();
|
this.renderForm();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ class RunTimeline extends HTMLElement {
|
|||||||
this.shadowRoot.getElementById("refresh").onclick = () => this.loadMessages();
|
this.shadowRoot.getElementById("refresh").onclick = () => this.loadMessages();
|
||||||
|
|
||||||
this._unsubSel = on("ui:selectedChat", async ({ chat_id }) => {
|
this._unsubSel = on("ui:selectedChat", async ({ chat_id }) => {
|
||||||
|
// Si es el mismo chat, no recargar (para no borrar burbujas optimistas)
|
||||||
|
if (this.chatId === chat_id) return;
|
||||||
this.chatId = chat_id;
|
this.chatId = chat_id;
|
||||||
await this.loadMessages();
|
await this.loadMessages();
|
||||||
});
|
});
|
||||||
@@ -76,7 +78,13 @@ class RunTimeline extends HTMLElement {
|
|||||||
|
|
||||||
// Listen for optimistic messages (show bubble immediately before API response)
|
// Listen for optimistic messages (show bubble immediately before API response)
|
||||||
this._unsubOptimistic = on("message:optimistic", (msg) => {
|
this._unsubOptimistic = on("message:optimistic", (msg) => {
|
||||||
if (!this.chatId || msg.chat_id !== this.chatId) return;
|
// Si no hay chatId seteado, setearlo al del mensaje
|
||||||
|
if (!this.chatId) {
|
||||||
|
this.chatId = msg.chat_id;
|
||||||
|
this.shadowRoot.getElementById("chat").textContent = msg.chat_id;
|
||||||
|
this.shadowRoot.getElementById("meta").textContent = "Nueva conversación";
|
||||||
|
}
|
||||||
|
if (msg.chat_id !== this.chatId) return;
|
||||||
this.addOptimisticBubble(msg);
|
this.addOptimisticBubble(msg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -135,8 +143,20 @@ class RunTimeline extends HTMLElement {
|
|||||||
meta.textContent = `Mostrando historial (últimos ${this.items.length}).`;
|
meta.textContent = `Mostrando historial (últimos ${this.items.length}).`;
|
||||||
count.textContent = this.items.length ? `${this.items.length} msgs` : "";
|
count.textContent = this.items.length ? `${this.items.length} msgs` : "";
|
||||||
|
|
||||||
|
// Capturar info de burbujas optimistas antes de limpiar
|
||||||
|
const optimisticBubbles = [...log.querySelectorAll('.bubble[data-message-id^="optimistic-"]')];
|
||||||
|
const optimisticTexts = optimisticBubbles.map(b => {
|
||||||
|
const textEl = b.querySelector("div:not(.name):not(.meta)");
|
||||||
|
return (textEl ? textEl.textContent : "").trim().toLowerCase();
|
||||||
|
});
|
||||||
|
|
||||||
log.innerHTML = "";
|
log.innerHTML = "";
|
||||||
|
|
||||||
|
// Obtener textos de mensajes IN del servidor (normalizados para comparación)
|
||||||
|
const serverUserTexts = this.items
|
||||||
|
.filter(m => m.direction === "in")
|
||||||
|
.map(m => (m.text || "").trim().toLowerCase());
|
||||||
|
|
||||||
for (const m of this.items) {
|
for (const m of this.items) {
|
||||||
const who = m.direction === "in" ? "user" : "bot";
|
const who = m.direction === "in" ? "user" : "bot";
|
||||||
const isErr = this.isErrorMsg(m);
|
const isErr = this.isErrorMsg(m);
|
||||||
@@ -164,8 +184,23 @@ class RunTimeline extends HTMLElement {
|
|||||||
log.appendChild(bubble);
|
log.appendChild(bubble);
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto-scroll
|
// Re-agregar burbujas optimistas SOLO si su texto no está ya en los mensajes del servidor
|
||||||
log.scrollTop = log.scrollHeight;
|
// Comparación case-insensitive y trimmed
|
||||||
|
let addedOptimistic = false;
|
||||||
|
for (let i = 0; i < optimisticBubbles.length; i++) {
|
||||||
|
const optText = optimisticTexts[i];
|
||||||
|
// Si el texto ya existe en un mensaje del servidor, no re-agregar
|
||||||
|
if (serverUserTexts.includes(optText)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log.appendChild(optimisticBubbles[i]);
|
||||||
|
addedOptimistic = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto-scroll solo si agregamos burbujas optimistas nuevas
|
||||||
|
if (addedOptimistic) {
|
||||||
|
log.scrollTop = log.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => this.emitLayout());
|
requestAnimationFrame(() => this.emitLayout());
|
||||||
this.bindScroll(log);
|
this.bindScroll(log);
|
||||||
@@ -193,29 +228,7 @@ class RunTimeline extends HTMLElement {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
emit("ui:bubblesLayout", { chat_id: this.chatId, items });
|
emit("ui:bubblesLayout", { chat_id: this.chatId, items });
|
||||||
// #region agent log
|
}
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H15",
|
|
||||||
location: "run-timeline.js:180",
|
|
||||||
message: "bubbles_layout",
|
|
||||||
data: {
|
|
||||||
count: items.length,
|
|
||||||
chat_id: this.chatId || null,
|
|
||||||
scroll_height: log.scrollHeight,
|
|
||||||
client_height: log.clientHeight,
|
|
||||||
host_height: this.getBoundingClientRect().height,
|
|
||||||
box_height: box ? box.getBoundingClientRect().height : null,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
highlightMessage(message_id) {
|
highlightMessage(message_id) {
|
||||||
const log = this.shadowRoot.getElementById("log");
|
const log = this.shadowRoot.getElementById("log");
|
||||||
@@ -275,7 +288,12 @@ class RunTimeline extends HTMLElement {
|
|||||||
bubble.appendChild(metaEl);
|
bubble.appendChild(metaEl);
|
||||||
|
|
||||||
log.appendChild(bubble);
|
log.appendChild(bubble);
|
||||||
log.scrollTop = log.scrollHeight;
|
|
||||||
|
// Solo hacer scroll si el usuario ya estaba cerca del final (dentro de 100px)
|
||||||
|
const wasNearBottom = (log.scrollHeight - log.scrollTop - log.clientHeight) < 100;
|
||||||
|
if (wasNearBottom) {
|
||||||
|
log.scrollTop = log.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// Emit layout update
|
// Emit layout update
|
||||||
requestAnimationFrame(() => this.emitLayout());
|
requestAnimationFrame(() => this.emitLayout());
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class UsersCrud extends HTMLElement {
|
|||||||
this.selected = null;
|
this.selected = null;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.searchQuery = "";
|
this.searchQuery = "";
|
||||||
|
this.wooFilter = false;
|
||||||
|
|
||||||
this.shadowRoot.innerHTML = `
|
this.shadowRoot.innerHTML = `
|
||||||
<style>
|
<style>
|
||||||
@@ -49,7 +50,9 @@ class UsersCrud extends HTMLElement {
|
|||||||
.loading { text-align:center; padding:40px; color:#8aa0b5; }
|
.loading { text-align:center; padding:40px; color:#8aa0b5; }
|
||||||
|
|
||||||
.stats { display:flex; gap:16px; margin-bottom:16px; }
|
.stats { display:flex; gap:16px; margin-bottom:16px; }
|
||||||
.stat { background:#0f1520; border:1px solid #253245; border-radius:8px; padding:12px; flex:1; text-align:center; }
|
.stat { background:#0f1520; border:1px solid #253245; border-radius:8px; padding:12px; flex:1; text-align:center; cursor:pointer; transition:all .15s; }
|
||||||
|
.stat:hover { border-color:#1f6feb; }
|
||||||
|
.stat.active { border-color:#1f6feb; background:#111b2a; }
|
||||||
.stat-value { font-size:24px; font-weight:700; color:#1f6feb; }
|
.stat-value { font-size:24px; font-weight:700; color:#1f6feb; }
|
||||||
.stat-label { font-size:11px; color:#8aa0b5; text-transform:uppercase; margin-top:4px; }
|
.stat-label { font-size:11px; color:#8aa0b5; text-transform:uppercase; margin-top:4px; }
|
||||||
</style>
|
</style>
|
||||||
@@ -58,11 +61,11 @@ class UsersCrud extends HTMLElement {
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-title">Usuarios</div>
|
<div class="panel-title">Usuarios</div>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat">
|
<div class="stat" id="statTotal">
|
||||||
<div class="stat-value" id="totalCount">—</div>
|
<div class="stat-value" id="totalCount">—</div>
|
||||||
<div class="stat-label">Total</div>
|
<div class="stat-label">Total</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat" id="statWoo">
|
||||||
<div class="stat-value" id="wooCount">—</div>
|
<div class="stat-value" id="wooCount">—</div>
|
||||||
<div class="stat-label">Con Woo ID</div>
|
<div class="stat-label">Con Woo ID</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,9 +95,29 @@ class UsersCrud extends HTMLElement {
|
|||||||
this._searchTimer = setTimeout(() => this.load(), 300);
|
this._searchTimer = setTimeout(() => this.load(), 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Stats click handlers
|
||||||
|
this.shadowRoot.getElementById("statTotal").onclick = () => {
|
||||||
|
this.wooFilter = false;
|
||||||
|
this.renderList();
|
||||||
|
this.updateStatStyles();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.shadowRoot.getElementById("statWoo").onclick = () => {
|
||||||
|
this.wooFilter = !this.wooFilter;
|
||||||
|
this.renderList();
|
||||||
|
this.updateStatStyles();
|
||||||
|
};
|
||||||
|
|
||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateStatStyles() {
|
||||||
|
const statTotal = this.shadowRoot.getElementById("statTotal");
|
||||||
|
const statWoo = this.shadowRoot.getElementById("statWoo");
|
||||||
|
statTotal.classList.toggle("active", !this.wooFilter);
|
||||||
|
statWoo.classList.toggle("active", this.wooFilter);
|
||||||
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.renderList();
|
this.renderList();
|
||||||
@@ -129,13 +152,18 @@ class UsersCrud extends HTMLElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.items.length) {
|
// Filter by woo ID if filter is active
|
||||||
|
const filteredItems = this.wooFilter
|
||||||
|
? this.items.filter(u => u.external_customer_id)
|
||||||
|
: this.items;
|
||||||
|
|
||||||
|
if (!filteredItems.length) {
|
||||||
list.innerHTML = `<div class="loading">No se encontraron usuarios</div>`;
|
list.innerHTML = `<div class="loading">No se encontraron usuarios</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
for (const item of this.items) {
|
for (const item of filteredItems) {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "item" + (this.selected?.chat_id === item.chat_id ? " active" : "");
|
el.className = "item" + (this.selected?.chat_id === item.chat_id ? " active" : "");
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,30 @@ export const api = {
|
|||||||
return fetch("/products/sync", { method: "POST" }).then(r => r.json());
|
return fetch("/products/sync", { method: "POST" }).then(r => r.json());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateProductUnit(wooProductId, { sell_unit }) {
|
||||||
|
return fetch(`/products/${encodeURIComponent(wooProductId)}/unit`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ sell_unit }),
|
||||||
|
}).then(r => r.json());
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateProduct(wooProductId, { sell_unit, categories }) {
|
||||||
|
return fetch(`/products/${encodeURIComponent(wooProductId)}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ sell_unit, categories }),
|
||||||
|
}).then(r => r.json());
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateProductsUnit(wooProductIds, { sell_unit }) {
|
||||||
|
return fetch(`/products/bulk/unit`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ woo_product_ids: wooProductIds, sell_unit }),
|
||||||
|
}).then(r => r.json());
|
||||||
|
},
|
||||||
|
|
||||||
// Aliases CRUD
|
// Aliases CRUD
|
||||||
async aliases({ q = "", woo_product_id = null, limit = 200 } = {}) {
|
async aliases({ q = "", woo_product_id = null, limit = 200 } = {}) {
|
||||||
const u = new URL("/aliases", location.origin);
|
const u = new URL("/aliases", location.origin);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { handleSearchProducts, handleListProducts, handleGetProduct, handleSyncProducts } from "../handlers/products.js";
|
import { handleSearchProducts, handleListProducts, handleGetProduct, handleSyncProducts, handleUpdateProductUnit, handleBulkUpdateProductUnit, handleUpdateProduct } from "../handlers/products.js";
|
||||||
|
|
||||||
export const makeSearchProducts = (tenantIdOrFn) => async (req, res) => {
|
export const makeSearchProducts = (tenantIdOrFn) => async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -54,3 +54,60 @@ export const makeSyncProducts = (tenantIdOrFn) => async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const makeUpdateProductUnit = (tenantIdOrFn) => async (req, res) => {
|
||||||
|
try {
|
||||||
|
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||||
|
const wooProductId = req.params.id;
|
||||||
|
const { sell_unit } = req.body || {};
|
||||||
|
|
||||||
|
if (!sell_unit || !["kg", "unit"].includes(sell_unit)) {
|
||||||
|
return res.status(400).json({ ok: false, error: "invalid_sell_unit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await handleUpdateProductUnit({ tenantId, wooProductId, sell_unit });
|
||||||
|
res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ ok: false, error: "internal_error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeBulkUpdateProductUnit = (tenantIdOrFn) => async (req, res) => {
|
||||||
|
try {
|
||||||
|
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||||
|
const { woo_product_ids, sell_unit } = req.body || {};
|
||||||
|
|
||||||
|
if (!sell_unit || !["kg", "unit"].includes(sell_unit)) {
|
||||||
|
return res.status(400).json({ ok: false, error: "invalid_sell_unit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(woo_product_ids) || !woo_product_ids.length) {
|
||||||
|
return res.status(400).json({ ok: false, error: "invalid_woo_product_ids" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await handleBulkUpdateProductUnit({ tenantId, wooProductIds: woo_product_ids, sell_unit });
|
||||||
|
res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ ok: false, error: "internal_error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeUpdateProduct = (tenantIdOrFn) => async (req, res) => {
|
||||||
|
try {
|
||||||
|
const tenantId = typeof tenantIdOrFn === "function" ? tenantIdOrFn() : tenantIdOrFn;
|
||||||
|
const wooProductId = req.params.id;
|
||||||
|
const { sell_unit, categories } = req.body || {};
|
||||||
|
|
||||||
|
if (sell_unit && !["kg", "unit"].includes(sell_unit)) {
|
||||||
|
return res.status(400).json({ ok: false, error: "invalid_sell_unit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await handleUpdateProduct({ tenantId, wooProductId, sell_unit, categories });
|
||||||
|
res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).json({ ok: false, error: "internal_error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export async function listProducts({ tenantId, q = "", limit = 2000, offset = 0
|
|||||||
categories,
|
categories,
|
||||||
attributes_normalized,
|
attributes_normalized,
|
||||||
updated_at as refreshed_at,
|
updated_at as refreshed_at,
|
||||||
raw as payload
|
raw as payload,
|
||||||
|
raw->>'_sell_unit_override' as sell_unit
|
||||||
from woo_products_snapshot
|
from woo_products_snapshot
|
||||||
where tenant_id = $1
|
where tenant_id = $1
|
||||||
and (name ilike $2 or coalesce(slug,'') ilike $2)
|
and (name ilike $2 or coalesce(slug,'') ilike $2)
|
||||||
@@ -41,7 +42,8 @@ export async function listProducts({ tenantId, q = "", limit = 2000, offset = 0
|
|||||||
categories,
|
categories,
|
||||||
attributes_normalized,
|
attributes_normalized,
|
||||||
updated_at as refreshed_at,
|
updated_at as refreshed_at,
|
||||||
raw as payload
|
raw as payload,
|
||||||
|
raw->>'_sell_unit_override' as sell_unit
|
||||||
from woo_products_snapshot
|
from woo_products_snapshot
|
||||||
where tenant_id = $1
|
where tenant_id = $1
|
||||||
order by name asc
|
order by name asc
|
||||||
@@ -65,7 +67,8 @@ export async function getProductByWooId({ tenantId, wooProductId }) {
|
|||||||
categories,
|
categories,
|
||||||
attributes_normalized,
|
attributes_normalized,
|
||||||
updated_at as refreshed_at,
|
updated_at as refreshed_at,
|
||||||
raw as payload
|
raw as payload,
|
||||||
|
raw->>'_sell_unit_override' as sell_unit
|
||||||
from woo_products_snapshot
|
from woo_products_snapshot
|
||||||
where tenant_id = $1 and woo_id = $2
|
where tenant_id = $1 and woo_id = $2
|
||||||
limit 1
|
limit 1
|
||||||
@@ -74,6 +77,66 @@ export async function getProductByWooId({ tenantId, wooProductId }) {
|
|||||||
return rows[0] || null;
|
return rows[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateProductSellUnit({ tenantId, wooProductId, sell_unit }) {
|
||||||
|
const sql = `
|
||||||
|
update woo_products_snapshot
|
||||||
|
set raw = jsonb_set(coalesce(raw, '{}'::jsonb), '{_sell_unit_override}', $3::jsonb)
|
||||||
|
where tenant_id = $1 and woo_id = $2
|
||||||
|
returning woo_id as woo_product_id
|
||||||
|
`;
|
||||||
|
const { rows } = await pool.query(sql, [tenantId, wooProductId, JSON.stringify(sell_unit)]);
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bulkUpdateProductSellUnit({ tenantId, wooProductIds, sell_unit }) {
|
||||||
|
if (!wooProductIds || !wooProductIds.length) return { updated: 0 };
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
update woo_products_snapshot
|
||||||
|
set raw = jsonb_set(coalesce(raw, '{}'::jsonb), '{_sell_unit_override}', $3::jsonb)
|
||||||
|
where tenant_id = $1 and woo_id = ANY($2::int[])
|
||||||
|
`;
|
||||||
|
const result = await pool.query(sql, [tenantId, wooProductIds, JSON.stringify(sell_unit)]);
|
||||||
|
return { updated: result.rowCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateProduct({ tenantId, wooProductId, sell_unit, categories }) {
|
||||||
|
// Build the JSONB update dynamically
|
||||||
|
let updates = [];
|
||||||
|
let params = [tenantId, wooProductId];
|
||||||
|
let paramIdx = 3;
|
||||||
|
|
||||||
|
if (sell_unit) {
|
||||||
|
updates.push(`raw = jsonb_set(coalesce(raw, '{}'::jsonb), '{_sell_unit_override}', $${paramIdx}::jsonb)`);
|
||||||
|
params.push(JSON.stringify(sell_unit));
|
||||||
|
paramIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categories) {
|
||||||
|
// Also update the categories column if it exists
|
||||||
|
updates.push(`categories = $${paramIdx}::jsonb`);
|
||||||
|
params.push(JSON.stringify(categories.map(name => ({ name }))));
|
||||||
|
paramIdx++;
|
||||||
|
|
||||||
|
// Also store in raw for persistence
|
||||||
|
updates.push(`raw = jsonb_set(coalesce(raw, '{}'::jsonb), '{_categories_override}', $${paramIdx}::jsonb)`);
|
||||||
|
params.push(JSON.stringify(categories));
|
||||||
|
paramIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updates.length) return null;
|
||||||
|
|
||||||
|
const sql = `
|
||||||
|
update woo_products_snapshot
|
||||||
|
set ${updates.join(", ")}
|
||||||
|
where tenant_id = $1 and woo_id = $2
|
||||||
|
returning woo_id as woo_product_id
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { rows } = await pool.query(sql, params);
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
// Aliases
|
// Aliases
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
@@ -188,7 +251,8 @@ export async function listRecommendations({ tenantId, q = "", limit = 200 }) {
|
|||||||
if (query) {
|
if (query) {
|
||||||
const like = `%${query}%`;
|
const like = `%${query}%`;
|
||||||
sql = `
|
sql = `
|
||||||
select id, rule_key, trigger, queries, boosts, ask_slots, active, priority, created_at, updated_at
|
select id, rule_key, trigger, queries, boosts, ask_slots, active, priority,
|
||||||
|
trigger_product_ids, recommended_product_ids, created_at, updated_at
|
||||||
from product_reco_rules
|
from product_reco_rules
|
||||||
where tenant_id = $1 and rule_key ilike $2
|
where tenant_id = $1 and rule_key ilike $2
|
||||||
order by priority desc, rule_key asc
|
order by priority desc, rule_key asc
|
||||||
@@ -197,7 +261,8 @@ export async function listRecommendations({ tenantId, q = "", limit = 200 }) {
|
|||||||
params = [tenantId, like, lim];
|
params = [tenantId, like, lim];
|
||||||
} else {
|
} else {
|
||||||
sql = `
|
sql = `
|
||||||
select id, rule_key, trigger, queries, boosts, ask_slots, active, priority, created_at, updated_at
|
select id, rule_key, trigger, queries, boosts, ask_slots, active, priority,
|
||||||
|
trigger_product_ids, recommended_product_ids, created_at, updated_at
|
||||||
from product_reco_rules
|
from product_reco_rules
|
||||||
where tenant_id = $1
|
where tenant_id = $1
|
||||||
order by priority desc, rule_key asc
|
order by priority desc, rule_key asc
|
||||||
@@ -212,7 +277,8 @@ export async function listRecommendations({ tenantId, q = "", limit = 200 }) {
|
|||||||
|
|
||||||
export async function getRecommendationById({ tenantId, id }) {
|
export async function getRecommendationById({ tenantId, id }) {
|
||||||
const sql = `
|
const sql = `
|
||||||
select id, rule_key, trigger, queries, boosts, ask_slots, active, priority, created_at, updated_at
|
select id, rule_key, trigger, queries, boosts, ask_slots, active, priority,
|
||||||
|
trigger_product_ids, recommended_product_ids, created_at, updated_at
|
||||||
from product_reco_rules
|
from product_reco_rules
|
||||||
where tenant_id = $1 and id = $2
|
where tenant_id = $1 and id = $2
|
||||||
limit 1
|
limit 1
|
||||||
@@ -230,11 +296,13 @@ export async function insertRecommendation({
|
|||||||
ask_slots = [],
|
ask_slots = [],
|
||||||
active = true,
|
active = true,
|
||||||
priority = 100,
|
priority = 100,
|
||||||
|
trigger_product_ids = [],
|
||||||
|
recommended_product_ids = [],
|
||||||
}) {
|
}) {
|
||||||
const sql = `
|
const sql = `
|
||||||
insert into product_reco_rules (tenant_id, rule_key, trigger, queries, boosts, ask_slots, active, priority)
|
insert into product_reco_rules (tenant_id, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids)
|
||||||
values ($1, $2, $3, $4, $5, $6, $7, $8)
|
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
returning id, rule_key, trigger, queries, boosts, ask_slots, active, priority, created_at, updated_at
|
returning id, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids, created_at, updated_at
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const { rows } = await pool.query(sql, [
|
const { rows } = await pool.query(sql, [
|
||||||
@@ -246,6 +314,8 @@ export async function insertRecommendation({
|
|||||||
JSON.stringify(ask_slots || []),
|
JSON.stringify(ask_slots || []),
|
||||||
active !== false,
|
active !== false,
|
||||||
priority || 100,
|
priority || 100,
|
||||||
|
trigger_product_ids || [],
|
||||||
|
recommended_product_ids || [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return rows[0];
|
return rows[0];
|
||||||
@@ -260,6 +330,8 @@ export async function updateRecommendation({
|
|||||||
ask_slots,
|
ask_slots,
|
||||||
active,
|
active,
|
||||||
priority,
|
priority,
|
||||||
|
trigger_product_ids,
|
||||||
|
recommended_product_ids,
|
||||||
}) {
|
}) {
|
||||||
const sql = `
|
const sql = `
|
||||||
update product_reco_rules
|
update product_reco_rules
|
||||||
@@ -270,9 +342,11 @@ export async function updateRecommendation({
|
|||||||
ask_slots = $6,
|
ask_slots = $6,
|
||||||
active = $7,
|
active = $7,
|
||||||
priority = $8,
|
priority = $8,
|
||||||
|
trigger_product_ids = $9,
|
||||||
|
recommended_product_ids = $10,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
where tenant_id = $1 and id = $2
|
where tenant_id = $1 and id = $2
|
||||||
returning id, rule_key, trigger, queries, boosts, ask_slots, active, priority, created_at, updated_at
|
returning id, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids, created_at, updated_at
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const { rows } = await pool.query(sql, [
|
const { rows } = await pool.query(sql, [
|
||||||
@@ -284,6 +358,8 @@ export async function updateRecommendation({
|
|||||||
JSON.stringify(ask_slots || []),
|
JSON.stringify(ask_slots || []),
|
||||||
active !== false,
|
active !== false,
|
||||||
priority || 100,
|
priority || 100,
|
||||||
|
trigger_product_ids || [],
|
||||||
|
recommended_product_ids || [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return rows[0] || null;
|
return rows[0] || null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { searchSnapshotItems } from "../../shared/wooSnapshot.js";
|
import { searchSnapshotItems } from "../../shared/wooSnapshot.js";
|
||||||
import { listProducts, getProductByWooId } from "../db/repo.js";
|
import { listProducts, getProductByWooId, updateProductSellUnit, bulkUpdateProductSellUnit, updateProduct } from "../db/repo.js";
|
||||||
|
|
||||||
export async function handleSearchProducts({ tenantId, q = "", limit = "10", forceWoo = "0" }) {
|
export async function handleSearchProducts({ tenantId, q = "", limit = "10", forceWoo = "0" }) {
|
||||||
const { items, source } = await searchSnapshotItems({
|
const { items, source } = await searchSnapshotItems({
|
||||||
@@ -25,3 +25,18 @@ export async function handleSyncProducts({ tenantId }) {
|
|||||||
return { ok: true, message: "Sync triggered (use import script for full sync)" };
|
return { ok: true, message: "Sync triggered (use import script for full sync)" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function handleUpdateProductUnit({ tenantId, wooProductId, sell_unit }) {
|
||||||
|
await updateProductSellUnit({ tenantId, wooProductId, sell_unit });
|
||||||
|
return { ok: true, woo_product_id: wooProductId, sell_unit };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleBulkUpdateProductUnit({ tenantId, wooProductIds, sell_unit }) {
|
||||||
|
await bulkUpdateProductSellUnit({ tenantId, wooProductIds, sell_unit });
|
||||||
|
return { ok: true, updated_count: wooProductIds.length, sell_unit };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleUpdateProduct({ tenantId, wooProductId, sell_unit, categories }) {
|
||||||
|
await updateProduct({ tenantId, wooProductId, sell_unit, categories });
|
||||||
|
return { ok: true, woo_product_id: wooProductId, sell_unit, categories };
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ export async function handleCreateRecommendation({
|
|||||||
ask_slots = [],
|
ask_slots = [],
|
||||||
active = true,
|
active = true,
|
||||||
priority = 100,
|
priority = 100,
|
||||||
|
trigger_product_ids = [],
|
||||||
|
recommended_product_ids = [],
|
||||||
}) {
|
}) {
|
||||||
return insertRecommendation({ tenantId, rule_key, trigger, queries, boosts, ask_slots, active, priority });
|
return insertRecommendation({ tenantId, rule_key, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleUpdateRecommendation({
|
export async function handleUpdateRecommendation({
|
||||||
@@ -37,8 +39,10 @@ export async function handleUpdateRecommendation({
|
|||||||
ask_slots,
|
ask_slots,
|
||||||
active,
|
active,
|
||||||
priority,
|
priority,
|
||||||
|
trigger_product_ids,
|
||||||
|
recommended_product_ids,
|
||||||
}) {
|
}) {
|
||||||
return updateRecommendation({ tenantId, id, trigger, queries, boosts, ask_slots, active, priority });
|
return updateRecommendation({ tenantId, id, trigger, queries, boosts, ask_slots, active, priority, trigger_product_ids, recommended_product_ids });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleDeleteRecommendation({ tenantId, id }) {
|
export async function handleDeleteRecommendation({ tenantId, id }) {
|
||||||
|
|||||||
@@ -6,27 +6,7 @@ import { debug as dbg } from "../../shared/debug.js";
|
|||||||
export async function handleEvolutionWebhook(body) {
|
export async function handleEvolutionWebhook(body) {
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
const parsed = parseEvolutionWebhook(body);
|
const parsed = parseEvolutionWebhook(body);
|
||||||
// #region agent log
|
if (!parsed.ok) {
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H1",
|
|
||||||
location: "evolution.js:9",
|
|
||||||
message: "parsed_webhook",
|
|
||||||
data: {
|
|
||||||
ok: parsed?.ok,
|
|
||||||
reason: parsed?.reason || null,
|
|
||||||
has_text: Boolean(parsed?.text),
|
|
||||||
source: parsed?.source || null,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
if (!parsed.ok) {
|
|
||||||
return { status: 200, payload: { ok: true, ignored: parsed.reason } };
|
return { status: 200, payload: { ok: true, ignored: parsed.reason } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { makeListRuns, makeGetRunById } from "../../0-ui/controllers/runs.js";
|
|||||||
import { makeSimSend } from "../controllers/sim.js";
|
import { makeSimSend } from "../controllers/sim.js";
|
||||||
import { makeGetConversationState } from "../../0-ui/controllers/conversationState.js";
|
import { makeGetConversationState } from "../../0-ui/controllers/conversationState.js";
|
||||||
import { makeListMessages } from "../../0-ui/controllers/messages.js";
|
import { makeListMessages } from "../../0-ui/controllers/messages.js";
|
||||||
import { makeSearchProducts, makeListProducts, makeGetProduct, makeSyncProducts } from "../../0-ui/controllers/products.js";
|
import { makeSearchProducts, makeListProducts, makeGetProduct, makeSyncProducts, makeUpdateProductUnit, makeBulkUpdateProductUnit, makeUpdateProduct } from "../../0-ui/controllers/products.js";
|
||||||
import { makeListAliases, makeCreateAlias, makeUpdateAlias, makeDeleteAlias } from "../../0-ui/controllers/aliases.js";
|
import { makeListAliases, makeCreateAlias, makeUpdateAlias, makeDeleteAlias } from "../../0-ui/controllers/aliases.js";
|
||||||
import { makeListRecommendations, makeGetRecommendation, makeCreateRecommendation, makeUpdateRecommendation, makeDeleteRecommendation } from "../../0-ui/controllers/recommendations.js";
|
import { makeListRecommendations, makeGetRecommendation, makeCreateRecommendation, makeUpdateRecommendation, makeDeleteRecommendation } from "../../0-ui/controllers/recommendations.js";
|
||||||
import { makeDeleteConversation, makeDeleteUser, makeListUsers, makeRetryLast } from "../../0-ui/controllers/admin.js";
|
import { makeDeleteConversation, makeDeleteUser, makeListUsers, makeRetryLast } from "../../0-ui/controllers/admin.js";
|
||||||
@@ -53,8 +53,11 @@ export function createSimulatorRouter({ tenantId }) {
|
|||||||
router.get("/messages", makeListMessages(getTenantId));
|
router.get("/messages", makeListMessages(getTenantId));
|
||||||
router.get("/products", makeListProducts(getTenantId));
|
router.get("/products", makeListProducts(getTenantId));
|
||||||
router.get("/products/search", makeSearchProducts(getTenantId));
|
router.get("/products/search", makeSearchProducts(getTenantId));
|
||||||
router.get("/products/:id", makeGetProduct(getTenantId));
|
router.patch("/products/bulk/unit", makeBulkUpdateProductUnit(getTenantId));
|
||||||
router.post("/products/sync", makeSyncProducts(getTenantId));
|
router.post("/products/sync", makeSyncProducts(getTenantId));
|
||||||
|
router.get("/products/:id", makeGetProduct(getTenantId));
|
||||||
|
router.patch("/products/:id/unit", makeUpdateProductUnit(getTenantId));
|
||||||
|
router.patch("/products/:id", makeUpdateProduct(getTenantId));
|
||||||
|
|
||||||
router.get("/aliases", makeListAliases(getTenantId));
|
router.get("/aliases", makeListAliases(getTenantId));
|
||||||
router.post("/aliases", makeCreateAlias(getTenantId));
|
router.post("/aliases", makeCreateAlias(getTenantId));
|
||||||
|
|||||||
@@ -565,7 +565,8 @@ export async function searchProductAliases({ tenant_id, q = "", limit = 20 }) {
|
|||||||
|
|
||||||
export async function getRecoRules({ tenant_id }) {
|
export async function getRecoRules({ tenant_id }) {
|
||||||
const sql = `
|
const sql = `
|
||||||
select id, tenant_id, rule_key, trigger, queries, boosts, ask_slots, active, priority, created_at, updated_at
|
select id, tenant_id, rule_key, trigger, queries, boosts, ask_slots, active, priority,
|
||||||
|
trigger_product_ids, recommended_product_ids, created_at, updated_at
|
||||||
from product_reco_rules
|
from product_reco_rules
|
||||||
where tenant_id=$1 and active=true
|
where tenant_id=$1 and active=true
|
||||||
order by priority asc, id asc
|
order by priority asc, id asc
|
||||||
@@ -574,9 +575,26 @@ export async function getRecoRules({ tenant_id }) {
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buscar reglas que tengan alguno de los productos como trigger.
|
||||||
|
*/
|
||||||
|
export async function getRecoRulesByProductIds({ tenant_id, product_ids = [] }) {
|
||||||
|
if (!product_ids?.length) return [];
|
||||||
|
const sql = `
|
||||||
|
select id, tenant_id, rule_key, trigger, queries, boosts, ask_slots, active, priority,
|
||||||
|
trigger_product_ids, recommended_product_ids, created_at, updated_at
|
||||||
|
from product_reco_rules
|
||||||
|
where tenant_id=$1 and active=true and trigger_product_ids && $2::int[]
|
||||||
|
order by priority asc, id asc
|
||||||
|
`;
|
||||||
|
const { rows } = await pool.query(sql, [tenant_id, product_ids]);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getRecoRuleByKey({ tenant_id, rule_key }) {
|
export async function getRecoRuleByKey({ tenant_id, rule_key }) {
|
||||||
const sql = `
|
const sql = `
|
||||||
select id, tenant_id, rule_key, trigger, queries, boosts, ask_slots, active, priority, created_at, updated_at
|
select id, tenant_id, rule_key, trigger, queries, boosts, ask_slots, active, priority,
|
||||||
|
trigger_product_ids, recommended_product_ids, created_at, updated_at
|
||||||
from product_reco_rules
|
from product_reco_rules
|
||||||
where tenant_id=$1 and rule_key=$2
|
where tenant_id=$1 and rule_key=$2
|
||||||
limit 1
|
limit 1
|
||||||
|
|||||||
@@ -123,28 +123,7 @@ export async function processMessage({
|
|||||||
meta = null,
|
meta = null,
|
||||||
}) {
|
}) {
|
||||||
const { started_at, mark, msBetween } = makePerf();
|
const { started_at, mark, msBetween } = makePerf();
|
||||||
// #region agent log
|
const prev = await touchConversationState({ tenant_id: tenantId, wa_chat_id: chat_id });
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H2",
|
|
||||||
location: "pipeline.js:128",
|
|
||||||
message: "processMessage_enter",
|
|
||||||
data: {
|
|
||||||
tenantId: tenantId || null,
|
|
||||||
provider,
|
|
||||||
chat_id: chat_id || null,
|
|
||||||
text_len: String(text || "").length,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
const prev = await touchConversationState({ tenant_id: tenantId, wa_chat_id: chat_id });
|
|
||||||
|
|
||||||
mark("start");
|
mark("start");
|
||||||
const stageDebug = dbg.perf;
|
const stageDebug = dbg.perf;
|
||||||
@@ -153,27 +132,7 @@ export async function processMessage({
|
|||||||
prev?.state_updated_at &&
|
prev?.state_updated_at &&
|
||||||
Date.now() - new Date(prev.state_updated_at).getTime() > 24 * 60 * 60 * 1000;
|
Date.now() - new Date(prev.state_updated_at).getTime() > 24 * 60 * 60 * 1000;
|
||||||
const prev_state = isStale ? "IDLE" : prev?.state || "IDLE";
|
const prev_state = isStale ? "IDLE" : prev?.state || "IDLE";
|
||||||
// #region agent log
|
let externalCustomerId = await getExternalCustomerIdByChat({
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H3",
|
|
||||||
location: "pipeline.js:150",
|
|
||||||
message: "conversation_state_loaded",
|
|
||||||
data: {
|
|
||||||
prev_state,
|
|
||||||
isStale: Boolean(isStale),
|
|
||||||
state_updated_at: prev?.state_updated_at || null,
|
|
||||||
has_context: Boolean(prev?.context && typeof prev?.context === "object"),
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
let externalCustomerId = await getExternalCustomerIdByChat({
|
|
||||||
tenant_id: tenantId,
|
tenant_id: tenantId,
|
||||||
wa_chat_id: chat_id,
|
wa_chat_id: chat_id,
|
||||||
provider: "woo",
|
provider: "woo",
|
||||||
@@ -203,6 +162,9 @@ export async function processMessage({
|
|||||||
logStage(stageDebug, "history", { has_history: Array.isArray(conversation_history), state: prev_state });
|
logStage(stageDebug, "history", { has_history: Array.isArray(conversation_history), state: prev_state });
|
||||||
|
|
||||||
let reducedContext = prev?.context && typeof prev?.context === "object" ? { ...prev.context } : {};
|
let reducedContext = prev?.context && typeof prev?.context === "object" ? { ...prev.context } : {};
|
||||||
|
// #region agent log
|
||||||
|
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"pipeline.js:164",message:"pipeline_loaded_context",data:{prev_state,has_prev_context:!!prev?.context,reducedContext_has_order_basket:!!reducedContext?.order_basket,reducedContext_basket_count:reducedContext?.order_basket?.items?.length||0,reducedContext_basket_labels:(reducedContext?.order_basket?.items||[]).map(i=>i.label)},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H3-H4"})}).catch(()=>{});
|
||||||
|
// #endregion
|
||||||
let decision;
|
let decision;
|
||||||
let plan;
|
let plan;
|
||||||
let llmMeta;
|
let llmMeta;
|
||||||
@@ -222,28 +184,7 @@ export async function processMessage({
|
|||||||
llmMeta = { kind: "nlu_v3", audit: decision.audit || null };
|
llmMeta = { kind: "nlu_v3", audit: decision.audit || null };
|
||||||
tools = [];
|
tools = [];
|
||||||
mark("after_turn_v3");
|
mark("after_turn_v3");
|
||||||
// #region agent log
|
const runStatus = llmMeta?.error ? "warn" : "ok";
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H4",
|
|
||||||
location: "pipeline.js:198",
|
|
||||||
message: "turn_v3_result",
|
|
||||||
data: {
|
|
||||||
intent: plan?.intent || null,
|
|
||||||
next_state: plan?.next_state || null,
|
|
||||||
missing_fields: Array.isArray(plan?.missing_fields) ? plan.missing_fields.length : null,
|
|
||||||
actions_count: Array.isArray(decision?.actions) ? decision.actions.length : null,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
const runStatus = llmMeta?.error ? "warn" : "ok";
|
|
||||||
const isSimulated = provider === "sim" || meta?.source === "sim";
|
const isSimulated = provider === "sim" || meta?.source === "sim";
|
||||||
|
|
||||||
const invariants = {
|
const invariants = {
|
||||||
@@ -401,6 +342,10 @@ export async function processMessage({
|
|||||||
woo_customer_error: wooCustomerError,
|
woo_customer_error: wooCustomerError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// #region agent log
|
||||||
|
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({location:"pipeline.js:345",message:"pipeline_saving_context",data:{reducedContext_basket_count:reducedContext?.order_basket?.items?.length||0,context_patch_basket_count:decision?.context_patch?.order_basket?.items?.length||0,final_context_basket_count:context?.order_basket?.items?.length||0,final_context_basket_labels:(context?.order_basket?.items||[]).map(i=>i.label),plan_intent:plan?.intent},timestamp:Date.now(),sessionId:"debug-session",hypothesisId:"H2-H4"})}).catch(()=>{});
|
||||||
|
// #endregion
|
||||||
|
|
||||||
const nextState = safeNextState(prev_state, context, { requested_checkout: plan.intent === "checkout" }).next_state;
|
const nextState = safeNextState(prev_state, context, { requested_checkout: plan.intent === "checkout" }).next_state;
|
||||||
plan.next_state = nextState;
|
plan.next_state = nextState;
|
||||||
|
|
||||||
@@ -429,6 +374,9 @@ export async function processMessage({
|
|||||||
await updateRunLatency({ tenant_id: tenantId, run_id, latency_ms: end_to_end_ms });
|
await updateRunLatency({ tenant_id: tenantId, run_id, latency_ms: end_to_end_ms });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Incluir carrito completo para la UI
|
||||||
|
const fullBasket = context?.order_basket?.items || [];
|
||||||
|
|
||||||
sseSend("run.created", {
|
sseSend("run.created", {
|
||||||
run_id,
|
run_id,
|
||||||
ts: nowIso(),
|
ts: nowIso(),
|
||||||
@@ -437,7 +385,7 @@ export async function processMessage({
|
|||||||
status: runStatus,
|
status: runStatus,
|
||||||
prev_state,
|
prev_state,
|
||||||
input: { text },
|
input: { text },
|
||||||
llm_output: { ...plan, _llm: llmMeta },
|
llm_output: { ...plan, _llm: llmMeta, full_basket: { items: fullBasket } },
|
||||||
tools,
|
tools,
|
||||||
invariants,
|
invariants,
|
||||||
final_reply: plan.reply,
|
final_reply: plan.reply,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
import { debug as dbg } from "../shared/debug.js";
|
import { debug as dbg } from "../shared/debug.js";
|
||||||
import { searchSnapshotItems } from "../shared/wooSnapshot.js";
|
import { searchSnapshotItems, getSnapshotItemsByIds } from "../shared/wooSnapshot.js";
|
||||||
import {
|
import {
|
||||||
searchProductAliases,
|
searchProductAliases,
|
||||||
getProductEmbedding,
|
getProductEmbedding,
|
||||||
@@ -137,48 +137,53 @@ export async function retrieveCandidates({
|
|||||||
|
|
||||||
const audit = { query: q, sources: {}, boosts: {}, embeddings: {} };
|
const audit = { query: q, sources: {}, boosts: {}, embeddings: {} };
|
||||||
|
|
||||||
|
// 1) Buscar aliases que matcheen la query
|
||||||
const aliases = await searchProductAliases({ tenant_id: tenantId, q, limit: 20 });
|
const aliases = await searchProductAliases({ tenant_id: tenantId, q, limit: 20 });
|
||||||
const aliasBoostByProduct = new Map();
|
const aliasBoostByProduct = new Map();
|
||||||
|
const aliasProductIds = new Set();
|
||||||
for (const a of aliases) {
|
for (const a of aliases) {
|
||||||
if (a?.woo_product_id) {
|
if (a?.woo_product_id) {
|
||||||
const id = Number(a.woo_product_id);
|
const id = Number(a.woo_product_id);
|
||||||
const boost = Number(a.boost || 0);
|
const boost = Number(a.boost || 0);
|
||||||
aliasBoostByProduct.set(id, Math.max(aliasBoostByProduct.get(id) || 0, boost || 0));
|
aliasBoostByProduct.set(id, Math.max(aliasBoostByProduct.get(id) || 0, boost || 0));
|
||||||
|
aliasProductIds.add(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
audit.sources.aliases = aliases.length;
|
audit.sources.aliases = aliases.length;
|
||||||
|
|
||||||
|
// 2) Buscar productos por nombre/slug (búsqueda literal)
|
||||||
const { items: wooItems, source: wooSource } = await searchSnapshotItems({
|
const { items: wooItems, source: wooSource } = await searchSnapshotItems({
|
||||||
tenantId,
|
tenantId,
|
||||||
q,
|
q,
|
||||||
limit: lim,
|
limit: lim,
|
||||||
});
|
});
|
||||||
audit.sources.snapshot = { source: wooSource, count: wooItems?.length || 0 };
|
audit.sources.snapshot = { source: wooSource, count: wooItems?.length || 0 };
|
||||||
// #region agent log
|
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H9",
|
|
||||||
location: "catalogRetrieval.js:158",
|
|
||||||
message: "catalog_sources",
|
|
||||||
data: {
|
|
||||||
query: q,
|
|
||||||
aliases_count: aliases.length,
|
|
||||||
snapshot_count: wooItems?.length || 0,
|
|
||||||
snapshot_source: wooSource || null,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
let candidates = (wooItems || []).map((c) => {
|
// 3) Traer productos que matchearon por alias pero no por búsqueda literal
|
||||||
|
const foundIds = new Set(wooItems.map(w => Number(w.woo_product_id)));
|
||||||
|
const missingAliasIds = [...aliasProductIds].filter(id => !foundIds.has(id));
|
||||||
|
let aliasItems = [];
|
||||||
|
if (missingAliasIds.length > 0) {
|
||||||
|
const { items: fromAlias } = await getSnapshotItemsByIds({
|
||||||
|
tenantId,
|
||||||
|
wooProductIds: missingAliasIds,
|
||||||
|
});
|
||||||
|
aliasItems = fromAlias || [];
|
||||||
|
audit.sources.alias_products = aliasItems.length;
|
||||||
|
}
|
||||||
|
// 4) Combinar productos de búsqueda literal + productos de aliases
|
||||||
|
const allItems = [...(wooItems || []), ...aliasItems];
|
||||||
|
|
||||||
|
let candidates = allItems.map((c) => {
|
||||||
const lit = literalScore(q, c);
|
const lit = literalScore(q, c);
|
||||||
const boost = aliasBoostByProduct.get(Number(c.woo_product_id)) || 0;
|
const boost = aliasBoostByProduct.get(Number(c.woo_product_id)) || 0;
|
||||||
return { ...c, _score: lit + boost, _score_detail: { literal: lit, alias_boost: boost } };
|
// Productos encontrados solo por alias tienen lit=0 pero boost alto
|
||||||
|
const finalScore = lit + boost + (aliasProductIds.has(Number(c.woo_product_id)) && lit < 0.3 ? 0.5 : 0);
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
_score: finalScore,
|
||||||
|
_score_detail: { literal: lit, alias_boost: boost, from_alias: aliasProductIds.has(Number(c.woo_product_id)) }
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// embeddings: opcional, si hay key y tenemos candidatos
|
// embeddings: opcional, si hay key y tenemos candidatos
|
||||||
|
|||||||
@@ -10,8 +10,11 @@
|
|||||||
export const ConversationState = Object.freeze({
|
export const ConversationState = Object.freeze({
|
||||||
IDLE: "IDLE",
|
IDLE: "IDLE",
|
||||||
BROWSING: "BROWSING",
|
BROWSING: "BROWSING",
|
||||||
|
CLARIFYING_ITEMS: "CLARIFYING_ITEMS", // Clarificando items pendientes uno por uno
|
||||||
AWAITING_QUANTITY: "AWAITING_QUANTITY",
|
AWAITING_QUANTITY: "AWAITING_QUANTITY",
|
||||||
CART_ACTIVE: "CART_ACTIVE",
|
CART_ACTIVE: "CART_ACTIVE",
|
||||||
|
CLARIFYING_PAYMENT: "CLARIFYING_PAYMENT", // Preguntando método de pago (efectivo/link)
|
||||||
|
CLARIFYING_SHIPPING: "CLARIFYING_SHIPPING", // Preguntando delivery o retiro
|
||||||
AWAITING_ADDRESS: "AWAITING_ADDRESS",
|
AWAITING_ADDRESS: "AWAITING_ADDRESS",
|
||||||
AWAITING_PAYMENT: "AWAITING_PAYMENT",
|
AWAITING_PAYMENT: "AWAITING_PAYMENT",
|
||||||
COMPLETED: "COMPLETED",
|
COMPLETED: "COMPLETED",
|
||||||
@@ -34,6 +37,16 @@ function hasPendingItem(ctx) {
|
|||||||
return Boolean(ctx?.pending_item?.product_id || ctx?.pending_item?.sku);
|
return Boolean(ctx?.pending_item?.product_id || ctx?.pending_item?.sku);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si hay items pendientes de clarificar (nuevo modelo acumulativo).
|
||||||
|
* Un item pendiente tiene status "needs_type" o "needs_quantity".
|
||||||
|
*/
|
||||||
|
function hasPendingItems(ctx) {
|
||||||
|
const items = ctx?.pending_items;
|
||||||
|
if (!Array.isArray(items) || items.length === 0) return false;
|
||||||
|
return items.some(i => i.status === "needs_type" || i.status === "needs_quantity");
|
||||||
|
}
|
||||||
|
|
||||||
function hasAddress(ctx) {
|
function hasAddress(ctx) {
|
||||||
return Boolean(ctx?.delivery_address?.text || ctx?.address?.text || ctx?.address_text);
|
return Boolean(ctx?.delivery_address?.text || ctx?.address?.text || ctx?.address_text);
|
||||||
}
|
}
|
||||||
@@ -55,6 +68,34 @@ function isPaid(ctx) {
|
|||||||
return st === "approved" || st === "paid";
|
return st === "approved" || st === "paid";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si estamos clarificando método de pago.
|
||||||
|
*/
|
||||||
|
function isClarifyingPayment(ctx) {
|
||||||
|
return ctx?.checkout_step === "payment_method";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si estamos clarificando shipping (delivery/retiro).
|
||||||
|
*/
|
||||||
|
function isClarifyingShipping(ctx) {
|
||||||
|
return ctx?.checkout_step === "shipping_method";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si ya se eligió método de pago.
|
||||||
|
*/
|
||||||
|
function hasPaymentMethod(ctx) {
|
||||||
|
return Boolean(ctx?.payment_method); // "cash" | "link"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica si ya se eligió método de envío.
|
||||||
|
*/
|
||||||
|
function hasShippingMethod(ctx) {
|
||||||
|
return Boolean(ctx?.shipping_method); // "delivery" | "pickup"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deriva el estado objetivo según el contexto actual y señales del turno.
|
* Deriva el estado objetivo según el contexto actual y señales del turno.
|
||||||
* `signals` es información determinística del motor del turno (no del LLM),
|
* `signals` es información determinística del motor del turno (no del LLM),
|
||||||
@@ -67,20 +108,35 @@ export function deriveNextState(prevState, ctx = {}, signals = {}) {
|
|||||||
// Regla 2: si ya existe orden + link de pago, estamos esperando pago
|
// Regla 2: si ya existe orden + link de pago, estamos esperando pago
|
||||||
if (hasWooOrder(ctx) && hasPaymentLink(ctx)) return ConversationState.AWAITING_PAYMENT;
|
if (hasWooOrder(ctx) && hasPaymentLink(ctx)) return ConversationState.AWAITING_PAYMENT;
|
||||||
|
|
||||||
// Regla 3: si intentó checkout pero falta dirección
|
// Regla 3: si estamos clarificando método de pago
|
||||||
if ((signals.requested_checkout || signals.requested_address) && hasBasketItems(ctx) && !hasAddress(ctx)) {
|
if (isClarifyingPayment(ctx)) {
|
||||||
|
return ConversationState.CLARIFYING_PAYMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regla 4: si estamos clarificando shipping
|
||||||
|
if (isClarifyingShipping(ctx)) {
|
||||||
|
return ConversationState.CLARIFYING_SHIPPING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regla 5: si intentó checkout, tiene shipping=delivery, pero falta dirección
|
||||||
|
if (signals.requested_address || (hasShippingMethod(ctx) && ctx.shipping_method === "delivery" && !hasAddress(ctx))) {
|
||||||
return ConversationState.AWAITING_ADDRESS;
|
return ConversationState.AWAITING_ADDRESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regla 4: si hay item pendiente sin completar cantidad
|
// Regla 6: si hay items pendientes de clarificar (nuevo modelo acumulativo)
|
||||||
|
if (hasPendingItems(ctx)) {
|
||||||
|
return ConversationState.CLARIFYING_ITEMS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regla 7: si hay item pendiente sin completar cantidad (modelo legacy)
|
||||||
if (hasPendingItem(ctx) && !signals.pending_item_completed) {
|
if (hasPendingItem(ctx) && !signals.pending_item_completed) {
|
||||||
return ConversationState.AWAITING_QUANTITY;
|
return ConversationState.AWAITING_QUANTITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regla 5: si hay carrito activo
|
// Regla 8: si hay carrito activo
|
||||||
if (hasBasketItems(ctx)) return ConversationState.CART_ACTIVE;
|
if (hasBasketItems(ctx)) return ConversationState.CART_ACTIVE;
|
||||||
|
|
||||||
// Regla 6: si estamos mostrando opciones / esperando selección
|
// Regla 9: si estamos mostrando opciones / esperando selección (modelo legacy)
|
||||||
if (hasPendingClarification(ctx) || signals.did_show_options || signals.is_browsing) {
|
if (hasPendingClarification(ctx) || signals.did_show_options || signals.is_browsing) {
|
||||||
return ConversationState.BROWSING;
|
return ConversationState.BROWSING;
|
||||||
}
|
}
|
||||||
@@ -92,30 +148,55 @@ const ALLOWED = Object.freeze({
|
|||||||
[ConversationState.IDLE]: [
|
[ConversationState.IDLE]: [
|
||||||
ConversationState.IDLE,
|
ConversationState.IDLE,
|
||||||
ConversationState.BROWSING,
|
ConversationState.BROWSING,
|
||||||
|
ConversationState.CLARIFYING_ITEMS,
|
||||||
ConversationState.AWAITING_QUANTITY,
|
ConversationState.AWAITING_QUANTITY,
|
||||||
ConversationState.CART_ACTIVE,
|
ConversationState.CART_ACTIVE,
|
||||||
ConversationState.ERROR_RECOVERY,
|
ConversationState.ERROR_RECOVERY,
|
||||||
],
|
],
|
||||||
[ConversationState.BROWSING]: [
|
[ConversationState.BROWSING]: [
|
||||||
ConversationState.BROWSING,
|
ConversationState.BROWSING,
|
||||||
|
ConversationState.CLARIFYING_ITEMS,
|
||||||
ConversationState.AWAITING_QUANTITY,
|
ConversationState.AWAITING_QUANTITY,
|
||||||
ConversationState.CART_ACTIVE,
|
ConversationState.CART_ACTIVE,
|
||||||
ConversationState.IDLE,
|
ConversationState.IDLE,
|
||||||
ConversationState.ERROR_RECOVERY,
|
ConversationState.ERROR_RECOVERY,
|
||||||
],
|
],
|
||||||
|
[ConversationState.CLARIFYING_ITEMS]: [
|
||||||
|
ConversationState.CLARIFYING_ITEMS,
|
||||||
|
ConversationState.CART_ACTIVE,
|
||||||
|
ConversationState.BROWSING,
|
||||||
|
ConversationState.IDLE,
|
||||||
|
ConversationState.ERROR_RECOVERY,
|
||||||
|
],
|
||||||
[ConversationState.AWAITING_QUANTITY]: [
|
[ConversationState.AWAITING_QUANTITY]: [
|
||||||
ConversationState.AWAITING_QUANTITY,
|
ConversationState.AWAITING_QUANTITY,
|
||||||
|
ConversationState.CLARIFYING_ITEMS,
|
||||||
ConversationState.CART_ACTIVE,
|
ConversationState.CART_ACTIVE,
|
||||||
ConversationState.BROWSING,
|
ConversationState.BROWSING,
|
||||||
ConversationState.ERROR_RECOVERY,
|
ConversationState.ERROR_RECOVERY,
|
||||||
],
|
],
|
||||||
[ConversationState.CART_ACTIVE]: [
|
[ConversationState.CART_ACTIVE]: [
|
||||||
ConversationState.CART_ACTIVE,
|
ConversationState.CART_ACTIVE,
|
||||||
|
ConversationState.CLARIFYING_ITEMS,
|
||||||
|
ConversationState.CLARIFYING_PAYMENT,
|
||||||
ConversationState.AWAITING_ADDRESS,
|
ConversationState.AWAITING_ADDRESS,
|
||||||
ConversationState.AWAITING_PAYMENT,
|
ConversationState.AWAITING_PAYMENT,
|
||||||
ConversationState.ERROR_RECOVERY,
|
ConversationState.ERROR_RECOVERY,
|
||||||
ConversationState.BROWSING,
|
ConversationState.BROWSING,
|
||||||
],
|
],
|
||||||
|
[ConversationState.CLARIFYING_PAYMENT]: [
|
||||||
|
ConversationState.CLARIFYING_PAYMENT,
|
||||||
|
ConversationState.CLARIFYING_SHIPPING,
|
||||||
|
ConversationState.CART_ACTIVE, // Volver si cancela
|
||||||
|
ConversationState.ERROR_RECOVERY,
|
||||||
|
],
|
||||||
|
[ConversationState.CLARIFYING_SHIPPING]: [
|
||||||
|
ConversationState.CLARIFYING_SHIPPING,
|
||||||
|
ConversationState.AWAITING_ADDRESS, // Si elige delivery
|
||||||
|
ConversationState.AWAITING_PAYMENT, // Si elige retiro (directo a crear orden)
|
||||||
|
ConversationState.CLARIFYING_PAYMENT, // Volver a cambiar pago
|
||||||
|
ConversationState.ERROR_RECOVERY,
|
||||||
|
],
|
||||||
[ConversationState.AWAITING_ADDRESS]: [
|
[ConversationState.AWAITING_ADDRESS]: [
|
||||||
ConversationState.AWAITING_ADDRESS,
|
ConversationState.AWAITING_ADDRESS,
|
||||||
ConversationState.AWAITING_PAYMENT,
|
ConversationState.AWAITING_PAYMENT,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const NluV3JsonSchema = {
|
|||||||
properties: {
|
properties: {
|
||||||
intent: {
|
intent: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["price_query", "browse", "add_to_cart", "remove_from_cart", "checkout", "greeting", "recommend", "other"],
|
enum: ["price_query", "browse", "add_to_cart", "remove_from_cart", "checkout", "confirm_order", "select_payment", "select_shipping", "provide_address", "greeting", "recommend", "view_cart", "other"],
|
||||||
},
|
},
|
||||||
confidence: { type: "number", minimum: 0, maximum: 1 },
|
confidence: { type: "number", minimum: 0, maximum: 1 },
|
||||||
language: { type: "string" },
|
language: { type: "string" },
|
||||||
@@ -103,6 +103,10 @@ const NluV3JsonSchema = {
|
|||||||
},
|
},
|
||||||
attributes: { type: "array", items: { type: "string" } },
|
attributes: { type: "array", items: { type: "string" } },
|
||||||
preparation: { type: "array", items: { type: "string" } },
|
preparation: { type: "array", items: { type: "string" } },
|
||||||
|
// Checkout: método de pago, envío, dirección
|
||||||
|
payment_method: { anyOf: [{ type: "string", enum: ["cash", "link"] }, { type: "null" }] },
|
||||||
|
shipping_method: { anyOf: [{ type: "string", enum: ["delivery", "pickup"] }, { type: "null" }] },
|
||||||
|
address: { anyOf: [{ type: "string" }, { type: "null" }] },
|
||||||
// Soporte para múltiples productos en un mensaje
|
// Soporte para múltiples productos en un mensaje
|
||||||
items: {
|
items: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
@@ -231,6 +235,10 @@ function normalizeNluOutput(parsed, input) {
|
|||||||
selection: entities.selection ?? null,
|
selection: entities.selection ?? null,
|
||||||
attributes: Array.isArray(entities.attributes) ? entities.attributes : [],
|
attributes: Array.isArray(entities.attributes) ? entities.attributes : [],
|
||||||
preparation: Array.isArray(entities.preparation) ? entities.preparation : [],
|
preparation: Array.isArray(entities.preparation) ? entities.preparation : [],
|
||||||
|
// Checkout entities (opcionales)
|
||||||
|
payment_method: entities.payment_method ?? null,
|
||||||
|
shipping_method: entities.shipping_method ?? null,
|
||||||
|
address: entities.address ?? null,
|
||||||
items: normalizedItems,
|
items: normalizedItems,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -250,27 +258,7 @@ function normalizeNluOutput(parsed, input) {
|
|||||||
const canInfer = hasShownOptions && !hasPendingItem;
|
const canInfer = hasShownOptions && !hasPendingItem;
|
||||||
const inferred = canInfer ? inferSelectionFromText(input?.last_user_message) : null;
|
const inferred = canInfer ? inferSelectionFromText(input?.last_user_message) : null;
|
||||||
out.entities.selection = inferred || null;
|
out.entities.selection = inferred || null;
|
||||||
// #region agent log
|
}
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H11",
|
|
||||||
location: "openai.js:129",
|
|
||||||
message: "selection_inferred",
|
|
||||||
data: {
|
|
||||||
inferred: Boolean(inferred),
|
|
||||||
pending_item: hasPendingItem,
|
|
||||||
has_shown_options: hasShownOptions,
|
|
||||||
text: String(input?.last_user_message || "").slice(0, 20),
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out.needs = {
|
out.needs = {
|
||||||
@@ -293,6 +281,9 @@ function nluV3Fallback() {
|
|||||||
selection: null,
|
selection: null,
|
||||||
attributes: [],
|
attributes: [],
|
||||||
preparation: [],
|
preparation: [],
|
||||||
|
payment_method: null,
|
||||||
|
shipping_method: null,
|
||||||
|
address: null,
|
||||||
items: null,
|
items: null,
|
||||||
},
|
},
|
||||||
needs: { catalog_lookup: false, knowledge_lookup: false },
|
needs: { catalog_lookup: false, knowledge_lookup: false },
|
||||||
@@ -322,7 +313,15 @@ export async function llmNluV3({ input, model } = {}) {
|
|||||||
"- selection SOLO aplica cuando hay opciones visibles para seleccionar (last_shown_options tiene elementos).\n" +
|
"- selection SOLO aplica cuando hay opciones visibles para seleccionar (last_shown_options tiene elementos).\n" +
|
||||||
"- Si el usuario responde 'mostrame más', poné intent='browse' y entities.selection=null (la paginación la maneja el servidor).\n" +
|
"- Si el usuario responde 'mostrame más', poné intent='browse' y entities.selection=null (la paginación la maneja el servidor).\n" +
|
||||||
"- needs.catalog_lookup debe ser true para intents price_query|browse|add_to_cart si NO es una pura selección sobre opciones ya mostradas.\n" +
|
"- needs.catalog_lookup debe ser true para intents price_query|browse|add_to_cart si NO es una pura selección sobre opciones ya mostradas.\n" +
|
||||||
"- Si el usuario pide recomendación/sugerencias, usá intent='recommend' y needs.catalog_lookup=true.\n" +
|
"- PREGUNTAS SOBRE DISPONIBILIDAD: Si el usuario pregunta si hay/venden/tienen un producto (ej: 'vendés vino?', 'tenés chimichurri?', 'hay provoleta?'), usá intent='browse' con product_query=ese producto. needs.catalog_lookup=true.\n" +
|
||||||
|
"- RECOMENDACIONES: SOLO usá intent='recommend' si el usuario pide sugerencias SIN mencionar ningún producto (ej: 'qué me recomendás?', 'qué me sugerís?'). Si menciona CUALQUIER producto, usá intent='add_to_cart' con product_query=ese producto. Ejemplos que son add_to_cart: 'me recomendás un vino?', 'recomendame un vino', 'qué vino me recomendás?', 'tenés algún vino bueno?' → TODOS son add_to_cart con product_query='vino'.\n" +
|
||||||
|
"- COMPRAR/PEDIR PRODUCTOS: Si el usuario quiere comprar/pedir/llevar productos (ej: 'quiero comprar X', 'quiero X', 'dame X', 'necesito X', 'anotame X'), usá intent='add_to_cart'. needs.catalog_lookup=true. Aunque incluya un saludo o pida recomendación, si menciona productos específicos es add_to_cart.\n" +
|
||||||
|
"- SALUDOS: Si el usuario SOLO saluda sin mencionar productos (hola, buen día, buenas tardes, buenas noches, qué tal, hey, hi), usá intent='greeting'. needs.catalog_lookup=false.\n" +
|
||||||
|
"- VER CARRITO: Si el usuario pregunta qué tiene anotado/pedido/en el carrito (ej: 'qué tengo?', 'qué llevó?', 'qué anoté?', 'mostrame mi pedido'), usá intent='view_cart'. needs.catalog_lookup=false.\n" +
|
||||||
|
"- CONFIRMAR ORDEN: Si el usuario quiere cerrar/confirmar el pedido (ej: 'listo', 'eso es todo', 'cerrar pedido', 'ya está', 'nada más'), usá intent='confirm_order'. needs.catalog_lookup=false.\n" +
|
||||||
|
"- SELECCIONAR PAGO: Si el usuario elige método de pago (ej: 'efectivo', 'tarjeta', 'link de pago', 'transferencia'), usá intent='select_payment'. Extraer entities.payment_method='cash'|'link'.\n" +
|
||||||
|
"- SELECCIONAR ENVÍO: Si el usuario elige envío (ej: 'delivery', 'envío', 'que me lo traigan', 'retiro', 'paso a buscar'), usá intent='select_shipping'. Extraer entities.shipping_method='delivery'|'pickup'.\n" +
|
||||||
|
"- DAR DIRECCIÓN: Si el usuario da una dirección de entrega, usá intent='provide_address'. Extraer entities.address con el texto de la dirección.\n" +
|
||||||
"- MULTI-ITEMS: Si el usuario menciona MÚLTIPLES productos en un mensaje (ej: '1 chimichurri y 2 provoletas'), usá entities.items con array de objetos.\n" +
|
"- MULTI-ITEMS: Si el usuario menciona MÚLTIPLES productos en un mensaje (ej: '1 chimichurri y 2 provoletas'), usá entities.items con array de objetos.\n" +
|
||||||
" Ejemplo: items:[{product_query:'chimichurri',quantity:1,unit:null},{product_query:'provoletas',quantity:2,unit:null}]\n" +
|
" Ejemplo: items:[{product_query:'chimichurri',quantity:1,unit:null},{product_query:'provoletas',quantity:2,unit:null}]\n" +
|
||||||
" En este caso, product_query/quantity/unit del nivel superior quedan null.\n" +
|
" En este caso, product_query/quantity/unit del nivel superior quedan null.\n" +
|
||||||
@@ -351,53 +350,14 @@ export async function llmNluV3({ input, model } = {}) {
|
|||||||
|
|
||||||
// intento 1
|
// intento 1
|
||||||
const first = await jsonCompletion({ system: systemBase, user, model });
|
const first = await jsonCompletion({ system: systemBase, user, model });
|
||||||
const firstNormalized = normalizeNluOutput(first.parsed, input);
|
const firstNormalized = normalizeNluOutput(first.parsed, input);
|
||||||
// #region agent log
|
const validationResult = validateNluV3(firstNormalized);
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
if (validationResult) {
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H10",
|
|
||||||
location: "openai.js:196",
|
|
||||||
message: "nlu_normalized_first",
|
|
||||||
data: {
|
|
||||||
intent: firstNormalized?.intent || null,
|
|
||||||
unit: firstNormalized?.entities?.unit || null,
|
|
||||||
selection: firstNormalized?.entities?.selection ? "set" : "null",
|
|
||||||
needs_catalog: Boolean(firstNormalized?.needs?.catalog_lookup),
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
if (validateNluV3(firstNormalized)) {
|
|
||||||
return { nlu: firstNormalized, raw_text: first.raw_text, model: first.model, usage: first.usage, schema: "v3", validation: { ok: true } };
|
return { nlu: firstNormalized, raw_text: first.raw_text, model: first.model, usage: first.usage, schema: "v3", validation: { ok: true } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors1 = nluV3Errors();
|
const errors1 = nluV3Errors();
|
||||||
// #region agent log
|
// retry 1 vez
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H7",
|
|
||||||
location: "openai.js:169",
|
|
||||||
message: "nlu_validation_failed_first",
|
|
||||||
data: {
|
|
||||||
errors_count: Array.isArray(errors1) ? errors1.length : null,
|
|
||||||
errors: Array.isArray(errors1) ? errors1.slice(0, 5) : null,
|
|
||||||
parsed_keys: first?.parsed ? Object.keys(first.parsed) : null,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// retry 1 vez
|
|
||||||
const systemRetry =
|
const systemRetry =
|
||||||
systemBase +
|
systemBase +
|
||||||
"\nTu respuesta anterior no validó el JSON Schema. Corregí el JSON para que cumpla estrictamente.\n" +
|
"\nTu respuesta anterior no validó el JSON Schema. Corregí el JSON para que cumpla estrictamente.\n" +
|
||||||
@@ -406,50 +366,11 @@ export async function llmNluV3({ input, model } = {}) {
|
|||||||
try {
|
try {
|
||||||
const second = await jsonCompletion({ system: systemRetry, user, model });
|
const second = await jsonCompletion({ system: systemRetry, user, model });
|
||||||
const secondNormalized = normalizeNluOutput(second.parsed, input);
|
const secondNormalized = normalizeNluOutput(second.parsed, input);
|
||||||
// #region agent log
|
if (validateNluV3(secondNormalized)) {
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H10",
|
|
||||||
location: "openai.js:242",
|
|
||||||
message: "nlu_normalized_retry",
|
|
||||||
data: {
|
|
||||||
intent: secondNormalized?.intent || null,
|
|
||||||
unit: secondNormalized?.entities?.unit || null,
|
|
||||||
selection: secondNormalized?.entities?.selection ? "set" : "null",
|
|
||||||
needs_catalog: Boolean(secondNormalized?.needs?.catalog_lookup),
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
if (validateNluV3(secondNormalized)) {
|
|
||||||
return { nlu: secondNormalized, raw_text: second.raw_text, model: second.model, usage: second.usage, schema: "v3", validation: { ok: true, retried: true } };
|
return { nlu: secondNormalized, raw_text: second.raw_text, model: second.model, usage: second.usage, schema: "v3", validation: { ok: true, retried: true } };
|
||||||
}
|
}
|
||||||
const errors2 = nluV3Errors();
|
const errors2 = nluV3Errors();
|
||||||
// #region agent log
|
return {
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H7",
|
|
||||||
location: "openai.js:187",
|
|
||||||
message: "nlu_validation_failed_retry",
|
|
||||||
data: {
|
|
||||||
errors_count: Array.isArray(errors2) ? errors2.length : null,
|
|
||||||
errors: Array.isArray(errors2) ? errors2.slice(0, 5) : null,
|
|
||||||
parsed_keys: second?.parsed ? Object.keys(second.parsed) : null,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
return {
|
|
||||||
nlu: nluV3Fallback(),
|
nlu: nluV3Fallback(),
|
||||||
raw_text: second.raw_text,
|
raw_text: second.raw_text,
|
||||||
model: second.model,
|
model: second.model,
|
||||||
@@ -517,5 +438,3 @@ export async function llmRecommendWriter({
|
|||||||
validation: { ok: false, errors: validateRecommendWriter.errors || [] },
|
validation: { ok: false, errors: validateRecommendWriter.errors || [] },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy llmPlan/llmExtract y NLU v2 removidos.
|
|
||||||
|
|||||||
@@ -1,102 +1,33 @@
|
|||||||
import { getRecoRules } from "../2-identity/db/repo.js";
|
import { getRecoRules, getRecoRulesByProductIds } from "../2-identity/db/repo.js";
|
||||||
import { retrieveCandidates } from "./catalogRetrieval.js";
|
import { getSnapshotItemsByIds } from "../shared/wooSnapshot.js";
|
||||||
import { buildPagedOptions } from "./turnEngineV3.pendingSelection.js";
|
import { buildPagedOptions } from "./turnEngineV3.pendingSelection.js";
|
||||||
import { llmRecommendWriter } from "./openai.js";
|
|
||||||
|
|
||||||
function normalizeText(s) {
|
/**
|
||||||
return String(s || "")
|
* Extrae los IDs de productos del carrito.
|
||||||
.toLowerCase()
|
*/
|
||||||
.replace(/[¿?¡!.,;:()"]/g, " ")
|
function getBasketProductIds(basket_items) {
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseYesNo(text) {
|
|
||||||
const t = normalizeText(text);
|
|
||||||
if (!t) return null;
|
|
||||||
if (/\b(si|sí|sisi|dale|ok|claro|obvio|tomo)\b/.test(t)) return true;
|
|
||||||
if (/\b(no|nop|nunca|nope|sin alcohol)\b/.test(t)) return false;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickBaseItem({ prev_context, basket_items }) {
|
|
||||||
const pending = prev_context?.pending_item;
|
|
||||||
if (pending?.name) {
|
|
||||||
return {
|
|
||||||
product_id: pending.product_id || null,
|
|
||||||
name: pending.name,
|
|
||||||
label: pending.name,
|
|
||||||
categories: pending.categories || [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const items = Array.isArray(basket_items) ? basket_items : [];
|
const items = Array.isArray(basket_items) ? basket_items : [];
|
||||||
const last = items[items.length - 1];
|
return items
|
||||||
if (!last) return null;
|
.map(item => item.product_id || item.woo_product_id)
|
||||||
return {
|
.filter(id => id != null)
|
||||||
product_id: last.product_id || null,
|
.map(Number);
|
||||||
name: last.label || last.name || "ese producto",
|
|
||||||
label: last.label || last.name || "ese producto",
|
|
||||||
categories: last.categories || [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ruleMatchesBase({ rule, base_item, slots }) {
|
/**
|
||||||
const trigger = rule?.trigger && typeof rule.trigger === "object" ? rule.trigger : {};
|
* Obtiene los IDs de productos recomendados de las reglas que matchean.
|
||||||
const text = normalizeText(base_item?.name || base_item?.label || "");
|
*/
|
||||||
const categories = Array.isArray(base_item?.categories) ? base_item.categories.map((c) => normalizeText(c?.name || c)) : [];
|
function collectRecommendedIds(rules, excludeIds = []) {
|
||||||
const keywords = Array.isArray(trigger.keywords) ? trigger.keywords.map(normalizeText).filter(Boolean) : [];
|
const excludeSet = new Set(excludeIds);
|
||||||
const cats = Array.isArray(trigger.categories) ? trigger.categories.map(normalizeText).filter(Boolean) : [];
|
const ids = new Set();
|
||||||
const always = Boolean(trigger.always);
|
for (const rule of rules) {
|
||||||
if (typeof trigger.alcohol === "boolean") {
|
const recoIds = Array.isArray(rule.recommended_product_ids) ? rule.recommended_product_ids : [];
|
||||||
if (slots?.alcohol == null) return false;
|
for (const id of recoIds) {
|
||||||
if (slots.alcohol !== trigger.alcohol) return false;
|
if (!excludeSet.has(id)) {
|
||||||
}
|
ids.add(id);
|
||||||
if (always) return true;
|
|
||||||
if (keywords.length && keywords.some((k) => text.includes(k))) return true;
|
|
||||||
if (cats.length && categories.some((c) => cats.includes(c))) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectAskSlots(rules) {
|
|
||||||
const out = [];
|
|
||||||
for (const r of rules) {
|
|
||||||
const ask = Array.isArray(r.ask_slots) ? r.ask_slots : [];
|
|
||||||
for (const slot of ask) {
|
|
||||||
if (slot && slot.slot) out.push(slot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectQueries({ rules, slots }) {
|
|
||||||
const out = [];
|
|
||||||
for (const r of rules) {
|
|
||||||
const q = Array.isArray(r.queries) ? r.queries : [];
|
|
||||||
for (const item of q) {
|
|
||||||
if (!item || typeof item !== "string") continue;
|
|
||||||
if (item.includes("{alcohol}")) {
|
|
||||||
const v = slots?.alcohol;
|
|
||||||
if (v == null) continue;
|
|
||||||
out.push(item.replace("{alcohol}", v ? "si" : "no"));
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
out.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out.map((x) => x.trim()).filter(Boolean);
|
return [...ids];
|
||||||
}
|
|
||||||
|
|
||||||
function mergeCandidates({ lists, excludeId }) {
|
|
||||||
const map = new Map();
|
|
||||||
for (const list of lists) {
|
|
||||||
for (const c of list || []) {
|
|
||||||
const id = Number(c?.woo_product_id);
|
|
||||||
if (!id || (excludeId && id === excludeId)) continue;
|
|
||||||
const prev = map.get(id);
|
|
||||||
if (!prev || (c._score || 0) > (prev._score || 0)) map.set(id, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [...map.values()].sort((a, b) => (b._score || 0) - (a._score || 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleRecommend({
|
export async function handleRecommend({
|
||||||
@@ -106,14 +37,16 @@ export async function handleRecommend({
|
|||||||
basket_items = [],
|
basket_items = [],
|
||||||
limit = 9,
|
limit = 9,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const reco = prev_context?.reco && typeof prev_context.reco === "object" ? prev_context.reco : {};
|
const context_patch = {};
|
||||||
const base_item = reco.base_item || pickBaseItem({ prev_context, basket_items });
|
const audit = { basket_product_ids: [], rules_used: [], recommended_ids: [] };
|
||||||
const context_patch = { reco: { ...reco, base_item } };
|
|
||||||
const audit = { base_item, rules_used: [], queries: [] };
|
|
||||||
|
|
||||||
if (!base_item?.name) {
|
// 1. Obtener IDs de productos en el carrito
|
||||||
|
const basketProductIds = getBasketProductIds(basket_items);
|
||||||
|
audit.basket_product_ids = basketProductIds;
|
||||||
|
|
||||||
|
if (!basketProductIds.length) {
|
||||||
return {
|
return {
|
||||||
reply: "¿Sobre qué producto querés recomendaciones?",
|
reply: "Primero agregá algo al carrito y después te sugiero complementos perfectos.",
|
||||||
actions: [],
|
actions: [],
|
||||||
context_patch,
|
context_patch,
|
||||||
audit,
|
audit,
|
||||||
@@ -122,63 +55,15 @@ export async function handleRecommend({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIMERO: Inicializar slots y procesar respuesta pendiente ANTES de filtrar reglas
|
// 2. Buscar reglas que matcheen con los productos del carrito
|
||||||
const slots = { ...(reco.slots || {}) };
|
const rules = await getRecoRulesByProductIds({ tenant_id: tenantId, product_ids: basketProductIds });
|
||||||
let asked_slot = null;
|
|
||||||
|
|
||||||
// Procesar respuesta de slot pendiente PRIMERO
|
|
||||||
if (reco.awaiting_slot === "alcohol") {
|
|
||||||
const yn = parseYesNo(text);
|
|
||||||
if (yn != null) {
|
|
||||||
slots.alcohol = yn;
|
|
||||||
context_patch.reco = { ...context_patch.reco, slots, awaiting_slot: null };
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
reply: "¿Tomás alcohol?",
|
|
||||||
actions: [],
|
|
||||||
context_patch: { ...context_patch, reco: { ...context_patch.reco, awaiting_slot: "alcohol" } },
|
|
||||||
audit,
|
|
||||||
asked_slot: "alcohol",
|
|
||||||
candidates: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DESPUÉS: Cargar y filtrar reglas CON SLOTS ACTUALIZADOS
|
|
||||||
const rulesRaw = await getRecoRules({ tenant_id: tenantId });
|
|
||||||
const rules = (rulesRaw || []).filter((r) => ruleMatchesBase({ rule: r, base_item, slots }));
|
|
||||||
audit.rules_used = rules.map((r) => ({ id: r.id, rule_key: r.rule_key, priority: r.priority }));
|
audit.rules_used = rules.map((r) => ({ id: r.id, rule_key: r.rule_key, priority: r.priority }));
|
||||||
|
|
||||||
// Verificar si hay slots pendientes por preguntar
|
if (!rules.length) {
|
||||||
const askSlots = collectAskSlots(rules);
|
// Fallback: no hay reglas configuradas para estos productos
|
||||||
if (!context_patch.reco.awaiting_slot) {
|
const basketNames = basket_items.map(i => i.label || i.name).filter(Boolean).slice(0, 3).join(", ");
|
||||||
const pending = askSlots.find((s) => s?.slot === "alcohol" && slots.alcohol == null);
|
|
||||||
if (pending) {
|
|
||||||
asked_slot = "alcohol";
|
|
||||||
context_patch.reco = { ...context_patch.reco, slots, awaiting_slot: "alcohol" };
|
|
||||||
return {
|
|
||||||
reply: pending.question || "¿Tomás alcohol?",
|
|
||||||
actions: [],
|
|
||||||
context_patch,
|
|
||||||
audit,
|
|
||||||
asked_slot,
|
|
||||||
candidates: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const queries = collectQueries({ rules, slots });
|
|
||||||
audit.queries = queries;
|
|
||||||
const lists = [];
|
|
||||||
for (const q of queries.slice(0, 6)) {
|
|
||||||
const { candidates } = await retrieveCandidates({ tenantId, query: q, limit });
|
|
||||||
lists.push(candidates || []);
|
|
||||||
}
|
|
||||||
const merged = mergeCandidates({ lists, excludeId: base_item.product_id });
|
|
||||||
|
|
||||||
if (!merged.length) {
|
|
||||||
return {
|
return {
|
||||||
reply: `No encontré recomendaciones para ${base_item.name}. ¿Querés que te sugiera algo distinto?`,
|
reply: `Por ahora no tengo recomendaciones especiales para ${basketNames}. ¿Te interesa algo más?`,
|
||||||
actions: [],
|
actions: [],
|
||||||
context_patch,
|
context_patch,
|
||||||
audit,
|
audit,
|
||||||
@@ -187,22 +72,46 @@ export async function handleRecommend({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { question, pending } = buildPagedOptions({ candidates: merged, pageSize: Math.min(9, limit) });
|
// 3. Obtener IDs de productos recomendados (excluyendo los que ya están en el carrito)
|
||||||
let reply = question;
|
const recommendedIds = collectRecommendedIds(rules, basketProductIds);
|
||||||
if (process.env.RECO_WRITER === "1") {
|
audit.recommended_ids = recommendedIds;
|
||||||
const writer = await llmRecommendWriter({
|
|
||||||
base_item,
|
if (!recommendedIds.length) {
|
||||||
slots,
|
return {
|
||||||
candidates: merged.slice(0, limit),
|
reply: "No encontré complementos adicionales para tu pedido. ¿Necesitás algo más?",
|
||||||
});
|
actions: [],
|
||||||
if (writer?.validation?.ok && writer.reply) {
|
context_patch,
|
||||||
reply = writer.reply;
|
audit,
|
||||||
}
|
asked_slot: null,
|
||||||
audit.writer = {
|
candidates: [],
|
||||||
ok: Boolean(writer?.validation?.ok),
|
|
||||||
model: writer?.model || null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Obtener detalles de los productos recomendados
|
||||||
|
const recommendedProducts = await getSnapshotItemsByIds({ tenantId, wooProductIds: recommendedIds.slice(0, limit) });
|
||||||
|
|
||||||
|
if (!recommendedProducts.length) {
|
||||||
|
return {
|
||||||
|
reply: "No encontré los productos recomendados disponibles. ¿Querés ver algo más?",
|
||||||
|
actions: [],
|
||||||
|
context_patch,
|
||||||
|
audit,
|
||||||
|
asked_slot: null,
|
||||||
|
candidates: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Construir respuesta con opciones
|
||||||
|
const { question, pending } = buildPagedOptions({ candidates: recommendedProducts, pageSize: Math.min(9, limit) });
|
||||||
|
|
||||||
|
// Personalizar el mensaje según lo que tiene en el carrito
|
||||||
|
const basketNames = basket_items.map(i => i.label || i.name).filter(Boolean).slice(0, 2).join(" y ");
|
||||||
|
const intro = basketNames
|
||||||
|
? `Para acompañar ${basketNames}, te recomiendo:`
|
||||||
|
: "Te recomiendo estos productos:";
|
||||||
|
|
||||||
|
const reply = `${intro}\n\n${question}`;
|
||||||
|
|
||||||
context_patch.pending_clarification = pending;
|
context_patch.pending_clarification = pending;
|
||||||
context_patch.pending_item = null;
|
context_patch.pending_item = null;
|
||||||
|
|
||||||
@@ -212,6 +121,6 @@ export async function handleRecommend({
|
|||||||
context_patch,
|
context_patch,
|
||||||
audit,
|
audit,
|
||||||
asked_slot: null,
|
asked_slot: null,
|
||||||
candidates: merged.slice(0, limit),
|
candidates: recommendedProducts.slice(0, limit),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -146,36 +146,7 @@ export async function searchSnapshotItems({ tenantId, q = "", limit = 12 }) {
|
|||||||
const query = String(q || "").trim();
|
const query = String(q || "").trim();
|
||||||
if (!query) return { items: [], source: "snapshot" };
|
if (!query) return { items: [], source: "snapshot" };
|
||||||
const like = `%${query}%`;
|
const like = `%${query}%`;
|
||||||
// #region agent log
|
const sql = `
|
||||||
const totalSnapshot = await pool.query(
|
|
||||||
"select count(*)::int as cnt from woo_products_snapshot where tenant_id=$1",
|
|
||||||
[tenantId]
|
|
||||||
);
|
|
||||||
const totalSellable = await pool.query(
|
|
||||||
"select count(*)::int as cnt from sellable_items where tenant_id=$1",
|
|
||||||
[tenantId]
|
|
||||||
);
|
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H8",
|
|
||||||
location: "wooSnapshot.js:152",
|
|
||||||
message: "snapshot_counts",
|
|
||||||
data: {
|
|
||||||
tenantId: tenantId || null,
|
|
||||||
total_snapshot: totalSnapshot?.rows?.[0]?.cnt ?? null,
|
|
||||||
total_sellable: totalSellable?.rows?.[0]?.cnt ?? null,
|
|
||||||
query,
|
|
||||||
limit: lim,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
const sql = `
|
|
||||||
select *
|
select *
|
||||||
from sellable_items
|
from sellable_items
|
||||||
where tenant_id=$1
|
where tenant_id=$1
|
||||||
@@ -184,26 +155,7 @@ export async function searchSnapshotItems({ tenantId, q = "", limit = 12 }) {
|
|||||||
limit $3
|
limit $3
|
||||||
`;
|
`;
|
||||||
const { rows } = await pool.query(sql, [tenantId, like, lim]);
|
const { rows } = await pool.query(sql, [tenantId, like, lim]);
|
||||||
// #region agent log
|
return { items: rows.map(snapshotRowToItem), source: "snapshot" };
|
||||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
sessionId: "debug-session",
|
|
||||||
runId: "pre-fix",
|
|
||||||
hypothesisId: "H8",
|
|
||||||
location: "wooSnapshot.js:168",
|
|
||||||
message: "snapshot_search_result",
|
|
||||||
data: {
|
|
||||||
query,
|
|
||||||
found: rows.length,
|
|
||||||
sample_names: rows.slice(0, 3).map((r) => r?.name).filter(Boolean),
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
}),
|
|
||||||
}).catch(() => {});
|
|
||||||
// #endregion
|
|
||||||
return { items: rows.map(snapshotRowToItem), source: "snapshot" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSnapshotPriceByWooId({ tenantId, wooId }) {
|
export async function getSnapshotPriceByWooId({ tenantId, wooId }) {
|
||||||
@@ -219,6 +171,27 @@ export async function getSnapshotPriceByWooId({ tenantId, wooId }) {
|
|||||||
return price == null ? null : Number(price);
|
return price == null ? null : Number(price);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene productos de sellable_items por sus woo_product_ids.
|
||||||
|
* Usado para incluir productos encontrados vía aliases.
|
||||||
|
*/
|
||||||
|
export async function getSnapshotItemsByIds({ tenantId, wooProductIds }) {
|
||||||
|
if (!Array.isArray(wooProductIds) || wooProductIds.length === 0) {
|
||||||
|
return { items: [], source: "snapshot_by_id" };
|
||||||
|
}
|
||||||
|
const ids = wooProductIds.map(id => Number(id)).filter(id => id > 0);
|
||||||
|
if (ids.length === 0) return { items: [], source: "snapshot_by_id" };
|
||||||
|
|
||||||
|
const placeholders = ids.map((_, i) => `$${i + 2}`).join(",");
|
||||||
|
const sql = `
|
||||||
|
select *
|
||||||
|
from sellable_items
|
||||||
|
where tenant_id=$1 and woo_id in (${placeholders})
|
||||||
|
`;
|
||||||
|
const { rows } = await pool.query(sql, [tenantId, ...ids]);
|
||||||
|
return { items: rows.map(snapshotRowToItem), source: "snapshot_by_id" };
|
||||||
|
}
|
||||||
|
|
||||||
export async function upsertSnapshotItems({ tenantId, items, runId = null }) {
|
export async function upsertSnapshotItems({ tenantId, items, runId = null }) {
|
||||||
const rows = Array.isArray(items) ? items : [];
|
const rows = Array.isArray(items) ? items : [];
|
||||||
for (const item of rows) {
|
for (const item of rows) {
|
||||||
|
|||||||
Reference in New Issue
Block a user