Nielo47 commited on
Commit
8fcd5ad
·
1 Parent(s): ba3c551
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.pdf filter=lfs diff=lfs merge=lfs -text
2
+ *.faiss filter=lfs diff=lfs merge=lfs -text
3
+ *.jpg filter=lfs diff=lfs merge=lfs -text
sandbox/escala_de_lawton.pdf → CIF/ListaCIF.pdf RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:1834624e8b2e6f4799445f2314177945c7064e86649d6e3efd70341f14924b4e
3
- size 2244469
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2add1fa3846d7e828037251381010d43e9f349d9ed86214f77986672baf85f9c
3
+ size 311858
pages/README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/
2
+
3
+ Páginas do aplicativo. O conceito geral é dividir a lógica de cada página em:
4
+
5
+ - `view.py`: Elementos visuais e sua lógica de mudança de estado.
6
+ - `strings.py`: Textos utilizados. Seu isolamento facilita manutenção e permite adaptações de idiomas (futuras).
7
+ - `scripts.py`: Lógica menor para comportamento da página entre seus componentes e códigos mais robustos em utils.
8
+
9
+ São páginas:
10
+
11
+ - `about`: Página que aborda a Classificação Internacional de Funcionalidade, Incapacidade e Saúde (CIF) e a proposta do projeto.
12
+ - `main`: Página principal, trata do envio de textos, recebimento da resposta da IA e relatório da vinculação gerada.
13
+ - `feedback`: Página responsável por receber comentários e armazená-los em uma planilha google para análise.
pages/about/strings.py CHANGED
@@ -1,3 +1,4 @@
 
