77- Fatos: Semântico (threshold) + fallback EXACT→SEMANTIC
88- Resposta no cache é NEUTRA (sem nome/cargo); personalização só na exibição
99- Contexto forte: reescreve prompts AMBÍGUOS com "(no contexto de ...)" + system rígido "não cite outros sentidos"
10+ - FLUSH por UI: por escopo A, por escopo B, ou Ambos (A+B) — NUNCA apaga índice
1011"""
1112
1213import json
1314import os
1415import re
1516import time
1617from datetime import datetime
17- from typing import Dict , List , Optional , Tuple
18+ from typing import Any , Dict , List , Optional , Tuple
1819
1920import gradio as gr
2021from dotenv import load_dotenv
@@ -123,21 +124,16 @@ def depersonalize_safe(text: str, person: Optional[str]) -> str:
123124
124125# ============== Contexto / Desambiguação ==============
125126
126- # termos ambíguos -> reforçar contexto
127127AMBIGUOUS_TERMS = [
128- r"\bc[eé]lula\b" , # célula (biologia vs computação/planilhas)
129- r"\bbanco\b" , # banco (financeiro vs margem de rio)
130- r"\brede\b" , # rede (computadores vs hospital/saúde pública)
131- r"\bmodelo\b" , # modelo (ML vs negócio)
132- r"\bpipeline\b" , # pipeline (dados/software vs oleoduto)
128+ r"\bc[eé]lula\b" ,
129+ r"\bbanco\b" ,
130+ r"\brede\b" ,
131+ r"\bmodelo\b" ,
132+ r"\bpipeline\b" ,
133133]
134134
135135def infer_domain (company : str , bu : str , role : Optional [str ]) -> str :
136- """
137- Retorna rótulo curto de domínio para instrução: 'saúde', 'software', 'dados', 'finanças', 'turismo', etc.
138- """
139136 text = f"{ company } { bu } { role or '' } " .lower ()
140-
141137 if any (k in text for k in ["saude" , "clínica" , "clinica" , "medic" , "hospital" ]):
142138 return "saúde"
143139 if any (k in text for k in ["engenharia" , "software" , "dev" , "produto" , "ti" , "tecnologia" , "tech" ]):
@@ -148,18 +144,13 @@ def infer_domain(company: str, bu: str, role: Optional[str]) -> str:
148144 return "finanças"
149145 if any (k in text for k in ["turismo" , "eco" , "aventura" , "hotel" , "viagem" ]):
150146 return "turismo"
151- # fallback neutro
152147 return "geral da área do usuário"
153148
154149def looks_ambiguous (prompt : str ) -> bool :
155150 p = (prompt or "" ).lower ()
156151 return any (re .search (pat , p , flags = re .IGNORECASE ) for pat in AMBIGUOUS_TERMS )
157152
158153def rewrite_with_domain (prompt : str , domain_label : str ) -> str :
159- """
160- Se a pergunta for ambígua, reforça explicitamente o contexto.
161- Ex.: "O que é uma célula? (no contexto de saúde)"
162- """
163154 clean = prompt .strip ()
164155 if not clean .endswith ("?" ):
165156 clean += "?"
@@ -183,17 +174,13 @@ def call_openai(
183174 "Se a pergunta for ambígua (ex.: 'célula', 'rede', 'banco'), "
184175 f"RESPONDA APENAS no sentido de { domain } e NÃO mencione outros significados."
185176 )
186-
187- # few-shot mínimo para ancorar comportamento
188177 examples = [
189178 {"role" : "user" , "content" : "O que é uma célula? (no contexto de saúde)" },
190179 {"role" : "assistant" , "content" : "Uma célula é a menor unidade estrutural e funcional dos seres vivos." },
191180 {"role" : "user" , "content" : "O que é uma célula? (no contexto de engenharia de software)" },
192181 {"role" : "assistant" , "content" : "Em computação, célula costuma se referir a uma unidade em uma tabela/planilha ou a um componente isolado de execução." },
193182 ]
194-
195183 msgs = [{"role" : "system" , "content" : system_ctx }, * examples , {"role" : "user" , "content" : prompt }]
196-
197184 resp = openai_client .chat .completions .create (
198185 model = OPENAI_MODEL ,
199186 messages = msgs ,
@@ -281,8 +268,7 @@ def search_and_answer(
281268 strategies = [SearchStrategy .EXACT ] if (SearchStrategy is not None ) else None
282269 sim_thr = None # desliga semântico
283270
284- # REESCRITA de prompt se for ambíguo (para o LLM e para o cache!)
285- # - Mantém isolamento adicional por atributos; a chave de cache leva a versão reescrita.
271+ # Reescrita de ambíguos
286272 rewritten_prompt = prompt_original
287273 domain_label = infer_domain (company , bu , None )
288274 if looks_ambiguous (prompt_original ):
@@ -334,10 +320,9 @@ def search_and_answer(
334320 if intent == "identity:role" :
335321 llm_answer_neutral = "Não tenho sua função ainda. Diga: “Minha função é <cargo>” para eu guardar."
336322 else :
337- # usar prompt reescrito quando ambíguo
338323 llm_answer_neutral = call_openai (
339324 rewritten_prompt if intent == "fact" else key_for_cache ,
340- person = None , # não usar nome em fatos
325+ person = None ,
341326 company = company , bu = bu , role = None
342327 )
343328 llm_latency = time .perf_counter () - t1
@@ -359,13 +344,81 @@ def search_and_answer(
359344 latency_txt = f"[Cache Miss] busca: { cache_latency :.3f} s, llm: { llm_latency :.3f} s"
360345 return display_answer , "llm" , json .dumps (debug , indent = 2 , ensure_ascii = False ), latency_txt , tokens_est
361346
347+ # ============== FLUSH helpers ==============
348+
349+ def parse_deleted_count (res : Any ) -> Optional [int ]:
350+ # Tenta extrair 'deleted_entries_count' como atributo ou chave
351+ if hasattr (res , "deleted_entries_count" ):
352+ return getattr (res , "deleted_entries_count" , None )
353+ if isinstance (res , dict ):
354+ return res .get ("deleted_entries_count" ) or res .get ("deleted" ) or res .get ("deleted_count" )
355+ return None
356+
357+ def flush_entries_with_attrs (attrs : Dict [str , str ]) -> Tuple [str , str ]:
358+ """
359+ Chama delete_query(attributes=attrs). Nunca apaga índice.
360+ O backend exige pelo menos 1 atributo (attributes != {}).
361+ """
362+ if not lang_cache :
363+ return "⚠️ LangCache não configurado; nenhum flush executado." , json .dumps ({"attributes" : attrs , "ok" : False }, ensure_ascii = False , indent = 2 )
364+ try :
365+ res = lang_cache .delete_query (attributes = attrs )
366+ deleted = parse_deleted_count (res )
367+ msg = f"✅ Flush executado. Escopo={ attrs } . Removidos={ deleted if deleted is not None else '—' } "
368+ debug = {"attributes" : attrs , "response" : getattr (res , '__dict__' , res )}
369+ return msg , json .dumps (debug , ensure_ascii = False , indent = 2 )
370+ except Exception as e :
371+ return f"❌ Erro no flush: { e } " , json .dumps ({"attributes" : attrs , "error" : str (e )}, ensure_ascii = False , indent = 2 )
372+
373+ def handle_flush_scope (company : str , bu : str , person : str , isolation : str ):
374+ attrs = build_attributes (company or "" , bu or "" , person or "" , isolation )
375+ if not attrs :
376+ return ("⚠️ Selecione um nível de isolamento diferente de 'none' para poder limpar por escopo." ,
377+ json .dumps ({"attributes" : attrs , "error" : "attributes cannot be blank" }, ensure_ascii = False , indent = 2 ))
378+ return flush_entries_with_attrs (attrs )
379+
380+ def handle_flush_both (
381+ a_company : str , a_bu : str , a_person : str ,
382+ b_company : str , b_bu : str , b_person : str ,
383+ isolation : str ,
384+ ):
385+ """
386+ Executa flush para os dois cenários (A e B), respeitando o isolamento atual.
387+ Útil porque o endpoint não aceita attributes={} (global).
388+ """
389+ attrs_a = build_attributes (a_company or "" , a_bu or "" , a_person or "" , isolation )
390+ attrs_b = build_attributes (b_company or "" , b_bu or "" , b_person or "" , isolation )
391+
392+ msgs = []
393+ debugs = []
394+
395+ if attrs_a :
396+ msg_a , dbg_a = flush_entries_with_attrs (attrs_a )
397+ msgs .append (msg_a )
398+ debugs .append (json .loads (dbg_a ))
399+ else :
400+ msgs .append ("⚠️ Escopo A: isolamento 'none' não pode ser limpo." )
401+ debugs .append ({"attributes" : attrs_a , "error" : "attributes cannot be blank" })
402+
403+ if attrs_b :
404+ msg_b , dbg_b = flush_entries_with_attrs (attrs_b )
405+ msgs .append (msg_b )
406+ debugs .append (json .loads (dbg_b ))
407+ else :
408+ msgs .append ("⚠️ Escopo B: isolamento 'none' não pode ser limpo." )
409+ debugs .append ({"attributes" : attrs_b , "error" : "attributes cannot be blank" })
410+
411+ final_msg = "<br/>" .join (msgs )
412+ return final_msg , json .dumps ({"A" : debugs [0 ], "B" : debugs [1 ]}, ensure_ascii = False , indent = 2 )
413+
362414# ============== UI / KPIs ==============
363415
364416DESCRICAO_LONGA = """
365417- Isolamento por atributos: company, business_unit, person.
366418- Nome: sem cache; Cargo: EXACT ONLY.
367419- Fatos: SEMANTIC + fallback EXACT→SEMANTIC.
368420- Desambiguação forte: prompts ambíguos são reescritos com “(no contexto de …)”.
421+ - FLUSH: limpe entradas por escopo A/B ou ambos (A+B). O endpoint exige attributes != {}.
369422"""
370423
371424def format_currency (v : float , currency : str = "USD" ) -> str :
@@ -533,6 +586,11 @@ def handle_submit(
533586 a_source = gr .Label (label = "Origem" )
534587 a_latency = gr .Label (label = "Latência" )
535588 a_debug = gr .Code (label = "Debug" )
589+ # FLUSH A
590+ gr .Markdown ("**Manutenção do Cache — A**" )
591+ a_flush_btn = gr .Button ("🧹 Limpar Cache (Escopo A)" )
592+ a_flush_status = gr .HTML ()
593+ a_flush_debug = gr .Code ()
536594
537595 with gr .Column ():
538596 gr .Markdown ("#### Cenário B" )
@@ -546,6 +604,11 @@ def handle_submit(
546604 b_source = gr .Label (label = "Origem" )
547605 b_latency = gr .Label (label = "Latência" )
548606 b_debug = gr .Code (label = "Debug" )
607+ # FLUSH B
608+ gr .Markdown ("**Manutenção do Cache — B**" )
609+ b_flush_btn = gr .Button ("🧹 Limpar Cache (Escopo B)" )
610+ b_flush_status = gr .HTML ()
611+ b_flush_debug = gr .Code ()
549612
550613 gr .Markdown ("### Indicadores" )
551614 with gr .Row (elem_classes = ["kpi-row" ]):
@@ -565,7 +628,14 @@ def handle_submit(
565628 col_count = (10 , "fixed" ),
566629 )
567630
568- # Eventos
631+ # FLUSH "Ambos"
632+ gr .Markdown ("---" )
633+ gr .Markdown ("### 🧹 Limpeza Combinada (A + B)" )
634+ flush_both_btn = gr .Button ("🧹 Limpar Ambos (A+B)" )
635+ flush_both_status = gr .HTML ()
636+ flush_both_debug = gr .Code ()
637+
638+ # Eventos de pergunta
569639 a_btn .click (
570640 fn = handle_submit ,
571641 inputs = [
@@ -600,6 +670,25 @@ def handle_submit(
600670 ],
601671 )
602672
673+ # Eventos de FLUSH
674+ a_flush_btn .click (
675+ fn = handle_flush_scope ,
676+ inputs = [a_company , a_bu , a_person , isolation_global ],
677+ outputs = [a_flush_status , a_flush_debug ],
678+ )
679+
680+ b_flush_btn .click (
681+ fn = handle_flush_scope ,
682+ inputs = [b_company , b_bu , b_person , isolation_global ],
683+ outputs = [b_flush_status , b_flush_debug ],
684+ )
685+
686+ flush_both_btn .click (
687+ fn = handle_flush_both ,
688+ inputs = [a_company , a_bu , a_person , b_company , b_bu , b_person , isolation_global ],
689+ outputs = [flush_both_status , flush_both_debug ],
690+ )
691+
603692if __name__ == "__main__" :
604693 if lang_cache :
605694 with lang_cache :
0 commit comments