Spaces:
Running
Running
Update modules/contexto.py
Browse files- modules/contexto.py +188 -302
modules/contexto.py
CHANGED
|
@@ -1,302 +1,188 @@
|
|
| 1 |
-
import logging
|
| 2 |
-
import re
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
try:
|
| 87 |
-
self.
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
if
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
#
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
#
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
sentimento
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
"
|
| 184 |
-
"
|
| 185 |
-
"
|
| 186 |
-
"
|
| 187 |
-
"
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
def balancear_contexto(self, mensagem_atual: str, nome_usuario: str, numero_usuario: str, mensagem_original: str, limite_historico: int, limite_contexto: int, is_reply: bool) -> str:
|
| 191 |
-
"""Balanceia o contexto com base no histórico e mensagem atual."""
|
| 192 |
-
historico = self.db.recuperar_mensagens(nome_usuario, limite=limite_historico)
|
| 193 |
-
contexto = f"Usuário: {nome_usuario} (ID: {numero_usuario}) | Mensagem atual: {mensagem_atual}"
|
| 194 |
-
if is_reply and mensagem_original:
|
| 195 |
-
contexto += f" | Resposta a: {mensagem_original}"
|
| 196 |
-
if historico:
|
| 197 |
-
contexto += f" | Histórico recente: {historico[-limite_contexto:]}"
|
| 198 |
-
return contexto
|
| 199 |
-
|
| 200 |
-
def selecionar_resposta_predefinida(self, contexto: str) -> str:
|
| 201 |
-
"""Seleciona uma resposta predefinida com base no contexto."""
|
| 202 |
-
contexto_lower = contexto.lower()
|
| 203 |
-
# Respostas muito curtas e neutras para saudações/despedidas
|
| 204 |
-
if any(w in contexto_lower for w in [" oi", "oi", "olá", "eai", "eae"]):
|
| 205 |
-
return "Oi! Tudo fixe?"
|
| 206 |
-
elif any(w in contexto_lower for w in ["tchau", "flw", "até"]):
|
| 207 |
-
return "Tchau! Fica bem."
|
| 208 |
-
return "" # String vazia quando não há resposta predefinida
|
| 209 |
-
|
| 210 |
-
# Métodos de integração com banco e aprendizado detalhado
|
| 211 |
-
def registrar_aprendizado_detalhado(self, chave, valor):
|
| 212 |
-
if not self.usuario:
|
| 213 |
-
logger.warning("Usuário não definido para aprendizado detalhado.")
|
| 214 |
-
return
|
| 215 |
-
self.db.salvar_aprendizado_detalhado(self.usuario, chave, valor)
|
| 216 |
-
|
| 217 |
-
def obter_aprendizado_detalhado(self, chave=None):
|
| 218 |
-
if not self.usuario:
|
| 219 |
-
logger.warning("Usuário não definido para consulta de aprendizado detalhado.")
|
| 220 |
-
return None
|
| 221 |
-
return self.db.recuperar_aprendizado_detalhado(self.usuario, chave)
|
| 222 |
-
|
| 223 |
-
def obter_historico(self, limite=5):
|
| 224 |
-
if not self.usuario:
|
| 225 |
-
logger.warning("Usuário não definido para histórico.")
|
| 226 |
-
return []
|
| 227 |
-
result = self.db.recuperar_mensagens(self.usuario, limite=limite)
|
| 228 |
-
return result if result else []
|
| 229 |
-
|
| 230 |
-
def atualizar_contexto(self, mensagem, resposta):
|
| 231 |
-
"""Salva a interação no banco de mensagens e aciona aprendizado de termos."""
|
| 232 |
-
if not self.usuario:
|
| 233 |
-
logger.warning("Usuário não definido para atualizar contexto; salvando como 'anonimo'.")
|
| 234 |
-
usuario = 'anonimo'
|
| 235 |
-
else:
|
| 236 |
-
usuario = self.usuario
|
| 237 |
-
try:
|
| 238 |
-
self.db.salvar_mensagem(usuario, mensagem, resposta)
|
| 239 |
-
# Aprender termos do histórico
|
| 240 |
-
historico = self.obter_historico(limite=10) # Últimas 10 mensagens
|
| 241 |
-
self.aprender_do_historico(mensagem, resposta, historico)
|
| 242 |
-
except Exception as e:
|
| 243 |
-
logger.warning(f'Falha ao salvar mensagem no DB: {e}')
|
| 244 |
-
|
| 245 |
-
def registrar_aprendizado(self, dado, valor):
|
| 246 |
-
if not self.usuario:
|
| 247 |
-
logger.warning("Usuário não definido para aprendizado simples.")
|
| 248 |
-
return
|
| 249 |
-
self.db.salvar_aprendizado(self.usuario, dado, valor)
|
| 250 |
-
|
| 251 |
-
def obter_aprendizado(self, dado):
|
| 252 |
-
if not self.usuario:
|
| 253 |
-
logger.warning("Usuário não definido para consulta de aprendizado simples.")
|
| 254 |
-
return None
|
| 255 |
-
return self.db.recuperar_aprendizado(self.usuario, dado)
|
| 256 |
-
|
| 257 |
-
def aprender_termo_regional(self, termo, contexto, significado):
|
| 258 |
-
"""Aprende um termo regional/gíria baseado no contexto."""
|
| 259 |
-
self.termo_contexto[termo] = {"contexto": contexto, "significado": significado}
|
| 260 |
-
self.registrar_aprendizado_detalhado("termos", self.termo_contexto)
|
| 261 |
-
logger.info(f"Termo '{termo}' aprendido: {significado} no contexto {contexto}")
|
| 262 |
-
|
| 263 |
-
def analisar_emocao(self, mensagem, sentimento):
|
| 264 |
-
"""Analisa e atualiza a emoção da IA baseada na mensagem e sentimento."""
|
| 265 |
-
if sentimento == "positivo":
|
| 266 |
-
self.emocao_atual = "feliz"
|
| 267 |
-
elif sentimento == "negativo":
|
| 268 |
-
self.emocao_atual = "irritada"
|
| 269 |
-
else:
|
| 270 |
-
self.emocao_atual = "neutra"
|
| 271 |
-
return self.emocao_atual
|
| 272 |
-
|
| 273 |
-
def ativar_espírito_crítico(self):
|
| 274 |
-
"""Ativa o espírito crítico para respostas questionadoras."""
|
| 275 |
-
self.espírito_crítico = True
|
| 276 |
-
return "Espírito crítico ativado para respostas questionadoras."
|
| 277 |
-
|
| 278 |
-
def aprender_do_historico(self, mensagem, resposta, historico):
|
| 279 |
-
"""Aprende termos do histórico de conversas."""
|
| 280 |
-
if len(historico) >= 2:
|
| 281 |
-
prev_msg = historico[-2][0].lower()
|
| 282 |
-
if "como vai" in prev_msg or "tudo bem" in prev_msg:
|
| 283 |
-
if "indo" in mensagem.lower():
|
| 284 |
-
self.aprender_termo_regional("indo", "bem_estar", "bem")
|
| 285 |
-
# Adicionar mais padrões aqui para outras gírias e contextos
|
| 286 |
-
# Ex.: if "blz" in mensagem.lower(): self.aprender_termo_regional("blz", "afirmacao", "beleza")
|
| 287 |
-
|
| 288 |
-
def substituir_termos_aprendidos(self, mensagem):
|
| 289 |
-
"""Substitui termos aprendidos na mensagem."""
|
| 290 |
-
for termo, info in self.termo_contexto.items():
|
| 291 |
-
if termo in mensagem:
|
| 292 |
-
mensagem = mensagem.replace(termo, info["significado"])
|
| 293 |
-
return mensagem
|
| 294 |
-
|
| 295 |
-
def obter_aprendizados(self):
|
| 296 |
-
"""Retorna os aprendizados do usuário, incluindo termos e emoções."""
|
| 297 |
-
aprendizados = {
|
| 298 |
-
"termos": self.termo_contexto,
|
| 299 |
-
"emocao_atual": self.emocao_atual,
|
| 300 |
-
"espírito_crítico": self.espírito_crítico
|
| 301 |
-
}
|
| 302 |
-
return aprendizados
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import re
|
| 3 |
+
import random
|
| 4 |
+
import time
|
| 5 |
+
import sqlite3
|
| 6 |
+
from typing import List, Dict, Any, Optional, Tuple
|
| 7 |
+
|
| 8 |
+
try:
|
| 9 |
+
from sentence_transformers import SentenceTransformer # type: ignore
|
| 10 |
+
except Exception as e:
|
| 11 |
+
logging.warning(f"sentence_transformers não disponível: {e}")
|
| 12 |
+
SentenceTransformer = None
|
| 13 |
+
|
| 14 |
+
from modules.database import Database
|
| 15 |
+
import modules.config as config
|
| 16 |
+
|
| 17 |
+
# Logging
|
| 18 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class Contexto:
|
| 23 |
+
"""
|
| 24 |
+
Gerencia contexto, histórico, aprendizado dinâmico, gírias, tom, emoção.
|
| 25 |
+
Consulta o banco a cada interação → aprendizado em tempo real.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
def __init__(self, db: Database, user_id: str):
|
| 29 |
+
self.db = db
|
| 30 |
+
self.user_id = user_id # número ou nome do usuário
|
| 31 |
+
self.historico: List[Tuple[str, str]] = []
|
| 32 |
+
self.girias_aprendidas: List[Dict[str, Any]] = []
|
| 33 |
+
self.ton_predominante: str = "neutro"
|
| 34 |
+
self.emocao_atual: str = "neutra"
|
| 35 |
+
self.termo_contexto: Dict[str, Dict] = {}
|
| 36 |
+
self.model = None
|
| 37 |
+
|
| 38 |
+
# Carrega tudo do banco na primeira vez
|
| 39 |
+
self._atualizar_do_banco()
|
| 40 |
+
|
| 41 |
+
def _atualizar_do_banco(self):
|
| 42 |
+
"""Consulta o banco e atualiza tudo (chamado a cada interação)"""
|
| 43 |
+
try:
|
| 44 |
+
# Histórico
|
| 45 |
+
msgs = self.db.recuperar_mensagens(self.user_id, limite=10)
|
| 46 |
+
self.historico = [(m, r) for m, r in (msgs or [])][::-1][-5:] # últimas 5
|
| 47 |
+
|
| 48 |
+
# Gírias aprendidas
|
| 49 |
+
self.girias_aprendidas = self.db.recuperar_girias_usuario(self.user_id)
|
| 50 |
+
|
| 51 |
+
# Tom predominante
|
| 52 |
+
self.ton_predominante = self.db.obter_tom_predominante(self.user_id) or "neutro"
|
| 53 |
+
|
| 54 |
+
# Termos aprendidos (antigo termo_contexto)
|
| 55 |
+
termos_raw = self.db.recuperar_aprendizado_detalhado(self.user_id, "termos")
|
| 56 |
+
if termos_raw and isinstance(termos_raw, str):
|
| 57 |
+
try:
|
| 58 |
+
import json
|
| 59 |
+
self.termo_contexto = json.loads(termos_raw)
|
| 60 |
+
except:
|
| 61 |
+
self.termo_contexto = {}
|
| 62 |
+
else:
|
| 63 |
+
self.termo_contexto = {}
|
| 64 |
+
|
| 65 |
+
# Emoção atual (baseada na última interação)
|
| 66 |
+
ultima = self.historico[-1] if self.historico else ("", "")
|
| 67 |
+
analise = self.db.analisar_emocoes_mensagem(ultima[0] + " " + ultima[1])
|
| 68 |
+
self.emocao_atual = analise["emocao"]
|
| 69 |
+
|
| 70 |
+
except Exception as e:
|
| 71 |
+
logger.error(f"Erro ao atualizar contexto do banco: {e}")
|
| 72 |
+
self.historico = []
|
| 73 |
+
self.girias_aprendidas = []
|
| 74 |
+
self.ton_predominante = "neutro"
|
| 75 |
+
self.termo_contexto = {}
|
| 76 |
+
|
| 77 |
+
def atualizar_aprendizados_do_banco(self):
|
| 78 |
+
"""FORÇA ATUALIZAÇÃO DO BANCO A CADA CHAMADA (usado na api.py)"""
|
| 79 |
+
self._atualizar_do_banco()
|
| 80 |
+
|
| 81 |
+
def obter_historico(self) -> List[Tuple[str, str]]:
|
| 82 |
+
return self.historico
|
| 83 |
+
|
| 84 |
+
def atualizar_contexto(self, mensagem: str, resposta: str):
|
| 85 |
+
"""Salva no banco + atualiza memória local"""
|
| 86 |
+
try:
|
| 87 |
+
self.db.salvar_mensagem(self.user_id, mensagem, resposta, numero=self.user_id)
|
| 88 |
+
self.historico.append((mensagem, resposta))
|
| 89 |
+
if len(self.historico) > 5:
|
| 90 |
+
self.historico.pop(0)
|
| 91 |
+
|
| 92 |
+
# Atualiza emoção
|
| 93 |
+
analise = self.db.analisar_emocoes_mensagem(mensagem + " " + resposta)
|
| 94 |
+
self.emocao_atual = analise["emocao"]
|
| 95 |
+
|
| 96 |
+
# Extrai gírias e salva no banco
|
| 97 |
+
self._extrair_e_salvar_girias(mensagem, resposta)
|
| 98 |
+
|
| 99 |
+
except Exception as e:
|
| 100 |
+
logger.warning(f"Erro ao atualizar contexto: {e}")
|
| 101 |
+
|
| 102 |
+
def _extrair_e_salvar_girias(self, msg: str, resp: str):
|
| 103 |
+
"""Extrai gírias e salva no banco"""
|
| 104 |
+
texto = f"{msg} {resp}".lower()
|
| 105 |
+
palavras = [p for p in re.findall(r'\b\w{4,}\b', texto)
|
| 106 |
+
if p not in {'não', 'que', 'com', 'pra', 'pro', 'uma', 'ele', 'ela'}]
|
| 107 |
+
|
| 108 |
+
contador = collections.Counter(palavras)
|
| 109 |
+
for palavra, freq in contador.most_common(5):
|
| 110 |
+
if freq > 1:
|
| 111 |
+
significado = "gíria local" if any(x in texto for x in ['puto', 'caralho', 'merda']) else "expressão comum"
|
| 112 |
+
self.db.salvar_giria_aprendida(self.user_id, palavra, significado, texto[:100])
|
| 113 |
+
|
| 114 |
+
def analisar_intencao_e_normalizar(self, mensagem: str, historico: List) -> Dict[str, Any]:
|
| 115 |
+
"""Normaliza, detecta intenção, sentimento, ironia, meia frase, etc."""
|
| 116 |
+
mensagem = mensagem.strip()
|
| 117 |
+
if not mensagem:
|
| 118 |
+
return {"intencao": "vazia", "sentimento": "neutro", "usar_nome": False}
|
| 119 |
+
|
| 120 |
+
# Normalização
|
| 121 |
+
msg_lower = re.sub(r'[^\w\s\.,!?]', '', mensagem.lower())
|
| 122 |
+
msg_normalizada = self._substituir_termos_aprendidos(msg_lower)
|
| 123 |
+
|
| 124 |
+
# Detecção básica
|
| 125 |
+
saudacao = any(p in msg_lower for p in ["oi", "olá", "eai", "eae", "bom dia"])
|
| 126 |
+
despedida = any(p in msg_lower for p in ["tchau", "flw", "até logo", "bazar"])
|
| 127 |
+
pergunta = "?" in mensagem or any(p in msg_lower for p in ["como", "onde", "quem", "por que"])
|
| 128 |
+
grosseria = any(p in msg_lower for p in ["caralho", "puto", "merda", "fdp", "burro", "idiota"])
|
| 129 |
+
|
| 130 |
+
# Sentimento
|
| 131 |
+
positivo = any(p in msg_lower for p in ["fixe", "bué", "bom", "legal", "gosto", "adoro", "kkk", "rsrs"])
|
| 132 |
+
negativo = any(p in msg_lower for p in ["ruim", "chato", "droga", "ódio", "triste", "puto"])
|
| 133 |
+
|
| 134 |
+
sentimento = "positivo" if positivo else ("negativo" if negativo else "neutro")
|
| 135 |
+
|
| 136 |
+
# Ironia
|
| 137 |
+
ironia = (positivo and "perdi" in msg_lower) or (negativo and "melhor" in msg_lower)
|
| 138 |
+
|
| 139 |
+
# Meia frase
|
| 140 |
+
meia_frase = len(msg_lower.split()) <= 3 or "..." in mensagem
|
| 141 |
+
|
| 142 |
+
# Usar nome?
|
| 143 |
+
usar_nome = False
|
| 144 |
+
if saudacao or despedida or any(p in msg_lower for p in ["obrigado", "valeu"]):
|
| 145 |
+
prob = getattr(config, 'USAR_NOME_PROBABILIDADE', 0.4)
|
| 146 |
+
usar_nome = random.random() < prob
|
| 147 |
+
|
| 148 |
+
# Estilo
|
| 149 |
+
estilo = "curto" if len(mensagem) < 30 else "normal"
|
| 150 |
+
if grosseria:
|
| 151 |
+
estilo = "rude"
|
| 152 |
+
|
| 153 |
+
return {
|
| 154 |
+
"texto_normalizado": msg_normalizada,
|
| 155 |
+
"intencao": "saudacao" if saudacao else ("despedida" if despedida else ("pergunta" if pergunta else "conversa")),
|
| 156 |
+
"sentimento": sentimento,
|
| 157 |
+
"estilo": estilo,
|
| 158 |
+
"ironia": ironia,
|
| 159 |
+
"meia_frase": meia_frase,
|
| 160 |
+
"usar_nome": usar_nome,
|
| 161 |
+
"grosseria": grosseria
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
def _substituir_termos_aprendidos(self, texto: str) -> str:
|
| 165 |
+
"""Substitui gírias aprendidas por significado (opcional, para análise)"""
|
| 166 |
+
for termo, info in self.termo_contexto.items():
|
| 167 |
+
if termo in texto:
|
| 168 |
+
texto = texto.replace(termo, info.get("significado", termo))
|
| 169 |
+
return texto
|
| 170 |
+
|
| 171 |
+
def aprender_termo_regional(self, termo: str, contexto: str, significado: str):
|
| 172 |
+
"""Registra novo termo no banco e na memória"""
|
| 173 |
+
self.termo_contexto[termo] = {"contexto": contexto, "significado": significado}
|
| 174 |
+
try:
|
| 175 |
+
import json
|
| 176 |
+
self.db.salvar_aprendizado_detalhado(self.user_id, "termos", json.dumps(self.termo_contexto))
|
| 177 |
+
except:
|
| 178 |
+
pass
|
| 179 |
+
|
| 180 |
+
def obter_aprendizados(self) -> Dict[str, Any]:
|
| 181 |
+
"""Retorna tudo que a IA aprendeu sobre o usuário"""
|
| 182 |
+
return {
|
| 183 |
+
"historico_recente": [f"U: {u} | A: {a}" for u, a in self.historico[-3:]],
|
| 184 |
+
"girias": [g["giria"] for g in self.girias_aprendidas[:5]],
|
| 185 |
+
"tom_predominante": self.ton_predominante,
|
| 186 |
+
"emocao_atual": self.emocao_atual,
|
| 187 |
+
"termos_aprendidos": list(self.termo_contexto.keys())[:5]
|
| 188 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|