1
  STRINGS = {
2
  "ABOUT_TITLE": "# CIFLink 2.0",
3
  # Descrição do aplicativo
 
1
+ # pages/about/strings.py
2
  STRINGS = {
3
  "ABOUT_TITLE": "# CIFLink 2.0",
4
  # Descrição do aplicativo
pages/about/view.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import gradio as gr
2
  import os
3
 
@@ -34,10 +35,6 @@ with gr.Blocks() as interface:
34
  gr.Markdown(STRINGS["SECTION_REFERENCES"])
35
  gr.Markdown(STRINGS["SECTION_REFERENCES_LINKS"])
36
  gr.Markdown(STRINGS["SECTION_REFERENCES_LIST"])
37
-
38
-
39
-
40
-
41
-
42
  if __name__ == "__main__":
43
  interface.launch()
 
1
+ # pages/about/view.py
2
  import gradio as gr
3
  import os
4
 
 
35
  gr.Markdown(STRINGS["SECTION_REFERENCES"])
36
  gr.Markdown(STRINGS["SECTION_REFERENCES_LINKS"])
37
  gr.Markdown(STRINGS["SECTION_REFERENCES_LIST"])
38
+
 
 
 
 
39
  if __name__ == "__main__":
40
  interface.launch()
pages/feedback/scripts.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import datetime
2
  import gspread
3
  import os
 
1
+ # pages/feedback/scripts.py
2
  import datetime
3
  import gspread
4
  import os
pages/feedback/strings.py CHANGED
@@ -1,3 +1,4 @@
 
1
  STRINGS = {
2
  "TITLE": "# Contate-nos",
3
  "SUBTITLE": "Ajude-nos a melhorar a aplicação enviando seu feedback. Seu comentário é muito importante para nós!",
 
1
+ # pages/feedback/strings.py
2
  STRINGS = {
3
  "TITLE": "# Contate-nos",
4
  "SUBTITLE": "Ajude-nos a melhorar a aplicação enviando seu feedback. Seu comentário é muito importante para nós!",
pages/feedback/view.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import gradio as gr
2
 
3
  from .scripts import submit_feedback_and_handle_errors
 
1
+ # pages/feedback/view.py
2
  import gradio as gr
3
 
4
  from .scripts import submit_feedback_and_handle_errors
pages/main/scripts.py CHANGED
@@ -1,18 +1,15 @@
 
1
  import faiss
2
  import gradio as gr
3
  from typing import Any, Generator
4
- from sentence_transformers import SentenceTransformer
5
- from utils.rag_llm_response import (
6
- generate_response_with_llm,
7
- ) # A função unificada agora trata as estratégias de RAG e LLM
8
- from utils.phrase_extractor import process_file_content
9
-
10
- # from utils.report_creation import generate_report
11
- from utils.api_gemini import api_generate
12
  from .strings import STRINGS
13
-
14
- # DEPRECATED: A função volta com a consolidação de um futuro OCR.
15
- # def extract_phrases_from_gradio_file(gradio_file: gr.File) -> gr.Textbox:
16
  # """
17
  # Utilizes the 'process_file' function from 'utils.phrase_extractor' to read the
18
  # file content and extract phrases, returning them as a text block for Gradio.
@@ -23,24 +20,22 @@ from .strings import STRINGS
23
  # try:
24
  # # Chama a função unificada de processamento de arquivo que retorna uma lista de frases
25
  # phrases = process_file_content(gradio_file.name)
26
- #
27
  # phrases_text = "\n".join(phrases)
28
  # return gr.Textbox(value=phrases_text, placeholder=STRINGS["TEXT_INPUT_PLACEHOLDER_LOADED"])
29
  # except Exception as e:
30
  # return gr.Textbox(value=f"Error: {e}", placeholder=STRINGS["TEXT_INPUT_PLACER_EMPTY"])
31
 
32
-
33
  # DEPRECATED: A função volta com a consolidação de um futuro RAG.
34
- def process_phrases_with_rag_llm(
35
- input_phrases_text: str, rag_docs: list[str], rag_index: faiss.Index, rag_embedder: SentenceTransformer
36
- ) -> Generator[tuple[gr.Textbox, gr.Textbox, gr.Tabs, gr.TabItem], None, None]:
37
  """
38
  Receives a block of text (phrases separated by newlines) and processes it
39
  with the RAG+LLM API (`res_generate_API`) using a multiple-context strategy.
40
  Returns a status textbox, a formatted responses textbox, and updates tabs to switch to the results tab.
41
  """
42
- print(f'Processando o bloco de frases para geração de resposta: "{input_phrases_text[:100]}..."')
43
- current_symbol = " ♾️" # Emojis para indicar status de processamento e sucesso
44
 
45
  # --- Ação 1: Mudar de aba IMEDIATAMENTE e mostrar mensagem de processamento ---
46
  # O 'yield' envia: (Status, Resultado, Tabs)
@@ -48,9 +43,9 @@ def process_phrases_with_rag_llm(
48
  gr.update(value=STRINGS["TXTBOX_STATUS_IDLE"], interactive=False),
49
  gr.update(value="", interactive=False),
50
  gr.update(selected=1),
51
- gr.update(label=STRINGS["TAB_1_TITLE"] + current_symbol, interactive=True),
52
- )
53
-
54
  # time.sleep(1) # Simula um pequeno atraso para processamento
55
 
56
  try:
@@ -62,16 +57,16 @@ def process_phrases_with_rag_llm(
62
  documents=rag_docs,
63
  index=rag_index,
64
  embedder=rag_embedder,
65
- llm_choice="gemini", # ou 'ollama', conforme a necessidade
66
- rag_strategy="multiple", # A chave para usar a busca por múltiplos contextos
67
  )
68
 
69
- # with open("./sandbox/respostateste.txt", "r", encoding="utf-8") as arquivo:
70
- # llm_response = arquivo.read() #TODO: Test Only
71
 
72
  status_message = STRINGS["TXTBOX_STATUS_OK"]
73
  formatted_output = f"--- Resposta Fornecida pela LLM ---\n{llm_response}\n"
74
- current_symbol = " ✅"
75
 
76
  except Exception as e:
77
  status_message = STRINGS["TXTBOX_STATUS_ERROR"]
@@ -84,64 +79,78 @@ def process_phrases_with_rag_llm(
84
  gr.update(value=status_message, interactive=False),
85
  gr.update(value=formatted_output, interactive=False),
86
  gr.update(),
87
- gr.update(label=STRINGS["TAB_1_TITLE"] + current_symbol, interactive=True),
88
  )
 
 
 
 
 
 
 
89
 
 
 
 
90
 
91
- def process_phrases_with_api_llm(
92
- input_phrases_text: str,
93
- ) -> Generator[tuple[gr.Textbox, gr.Textbox, gr.Tabs, gr.TabItem], None, None]:
94
- """
95
- Receives a block of text and processes it
96
- with the API (`res_generate_API`).
97
- Returns a status textbox, a formatted responses textbox, and updates tabs to switch to the results tab.
98
  """
99
- print(f'Processando o bloco de frases para geração de resposta: "{input_phrases_text[:100]}..."')
100
- current_symbol = " ♾️" # Emojis para indicar status de processamento e sucesso
 
101
 
102
- # --- Ação 1: Mudar de aba IMEDIATAMENTE e mostrar mensagem de processamento ---
103
- # O 'yield' envia: (Status, Resultado, Tabs)
104
  yield (
105
- gr.update(value=STRINGS["TXTBOX_STATUS_IDLE"], interactive=False),
106
  gr.update(value="", interactive=False),
107
- gr.update(selected=1),
108
- gr.update(label=STRINGS["TAB_1_TITLE"] + current_symbol, interactive=True),
109
  )
110
 
111
- # time.sleep(1) # Simula um pequeno atraso para processamento
112
-
113
  try:
114
- # Chama a função unificada de geração de resposta, especificando a estratégia RAG
115
- # O LLM então usará os múltiplos contextos recuperados para gerar uma única resposta consolidada.
116
-
117
- # llm_response = generate_response_with_llm(
118
- # input_phrase=input_phrases_text,
119
- # documents=rag_docs,
120
- # index=rag_index,
121
- # embedder=rag_embedder,
122
- # llm_choice='gemini', # ou 'ollama', conforme a necessidade
123
- # rag_strategy='multiple' # A chave para usar a busca por múltiplos contextos
124
- # )
125
-
126
- # with open("./sandbox/respostateste.txt", "r", encoding="utf-8") as arquivo:
127
- # llm_response = arquivo.read() #TEST: Test Only
128
-
129
- llm_response = api_generate(user_input=input_phrases_text)
 
 
 
 
 
 
130
 
131
  status_message = STRINGS["TXTBOX_STATUS_OK"]
132
  formatted_output = f"--- Resposta Fornecida pela LLM ---\n{llm_response}\n"
133
  current_symbol = " ✅"
134
 
135
  except Exception as e:
 
136
  status_message = STRINGS["TXTBOX_STATUS_ERROR"]
137
- formatted_output = f"\n{STRINGS['--- Erro ---']}\nDetalhes: {e}"
138
  current_symbol = " ⚠️"
 
139
 
140
- # --- Ação 3: Retornar o resultado final e o status ---
141
- # A aba já está selecionada, então gr.Tabs() aqui apenas satisfaz a assinatura e mantém a aba atual.
142
  yield (
143
  gr.update(value=status_message, interactive=False),
144
- gr.update(value=formatted_output, interactive=False),
145
  gr.update(),
146
- gr.update(label=STRINGS["TAB_1_TITLE"] + current_symbol, interactive=True),
147
- )
 
1
+ # pages/main/scripts.py
2
  import faiss
3
  import gradio as gr
4
  from typing import Any, Generator
5
+ #from sentence_transformers import SentenceTransformer
6
+ #from utils.rag_llm_response import generate_response_with_llm # A função unificada agora trata as estratégias de RAG e LLM
7
+ #from utils.phrase_extractor import process_file_content
8
+ #from utils.report_creation import generate_report
9
+ from utils.apis.gemini import api_generate
 
 
 
10
  from .strings import STRINGS
11
+ # DEPRECATED: A função era um protótipo para criação de Contexto RAG.
12
+ #def extract_phrases_from_gradio_file(gradio_file: gr.File) -> gr.Textbox:
 
13
  # """
14
  # Utilizes the 'process_file' function from 'utils.phrase_extractor' to read the
15
  # file content and extract phrases, returning them as a text block for Gradio.
 
20
  # try:
21
  # # Chama a função unificada de processamento de arquivo que retorna uma lista de frases
22
  # phrases = process_file_content(gradio_file.name)
23
+ #
24
  # phrases_text = "\n".join(phrases)
25
  # return gr.Textbox(value=phrases_text, placeholder=STRINGS["TEXT_INPUT_PLACEHOLDER_LOADED"])
26
  # except Exception as e:
27
  # return gr.Textbox(value=f"Error: {e}", placeholder=STRINGS["TEXT_INPUT_PLACER_EMPTY"])
28
 
 
29
  # DEPRECATED: A função volta com a consolidação de um futuro RAG.
30
+ '''
31
+ def process_phrases_with_rag_llm(input_phrases_text: str, rag_docs:list[str], rag_index:faiss.Index, rag_embedder:SentenceTransformer) -> Generator[tuple[gr.Textbox, gr.Textbox, gr.Tabs, gr.TabItem]]:
 
32
  """
33
  Receives a block of text (phrases separated by newlines) and processes it
34
  with the RAG+LLM API (`res_generate_API`) using a multiple-context strategy.
35
  Returns a status textbox, a formatted responses textbox, and updates tabs to switch to the results tab.
36
  """
37
+ print(f"Processando o bloco de frases para geração de resposta: \"{input_phrases_text[:100]}...\"")
38
+ current_symbol = " ♾️" # Emojis para indicar status de processamento e sucesso
39
 
40
  # --- Ação 1: Mudar de aba IMEDIATAMENTE e mostrar mensagem de processamento ---
41
  # O 'yield' envia: (Status, Resultado, Tabs)
 
43
  gr.update(value=STRINGS["TXTBOX_STATUS_IDLE"], interactive=False),
44
  gr.update(value="", interactive=False),
45
  gr.update(selected=1),
46
+ gr.update(label=STRINGS["TAB_1_TITLE"]+current_symbol, interactive=True)
47
+ )
48
+
49
  # time.sleep(1) # Simula um pequeno atraso para processamento
50
 
51
  try:
 
57
  documents=rag_docs,
58
  index=rag_index,
59
  embedder=rag_embedder,
60
+ llm_choice='gemini', # ou 'ollama', conforme a necessidade
61
+ rag_strategy='multiple' # A chave para usar a busca por múltiplos contextos
62
  )
63
 
64
+ # with open("./sandbox/respostateste.txt", "r", encoding="utf-8") as arquivo:
65
+ # llm_response = arquivo.read() #TODO: Test Only
66
 
67
  status_message = STRINGS["TXTBOX_STATUS_OK"]
68
  formatted_output = f"--- Resposta Fornecida pela LLM ---\n{llm_response}\n"
69
+ current_symbol = " ✅"
70
 
71
  except Exception as e:
72
  status_message = STRINGS["TXTBOX_STATUS_ERROR"]
 
79
  gr.update(value=status_message, interactive=False),
80
  gr.update(value=formatted_output, interactive=False),
81
  gr.update(),
82
+ gr.update(label=STRINGS["TAB_1_TITLE"]+current_symbol, interactive=True)
83
  )
84
+ '''
85
+ def process_inputs_to_api(
86
+ input_text: str,
87
+ input_file: Any # Objeto de arquivo do Gradio (ex: tempfile._TemporaryFileWrapper)
88
+ ) -> Generator[tuple, None, None]:
89
+ """
90
+ Processa a entrada do usuário (texto ou arquivo) com a API do Gemini.
91
 
92
+ Esta função serve como o handler para a interface Gradio. Ela implementa a
93
+ lógica XOR para garantir que apenas uma forma de entrada seja fornecida,
94
+ atualiza a UI com o status e exibe o resultado da análise.
95
 
96
+ Args:
97
+ input_text: O conteúdo do componente gr.Textbox.
98
+ input_file: O objeto do componente gr.File. É None se nenhum arquivo for carregado.
99
+
100
+ Yields:
101
+ Atualizações para os componentes da interface do Gradio.
 
102
  """
103
+ current_symbol = " ♾️" # Símbolo de processamento
104
+ formatted_output = ""
105
+ status_message = STRINGS["TXTBOX_STATUS_IDLE"]
106
 
107
+ # --- Ação 1: Atualiza a UI para mostrar que o processamento começou ---
 
108
  yield (
109
+ gr.update(value=status_message, interactive=False),
110
  gr.update(value="", interactive=False),
111
+ gr.update(selected=1), # Muda para a aba de resultados
112
+ gr.update(label=STRINGS["TAB_1_TITLE"] + current_symbol, interactive=True)
113
  )
114
 
 
 
115
  try:
116
+ # --- Ação 2: Lógica de validação XOR para as entradas da UI ---
117
+ texto_fornecido = bool(input_text and input_text.strip())
118
+ arquivo_fornecido = input_file is not None
119
+
120
+ if texto_fornecido and arquivo_fornecido:
121
+ raise ValueError("Por favor, forneça texto OU um arquivo PDF, não ambos.")
122
+
123
+ if not texto_fornecido and not arquivo_fornecido:
124
+ raise ValueError("Nenhuma entrada fornecida. Por favor, digite um texto ou faça o upload de um arquivo.")
125
+
126
+ # --- Ação 3: Chama o backend com o parâmetro correto ---
127
+ params_para_api = {}
128
+ if texto_fornecido:
129
+ print(f"Processando via texto: \"{input_text[:100]}...\"")
130
+ params_para_api['input_text'] = input_text
131
+ elif arquivo_fornecido:
132
+ # O objeto do Gradio tem um atributo .name que contém o caminho temporário do arquivo
133
+ print(f"Processando via arquivo: {input_file.name}")
134
+ params_para_api['input_file'] = input_file.name
135
+
136
+ # Chama a função de backend com os parâmetros corretos
137
+ llm_response = api_generate(**params_para_api)
138
 
139
  status_message = STRINGS["TXTBOX_STATUS_OK"]
140
  formatted_output = f"--- Resposta Fornecida pela LLM ---\n{llm_response}\n"
141
  current_symbol = " ✅"
142
 
143
  except Exception as e:
144
+ # Captura qualquer erro (de validação ou da API) e o exibe na UI
145
  status_message = STRINGS["TXTBOX_STATUS_ERROR"]
146
+ formatted_output = f"\n--- Erro ao Processar ---\nDetalhes: {e}"
147
  current_symbol = " ⚠️"
148
+ print(f"ERRO na interface Gradio: {e}") # Loga o erro completo no console
149
 
150
+ # --- Ação Final: Retorna o resultado (sucesso ou erro) para a UI ---
 
151
  yield (
152
  gr.update(value=status_message, interactive=False),
153
+ gr.update(value=formatted_output, interactive=True), # Permite copiar o resultado
154
  gr.update(),
155
+ gr.update(label=STRINGS["TAB_1_TITLE"] + current_symbol, interactive=True)
156
+ )
pages/main/strings.py CHANGED
@@ -1,9 +1,10 @@
 
1
  STRINGS = {
2
  "APP_TITLE": "Sistema para Vinculação CIF de Frases",
3
  "APP_DESCRIPTION": "Insira frases, obtenha as vinculações dos conceitos significativos à CIF feitas por IA.",
4
  # tab id = 0
5
- "TAB_0_TITLE": "Entrada de Frases via Arquivos",
6
- "TAB_0_SUBTITLE": "## 📝 Passo 1: Forneça as Frases",
7
  # tab id = 1
8
  "TAB_1_TITLE": "Resultados da Vinculação",
9
  "TAB_1_SUBTITLE": "## 🤖 Passo 2: Visualize os Resultados",
@@ -19,6 +20,8 @@ STRINGS = {
19
  # button_process_input
20
  "BTN_PROCESS_INPUT_LABEL_DISABLED": "Aguardando Frases...",
21
  "BTN_PROCESS_INPUT_LABEL_ENABLED": "Vincular Frases",
 
 
22
  # textbox_output_status
23
  "TXTBOX_STATUS_LABEL": "Status da Geração da Resposta:",
24
  "TXTBOX_STATUS_IDLE": "Gerando resposta, aguarde...",
 
1
+ # pages/main/strings.py
2
  STRINGS = {
3
  "APP_TITLE": "Sistema para Vinculação CIF de Frases",
4
  "APP_DESCRIPTION": "Insira frases, obtenha as vinculações dos conceitos significativos à CIF feitas por IA.",
5
  # tab id = 0
6
+ "TAB_0_TITLE": "Entrada de Texto",
7
+ "TAB_0_SUBTITLE": "## 📝 Passo 1: Forneça o Texto",
8
  # tab id = 1
9
  "TAB_1_TITLE": "Resultados da Vinculação",
10
  "TAB_1_SUBTITLE": "## 🤖 Passo 2: Visualize os Resultados",
 
20
  # button_process_input
21
  "BTN_PROCESS_INPUT_LABEL_DISABLED": "Aguardando Frases...",
22
  "BTN_PROCESS_INPUT_LABEL_ENABLED": "Vincular Frases",
23
+ "BTN_PROCESS_FILE_LABEL": "Vincular por Documento 📙",
24
+ "BTN_PROCESS_TEXT_LABEL": "Vincular por Texto ✍️",
25
  # textbox_output_status
26
  "TXTBOX_STATUS_LABEL": "Status da Geração da Resposta:",
27
  "TXTBOX_STATUS_IDLE": "Gerando resposta, aguarde...",
pages/main/tab01_input.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/main/tab01_input.py
2
+ import gradio as gr
3
+ from typing import Dict, Any
4
+ from .strings import STRINGS
5
+
6
+ def create_input_components() -> Dict[str, Any]:
7
+ """
8
+ Cria e retorna os componentes de entrada, onde um seletor de rádio
9
+ controla a visibilidade de grupos distintos para upload de arquivo e
10
+ entrada de texto, cada um com seu próprio botão.
11
+ """
12
+
13
+ input_type_radio = gr.Radio(
14
+ ["Vinculação por documento 📙", "Vinculação manual ✍️"],
15
+ label="Selecione o tipo de entrada",
16
+ value="Vinculação por documento 📙"
17
+ )
18
+
19
+ # --- Grupo de Upload de Arquivo ---
20
+ # Usando gr.Group para controlar a visibilidade do bloco.
21
+ # visible=True porque é a opção padrão do Radio.
22
+ with gr.Group(visible=True) as file_input_group:
23
+ file_input = gr.File(
24
+ label="Carregue o documento (PDF, TXT)",
25
+ file_types=['.pdf', '.txt'],
26
+ )
27
+ button_process_file = gr.Button(
28
+ value=STRINGS["BTN_PROCESS_FILE_LABEL"],
29
+ interactive=False,
30
+ variant="primary"
31
+ )
32
+
33
+ # --- Grupo de Entrada de Texto ---
34
+ # Usando gr.Group com visible=False porque não é a opção padrão.
35
+ with gr.Group(visible=False) as text_input_group:
36
+ text_input = gr.Textbox(
37
+ label="Insira o texto para análise",
38
+ lines=8,
39
+ placeholder="relato de dor persistente na articulação do joelho direito...",
40
+ )
41
+ button_process_text = gr.Button(
42
+ value=STRINGS["BTN_PROCESS_TEXT_LABEL"],
43
+ interactive=False,
44
+ variant="primary"
45
+ )
46
+
47
+ def _switch_input_visibility(selection: str) -> Dict[gr.Group, Dict[str, bool]]:
48
+ """Alterna a visibilidade dos grupos de entrada."""
49
+ is_document_selected = "documento" in selection
50
+ return {
51
+ file_input_group: gr.update(visible=is_document_selected),
52
+ text_input_group: gr.update(visible=not is_document_selected)
53
+ }
54
+
55
+ input_type_radio.change(
56
+ fn=_switch_input_visibility,
57
+ inputs=input_type_radio,
58
+ outputs=[file_input_group, text_input_group]
59
+ )
60
+
61
+ # --- Lógica de habilitação dos botões ---
62
+
63
+ def _update_file_button_state(file_obj: Any) -> gr.Button:
64
+ """Habilita o botão de arquivo apenas se um arquivo for carregado."""
65
+ return gr.update(interactive=file_obj is not None)
66
+
67
+ def _update_text_button_state(text: str) -> gr.Button:
68
+ """Habilita o botão de texto apenas se o texto tiver conteúdo."""
69
+ return gr.update(interactive=bool(text and text.strip()))
70
+
71
+ file_input.change(
72
+ fn=_update_file_button_state,
73
+ inputs=file_input,
74
+ outputs=button_process_file
75
+ )
76
+
77
+ text_input.change(
78
+ fn=_update_text_button_state,
79
+ inputs=text_input,
80
+ outputs=button_process_text
81
+ )
82
+
83
+ # Retornamos os componentes interativos que a view.py precisa manipular.
84
+ # Os próprios grupos não precisam ser retornados, a menos que se queira manipulá-los.
85
+ return {
86
+ "input_type_radio": input_type_radio,
87
+ "file_input": file_input,
88
+ "text_input": text_input,
89
+ "button_process_file": button_process_file,
90
+ "button_process_text": button_process_text,
91
+ }
pages/main/tab02_results.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/main/tab02_results.py
2
+ import gradio as gr
3
+ from .strings import STRINGS
4
+
5
+ def _handle_status_text_change(status_text: str) -> gr.Button:
6
+ """
7
+ Listener for the status textbox. Updates the report creation button
8
+ based on the content of the status textbox.
9
+ """
10
+ if status_text == STRINGS["TXTBOX_STATUS_OK"]:
11
+ return gr.update(value=STRINGS["BTN_CREATE_REPORT_LABEL_ENABLED"], interactive=True, variant="primary")
12
+ else:
13
+ return gr.update(value=STRINGS["BTN_CREATE_REPORT_LABEL_DISABLED"], interactive=False, variant="secondary")
14
+
15
+ def create_tab_results():
16
+ """
17
+ Cria e retorna um dicionário com os componentes da UI para a aba de resultados.
18
+ """
19
+ gr.Markdown(STRINGS["TAB_1_SUBTITLE"])
20
+
21
+ textbox_output_status = gr.Textbox(
22
+ label=STRINGS["TXTBOX_STATUS_LABEL"],
23
+ interactive=False,
24
+ value=""
25
+ )
26
+
27
+ textbox_output_llm_response = gr.Textbox(
28
+ label=STRINGS["TXTBOX_OUTPUT_LLM_RESPONSE_LABEL"],
29
+ lines=15,
30
+ interactive=False,
31
+ placeholder=STRINGS["TXTBOX_OUTPUT_LLM_RESPONSE_PLACEHOLDER"]
32
+ )
33
+
34
+ button_create_report = gr.Button(
35
+ STRINGS["BTN_CREATE_REPORT_LABEL_DISABLED"],
36
+ interactive=False,
37
+ variant="secondary"
38
+ )
39
+
40
+ button_return_to_input_tab_from_results = gr.Button(
41
+ STRINGS["BTN_RETURN_LABEL"],
42
+ variant="secondary"
43
+ )
44
+
45
+ # Evento para habilitar o botão de criar relatório
46
+ textbox_output_status.change(
47
+ fn=_handle_status_text_change,
48
+ inputs=textbox_output_status,
49
+ outputs=button_create_report
50
+ )
51
+
52
+ return {
53
+ "textbox_output_status": textbox_output_status,
54
+ "textbox_output_llm_response": textbox_output_llm_response,
55
+ "button_create_report": button_create_report,
56
+ "button_return_to_input_tab_from_results": button_return_to_input_tab_from_results
57
+ }
pages/main/tab03_report.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/main/tab03_report.py
2
+ import gradio as gr
3
+ import pandas as pd
4
+ import plotly.graph_objects as go
5
+ from typing import Tuple, Optional
6
+ from .strings import STRINGS
7
+
8
+ # --- Funções de Atualização (Lógica Interna da Aba) ---
9
+
10
+ def update_dataframe_components(
11
+ group_data_df: Optional[pd.DataFrame],
12
+ group_description_df: Optional[pd.DataFrame],
13
+ individuals_data_df: Optional[pd.DataFrame],
14
+ individuals_description_df: Optional[pd.DataFrame]
15
+ ) -> Tuple[gr.DataFrame, gr.DataFrame, gr.DataFrame, gr.DataFrame]:
16
+ """Atualiza os componentes visíveis de DataFrame do Gradio com novos dados."""
17
+ return (
18
+ gr.DataFrame(value=group_data_df),
19
+ gr.DataFrame(value=group_description_df),
20
+ gr.DataFrame(value=individuals_data_df),
21
+ gr.DataFrame(value=individuals_description_df)
22
+ )
23
+
24
+ def update_plot_components(
25
+ pie_chart_figure: Optional[go.Figure],
26
+ bar_chart_figure: Optional[go.Figure],
27
+ tree_map_figure: Optional[go.Figure]
28
+ ) -> Tuple[gr.Plot, gr.Plot, gr.Plot]:
29
+ """Atualiza os componentes visíveis de Gráfico do Gradio com novas figuras."""
30
+ return (
31
+ gr.Plot(value=pie_chart_figure),
32
+ gr.Plot(value=bar_chart_figure),
33
+ gr.Plot(value=tree_map_figure)
34
+ )
35
+
36
+ def update_download_button_component(report_file_path: Optional[str]) -> gr.DownloadButton:
37
+ """Atualiza o componente de DownloadButton do Gradio com o caminho do PDF."""
38
+ if report_file_path:
39
+ return gr.update(value=report_file_path, label=STRINGS["DOWNLOAD_BTN_REPORT_LABEL_ENABLED"], interactive=True, variant="primary")
40
+ else:
41
+ return gr.update(label=STRINGS["DOWNLOAD_BTN_REPORT_LABEL_ERROR"], interactive=False, variant="secondary")
42
+
43
+ def create_tab_report() -> dict:
44
+ """
45
+ Cria a aba de relatório e retorna um dicionário contendo
46
+ os componentes da UI e as funções para atualizá-los.
47
+ """
48
+ gr.Markdown(STRINGS["TAB_2_SUBTITLE"])
49
+
50
+ with gr.Row():
51
+ dataframe_display_grouped_data = gr.DataFrame(label=STRINGS["DF_GROUP_DATA"])
52
+ dataframe_display_grouped_description = gr.DataFrame(label=STRINGS["DF_GROUP_DESC"])
53
+ # ... (outros componentes são criados aqui como antes) ...
54
+ with gr.Row():
55
+ dataframe_display_individual_data = gr.DataFrame(label=STRINGS["DF_INDIVIDUAL_DATA"])
56
+ dataframe_display_individual_description = gr.DataFrame(label=STRINGS["DF_INDIVIDUAL_DESC"])
57
+
58
+ plot_display_pie_chart = gr.Plot(label=STRINGS["PLOT_PIE_LABEL"])
59
+ plot_display_bar_chart = gr.Plot(label=STRINGS["PLOT_BAR_LABEL"])
60
+ plot_display_tree_map = gr.Plot(label=STRINGS["PLOT_TREE_LABEL"])
61
+
62
+ download_button_report_pdf = gr.DownloadButton(
63
+ label=STRINGS["DOWNLOAD_BTN_REPORT_LABEL_DISABLED"],
64
+ interactive=False,
65
+ variant="secondary"
66
+ )
67
+
68
+ button_return_to_input_tab_from_report = gr.Button(
69
+ STRINGS["BTN_RETURN_LABEL"],
70
+ variant="secondary"
71
+ )
72
+
73
+ # MODIFICAÇÃO: Retornamos um dicionário estruturado
74
+ return {
75
+ "components": {
76
+ "dataframe_display_grouped_data": dataframe_display_grouped_data,
77
+ "dataframe_display_grouped_description": dataframe_display_grouped_description,
78
+ "dataframe_display_individual_data": dataframe_display_individual_data,
79
+ "dataframe_display_individual_description": dataframe_display_individual_description,
80
+ "plot_display_pie_chart": plot_display_pie_chart,
81
+ "plot_display_bar_chart": plot_display_bar_chart,
82
+ "plot_display_tree_map": plot_display_tree_map,
83
+ "download_button_report_pdf": download_button_report_pdf,
84
+ "button_return_to_input_tab_from_report": button_return_to_input_tab_from_report
85
+ },
86
+ "update_fns": {
87
+ "dataframes": update_dataframe_components,
88
+ "plots": update_plot_components,
89
+ "download": update_download_button_component
90
+ }
91
+ }
pages/main/view.py CHANGED
@@ -1,265 +1,144 @@
 
1
  import os
2
- import pandas as pd # Importado para type hinting em _update_dataframes_from_states
3
- import plotly.graph_objects as go # Importado para type hinting em _update_plots_from_states
4
  import gradio as gr
5
- from typing import Any, Generator, Tuple, Optional
6
- from functools import partial
7
 
8
- from utils.rag_retriever import initialize_rag_system
9
- from utils.report_creation import process_report_data, create_report_plots, generate_report_pdf
 
10
 
11
- #from .scripts import extract_phrases_from_gradio_file, process_phrases_with_rag_llm
12
- from .scripts import process_phrases_with_api_llm
13
  from .strings import STRINGS
 
 
 
14
 
15
- # --- Configurações Iniciais do RAG ---
16
- #rag_docs, rag_index, rag_embedder = [None, None, None] # TODO: Apenas para Teste
17
- # rag_docs, rag_index, rag_embedder = initialize_rag_system() # DEPRECATED
18
  img1 = os.path.join(os.getcwd(), "static", "images", "logo.jpg")
19
 
20
- # --- Função Auxiliadora para Processamento de Frases ---
21
- process_fn_with_rag_args = partial(process_phrases_with_api_llm)
22
-
23
- # --- Funções Auxiliares (Listeners e Controladores de UI) ---
24
- def _handle_input_text_change(text_input: str) -> gr.Button:
25
- """
26
- Listener for the input textbox. Updates the generation button
27
- based on the content of the textbox.
28
- """
29
- if len(text_input.strip()) > 2:
30
- return gr.update(value=STRINGS["BTN_PROCESS_INPUT_LABEL_ENABLED"], interactive=True, variant="primary")
31
- else:
32
- return gr.update(value=STRINGS["BTN_PROCESS_INPUT_LABEL_DISABLED"], interactive=False, variant="secondary")
33
-
34
- def _handle_status_text_change(status_text: str) -> gr.Button:
35
- """
36
- Listener for the status textbox. Updates the report creation button
37
- based on the content of the status textbox.
38
- """
39
- if status_text == STRINGS["TXTBOX_STATUS_OK"]:
40
- return gr.update(value=STRINGS["BTN_CREATE_REPORT_LABEL_ENABLED"], interactive=True, variant="primary")
41
- else:
42
- return gr.update(value=STRINGS["BTN_CREATE_REPORT_LABEL_DISABLED"], interactive=False, variant="secondary")
43
-
44
  def _switch_to_report_tab_and_enable_interaction() -> Tuple[gr.Tabs, gr.TabItem]:
45
- """
46
- Switches to the report tab and enables interaction for it.
47
- Returns updated Tabs and TabItem components.
48
- """
49
  return gr.update(selected=2), gr.update(label=STRINGS["TAB_2_TITLE"] + " ✅", interactive=True)
50
 
51
- # --- Atualizar Componentes Visíveis a partir de States ---
52
- def _update_dataframe_components(group_data_df: Optional[pd.DataFrame],
53
- group_description_df: Optional[pd.DataFrame],
54
- individuals_data_df: Optional[pd.DataFrame],
55
- individuals_description_df: Optional[pd.DataFrame]
56
- ) -> Tuple[gr.DataFrame, gr.DataFrame, gr.DataFrame, gr.DataFrame]:
57
- """
58
- Updates the visible Gradio DataFrame components with new data.
59
- """
60
- return (
61
- gr.DataFrame(value=group_data_df),
62
- gr.DataFrame(value=group_description_df),
63
- gr.DataFrame(value=individuals_data_df),
64
- gr.DataFrame(value=individuals_description_df)
65
- )
66
-
67
- def _update_plot_components(pie_chart_figure: Optional[go.Figure],
68
- bar_chart_figure: Optional[go.Figure],
69
- tree_map_figure: Optional[go.Figure]
70
- ) -> Tuple[gr.Plot, gr.Plot, gr.Plot]:
71
- """
72
- Updates the visible Gradio Plot components with new figures.
73
- """
74
- print("Atualizando gráficos visíveis...")
75
- return (
76
- gr.Plot(value=pie_chart_figure),
77
- gr.Plot(value=bar_chart_figure),
78
- gr.Plot(value=tree_map_figure)
79
- )
80
-
81
- def _update_download_button_component(report_file_path: Optional[str]) -> gr.DownloadButton:
82
- """
83
- Updates the Gradio DownloadButton component with the PDF path.
84
- """
85
- if report_file_path:
86
- return gr.update(value=report_file_path, label=STRINGS["DOWNLOAD_BTN_REPORT_LABEL_ENABLED"], interactive=True, variant="primary")
87
- else:
88
- return gr.update(label=STRINGS["DOWNLOAD_BTN_REPORT_LABEL_ERROR"], interactive=False, variant="secondary")
89
-
90
-
91
- # --- Construção da Interface Gradio ---
92
  with gr.Blocks(title=STRINGS["APP_TITLE"]) as interface:
93
- # --- States para Armazenar Dados Brutos (entre as etapas do .then()) ---
94
- state_dataframe_group = gr.State(None)
95
- state_dataframe_group_description = gr.State(None)
96
- state_dataframe_individuals = gr.State(None)
97
- state_dataframe_individuals_description = gr.State(None)
98
- state_figure_pie_chart = gr.State(None)
99
- state_figure_bar_chart = gr.State(None)
100
- state_figure_tree_map = gr.State(None)
101
- state_report_file_path = gr.State(None)
102
- state_llm_response = gr.State(None)
103
-
 
 
 
104
  with gr.Row():
105
  with gr.Column(scale=1):
106
- gr.Markdown(
107
- f"# {STRINGS['APP_TITLE']}",
108
- elem_id="md_app_title",
109
- )
110
- gr.Markdown(
111
- f"{STRINGS['APP_DESCRIPTION']}",
112
- elem_id="md_app_description",
113
- )
114
-
115
- gr.Image(
116
- value=img1,
117
- height=64,
118
- elem_id="logo_img",
119
- placeholder="CIF Link Logo",
120
- container=False,
121
- show_label=False,
122
- show_download_button=False,
123
- scale=0
124
- )
125
 
 
 
126
  with gr.Tabs() as tabs_main_navigation:
127
- with gr.TabItem(STRINGS["TAB_0_TITLE"], id=0):
128
- gr.Markdown(STRINGS["TAB_0_SUBTITLE"])
129
- # DEPRECATED: gr.File volta em uma futura versão
130
- # file_input_user_document = gr.File(
131
- # label=STRINGS["FILE_INPUT_LABEL"],
132
- # type="filepath",
133
- # file_types=['.txt', '.pdf', '.docx'],
134
- # interactive=False
135
- # )
136
-
137
- textbox_input_phrases = gr.Textbox(
138
- label=STRINGS["TXTBOX_INPUT_PHRASES_LABEL"],
139
- placeholder=STRINGS["TXTBOX_INPUT_PHRASES_PLACEHOLDER"],
140
- lines=10,
141
- interactive=True
142
- )
143
-
144
- button_process_input = gr.Button(STRINGS["BTN_PROCESS_INPUT_LABEL_DISABLED"], interactive=False, variant="secondary")
145
-
146
- # file_input_user_document.upload(
147
- # fn=extract_phrases_from_gradio_file,
148
- # inputs=file_input_user_document,
149
- # outputs=textbox_input_phrases
150
- # )
151
-
152
- textbox_input_phrases.change(
153
- fn=_handle_input_text_change,
154
- inputs=textbox_input_phrases,
155
- outputs=button_process_input
156
- )
157
-
158
- with gr.TabItem(STRINGS["TAB_1_TITLE"] + " 🔒", interactive=False, id=1) as tab_item_processing_results:
159
- gr.Markdown(STRINGS["TAB_1_SUBTITLE"])
160
-
161
- textbox_output_status = gr.Textbox(
162
- label=STRINGS["TXTBOX_STATUS_LABEL"],
163
- interactive=False,
164
- value=""
165
- )
166
-
167
- textbox_output_llm_response = gr.Textbox(
168
- label=STRINGS["TXTBOX_OUTPUT_LLM_RESPONSE_LABEL"],
169
- lines=15,
170
- interactive=False,
171
- placeholder=STRINGS["TXTBOX_OUTPUT_LLM_RESPONSE_PLACEHOLDER"]
172
- )
173
-
174
- button_create_report = gr.Button(STRINGS["BTN_CREATE_REPORT_LABEL_DISABLED"], interactive=False, variant="secondary")
175
- button_return_to_input_tab_from_results = gr.Button(STRINGS["BTN_RETURN_LABEL"], variant="secondary")
176
-
177
- textbox_output_status.change(
178
- fn=_handle_status_text_change,
179
- inputs=textbox_output_status,
180
- outputs=button_create_report
181
- )
182
-
183
- # Captura a resposta da LLM no estado para uso posterior em outras funções
184
- textbox_output_llm_response.change(
185
- fn=lambda response_text: response_text, # Função identidade para passar o valor
186
- inputs=textbox_output_llm_response,
187
- outputs=state_llm_response
188
- )
189
-
190
- with gr.TabItem(STRINGS["TAB_2_TITLE"] + " 🔒", interactive=False, id=2) as tab_item_report_visualization:
191
- gr.Markdown(STRINGS["TAB_2_SUBTITLE"])
192
-
193
- with gr.Row():
194
- dataframe_display_grouped_data = gr.DataFrame(label=STRINGS["DF_GROUP_DATA"])
195
- dataframe_display_grouped_description = gr.DataFrame(label=STRINGS["DF_GROUP_DESC"])
196
-
197
- with gr.Row():
198
- dataframe_display_individual_data = gr.DataFrame(label=STRINGS["DF_INDIVIDUAL_DATA"])
199
- dataframe_display_individual_description = gr.DataFrame(label=STRINGS["DF_INDIVIDUAL_DESC"])
200
-
201
- plot_display_pie_chart = gr.Plot(label=STRINGS["PLOT_PIE_LABEL"])
202
- plot_display_bar_chart = gr.Plot(label=STRINGS["PLOT_BAR_LABEL"])
203
- plot_display_tree_map = gr.Plot(label=STRINGS["PLOT_TREE_LABEL"])
204
 
205
- download_button_report_pdf = gr.DownloadButton(label=STRINGS["DOWNLOAD_BTN_REPORT_LABEL_DISABLED"], interactive=False, variant="secondary")
206
- button_return_to_input_tab_from_report = gr.Button(STRINGS["BTN_RETURN_LABEL"], variant="secondary") # Botão para voltar à aba 0 da aba 2
 
 
 
 
 
207
 
208
- # --- FLUXO DE EVENTOS MULTI-CHAINING PARA O RELATÓRIO ---
209
- button_process_input.click(
210
- fn=process_fn_with_rag_args,
211
- inputs=[textbox_input_phrases],
212
- outputs=[textbox_output_status, textbox_output_llm_response, tabs_main_navigation, tab_item_processing_results]
213
  )
214
 
215
- button_create_report.click(
216
- fn=_switch_to_report_tab_and_enable_interaction, # 1. Muda de aba e a habilita - Switches tab and enables it
217
- inputs=[],
218
- outputs=[tabs_main_navigation, tab_item_report_visualization]
219
  ).then(
220
- fn=process_report_data, # 2. Processa a resposta da LLM e salva os DataFrames brutos nos states
221
- inputs=[state_llm_response],
 
 
 
 
222
  outputs=[
223
- state_dataframe_group, state_dataframe_group_description,
224
- state_dataframe_individuals, state_dataframe_individuals_description
 
 
225
  ]
226
  ).then(
227
- fn=_update_dataframe_components, # 3. Atualiza os componentes Gradio DataFrame visíveis
228
- inputs=[state_dataframe_group, state_dataframe_group_description, state_dataframe_individuals, state_dataframe_individuals_description],
229
- outputs=[dataframe_display_grouped_data, dataframe_display_grouped_description, dataframe_display_individual_data, dataframe_display_individual_description]
230
- ).then(
231
- fn=create_report_plots, # 4. Pega DataFrames dos states e gera os gráficos Plotly brutos nos states
232
- inputs=[state_dataframe_group, state_dataframe_individuals],
233
- outputs=[state_figure_pie_chart, state_figure_bar_chart, state_figure_tree_map]
234
  ).then(
235
- fn=_update_plot_components, # 5. Atualiza os componentes Gradio Plot visíveis
236
- inputs=[state_figure_pie_chart, state_figure_bar_chart, state_figure_tree_map],
237
- outputs=[plot_display_pie_chart, plot_display_bar_chart, plot_display_tree_map]
 
 
 
 
238
  ).then(
239
- fn=generate_report_pdf, # 6. Gera o PDF a partir de todos os dados e gráficos (states)
240
  inputs=[
241
- state_llm_response, # Resposta LLM original - Original LLM response
242
- state_dataframe_group, state_dataframe_group_description, state_dataframe_individuals, state_dataframe_individuals_description,
243
- state_figure_pie_chart, state_figure_bar_chart, state_figure_tree_map
 
 
 
 
 
244
  ],
245
- outputs=[state_report_file_path] # Atualiza o state do caminho do PDF
246
  ).then(
247
- fn=_update_download_button_component, # 7. Atualiza o botão de download
248
- inputs=[state_report_file_path],
249
- outputs=[download_button_report_pdf]
250
  )
251
 
252
- # --- Eventos para voltar para a aba de entrada ---
253
- button_return_to_input_tab_from_results.click(
254
- fn=lambda: gr.Tabs(selected=0),
255
- inputs=[],
256
- outputs=tabs_main_navigation
257
- )
258
- button_return_to_input_tab_from_report.click(
259
- fn=lambda: gr.Tabs(selected=0),
260
- inputs=[],
261
- outputs=tabs_main_navigation
262
- )
263
 
264
  if __name__ == "__main__":
265
  print("Executando a aplicação Gradio...")
 
1
+ # pages/main/view.py
2
  import os
 
 
3
  import gradio as gr
4
+ from typing import Tuple, Any
 
5
 
6
+ from utils.report.report_creation import generate_report_pdf
7
+ from utils.report.graph_creation import create_report_plots
8
+ from utils.report.dataframe_creation import process_report_data
9
 
10
+ from .scripts import process_inputs_to_api
 
11
  from .strings import STRINGS
12
+ from .tab01_input import create_input_components
13
+ from .tab02_results import create_tab_results
14
+ from .tab03_report import create_tab_report
15
 
 
 
 
16
  img1 = os.path.join(os.getcwd(), "static", "images", "logo.jpg")
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  def _switch_to_report_tab_and_enable_interaction() -> Tuple[gr.Tabs, gr.TabItem]:
19
+ """Muda para a aba de relatório e a torna interativa."""
 
 
 
20
  return gr.update(selected=2), gr.update(label=STRINGS["TAB_2_TITLE"] + " ✅", interactive=True)
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  with gr.Blocks(title=STRINGS["APP_TITLE"]) as interface:
23
+ # --- States ---
24
+ states = {
25
+ "dataframe_group": gr.State(None),
26
+ "dataframe_group_description": gr.State(None),
27
+ "dataframe_individuals": gr.State(None),
28
+ "dataframe_individuals_description": gr.State(None),
29
+ "figure_pie_chart": gr.State(None),
30
+ "figure_bar_chart": gr.State(None),
31
+ "figure_tree_map": gr.State(None),
32
+ "report_file_path": gr.State(None),
33
+ "llm_response": gr.State(None)
34
+ }
35
+
36
+ # --- Header ---
37
  with gr.Row():
38
  with gr.Column(scale=1):
39
+ gr.Markdown(f"# {STRINGS['APP_TITLE']}")
40
+ gr.Markdown(f"{STRINGS['APP_DESCRIPTION']}")
41
+ gr.Image(value=img1, height=64, container=False, show_label=False, scale=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ # --- Estrutura das Abas ---
44
+ components = {}
45
  with gr.Tabs() as tabs_main_navigation:
46
+ with gr.TabItem(STRINGS["TAB_0_TITLE"], id=0) as tab_input:
47
+ # Cria os componentes de entrada da Tab 1
48
+ components.update(create_input_components())
49
+
50
+ with gr.TabItem(STRINGS["TAB_1_TITLE"] + " 🔒", interactive=False, id=1) as tab_results:
51
+ components.update(create_tab_results())
52
+ components["tab_item_processing_results"] = tab_results
53
+
54
+ with gr.TabItem(STRINGS["TAB_2_TITLE"] + " 🔒", interactive=False, id=2) as tab_report:
55
+ report_elements = create_tab_report()
56
+ components.update(report_elements["components"])
57
+ components["tab_item_report_visualization"] = tab_report
58
+
59
+ # --- Ações dos Botões de Processamento ---
60
+
61
+ # Saídas comuns para ambos os botões de processamento
62
+ common_api_outputs = [
63
+ components["textbox_output_status"],
64
+ components["textbox_output_llm_response"],
65
+ tabs_main_navigation,
66
+ components["tab_item_processing_results"]
67
+ ]
68
+
69
+ # Botão para processar ARQUIVO
70
+ components["button_process_file"].click(
71
+ fn=process_inputs_to_api,
72
+ # Inputs: (None para texto, objeto de arquivo)
73
+ inputs=[gr.State(None), components["file_input"]],
74
+ outputs=common_api_outputs
75
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ # Botão para processar TEXTO
78
+ components["button_process_text"].click(
79
+ fn=process_inputs_to_api,
80
+ # Inputs: (string de texto, None para arquivo)
81
+ inputs=[components["text_input"], gr.State(None)],
82
+ outputs=common_api_outputs
83
+ )
84
 
85
+ components["textbox_output_llm_response"].change(
86
+ fn=lambda response_text: response_text,
87
+ inputs=components["textbox_output_llm_response"],
88
+ outputs=states["llm_response"]
 
89
  )
90
 
91
+ # --- Fluxo de Geração de Relatório ---
92
+ components["button_create_report"].click(
93
+ fn=_switch_to_report_tab_and_enable_interaction,
94
+ outputs=[tabs_main_navigation, components["tab_item_report_visualization"]]
95
  ).then(
96
+ fn=process_report_data,
97
+ inputs=[states["llm_response"]],
98
+ outputs=list(states.values())[:4]
99
+ ).then(
100
+ fn=report_elements["update_fns"]["dataframes"],
101
+ inputs=list(states.values())[:4],
102
  outputs=[
103
+ components["dataframe_display_grouped_data"],
104
+ components["dataframe_display_grouped_description"],
105
+ components["dataframe_display_individual_data"],
106
+ components["dataframe_display_individual_description"],
107
  ]
108
  ).then(
109
+ fn=create_report_plots,
110
+ inputs=[states["dataframe_group"], states["dataframe_individuals"]],
111
+ outputs=[states["figure_pie_chart"], states["figure_bar_chart"], states["figure_tree_map"]]
 
 
 
 
112
  ).then(
113
+ fn=report_elements["update_fns"]["plots"],
114
+ inputs=[states["figure_pie_chart"], states["figure_bar_chart"], states["figure_tree_map"]],
115
+ outputs=[
116
+ components["plot_display_pie_chart"],
117
+ components["plot_display_bar_chart"],
118
+ components["plot_display_tree_map"]
119
+ ]
120
  ).then(
121
+ fn=generate_report_pdf,
122
  inputs=[
123
+ states["llm_response"],
124
+ states["dataframe_group"],
125
+ states["dataframe_group_description"],
126
+ states["dataframe_individuals"],
127
+ states["dataframe_individuals_description"],
128
+ states["figure_pie_chart"],
129
+ states["figure_bar_chart"],
130
+ states["figure_tree_map"]
131
  ],
132
+ outputs=[states["report_file_path"]]
133
  ).then(
134
+ fn=report_elements["update_fns"]["download"],
135
+ inputs=[states["report_file_path"]],
136
+ outputs=[components["download_button_report_pdf"]]
137
  )
138
 
139
+ # --- Botões de Navegação "Voltar" ---
140
+ components["button_return_to_input_tab_from_results"].click(fn=lambda: gr.update(selected=0), outputs=tabs_main_navigation)
141
+ components["button_return_to_input_tab_from_report"].click(fn=lambda: gr.update(selected=0), outputs=tabs_main_navigation)
 
 
 
 
 
 
 
 
142
 
143
  if __name__ == "__main__":
144
  print("Executando a aplicação Gradio...")
pages/theme.py CHANGED
@@ -1,7 +1,6 @@
 
1
  import gradio as gr
2
 
3
- font=gr.themes.GoogleFont('Montserrat'),
4
-
5
  softCIF = gr.themes.Soft(
6
  primary_hue=gr.themes.Color(c100="#F7CDC9", c200="#F1A9A2", c300="#EB867B", c400="#E56556", c50="#FCEEED", c500="#D4291A", c600="#C02417", c700="#A91F14", c800="#991b1b", c900="#921A11", c950="#7B150E"),
7
  secondary_hue="teal",
 
1
+ # pages/theme.py
2
  import gradio as gr
3
 
 
 
4
  softCIF = gr.themes.Soft(
5
  primary_hue=gr.themes.Color(c100="#F7CDC9", c200="#F1A9A2", c300="#EB867B", c400="#E56556", c50="#FCEEED", c500="#D4291A", c600="#C02417", c700="#A91F14", c800="#991b1b", c900="#921A11", c950="#7B150E"),
6
  secondary_hue="teal",
sandbox/README.md CHANGED
@@ -1,3 +1,5 @@
1
  # /Sandbox
2
 
3
- Diretório específico para scripts de testes e/ou versõess alternativas.
 
 
 
1
  # /Sandbox
2
 
3
+ Diretório específico para scripts de testes e/ou versõess alternativas, além de arquivos de texto para referência ou rápida utilização em testes.
4
+
5
+ Verifique se .env e credentials/ estão corretamente configurados.
sandbox/questionariosf-36.txt DELETED
@@ -1,95 +0,0 @@
1
- 1- Em geral você diria que sua saúde é:
2
- 2- Comparada há um ano atrás, como você se classificaria sua idade em geral, agora?
3
- 3- Os seguintes itens são sobre atividades que você poderia fazer atualmente durante um dia comum.
4
- a) Atividades Rigorosas, que exigem muito esforço, tais como correr, levantar objetos pesados, participar em
5
- esportes árduos.
6
- b) Atividades moderadas, tais como mover uma mesa, passar aspirador de pó, jogar bola, varrer a casa.
7
- c) Levantar ou carregar mantimentos
8
- d) Subir vários lances de escada
9
- e) Subir um lance de escada
10
- f) Curvar-se, ajoelhar-se ou dobrar- se
11
- g) Andar mais de 1 quilômetro
12
- h) Andar vários quarteirões
13
- i) Andar um quarteirão
14
- j) Tomar banho ou vestir-se
15
- 4- Durante as últimas 4 semanas, você teve algum dos seguintes problemas com seu trabalho ou com alguma atividade regular, como conseqüência de sua saúde física?
16
- a) Você diminui a quantidade de tempo que se dedicava ao seu trabalho ou a outras atividades?
17
- b) Realizou menos tarefas do que você gostaria?
18
- c) Esteve limitado no seu tipo de trabalho ou a outras atividades.
19
- d) Teve dificuldade de fazer seu trabalho ou outras atividades
20
- 5- Durante as últimas 4 semanas, você teve algum dos seguintes problemas com seu trabalho
21
- ou outra atividade regular diária, como conseqüência de algum problema emocional (como
22
- se sentir deprimido ou ansioso)?
23
- a) Você diminui a quantidade de tempo que se dedicava ao seu trabalho ou a outras atividades?
24
- b) Realizou menos tarefas do que você gostaria?
25
- c) Não realizou ou fez qualquer das atividades com tanto cuidado como geralmente faz.
26
- 6- Durante as últimas 4 semanas, de que maneira sua saúde física ou problemas emocionais interferiram nas suas atividades sociais normais, em relação à família, amigos ou em grupo?
27
- 7- Quanta dor no corpo você teve durante as últimas 4 semanas?
28
- 8- Durante as últimas 4 semanas, quanto a dor interferiu com seu trabalho normal (incluindo o trabalho dentro de casa)?
29
- 9- Estas questões são sobre como você se sente e como tudo tem acontecido com você
30
- durante as últimas 4 semanas.
31
- Para cada questão, por favor dê uma resposta que mais se
32
- aproxime de maneira como você se sente, em relação às últimas 4 semanas.
33
- Uma
34
- A maior Uma boa Alguma
35
- Todo pequena
36
- parte do parte do parte do Nunca
37
- Tempo parte do
38
- tempo tempo tempo
39
- tempo
40
- a) Quanto tempo você
41
- tem se sentindo cheio de
42
- 1 2 3 4 5 6
43
- vigor, de vontade, de
44
- força?
45
- b) Quanto tempo você
46
- tem se sentido uma 1 2 3 4 5 6
47
- pessoa muito nervosa?
48
- c) Quanto tempo você
49
- tem se sentido tão
50
- 1 2 3 4 5 6
51
- deprimido que nada
52
- pode anima-lo?
53
- d) Quanto tempo você
54
- tem se sentido calmo ou 1 2 3 4 5 6
55
- tranqüilo?
56
- e) Quanto tempo você
57
- tem se sentido com 1 2 3 4 5 6
58
- muita energia?
59
- f) Quanto tempo você
60
- tem se sentido 1 2 3 4 5 6
61
- desanimado ou abatido?
62
- g) Quanto tempo você
63
- tem se sentido 1 2 3 4 5 6
64
- esgotado?
65
- h) Quanto tempo você
66
- tem se sentido uma 1 2 3 4 5 6
67
- pessoa feliz?
68
- i) Quanto tempo você
69
- 1 2 3 4 5 6
70
- tem se sentido cansado?
71
- 10- Durante as últimas 4 semanas, quanto de seu tempo a sua saúde física ou problemas
72
- emocionais interferiram com as suas atividades sociais (como visitar amigos, parentes, etc)?
73
- Todo A maior parte do Alguma parte do Uma pequena Nenhuma parte
74
- Tempo tempo tempo parte do tempo do tempo
75
- 1 2 3 4 5
76
- 11- O quanto verdadeiro ou falso é cada uma das afirmações para você?
77
- A maioria A maioria
78
- Definitivamente Não Definitiva-
79
- das vezes das vezes
80
- verdadeiro sei mente falso
81
- verdadeiro falso
82
- a) Eu costumo obedecer
83
- um pouco mais
84
- 1 2 3 4 5
85
- facilmente que as outras
86
- pessoas
87
- b) Eu sou tão saudável
88
- quanto qualquer pessoa 1 2 3 4 5
89
- que eu conheço
90
- c) Eu acho que a minha
91
- 1 2 3 4 5
92
- saúde vai piorar
93
- d) Minha saúde é
94
- 1 2 3 4 5
95
- excelente
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/images/logo.jpg CHANGED

Git LFS Details

  • SHA256: 7738223106da5e076476975d8185535c1aea8a3c0e6e0669609a780ebe2352f1
  • Pointer size: 130 Bytes
  • Size of remote file: 43.2 kB
utils/api_gemini.py DELETED
@@ -1,53 +0,0 @@
1
- import os
2
-
3
- from dotenv import load_dotenv
4
- from google import genai
5
- from google.genai import types
6
-
7
- from utils.prompts import icf_gemini_prompt
8
-
9
- # Carrega as variáveis de ambiente (se você usar .env)
10
- load_dotenv()
11
- GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
12
- MODEL_ID = os.getenv('MODEL_ID')
13
- CONTEXT_FIXED = ""
14
- context_path = os.path.join(os.getcwd(), "RAG", "CIF_Lista.txt")
15
-
16
- try:
17
- with open(context_path, 'r', encoding='utf-8') as f:
18
- CONTEXT_FIXED = f.read()
19
- except FileNotFoundError:
20
- CONTEXT_FIXED = "Erro: Arquivo de contexto não encontrado."
21
-
22
- print("Context: ", CONTEXT_FIXED[:100])
23
-
24
- def api_generate(user_input: str) -> str:
25
- client = genai.Client(api_key=GEMINI_API_KEY)
26
-
27
- llm_config = types.GenerateContentConfig(
28
- response_mime_type='text/plain',
29
- seed=1,
30
- system_instruction=icf_gemini_prompt,
31
- )
32
-
33
- user_prompt_content = types.Content(
34
- role='user',
35
- parts=[
36
- types.Part.from_text(text=CONTEXT_FIXED),
37
- types.Part.from_text(text=user_input)
38
- ],
39
- )
40
-
41
- response = client.models.generate_content(
42
- model=MODEL_ID,
43
- contents=user_prompt_content,
44
- config=llm_config
45
- )
46
-
47
- return response.text
48
-
49
- if __name__ == "__main__":
50
- test_string = "O paciente sente dores abdominais agudas, localizadas principalmente na região inferior do abdômen. Fadiga. Náuseas. Vômitos. Diarreia. Dificuldade para respirar. Dor no peito. O paciente observa vermelhidão persistente na pele, acompanhada de coceira em áreas específicas. Eu não consigo enxergar objetos a longas distâncias, com visão embaçada ao tentar focar. Tontura ou perda de equilíbrio. O paciente apresenta fraqueza súbita em um lado do corpo, dificultando movimentos do braço e perna."
51
- print(f"Enviando...\n{test_string}")
52
- res = api_generate(test_string)
53
- print(res)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/apis/README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Utils/apis
2
+
3
+ Aqui se armazena os códigos relacionados a chamada de APIs: `genai` e `gspread`.
utils/apis/gemini.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils/apis/gemini.py
2
+ import os
3
+ import pathlib
4
+
5
+ from typing import Optional, Union, List
6
+ from dotenv import load_dotenv
7
+ from google import genai
8
+ from google.genai import types
9
+
10
+ from utils.prompts import icf_gemini_prompt
11
+
12
+ load_dotenv()
13
+
14
+ # Chave da API e ID do Modelo obtidos do ambiente
15
+ GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
16
+ if not GEMINI_API_KEY:
17
+ raise ValueError("A variável de ambiente 'GEMINI_API_KEY' não foi definida.")
18
+
19
+ MODEL_ID = os.getenv('MODEL_ID', 'gemini-2.5-flash')
20
+ # --- CAMINHOS E ARQUIVOS DE CONTEXTO ---
21
+
22
+ # Define o caminho para o prompt do sistema e o PDF de contexto usando pathlib para compatibilidade de SO
23
+ BASE_DIR = pathlib.Path(__file__).parent.parent.parent
24
+ PDF_CONTEXT_PATH = BASE_DIR / "CIF" / "ListaCIF.pdf"
25
+ SYSTEM_PROMPT_PATH = BASE_DIR / "utils" / "prompts.py"
26
+
27
+ def _load_sys_instruction(caminho: pathlib.Path) -> str:
28
+ """Carrega a string do prompt do sistema a partir de um arquivo Python."""
29
+ try:
30
+ return icf_gemini_prompt
31
+ except (ImportError, FileNotFoundError):
32
+ print(f"Aviso: Não foi possível encontrar ou importar o prompt do sistema de '{caminho}'. Usando um prompt padrão.")
33
+ return "Você é um especialista na Classificação Internacional de Funcionalidade (CIF). Classifique o texto fornecido de acordo com a CIF e forneça uma análise detalhada."
34
+
35
+ def _create_file_part(file_path_str: str) -> types.Part:
36
+ """
37
+ Valida, lê e cria um objeto Part a partir de um caminho de arquivo.
38
+
39
+ Esta função verifica se o arquivo existe e se sua extensão (.txt ou .pdf) é
40
+ suportada. Em caso afirmativo, lê os bytes do arquivo e retorna um objeto
41
+ `types.Part` com o MIME type correto.
42
+
43
+ Args:
44
+ file_path_str: O caminho para o arquivo, recebido como string.
45
+
46
+ Returns:
47
+ Um objeto `types.Part` pronto para ser enviado à API Gemini.
48
+
49
+ Raises:
50
+ FileNotFoundError: Se o arquivo não for encontrado no caminho especificado.
51
+ ValueError: Se a extensão do arquivo não for suportada.
52
+ """
53
+ input_file_path = pathlib.Path(file_path_str)
54
+
55
+ if not input_file_path.is_file():
56
+ raise FileNotFoundError(f"O arquivo de entrada do usuário não foi encontrado: {input_file_path}")
57
+
58
+ file_extension = input_file_path.suffix.lower()
59
+
60
+ if file_extension == '.pdf':
61
+ mime_type = 'application/pdf'
62
+ elif file_extension == '.txt':
63
+ mime_type = 'text/plain'
64
+ else:
65
+ raise ValueError(
66
+ f"Tipo de arquivo '{file_extension}' não suportado. "
67
+ "Por favor, envie um arquivo .txt ou .pdf."
68
+ )
69
+
70
+ return types.Part.from_bytes(
71
+ data=input_file_path.read_bytes(),
72
+ mime_type=mime_type
73
+ )
74
+
75
+ def api_generate(
76
+ input_text: Optional[str] = None,
77
+ input_file: Optional[Union[str, pathlib.Path]] = None,
78
+ ) -> str:
79
+ """
80
+ Gera uma análise baseada na CIF a partir de um texto ou arquivo de entrada.
81
+
82
+ Utiliza um PDF da CIF como contexto fixo e combina com a entrada do usuário
83
+ (seja um texto direto ou o conteúdo de um arquivo) para gerar uma resposta
84
+ usando a API do Gemini.
85
+
86
+ Args:
87
+ input_text: Uma string contendo o texto a ser analisado.
88
+ input_file: O caminho para um arquivo de texto (.txt) cujo conteúdo
89
+ será analisado.
90
+
91
+ Returns:
92
+ A string com a análise gerada pelo modelo.
93
+
94
+ Raises:
95
+ ValueError: Se ambos `input_text` e `input_file` forem fornecidos, ou se
96
+ nenhum dos dois for fornecido.
97
+ FileNotFoundError: Se o arquivo `input_file` ou o PDF de contexto
98
+ não forem encontrados.
99
+ """
100
+
101
+ # 1. Validação da entrada (garante que ou texto ou arquivo foi fornecido, mas não ambos)
102
+ if not (input_text is None) ^ (input_file is None):
103
+ raise ValueError("Forneça exatamente um dos parâmetros: 'input_text' ou 'input_file'.")
104
+
105
+ # 2. Preparação do Conteúdo (Contents)
106
+ if not PDF_CONTEXT_PATH.is_file():
107
+ raise FileNotFoundError(f"Arquivo de contexto PDF não encontrado em: {PDF_CONTEXT_PATH}")
108
+
109
+
110
+ client = genai.Client(api_key=GEMINI_API_KEY)
111
+
112
+ system_instruction = _load_sys_instruction(SYSTEM_PROMPT_PATH)
113
+
114
+ llm_config = types.GenerateContentConfig(
115
+ thinking_config = types.ThinkingConfig(
116
+ thinking_budget=-1,
117
+ ),
118
+ response_mime_type='text/plain',
119
+ seed=1,
120
+ system_instruction=[
121
+ types.Part.from_text(text=system_instruction),
122
+ ],
123
+ )
124
+
125
+ user_contents = [
126
+ types.Part.from_bytes(
127
+ data=PDF_CONTEXT_PATH.read_bytes(),
128
+ mime_type='application/pdf'
129
+ )
130
+ ]
131
+
132
+ # Se a entrada for texto, adiciona um 'Part' de texto.
133
+ if input_text:
134
+ user_contents.append(
135
+ types.Part.from_text(
136
+ text=input_text,
137
+ )
138
+ )
139
+
140
+ # Adiciona o arquivo do usuário como um 'Part' de PDF, enviando seus bytes.
141
+ if input_file:
142
+ file_part = _create_file_part(input_file)
143
+ user_contents.append(file_part)
144
+ '''
145
+ input_file_path = pathlib.Path(input_file)
146
+ user_contents.append(
147
+ types.Part.from_bytes(
148
+ data=input_file_path.read_bytes(),
149
+ mime_type='application/pdf'
150
+ )
151
+ )
152
+ '''
153
+
154
+ response = client.models.generate_content(
155
+ model=MODEL_ID,
156
+ contents=user_contents,
157
+ config=llm_config
158
+ )
159
+
160
+ return response.text
utils/phrase_extractor.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import fitz # PyMuPDF: Library for working with PDF files
2
  from docx import Document # python-docx: Library for working with DOCX files
3
  import os # Module for interacting with the operating system (file paths)
 
1
+ # DEPRECATED: Este script foi uma tentativa de extrair frases localmente.
2
  import fitz # PyMuPDF: Library for working with PDF files
3
  from docx import Document # python-docx: Library for working with DOCX files
4
  import os # Module for interacting with the operating system (file paths)
utils/prompts.py CHANGED
@@ -17,27 +17,49 @@ def icf_classifier_prompt(context, input_text):
17
  - **Justificativa**: [Explicação baseada no Contexto]
18
  """
19
 
20
- icf_gemini_prompt="""
21
- Você é um assistente especializado na Classificação Internacional de Funcionalidade, Incapacidade e Saúde (CIF). Sua tarefa é analisar frases de entrada e classificá-la de acordo com os componentes da CIF, usando o arquivo de *Contexto CIF* e aplicando seu conhecimento sobre a CIF para identificar conceitos que podem não estar explicitamente no *Contexto CIF*, mas que são relevantes.
22
-
23
- **Instruções para a Classificação:**
24
- 1. *Conceito Significativo:* Extraia o propósito, a ideia central, de cada frase preesente no texto de entrada, independentemente de sua forma (pergunta ou afirmação). Extraia frases por ";", "." e "\n" (quebras de linha). Em caso de vírgulas, avalie se as frases se complementam ou se possuem conceito significativo distintos.
25
- 2. *Verifique a Vinculação com a CIF (priorizando o *contexto CIF*, mas não se limitando a ele):*
26
- - *Priorize o Contexto CIF:* Primeiramente, examine o *Contexto CIF* fornecido. Se houver termos, códigos ou descrições que se relacionam diretamente com o "Conceito Significativo" da frase, utilize-os.
27
- - *Aplique Conhecimento Adicional da CIF:* Se o *Conceito Significativo* não for explicitamente coberto ou detalhado o suficiente no *Contexto CIF*, use seu conhecimento abrangente da CIF para identificar a correspondência mais próxima. Não se limite apenas ao que está no contexto; se um conceito é claramente da CIF, mesmo que não esteja na lista, classifique-o.
28
- - *Não Coberto:* Se, após a análise do *contexto CIF* e do seu conhecimento geral da CIF, o termo ou conceito não puder ser razoavelmente vinculado a nenhum domínio da CIF, classifique-o como "Não coberto."
29
- 3. **Determine o Componente da CIF:** Para os conceitos vinculados à CIF, identifique a qual dos quatro componentes principais ele pertence, baseado na natureza do conceito e no código (se disponível):
30
- - *Funções Corporais (b)*; *Estruturas Corporais (s)*; *Atividades e Participação (d)*; *Fatores Ambientais (e)*;
31
- 4. *Não Definido:* Se um termo ou conceito for claramente mencionado na CIF (seja no contexto ou no seu conhecimento geral), mas não puder ser categorizado em nenhum dos quatro componentes principais da CIF, classifique-o como "Não definido." Isso é possível para termos mais genéricos ou que exigem mais contexto para uma vinculação específica.
32
-
33
- **Formato da Saída:**
34
- Para cada *Conceito Significativo* identificado na `Frase de Entrada do Usuário`, retorne um bloco de texto, respeitando o idioma de entrada, com a seguinte estrutura:
35
-
36
- - Frase de Entrada: [A frase original]
37
- - Conceito Significativo: [O conceito significativo extraído da frase]
38
- - Status de Cobertura pela CIF: ["Coberto", "Não Coberto (N.C.)", ou "Não Definido (N.D.)"]
39
- - Categoria CIF: [Se "Coberto", indique: "Funções Corporais", "Estruturas Corporais", "Atividades e Participação", "Fatores Ambientais". Caso contrário, retorne "N.C." ou "N.D."]
40
- - Codificação CIF: [Código + Título] [Se "Coberto", o código e título mais relevante da CIF. Caso contrário, retorne "N.C." ou "N.D."]
41
- - Descrição CIF: [Se "Coberto", a descrição completa ou parte dela que se relaciona mais diretamente com o conceito. Se "Não Coberto": o conceito não está representado nem como código nem como referência na CIF, Se "Não Definido": conceito está referenciado pela CIF, mas não tem um código específico nem pertence a um componente]
42
- - Justificativa da Classificação: [Explique brevemente por que o conceito foi classificado dessa forma, referenciando o contexto RAG quando usado, ou explicando a lógica da classificação com base no seu conhecimento da CIF.]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  """
 
17
  - **Justificativa**: [Explicação baseada no Contexto]
18
  """
19
 
20
+ icf_gemini_prompt="""Você é um especialista na Classificação Internacional de Funcionalidade, Incapacidade e Saúde (CIF), uma ferramenta da OMS para descrever a saúde. Sua análise deve ser rigorosa, técnica e fundamentada nos princípios da CIF, tendo como principal referência as fontes fornecidas.
21
+
22
+ **ESTRUTURA DOS INPUTS**
23
+
24
+ Você receberá duas informações:
25
+ - **[LISTA CIF]:** Um arquivo contendo a lista de referência da CIF. Utilize este documento como sua principal fonte de consulta para garantir a precisão dos códigos e definições.
26
+ - **[ENTRADA DO USUÁRIO]:** O conteúdo a ser analisado (pode ser um texto simples ou um arquivo).
27
+
28
+ **TAREFA PRINCIPAL**
29
+
30
+ Sua tarefa é analisar o conteúdo fornecido em **[ENTRADA DO USUÁRIO]**:
31
+ 1. Segmente o conteúdo em frases ou ideias centrais que permitem avaliar as condições de uma pessoa.
32
+ 2. Para cada frase/ideia, realize o processo de classificação detalhado abaixo.
33
+
34
+ **PROCESSO DE CLASSIFICAÇÃO**
35
+
36
+ Para cada frase ou trecho relevante encontrado:
37
+ 1. **Extração:** Recupere a frase original.
38
+ 2. **Contextualização:** Identifique e resuma o "Contexto Significativo" (ideia central) da frase.
39
+ 3. **Verificação de Cobertura:** Com base no seu conhecimento e consultando a **[LISTA CIF]**, determine se o Contexto Significativo está: "Coberto", "Não Coberto (N.C.)" ou "Não Definido (N.D.)".
40
+ 4. **Classificação:** Se o status for "Coberto", identifique o código CIF e o título mais preciso, confirmando-os com o documento **[LISTA CIF]**.
41
+
42
+ **ESTRUTURA E REGRAS RÍGIDAS DE SAÍDA**
43
+
44
+ - **Formato Fixo:** Para cada análise, siga estritamente o formato abaixo.
45
+ - **Separador:** Utilize `---` (três hífens) para separar cada análise completa.
46
+ - **Sem Markdown:** A saída deve ser apenas em texto puro.
47
+
48
+ **ESTRUTURA DE SAÍDA INDIVIDUAL:**
49
+
50
+ Frase Extraída: [Trecho exato obtido do texto ou documento analisado]
51
+ - Contexto Significativo: [Conceito significativo obtido do trecho]
52
+ - Status da Cobertura: [Coberto; Não Coberto (N.C.); Não Definido (N.D.)]
53
+ - Codificação CIF: [Se Coberto, insira o Código e o Título do código; N.C.; N.D.]
54
+ - Justificativa: [Breve explicação da escolha do código e da cobertura]
55
+
56
+ **EXEMPLO DE EXECUÇÃO PERFEITA:**
57
+
58
+ *Input do Usuário:*: O paciente relata cansaço ao caminhar mais de um quarteirão.
59
+ *Sua Saída Esperada:*
60
+ Frase Extraída: O paciente relata cansaço ao caminhar mais de um quarteirão.
61
+ - Contexto Significativo: Dificuldade para andar longas distâncias.
62
+ - Status da Cobertura: Coberto
63
+ - Codificação CIF: d450 Andar
64
+ - Justificativa: A atividade de 'caminhar' é diretamente coberta pelo código d450, que se refere a andar distâncias variadas.
65
  """
utils/rag_retriever.py CHANGED
@@ -9,23 +9,21 @@ from nltk import sent_tokenize
9
  import nltk
10
 
11
  # Baixar o tokenizador de frases do NLTK (necessário apenas uma vez)
12
- # try:
13
- # print("tentanto encontrar o tokenizador de frases do NLTK...")
14
- # nltk.data.find('tokenizers/punkt') or nltk.download('tokenizers/punkt_tab')
15
- # except nltk.downloader.DownloadError:
16
- # print("Tokenizador de frases do NLTK não encontrado. Baixando...")
17
- # nltk.download('punkt_tab')
18
- nltk.download("punkt")
19
 
20
  # Configurações
21
  # Configurações
22
- RAG_DIR = r".\RAG"
23
- DATA_DIR = os.path.join(RAG_DIR, "data")
24
- FAISS_INDEX_DIR = os.path.join(RAG_DIR, "FAISS") # Renamed from FAISS_DIR for clarity
25
- CONTEXT_FAISS_INDEX_PATH = os.path.join(FAISS_INDEX_DIR, "context_index.faiss") # Renamed variable
26
- CONTEXT_JSON_TEXT_PATH = os.path.join(FAISS_INDEX_DIR, "context_texts.json") # Renamed variable
27
- EMBEDDING_MODEL_NAME = "nomic-ai/nomic-embed-text-v2-moe" # Renamed variable
28
-
29
 
30
  def _load_embedding_model() -> SentenceTransformer:
31
  """
@@ -40,7 +38,6 @@ def _load_embedding_model() -> SentenceTransformer:
40
  print(f"Carregando modelo de embeddings {EMBEDDING_MODEL_NAME}...")
41
  return SentenceTransformer(EMBEDDING_MODEL_NAME, trust_remote_code=True)
42
 
43
-
44
  def _load_existing_index_and_documents() -> tuple[list | None, faiss.Index | None]:
45
  """
46
  Attempts to load an existing FAISS index and its associated text documents
@@ -59,7 +56,7 @@ def _load_existing_index_and_documents() -> tuple[list | None, faiss.Index | Non
59
  print("Carregando índice e documentos existentes...")
60
  try:
61
  faiss_index = faiss.read_index(CONTEXT_FAISS_INDEX_PATH)
62
- with open(CONTEXT_JSON_TEXT_PATH, "r", encoding="utf-8") as f:
63
  loaded_documents = json.load(f)
64
  print(f"Carregados {len(loaded_documents)} documentos do índice existente.")
65
  return loaded_documents, faiss_index
@@ -68,7 +65,6 @@ def _load_existing_index_and_documents() -> tuple[list | None, faiss.Index | Non
68
  return None, None
69
  return None, None
70
 
71
-
72
  def _load_source_documents() -> list[str]:
73
  """
74
  Loads and preprocesses text documents from the data folder (DATA_DIR).
@@ -85,16 +81,16 @@ def _load_source_documents() -> list[str]:
85
  ValueError: If no '.txt' files are found in the data directory
86
  or if no valid documents are loaded after processing.
87
  """
88
- file_paths = glob.glob(os.path.join(DATA_DIR, "*.txt"))
89
  if not file_paths:
90
  raise ValueError(f"Nenhum arquivo .txt encontrado em {DATA_DIR}. Por favor, adicione documentos.")
91
 
92
  context_chunks = []
93
  for file_path in file_paths:
94
  try:
95
- with open(file_path, "r", encoding="utf-8") as f:
96
  # Splits by double newline, strips whitespace, and filters out empty strings
97
- context_chunks.extend(list(filter(None, map(str.strip, f.read().split("\n\n")))))
98
  except Exception as e:
99
  print(f"Erro ao ler o arquivo {file_path}: {e}")
100
  continue
@@ -105,7 +101,6 @@ def _load_source_documents() -> list[str]:
105
  print(f"Carregados {len(context_chunks)} documentos.")
106
  return context_chunks
107
 
108
-
109
  def _generate_text_embeddings(embedder_model: SentenceTransformer, text_documents: list[str]) -> np.ndarray:
110
  """
111
  Generates numerical embeddings for a list of text documents using the provided embedder.
@@ -128,9 +123,9 @@ def _generate_text_embeddings(embedder_model: SentenceTransformer, text_document
128
  batch_size = 32
129
  generated_embeddings_list = []
130
  for i in range(0, len(text_documents), batch_size):
131
- batch = text_documents[i : i + batch_size]
132
  try:
133
- if batch: # Ensure the batch is not empty
134
  generated_embeddings_list.extend(embedder_model.encode(batch, show_progress_bar=False))
135
  except Exception as e:
136
  print(f"Erro ao gerar embeddings para lote {i//batch_size if batch_size > 0 else i}: {e}")
@@ -143,7 +138,6 @@ def _generate_text_embeddings(embedder_model: SentenceTransformer, text_document
143
 
144
  return np.array(generated_embeddings_list, dtype=np.float32)
145
 
146
-
147
  def _create_faiss_index(document_embeddings: np.ndarray) -> faiss.Index:
148
  """
149
  Creates and populates a FAISS (Facebook AI Similarity Search) index from a set of embeddings.
@@ -165,7 +159,6 @@ def _create_faiss_index(document_embeddings: np.ndarray) -> faiss.Index:
165
  faiss_index.add(document_embeddings)
166
  return faiss_index
167
 
168
-
169
  def initialize_rag_system() -> tuple[list[str], faiss.Index, SentenceTransformer]:
170
  """
171
  Initializes the complete RAG (Retrieval Augmented Generation) system.
@@ -188,27 +181,20 @@ def initialize_rag_system() -> tuple[list[str], faiss.Index, SentenceTransformer
188
  text_embedder = _load_embedding_model()
189
  context_documents, faiss_index = _load_existing_index_and_documents()
190
 
191
- if faiss_index is None: # If the index doesn't exist or an error occurred loading it, rebuild
192
  print("Índice FAISS não encontrado ou corrompido. Reconstruindo...")
193
  context_documents = _load_source_documents()
194
  document_embeddings = _generate_text_embeddings(text_embedder, context_documents)
195
  faiss_index = _create_faiss_index(document_embeddings)
196
 
197
  faiss.write_index(faiss_index, CONTEXT_FAISS_INDEX_PATH)
198
- with open(CONTEXT_JSON_TEXT_PATH, "w", encoding="utf-8") as f:
199
- json.dump(context_documents, f, ensure_ascii=False, indent=4) # Added indent for readability
200
  print("Novo índice e documentos salvos com sucesso.")
201
 
202
  return context_documents, faiss_index, text_embedder
203
 
204
-
205
- def search_with_full_query(
206
- full_question_text: str,
207
- context_documents: list[str],
208
- faiss_index: faiss.Index,
209
- embedder_model: SentenceTransformer,
210
- k_results: int = 3,
211
- ) -> list[tuple[int, str, float]]:
212
  """
213
  Searches for the 'k_results' most relevant documents for the **entire question**,
214
  treating it as a single search unit. This function does not segment the question into sentences.
@@ -252,14 +238,7 @@ def search_with_full_query(
252
  print(f"Erro ao buscar contexto completo: {e}")
253
  return []
254
 
255
-
256
- def search_with_multiple_sentences(
257
- question_text: str,
258
- context_documents: list[str],
259
- faiss_index: faiss.Index,
260
- embedder_model: SentenceTransformer,
261
- k_per_sentence: int = 2,
262
- ) -> list[tuple[int, str, float]]:
263
  """
264
  Segments the question into sentences and searches for the 'k_per_sentence' most relevant
265
  documents for **EACH sentence**, then consolidates and returns only unique contexts.
@@ -285,7 +264,7 @@ def search_with_multiple_sentences(
285
 
286
  print(f"Buscando múltiplos contextos para: '{question_text}'")
287
 
288
- sentences = sent_tokenize(question_text, language="portuguese")
289
  if not sentences:
290
  print("Nenhuma frase detectada na pergunta para busca de múltiplos contextos.")
291
  return []
@@ -298,7 +277,7 @@ def search_with_multiple_sentences(
298
  try:
299
  for sentence in sentences:
300
  print(f"Processando frase para múltiplos contextos: '{sentence}'")
301
- if not sentence.strip(): # Skip empty sentences that might be produced by sent_tokenize
302
  continue
303
  query_embedding = np.array(embedder_model.encode([sentence]), dtype=np.float32)
304
  distances, indices = faiss_index.search(query_embedding, k_per_sentence)
@@ -309,15 +288,8 @@ def search_with_multiple_sentences(
309
 
310
  if 0 <= document_index < len(context_documents):
311
  # If the document has already been found, update if the new distance is smaller (more relevant)
312
- if (
313
- document_index not in consolidated_contexts_map
314
- or distance_score < consolidated_contexts_map[document_index][2]
315
- ):
316
- consolidated_contexts_map[document_index] = (
317
- document_index,
318
- context_documents[document_index],
319
- distance_score,
320
- )
321
 
322
  # Convert the dictionary of consolidated contexts back to a list
323
  results_list = list(consolidated_contexts_map.values())
@@ -330,7 +302,6 @@ def search_with_multiple_sentences(
330
  print(f"Erro ao buscar múltiplos contextos: {e}")
331
  return []
332
 
333
-
334
  # --- Funções de Teste ---
335
  def test_context_search_interactive():
336
  """
@@ -348,7 +319,7 @@ def test_context_search_interactive():
348
 
349
  while True:
350
  user_question = input("\nDigite uma pergunta (ou 'sair' para encerrar): ")
351
- if user_question.lower() == "sair":
352
  break
353
 
354
  print("\nEscolha o tipo de busca:")
@@ -357,16 +328,12 @@ def test_context_search_interactive():
357
  search_choice = input("Opção (1 ou 2): ")
358
 
359
  retrieved_contexts = []
360
- if search_choice == "1":
361
  print(f"\nRealizando busca de contexto completo para: '{user_question}'")
362
- retrieved_contexts = search_with_full_query(
363
- user_question, context_documents, faiss_index, text_embedder, k_results=5
364
- )
365
- elif search_choice == "2":
366
  print(f"\nRealizando busca de múltiplos contextos para: '{user_question}'")
367
- retrieved_contexts = search_with_multiple_sentences(
368
- user_question, context_documents, faiss_index, text_embedder, k_per_sentence=3
369
- )
370
  else:
371
  print("Opção inválida. Tente novamente.")
372
  continue
@@ -378,9 +345,8 @@ def test_context_search_interactive():
378
  print("\nContextos mais relevantes:")
379
  for doc_idx, text_content, distance_score in retrieved_contexts:
380
  print(f"\nÍndice Original do Documento: {doc_idx}, Distância: {distance_score:.4f}")
381
- print(f"Texto: {text_content[:500]}...") # Limita o texto para melhor visualização
382
  print("-" * 50)
383
 
384
-
385
  if __name__ == "__main__":
386
- test_context_search_interactive()
 
9
  import nltk
10
 
11
  # Baixar o tokenizador de frases do NLTK (necessário apenas uma vez)
12
+ try:
13
+ print("tentanto encontrar o tokenizador de frases do NLTK...")
14
+ nltk.data.find('tokenizers/punkt') or nltk.download('tokenizers/punkt_tab')
15
+ except nltk.downloader.DownloadError:
16
+ print("Tokenizador de frases do NLTK não encontrado. Baixando...")
17
+ nltk.download('punkt_tab')
 
18
 
19
  # Configurações
20
  # Configurações
21
+ RAG_DIR = r'.\RAG'
22
+ DATA_DIR = os.path.join(RAG_DIR, 'data')
23
+ FAISS_INDEX_DIR = os.path.join(RAG_DIR, 'FAISS') # Renamed from FAISS_DIR for clarity
24
+ CONTEXT_FAISS_INDEX_PATH = os.path.join(FAISS_INDEX_DIR, 'context_index.faiss') # Renamed variable
25
+ CONTEXT_JSON_TEXT_PATH = os.path.join(FAISS_INDEX_DIR, 'context_texts.json') # Renamed variable
26
+ EMBEDDING_MODEL_NAME = 'nomic-ai/nomic-embed-text-v2-moe' # Renamed variable
 
27
 
28
  def _load_embedding_model() -> SentenceTransformer:
29
  """
 
38
  print(f"Carregando modelo de embeddings {EMBEDDING_MODEL_NAME}...")
39
  return SentenceTransformer(EMBEDDING_MODEL_NAME, trust_remote_code=True)
40
 
 
41
  def _load_existing_index_and_documents() -> tuple[list | None, faiss.Index | None]:
42
  """
43
  Attempts to load an existing FAISS index and its associated text documents
 
56
  print("Carregando índice e documentos existentes...")
57
  try:
58
  faiss_index = faiss.read_index(CONTEXT_FAISS_INDEX_PATH)
59
+ with open(CONTEXT_JSON_TEXT_PATH, 'r', encoding='utf-8') as f:
60
  loaded_documents = json.load(f)
61
  print(f"Carregados {len(loaded_documents)} documentos do índice existente.")
62
  return loaded_documents, faiss_index
 
65
  return None, None
66
  return None, None
67
 
 
68
  def _load_source_documents() -> list[str]:
69
  """
70
  Loads and preprocesses text documents from the data folder (DATA_DIR).
 
81
  ValueError: If no '.txt' files are found in the data directory
82
  or if no valid documents are loaded after processing.
83
  """
84
+ file_paths = glob.glob(os.path.join(DATA_DIR, '*.txt'))
85
  if not file_paths:
86
  raise ValueError(f"Nenhum arquivo .txt encontrado em {DATA_DIR}. Por favor, adicione documentos.")
87
 
88
  context_chunks = []
89
  for file_path in file_paths:
90
  try:
91
+ with open(file_path, 'r', encoding='utf-8') as f:
92
  # Splits by double newline, strips whitespace, and filters out empty strings
93
+ context_chunks.extend(list(filter(None, map(str.strip, f.read().split('\n\n')))))
94
  except Exception as e:
95
  print(f"Erro ao ler o arquivo {file_path}: {e}")
96
  continue
 
101
  print(f"Carregados {len(context_chunks)} documentos.")
102
  return context_chunks
103
 
 
104
  def _generate_text_embeddings(embedder_model: SentenceTransformer, text_documents: list[str]) -> np.ndarray:
105
  """
106
  Generates numerical embeddings for a list of text documents using the provided embedder.
 
123
  batch_size = 32
124
  generated_embeddings_list = []
125
  for i in range(0, len(text_documents), batch_size):
126
+ batch = text_documents[i:i + batch_size]
127
  try:
128
+ if batch: # Ensure the batch is not empty
129
  generated_embeddings_list.extend(embedder_model.encode(batch, show_progress_bar=False))
130
  except Exception as e:
131
  print(f"Erro ao gerar embeddings para lote {i//batch_size if batch_size > 0 else i}: {e}")
 
138
 
139
  return np.array(generated_embeddings_list, dtype=np.float32)
140
 
 
141
  def _create_faiss_index(document_embeddings: np.ndarray) -> faiss.Index:
142
  """
143
  Creates and populates a FAISS (Facebook AI Similarity Search) index from a set of embeddings.
 
159
  faiss_index.add(document_embeddings)
160
  return faiss_index
161
 
 
162
  def initialize_rag_system() -> tuple[list[str], faiss.Index, SentenceTransformer]:
163
  """
164
  Initializes the complete RAG (Retrieval Augmented Generation) system.
 
181
  text_embedder = _load_embedding_model()
182
  context_documents, faiss_index = _load_existing_index_and_documents()
183
 
184
+ if faiss_index is None: # If the index doesn't exist or an error occurred loading it, rebuild
185
  print("Índice FAISS não encontrado ou corrompido. Reconstruindo...")
186
  context_documents = _load_source_documents()
187
  document_embeddings = _generate_text_embeddings(text_embedder, context_documents)
188
  faiss_index = _create_faiss_index(document_embeddings)
189
 
190
  faiss.write_index(faiss_index, CONTEXT_FAISS_INDEX_PATH)
191
+ with open(CONTEXT_JSON_TEXT_PATH, 'w', encoding='utf-8') as f:
192
+ json.dump(context_documents, f, ensure_ascii=False, indent=4) # Added indent for readability
193
  print("Novo índice e documentos salvos com sucesso.")
194
 
195
  return context_documents, faiss_index, text_embedder
196
 
197
+ def search_with_full_query(full_question_text: str, context_documents: list[str], faiss_index: faiss.Index, embedder_model: SentenceTransformer, k_results: int = 3) -> list[tuple[int, str, float]]:
 
 
 
 
 
 
 
198
  """
199
  Searches for the 'k_results' most relevant documents for the **entire question**,
200
  treating it as a single search unit. This function does not segment the question into sentences.
 
238
  print(f"Erro ao buscar contexto completo: {e}")
239
  return []
240
 
241
+ def search_with_multiple_sentences(question_text: str, context_documents: list[str], faiss_index: faiss.Index, embedder_model: SentenceTransformer, k_per_sentence: int = 2) -> list[tuple[int, str, float]]:
 
 
 
 
 
 
 
242
  """
243
  Segments the question into sentences and searches for the 'k_per_sentence' most relevant
244
  documents for **EACH sentence**, then consolidates and returns only unique contexts.
 
264
 
265
  print(f"Buscando múltiplos contextos para: '{question_text}'")
266
 
267
+ sentences = sent_tokenize(question_text, language='portuguese')
268
  if not sentences:
269
  print("Nenhuma frase detectada na pergunta para busca de múltiplos contextos.")
270
  return []
 
277
  try:
278
  for sentence in sentences:
279
  print(f"Processando frase para múltiplos contextos: '{sentence}'")
280
+ if not sentence.strip(): # Skip empty sentences that might be produced by sent_tokenize
281
  continue
282
  query_embedding = np.array(embedder_model.encode([sentence]), dtype=np.float32)
283
  distances, indices = faiss_index.search(query_embedding, k_per_sentence)
 
288
 
289
  if 0 <= document_index < len(context_documents):
290
  # If the document has already been found, update if the new distance is smaller (more relevant)
291
+ if document_index not in consolidated_contexts_map or distance_score < consolidated_contexts_map[document_index][2]:
292
+ consolidated_contexts_map[document_index] = (document_index, context_documents[document_index], distance_score)
 
 
 
 
 
 
 
293
 
294
  # Convert the dictionary of consolidated contexts back to a list
295
  results_list = list(consolidated_contexts_map.values())
 
302
  print(f"Erro ao buscar múltiplos contextos: {e}")
303
  return []
304
 
 
305
  # --- Funções de Teste ---
306
  def test_context_search_interactive():
307
  """
 
319
 
320
  while True:
321
  user_question = input("\nDigite uma pergunta (ou 'sair' para encerrar): ")
322
+ if user_question.lower() == 'sair':
323
  break
324
 
325
  print("\nEscolha o tipo de busca:")
 
328
  search_choice = input("Opção (1 ou 2): ")
329
 
330
  retrieved_contexts = []
331
+ if search_choice == '1':
332
  print(f"\nRealizando busca de contexto completo para: '{user_question}'")
333
+ retrieved_contexts = search_with_full_query(user_question, context_documents, faiss_index, text_embedder, k_results=5)
334
+ elif search_choice == '2':
 
 
335
  print(f"\nRealizando busca de múltiplos contextos para: '{user_question}'")
336
+ retrieved_contexts = search_with_multiple_sentences(user_question, context_documents, faiss_index, text_embedder, k_per_sentence=3)
 
 
337
  else:
338
  print("Opção inválida. Tente novamente.")
339
  continue
 
345
  print("\nContextos mais relevantes:")
346
  for doc_idx, text_content, distance_score in retrieved_contexts:
347
  print(f"\nÍndice Original do Documento: {doc_idx}, Distância: {distance_score:.4f}")
348
+ print(f"Texto: {text_content[:500]}...") # Limita o texto para melhor visualização
349
  print("-" * 50)
350
 
 
351
  if __name__ == "__main__":
352
+ test_context_search_interactive()
utils/report/README.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # utils/report
2
+
3
+ Aqui se armazena os códigos relacionados a geração de relatórios. Separa-se o tratamento de texto, de dataframe e de gráficos para sua união na interface gradio e no arquivo .pdf gerado.
utils/{dataframe_creation.py → report/dataframe_creation.py} RENAMED
@@ -1,3 +1,4 @@
 
1
  import re
2
  import pandas as pd
3
  from typing import Dict, Tuple, List
@@ -47,7 +48,6 @@ def _count_group_frequencies(llm_res: str) -> Dict[str, int]:
47
  print(f"Frequências por grupo atualizadas: {group_frequencies}")
48
  return group_frequencies
49
 
50
-
51
  def _count_individual_frequencies(llm_res: str) -> Dict[str, int]:
52
  """Conta a frequência de cada código CIF individualmente."""
53
  print("Contando frequências individuais...")
 
1
+ # utils/report/dataframe_creation.py
2
  import re
3
  import pandas as pd
4
  from typing import Dict, Tuple, List
 
48
  print(f"Frequências por grupo atualizadas: {group_frequencies}")
49
  return group_frequencies
50
 
 
51
  def _count_individual_frequencies(llm_res: str) -> Dict[str, int]:
52
  """Conta a frequência de cada código CIF individualmente."""
53
  print("Contando frequências individuais...")
utils/{graph_creation.py → report/graph_creation.py} RENAMED
@@ -1,7 +1,8 @@
 
1
  import plotly.express as px
2
  import plotly.graph_objects as go
3
  import pandas as pd
4
- from typing import Optional # Dict e List não são mais necessários para as constantes globais
5
 
6
  # Importa a Enum para centralizar as definições de categoria, rótulos e cores
7
  from .icf_categories import ICFComponent
@@ -66,7 +67,6 @@ def create_pie_chart(
66
  )
67
  return figure
68
 
69
-
70
  def create_bar_chart(
71
  input_df: pd.DataFrame,
72
  title: str = "Frequência da Classificação"
@@ -120,7 +120,6 @@ def create_bar_chart(
120
  )
121
  return figure
122
 
123
-
124
  def create_tree_map_chart(
125
  tree_map_df: pd.DataFrame,
126
  title: str = "Treemap de Frequências por Hierarquia de Códigos"
@@ -176,4 +175,24 @@ def create_tree_map_chart(
176
  font_size=12,
177
  )
178
  )
179
- return figure
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils/report/graph_creation.py
2
  import plotly.express as px
3
  import plotly.graph_objects as go
4
  import pandas as pd
5
+ from typing import Optional, Tuple # Dict e List não são mais necessários para as constantes globais
6
 
7
  # Importa a Enum para centralizar as definições de categoria, rótulos e cores
8
  from .icf_categories import ICFComponent
 
67
  )
68
  return figure
69
 
 
70
  def create_bar_chart(
71
  input_df: pd.DataFrame,
72
  title: str = "Frequência da Classificação"
 
120
  )
121
  return figure
122
 
 
123
  def create_tree_map_chart(
124
  tree_map_df: pd.DataFrame,
125
  title: str = "Treemap de Frequências por Hierarquia de Códigos"
 
175
  font_size=12,
176
  )
177
  )
178
+ return figure
179
+
180
+ def create_report_plots(df_group: pd.DataFrame, df_individual_treemap: pd.DataFrame) -> Tuple[go.Figure, go.Figure, go.Figure]:
181
+ """
182
+ Cria as figuras Plotly dos gráficos a partir dos DataFrames processados.
183
+
184
+ Args:
185
+ df_group (pd.DataFrame): DataFrame de frequência por grupo CIF.
186
+ df_individual_treemap (pd.DataFrame): DataFrame para o treemap de códigos individuais.
187
+ (Esperado ter colunas: 'Filho', 'Parent', 'Frequencia')
188
+
189
+ Returns:
190
+ Tuple[go.Figure, go.Figure, go.Figure]: Figuras de pizza, barras e treemap.
191
+ """
192
+ print("Gerando gráficos...")
193
+
194
+ fig_pie = create_pie_chart(df_group, title="Distribuição da Classificação por Componentes CIF")
195
+ fig_bar = create_bar_chart(df_group, title="Frequência da Classificação por Componentes CIF")
196
+ fig_tree_map = create_tree_map_chart(df_individual_treemap, title="Treemap de Frequência por Código CIF")
197
+
198
+ return fig_pie, fig_bar, fig_tree_map
utils/{icf_categories.py → report/icf_categories.py} RENAMED
@@ -1,3 +1,4 @@
 
1
  from enum import Enum
2
  from typing import List, Dict
3
 
 
1
+ # utils/report/icf_categories.py
2
  from enum import Enum
3
  from typing import List, Dict
4
 
utils/{pdf_creation.py → report/pdf_creation.py} RENAMED
@@ -1,3 +1,4 @@
 
1
  import io
2
  import re
3
  import os
@@ -18,28 +19,17 @@ _STRINGS = {
18
  }
19
 
20
  # --- Regex Patterns ---
21
- # Regex para identificar e tratar 'Frase de Entrada: ...'.
22
- _INPUT_PHRASE_REGEX = re.compile(r'^-?\s*(Frase de Entrada:.*)', re.IGNORECASE)
 
23
 
24
  # Regex para tratar itens de lista.
25
  # Captura caracteres válidos.
26
  _LIST_ITEM_CONTENT_REGEX = re.compile(r'^-?\s*(.*)')
27
 
28
- # --- Constantes para lidar com Plotlys ---
29
- _PLOT_IMAGE_COMMON_WIDTH_EXPORT = 800
30
- _PLOT_IMAGE_DEFAULT_HEIGHT_EXPORT = 500
31
- _PLOT_IMAGE_SPECIAL_HEIGHT_EXPORT = 800 # TreeMap
32
- _PLOT_IMAGE_SCALE = 2
33
-
34
- _PLOT_IMAGE_COMMON_DRAW_WIDTH = 550
35
- _PLOT_IMAGE_DEFAULT_DRAW_HEIGHT = 350
36
- _PLOT_IMAGE_SPECIAL_DRAW_HEIGHT = 550 # TreeMap
37
-
38
- _SPECIAL_PLOT_INDEX = 2 # TreeMap
39
-
40
  # --- Constants for Text Styling ---
41
  _LLM_RESPONSE_STARTERS = (
42
- 'resposta fornecida pela llm',
43
  )
44
 
45
  def _handle_text_content(story: list, text_content: str, styles: dict) -> None:
@@ -95,13 +85,13 @@ def _handle_text_content(story: list, text_content: str, styles: dict) -> None:
95
  )
96
 
97
  # Timestamp & Disclaimer (gerado uma vez no início do conteúdo textual)
98
- if not story: # Adiciona apenas se a story estiver vazia, para não repetir a cada chamada se a função for reutilizada
99
- generation_timestamp_text = _STRINGS['TXT_TIMESTAMP'](datetime.now().strftime('%d-%m-%Y'))
100
- story.append(Paragraph(generation_timestamp_text, h2_bold_centered_style))
101
 
102
- disclaimer_text = _STRINGS['TXT_DISCLAIMER']
103
- story.append(Paragraph(disclaimer_text, alert_message_style))
104
- story.append(Spacer(1, 20))
105
 
106
  # Processamento do conteúdo principal
107
  text_blocks = text_content.split('---')
@@ -157,35 +147,36 @@ def _handle_text_content(story: list, text_content: str, styles: dict) -> None:
157
 
158
 
159
  def _handle_dataframe_content(story: list, dataframes_list: list[pd.DataFrame], styles: dict) -> None:
160
- """
161
- Adds pandas DataFrames to the PDF story, formatted as tables.
162
-
163
- Each DataFrame is preceded by a page break and a title.
164
- Table styling includes a header row with a grey background and white text,
165
- and a beige background for data rows, with a grid.
 
 
 
166
 
167
- Args:
168
- story (list): The list of ReportLab Platypus elements.
169
- dataframes_list (list[pd.DataFrame]): A list of pandas DataFrames to include.
170
- styles (dict): A dictionary of ReportLab sample paragraph styles.
171
- """
172
  for df_index, df in enumerate(dataframes_list):
 
 
 
173
  story.append(PageBreak())
174
- story.append(Paragraph(f"Data (DataFrame {df_index + 1}):", styles['h2']))
175
  story.append(Spacer(1, 1))
176
 
177
- # Preparação do dataframe para tabela
178
  table_data = [df.columns.tolist()] + df.values.tolist()
179
  pdf_table = Table(table_data)
180
 
181
  pdf_table.setStyle(TableStyle([
182
- ('BACKGROUND', (0, 0), (-1, 0), colors.grey), # Header row background
183
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), # Header row text color
184
- ('ALIGN', (0, 0), (-1, -1), 'CENTER'), # Center alignment for all cells
185
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), # Header row font
186
- ('BOTTOMPADDING', (0, 0), (-1, 0), 12), # Header row bottom padding
187
- ('BACKGROUND', (0, 1), (-1, -1), colors.beige), # Data rows background
188
- ('GRID', (0, 0), (-1, -1), 1, colors.black) # Grid for the entire table
189
  ]))
190
 
191
  story.append(pdf_table)
@@ -193,51 +184,57 @@ def _handle_dataframe_content(story: list, dataframes_list: list[pd.DataFrame],
193
 
194
 
195
  def _handle_plotly_plot(story: list, plotly_figures: list[go.Figure], styles: dict) -> None:
196
- """
197
- Converts Plotly figures to PNG images and adds them to the PDF story.
 
 
 
 
 
 
 
 
 
 
 
198
 
199
- Each plot is preceded by a page break and a title.
200
- Handles potential errors during image conversion.
201
- The third plot (index 2) has specific dimensions.
202
-
203
- Args:
204
- story (list): The list of ReportLab Platypus elements.
205
- plotly_figures (list[go.Figure]): A list of Plotly Figure objects.
206
- styles (dict): A dictionary of ReportLab sample paragraph styles.
207
- """
208
  for fig_index, plotly_figure in enumerate(plotly_figures):
209
  try:
 
 
 
 
 
 
 
210
  image_buffer = io.BytesIO()
211
 
212
- # Determine export and draw dimensions based on plot index
213
- if fig_index == _SPECIAL_PLOT_INDEX:
214
- export_height = _PLOT_IMAGE_SPECIAL_HEIGHT_EXPORT
215
- draw_height = _PLOT_IMAGE_SPECIAL_DRAW_HEIGHT
216
  else:
217
- export_height = _PLOT_IMAGE_DEFAULT_HEIGHT_EXPORT
218
- draw_height = _PLOT_IMAGE_DEFAULT_DRAW_HEIGHT
219
 
220
  plotly_figure.write_image(
221
- image_buffer,
222
- format="png",
223
- width=_PLOT_IMAGE_COMMON_WIDTH_EXPORT,
224
- height=export_height,
225
- scale=_PLOT_IMAGE_SCALE
226
  )
227
  image_buffer.seek(0)
228
 
229
  reportlab_image = Image(image_buffer)
230
  reportlab_image.drawHeight = draw_height
231
- reportlab_image.drawWidth = _PLOT_IMAGE_COMMON_DRAW_WIDTH
232
 
233
  story.append(PageBreak())
234
- story.append(Paragraph(f"Generated Plot ({fig_index + 1}):", styles['h2']))
235
  story.append(Spacer(1, 1))
236
  story.append(reportlab_image)
237
  story.append(Spacer(1, 4))
238
 
239
  except Exception as e:
240
- error_message = f"Error adding Plotly plot {fig_index + 1}: {e}"
241
  story.append(Paragraph(error_message, styles['Normal']))
242
  story.append(Spacer(1, 2))
243
 
 
1
+ # utils/pdf_creation.py
2
  import io
3
  import re
4
  import os
 
19
  }
20
 
21
  # --- Regex Patterns ---
22
+ # Regex para identificar e tratar (o acento pode ser problemático para llms as vezes) 'Frase de Extraída: ...'.
23
+
24
+ _INPUT_PHRASE_REGEX = re.compile(r'^-?\s*(Frase Extra.*:.*)', re.IGNORECASE)
25
 
26
  # Regex para tratar itens de lista.
27
  # Captura caracteres válidos.
28
  _LIST_ITEM_CONTENT_REGEX = re.compile(r'^-?\s*(.*)')
29
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  # --- Constants for Text Styling ---
31
  _LLM_RESPONSE_STARTERS = (
32
+ 'Resposta Fornecida pela LLM',
33
  )
34
 
35
  def _handle_text_content(story: list, text_content: str, styles: dict) -> None:
 
85
  )
86
 
87
  # Timestamp & Disclaimer (gerado uma vez no início do conteúdo textual)
88
+ # if not story: # Adiciona apenas se a story estiver vazia, para não repetir a cada chamada se a função for reutilizada
89
+ generation_timestamp_text = _STRINGS['TXT_TIMESTAMP'](datetime.now().strftime('%d-%m-%Y'))
90
+ story.append(Paragraph(generation_timestamp_text, h2_bold_centered_style))
91
 
92
+ disclaimer_text = _STRINGS['TXT_DISCLAIMER']
93
+ story.append(Paragraph(disclaimer_text, alert_message_style))
94
+ story.append(Spacer(1, 20))
95
 
96
  # Processamento do conteúdo principal
97
  text_blocks = text_content.split('---')
 
147
 
148
 
149
  def _handle_dataframe_content(story: list, dataframes_list: list[pd.DataFrame], styles: dict) -> None:
150
+ """Adiciona DataFrames ao PDF, usando um dicionário interno para títulos descritivos."""
151
+
152
+ # Dicionário que mapeia o índice do DataFrame ao seu título específico.
153
+ DATAFRAME_TITLES = {
154
+ 0: "Tabela de Frequência por Componente CIF",
155
+ 1: "Estatísticas Descritivas dos Componentes",
156
+ 2: "Tabela de Frequência por Código CIF Específico",
157
+ 3: "Estatísticas Descritivas dos Códigos CIF"
158
+ }
159
 
 
 
 
 
 
160
  for df_index, df in enumerate(dataframes_list):
161
+ # Usa o título do dicionário se o índice existir; senão, usa um título genérico.
162
+ title = DATAFRAME_TITLES.get(df_index, f"Data (DataFrame {df_index + 1})")
163
+
164
  story.append(PageBreak())
165
+ story.append(Paragraph(title, styles['h2']))
166
  story.append(Spacer(1, 1))
167
 
168
+ # O restante da lógica para criar a tabela permanece o mesmo.
169
  table_data = [df.columns.tolist()] + df.values.tolist()
170
  pdf_table = Table(table_data)
171
 
172
  pdf_table.setStyle(TableStyle([
173
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
174
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
175
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
176
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
177
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
178
+ ('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
179
+ ('GRID', (0, 0), (-1, -1), 1, colors.black)
180
  ]))
181
 
182
  story.append(pdf_table)
 
184
 
185
 
186
  def _handle_plotly_plot(story: list, plotly_figures: list[go.Figure], styles: dict) -> None:
187
+ """Converte e adiciona gráficos Plotly, usando dicionários internos para títulos e configurações."""
188
+
189
+ # Dicionário que mapeia o índice do gráfico ao seu título.
190
+ PLOT_TITLES = {
191
+ 0: "Distribuição Percentual por Componente",
192
+ 1: "Gráfico de Frequência por Componente CIF",
193
+ 2: "Análise Hierárquica de Códigos CIF (Treemap)"
194
+ }
195
+
196
+ # Dicionário que mapeia o índice do gráfico a uma configuração especial.
197
+ PLOT_CONFIGS = {
198
+ 2: {'type': 'treemap'} # O gráfico de índice 2 é um treemap
199
+ }
200
 
 
 
 
 
 
 
 
 
 
201
  for fig_index, plotly_figure in enumerate(plotly_figures):
202
  try:
203
+ # Pega o título do dicionário, com fallback para o genérico.
204
+ title = PLOT_TITLES.get(fig_index, f"Generated Plot ({fig_index + 1})")
205
+
206
+ # Pega a configuração do dicionário.
207
+ config = PLOT_CONFIGS.get(fig_index, {})
208
+ plot_type = config.get('type', 'default')
209
+
210
  image_buffer = io.BytesIO()
211
 
212
+ # Determina as dimensões com base no 'type' obtido da configuração.
213
+ if plot_type == 'treemap':
214
+ export_height = 800 # _PLOT_IMAGE_SPECIAL_HEIGHT_EXPORT
215
+ draw_height = 550 # _PLOT_IMAGE_SPECIAL_DRAW_HEIGHT
216
  else:
217
+ export_height = 500 # _PLOT_IMAGE_DEFAULT_HEIGHT_EXPORT
218
+ draw_height = 350 # _PLOT_IMAGE_DEFAULT_DRAW_HEIGHT
219
 
220
  plotly_figure.write_image(
221
+ image_buffer, format="png", width=800, # _PLOT_IMAGE_COMMON_WIDTH_EXPORT
222
+ height=export_height, scale=2 # _PLOT_IMAGE_SCALE
 
 
 
223
  )
224
  image_buffer.seek(0)
225
 
226
  reportlab_image = Image(image_buffer)
227
  reportlab_image.drawHeight = draw_height
228
+ reportlab_image.drawWidth = 550 # _PLOT_IMAGE_COMMON_DRAW_WIDTH
229
 
230
  story.append(PageBreak())
231
+ story.append(Paragraph(title, styles['h2']))
232
  story.append(Spacer(1, 1))
233
  story.append(reportlab_image)
234
  story.append(Spacer(1, 4))
235
 
236
  except Exception as e:
237
+ error_message = f"Error adding Plotly plot '{title}': {e}"
238
  story.append(Paragraph(error_message, styles['Normal']))
239
  story.append(Spacer(1, 2))
240
 
utils/{report_creation.py → report/report_creation.py} RENAMED
@@ -1,36 +1,12 @@
 
1
  import plotly.graph_objects as go
2
  import pandas as pd # Ainda pode ser necessário para type hints ou manipulações leves
3
  from typing import Tuple # Remover Dict, Optional, List se não forem mais usados diretamente aqui
4
 
5
- from .graph_creation import create_pie_chart, create_bar_chart, create_tree_map_chart
6
  from .pdf_creation import generate_pdf_report_temp
7
  from .dataframe_creation import process_report_data
8
 
9
- # --- FUNÇÃO: Gera os gráficos a partir dos DataFrames ---
10
- def create_report_plots(df_group: pd.DataFrame, df_individual_treemap: pd.DataFrame) -> Tuple[go.Figure, go.Figure, go.Figure]:
11
- """
12
- Cria as figuras Plotly dos gráficos a partir dos DataFrames processados.
13
-
14
- Args:
15
- df_group (pd.DataFrame): DataFrame de frequência por grupo CIF.
16
- df_individual_treemap (pd.DataFrame): DataFrame para o treemap de códigos individuais.
17
- (Esperado ter colunas: 'Filho', 'Parent', 'Frequencia')
18
-
19
- Returns:
20
- Tuple[go.Figure, go.Figure, go.Figure]: Figuras de pizza, barras e treemap.
21
- """
22
- print("Gerando gráficos...")
23
-
24
- fig_pie = create_pie_chart(df_group, title="Distribuição da Classificação por Componentes CIF")
25
- fig_bar = create_bar_chart(df_group, title="Frequência da Classificação por Componentes CIF")
26
-
27
- # Para o treemap, a função create_tree_map_chart precisa ser compatível com o DataFrame
28
- # df_individual_treemap que agora inclui 'Filho', 'Parent', 'Subparent', 'Frequencia'.
29
- # Ela usará 'Filho' como labels, 'Parent' (ou 'Subparent') como parents, e 'Frequencia' como values.
30
- fig_tree_map = create_tree_map_chart(df_individual_treemap, title="Treemap de Frequência por Código CIF")
31
-
32
- return fig_pie, fig_bar, fig_tree_map
33
-
34
  # --- FUNÇÃO: Gera o PDF a partir de DataFrames e Figuras ---
35
  def generate_report_pdf(llm_res: str, df_group: pd.DataFrame, df_group_describe: pd.DataFrame,
36
  df_individual_treemap: pd.DataFrame, df_treemap_describe: pd.DataFrame,
 
1
+ # utils/report/report_creation.py
2
  import plotly.graph_objects as go
3
  import pandas as pd # Ainda pode ser necessário para type hints ou manipulações leves
4
  from typing import Tuple # Remover Dict, Optional, List se não forem mais usados diretamente aqui
5
 
6
+ from .graph_creation import create_report_plots
7
  from .pdf_creation import generate_pdf_report_temp
8
  from .dataframe_creation import process_report_data
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  # --- FUNÇÃO: Gera o PDF a partir de DataFrames e Figuras ---
11
  def generate_report_pdf(llm_res: str, df_group: pd.DataFrame, df_group_describe: pd.DataFrame,
12
  df_individual_treemap: pd.DataFrame, df_treemap_describe: pd.DataFrame,