#include "Cuentas32.ch" Static cApiKey Static hCacheGeneral Static hCacheManual Static cLastQuestion Static nLastTime Static nUltimaHora Static oPregunta, oRespuesta Static mRespuesta Static nPreguntasUltimoMinuto ***************************************** Function Fnct_ActivarAsistenteIA(cKey) local oDlgIA, oBtnSalir, oBtnPregunta, oFontSays, oLabel, oBtnFreeAsk, oBtnLimpiar local oPregunta local mPregunta := '' local nSayBackColor := Rgb(92,156,227) local nSayTextColor := Rgb(77,66,202) local lSalir:=.f. local mDummy := "" mRespuesta := '' if Empty(cKey) .OR. !("sk-" $ cKey) .OR. Len(AllTrim(cKey)) < 20 MsgStop("API Key no válida. Debe comenzar con 'sk-' y tener al menos 20 caracteres.", "Error") return .f. endif cLastQuestion := "" nLastTime := 0 nPreguntasUltimoMinuto := 0 nUltimaHora := Seconds() iif(file("cache_general.txt"), ferase("cache_general.txt"),) iif(file("cache_manual.txt"), ferase("cache_manual.txt"),) hCacheGeneral := LoadCacheFromFile("cache_general.txt") if hCacheGeneral == nil hCacheGeneral := { => } endif hCacheManual := LoadCacheFromFile( "cache_manual.txt" ) if hCacheManual == nil hCacheManual := { => } endif cApiKey := cKey Define Font oFontSays Name 'Courier New' Size 0, 18 Bold Define Dialog oDlgIA Resource "oDlg_AsistenteIA" Title 'Bienvenido al Asistente Virtual de CUENTANUBE' Redefine Say oLabel Id 4011 Of oDlgIA oLabel:SetColor( nSayTextColor, nSayBackColor ) oLabel:SetText('¿En que Puedo Ayudarte?') oLabel:SetFont(oFontSays) oLabel:lTransparent := .t. Redefine Get oPregunta Var mPregunta Id 4001 Of oDlgIA Memo on change (mPregunta:=oPregunta:GetText(), oBtnPregunta:ForWhen(), oBtnFreeAsk:ForWhen()) oPregunta:SetFont(oFontSays) Redefine Get oRespuesta Var mRespuesta Id 4004 Of oDlgIA Memo multiline oRespuesta:SetFont(oFontSays) oRespuesta:lReadOnly := .t. Redefine BTNBMP oBtnLimpiar Resource 'Limpiar' Id 4007 Of oDlgIA ; Action (mPregunta:='', mRespuesta:='', oPregunta:VarPut(mPregunta), oRespuesta:VarPut(mRespuesta), oPregunta:Refresh(), oRespuesta:Refresh()); Prompt "" ToolTip "Limpiar Cuadro de Preguntas y Respuestas" when len(mPregunta)>0 .or. len(mRespuesta)>0 Redefine BTNBMP oBtnPregunta Resource 'Manual' Id 4002 Of oDlgIA ; Action ( mRespuesta:='', oRespuesta:VarPut(mRespuesta), oRespuesta:Refresh(), Fnct_AccionManual( oPregunta:GetText(), oRespuesta )) ; Prompt "" ToolTip "Pregúntale al Asistente sobre el Manual" when !empty(mPregunta) Redefine BTNBMP oBtnFreeAsk Resource 'Pregunta' Id 4003 Of oDlgIA ; Action ( mRespuesta:='', oRespuesta:VarPut(mRespuesta), oRespuesta:Refresh(), mPregunta:=mPregunta+" Responde sin emojis ni íconos visuales. Solo texto plano.", Fnct_AccionLibre( oPregunta:GetText())) ;//, oRespuesta )) ; Prompt "" ToolTip "Pregunta Abierta (Tema General)" when !empty(mPregunta) Redefine BTNBMP oBtnSalir Resource 'Btn_Salir' Id 270 Of oDlgIA Action (SaveCacheToFile(hCacheGeneral, "cache_general.txt"), ; // Guarda cache general SaveCacheToFile(hCacheManual, "cache_manual.txt"), ; // Guarda cache manual lSalir:=.t. ,; oDlgIA:End()) ; Prompt "" ToolTip "Finalizar" Activate Dialog oDlgIA Centered return .t. ***************************************** // Función central de IA Static Function Fnct_AsistenteIA( cQuestion, cPromptSistema, hCache ) local cUrl, cEscapedQuestion, cMessages, oWait, oBrush local cEscapedPrompt, cJsonBody, oHttp, cResponse, oError local nIntentos := 0, lExito := .F. local cQuestionNorm, cTexto // Validar conexión if !IsInternet() MsgStop( "Se requiere conexión a internet para usar el asistente.", "Error de Conexión" ) return NIL endif // Normalizar pregunta cQuestionNorm := Lower( AllTrim( cQuestion ) ) // Verificar cache if !Empty( hCache ) .AND. hb_HHasKey( hCache, cQuestionNorm ) oRespuesta:VarPut( hCache[cQuestionNorm] ) oRespuesta:Refresh() return NIL endif // Ventana de espera DEFINE BRUSH oBrush RESOURCE "FondoPreguntaIa" STRETCH DEFINE DIALOG oWait FROM 12, 68 to 15, 92 TITLE "Consultando IA..." STYLE DS_MODALFRAME + WS_POPUP + WS_CAPTION BRUSH oBrush @ 0.5, 2 trSAY "Procesando su pregunta..." OF oWait SIZE 350, 20 CENTER ACTIVATE DIALOG oWait NOWAIT SysRefresh() // Preparar mensaje cUrl := "https://api.deepseek.com/v1/chat/completions" cEscapedQuestion := StrTran( cQuestion, '"', '\"' ) if Empty( cPromptSistema ) cMessages := '[{"role": "user", "content": "' + cEscapedQuestion + '"}]' else cEscapedPrompt := StrTran( cPromptSistema, '"', '\"' ) cMessages := '[{"role": "system", "content": "' + cEscapedPrompt + '"}, ' + ; '{"role": "user", "content": "' + cEscapedQuestion + '"}]' endif cJsonBody := '{' + ; '"model": "deepseek-chat",' + ; '"messages": ' + cMessages + ',' + ; '"max_tokens": 500' + ; '}' // Intentos de conexión while nIntentos < 3 .AND. !lExito nIntentos++ BEGIN SEQUENCE oHttp := CreateObject( "WinHttp.WinHttpRequest.5.1" ) oHttp:SetTimeouts( 10000, 10000, 15000, 20000 ) oHttp:Open( "POST", cUrl, .F. ) oHttp:SetRequestHeader( "Content-Type", "application/json" ) oHttp:SetRequestHeader( "Authorization", "Bearer " + cApiKey ) oHttp:Send( cJsonBody ) oHttp:WaitForResponse() cResponse := oHttp:ResponseText // Extraer contenido desde JSON cTexto := Fnct_NormalizarRespuestaIA(Fnct_ExtraerContenidoDeepSeek( cResponse )) cTexto := StrTran( cTexto, Chr(0), "" ) cTexto := Fnct_LimpiarRespuesta( cTexto ) // Mostrar en pantalla cTexto := StrTran( cTexto, Chr(10), Chr(13)+Chr(10) ) cTexto := Fnct_InsertarSaltosPorNumeracion( cTexto ) mRespuesta := cTexto oRespuesta:VarPut( cTexto ) oRespuesta:Refresh() lExito := .T. // Guardar en cache si es válido if !("Error" $ cTexto) .AND. !("error" $ Lower(cTexto)) .AND. ; !("no válida" $ cTexto) .AND. !Empty(cTexto) hCache[cQuestionNorm] := cTexto if hCache == hCacheGeneral SaveCacheToFile( hCache, "cache_general.txt" ) else SaveCacheToFile( hCache, "cache_manual.txt" ) endif endif RECOVER USING oError cTexto := "Error de conexión: " + oError:Description oRespuesta:VarPut( cTexto ) oRespuesta:Refresh() END END oWait:End() if !lExito MsgStop( "No se pudo conectar después de 3 intentos.", "Error de Conexión" ) endif return NIL ***************************************** Static Function Fnct_AccionLibre( cPregunta ) local cPromptGeneral // VALIDACIONES EXISTENTES if Len( AllTrim( cPregunta ) ) < 3 MsgStop( "Por favor, escriba una pregunta más específica (mínimo 3 caracteres).", "Pregunta muy corta" ) return nil endif if Len( cPregunta ) > 500 MsgStop( "La pregunta es demasiado larga. Por favor, sea más conciso.", "Pregunta muy larga" ) return nil endif // 3. VALIDAR CARACTERES PELIGROSOS (Nueva validación) if "<" $ cPregunta .OR. ">" $ cPregunta .OR. "{" $ cPregunta .OR. "}" $ cPregunta .OR. ";" $ cPregunta MsgStop("La pregunta contiene caracteres no permitidos.", "Error") return nil endif // 4. VALIDAR VERBOS DE ACCIÓN (Nueva validación) if Fnct_ContieneVerbosAccion( cPregunta ) MsgStop("Lo siento, solo puedo responder preguntas informativas. No puedo realizar acciones.", "Acción no permitida") return nil endif // 5. LIMITAR USO EXCESIVO (Nueva validación) if Seconds() - nUltimaHora > 60 nPreguntasUltimoMinuto := 0 nUltimaHora := Seconds() endif nPreguntasUltimoMinuto++ if nPreguntasUltimoMinuto > 10 MsgStop("Límite de uso excedido. Espere un momento.", "Advertencia") return nil endif // Validación de pregunta repetida existente if cPregunta == cLastQuestion .AND. ( Seconds() - nLastTime ) < 2 MsgStop( "Ya respondió esa pregunta. Espere unos segundos.", "Pregunta repetida" ) return nil endif cLastQuestion := cPregunta nLastTime := Seconds() cPromptGeneral := "Eres un asistente IA útil y amable. Responde de forma concisa y clara en español." Fnct_AsistenteIA( cPregunta, cPromptGeneral, hCacheGeneral ) return nil ***************************************** // Botón de Manual de Usuario Static Function Fnct_AccionManual( cPregunta ) local cPromptManual := "Eres un experto en CUENTANUBE. Responde basándote en el manual: ..." // Aplicar mismas validaciones que Fnct_AccionLibre if Len( AllTrim( cPregunta ) ) < 3 MsgStop( "Por favor, escriba una pregunta más específica (mínimo 3 caracteres).", "Pregunta muy corta" ) return nil endif if Len( cPregunta ) > 500 MsgStop( "La pregunta es demasiado larga. Por favor, sea más conciso.", "Pregunta muy larga" ) return nil endif if "<" $ cPregunta .OR. ">" $ cPregunta .OR. "{" $ cPregunta .OR. "}" $ cPregunta .OR. ";" $ cPregunta MsgStop("La pregunta contiene caracteres no permitidos.", "Error") return nil endif if Fnct_ContieneVerbosAccion( cPregunta ) MsgStop("Lo siento, solo puedo responder preguntas informativas. No puedo realizar acciones.", "Acción no permitida") return nil endif if Seconds() - nUltimaHora > 60 nPreguntasUltimoMinuto := 0 nUltimaHora := Seconds() endif nPreguntasUltimoMinuto++ if nPreguntasUltimoMinuto > 10 MsgStop("Límite de uso excedido. Espere un momento.", "Advertencia") return nil endif Fnct_ConsultarManual( cPregunta ) return nil ***************************************** Static Function LoadCacheFromFile( cFile ) local hCache := { => } local nHandle, cContent, aLines, i, aParts // Validar que se pasó un archivo if Empty( cFile ) return hCache // Retorna hash vacío endif if File( cFile ) nHandle := FOpen( cFile ) if nHandle != -1 cContent := Space( FSeek( nHandle, 0, 2 ) ) FSeek( nHandle, 0, 0 ) FRead( nHandle, @cContent, Len( cContent ) ) FClose( nHandle ) aLines := hb_ATokens( cContent, Chr(13) + Chr(10) ) for i := 1 to Len( aLines ) if !Empty( aLines[i] ) aParts := hb_ATokens( aLines[i], "|" ) if Len( aParts ) >= 2 hCache[AllTrim(aParts[1])] := hb_StrReplace( AllTrim(aParts[2]), "\\n", Chr(13) + Chr(10) ) endif endif next endif endif return hCache ***************************************** Static Function SaveCacheToFile( hCache, cFile ) local nHandle, cContent := "", aKeys, i, cKey // Validar que se pasó un archivo if Empty( cFile ) MsgStop("Error: No se especificó archivo para guardar cache", "Error") return nil endif // Obtener todas las llaves del hash recibido aKeys := hb_HKeys( hCache ) // Crear archivo con manejo de errores nHandle := FCreate( cFile ) if nHandle == -1 nHandle := FOpen( cFile, 2 ) // Intenta abrir en modo escritura if nHandle == -1 MsgStop("Error: No se pudo crear o abrir el archivo " + cFile, "Error") return nil endif endif // Iterar y formar el contenido for i := 1 to Len( aKeys ) cKey := aKeys[i] cContent += cKey + "|" + hb_StrReplace( hCache[cKey], Chr(13) + Chr(10), "\\n" ) + Chr(13) + Chr(10) next FWrite( nHandle, cContent, Len( cContent ) ) FClose( nHandle ) return nil ***************************************** Static Function Fnct_Utf8ToAnsi( cText ) local cResult := "", i := 1, nLen := Len( cText ), nSkip := 1 local cPair while i <= nLen cPair := SubStr( cText, i, 2 ) nSkip := 1 DO Case // Minúsculas con acento Case cPair == Chr(195)+Chr(161) ; cResult += "a" ; nSkip := 2 // á Case cPair == Chr(195)+Chr(169) ; cResult += "e" ; nSkip := 2 // é Case cPair == Chr(195)+Chr(173) ; cResult += "i" ; nSkip := 2 // í Case cPair == Chr(195)+Chr(179) ; cResult += "o" ; nSkip := 2 // ó Case cPair == Chr(195)+Chr(186) ; cResult += "u" ; nSkip := 2 // ú Case cPair == Chr(195)+Chr(177) ; cResult += "ñ" ; nSkip := 2 // ñ // Mayúsculas con acento Case cPair == Chr(195)+Chr(129) ; cResult += "A" ; nSkip := 2 // Á Case cPair == Chr(195)+Chr(137) ; cResult += "E" ; nSkip := 2 // É Case cPair == Chr(195)+Chr(141) ; cResult += "I" ; nSkip := 2 // Í Case cPair == Chr(195)+Chr(147) ; cResult += "O" ; nSkip := 2 // Ó Case cPair == Chr(195)+Chr(154) ; cResult += "U" ; nSkip := 2 // Ú Case cPair == Chr(195)+Chr(145) ; cResult += "Ñ" ; nSkip := 2 OtherWise cResult += SubStr( cText, i, 1 ) endcase i += nSkip enddo return cResult ***************************************** Static Function Fnct_LimpiarRespuesta( cTexto ) local n, cNum, nUltimo := 0 // ?? Activación condicional: ¿hay síntomas de corrupción? if "Ã" $ cTexto .OR. "ðŸ" $ cTexto .OR. "¿" $ cTexto .OR. "�" $ cTexto // ?? Corrección de codificación UTF-8 mal interpretada cTexto := StrTran( cTexto, "á", "á" ) cTexto := StrTran( cTexto, "é", "é" ) cTexto := StrTran( cTexto, "í", "í" ) cTexto := StrTran( cTexto, "ó", "ó" ) cTexto := StrTran( cTexto, "ú", "ú" ) cTexto := StrTran( cTexto, "ñ", "ñ" ) cTexto := StrTran( cTexto, "Ã", "" ) // ?? Limpieza de signos de interrogación corruptos cTexto := StrTran( cTexto, "¿", "¿" ) cTexto := StrTran( cTexto, "�", "¿" ) // ?? Limpieza de emojis mal codificados cTexto := StrTran( cTexto, "ðŸ˜", "" ) cTexto := StrTran( cTexto, "ðŸ‘", "" ) cTexto := StrTran( cTexto, "ðŸŠ", "" ) cTexto := StrTran( cTexto, "ðŸ¤", "" ) cTexto := StrTran( cTexto, "ðŸŒ", "" ) cTexto := StrTran( cTexto, "ðŸŽ", "" ) cTexto := StrTran( cTexto, "ðŸ", "" ) cTexto := StrTran( cTexto, "Š" , "") cTexto := StrTran( cTexto, " Š" , "") // ?? Limpieza de caracteres invisibles o corruptos cTexto := StrTran( cTexto, Chr(0), "" ) cTexto := StrTran( cTexto, Chr(11), "" ) cTexto := StrTran( cTexto, Chr(12), "" ) cTexto := StrTran( cTexto, Chr(13)+Chr(13), Chr(13) ) cTexto := StrTran( cTexto, Chr(10)+Chr(10), Chr(10) ) // ?? Limpieza final de carácter extraño al inicio y final if Left( cTexto, 1 ) $ Chr(0)+Chr(11)+Chr(12)+Chr(13)+Chr(10)+"Âï" cTexto := SubStr( cTexto, 2 ) endif if Right( cTexto, 1 ) $ "ÂïðŸ" cTexto := Left( cTexto, Len( cTexto ) - 1 ) endif endif // ?? Normalización de saltos en listas numeradas cTexto := StrTran( cTexto, ":1.", ":" + Chr(13)+Chr(10) + "1." ) for n := 2 to 99 cNum := " " + AllTrim( Str( n ) ) + "." if At( cNum, cTexto ) > 0 if ! ( SubStr( cTexto, At( cNum, cTexto ) - 2, 2 ) == Chr(13)+Chr(10) ) cTexto := StrTran( cTexto, cNum, Chr(13)+Chr(10) + AllTrim( Str( n ) ) + "." ) endif nUltimo := n endif next if nUltimo > 0 cNum := AllTrim( Str( nUltimo ) ) + "." if At( cNum, cTexto ) > 0 if Len( SubStr( cTexto, At( cNum, cTexto ) + Len( cNum ) ) ) > 0 cTexto := StrTran( cTexto, cNum, cNum + Chr(13)+Chr(10) ) endif endif endif // ?? Reformatear Markdown y guiones cTexto := StrTran( cTexto, "--", Chr(13)+Chr(10) + "--" ) cTexto := StrTran( cTexto, " - ", Chr(13)+Chr(10) + "- " ) cTexto := StrTran( cTexto, ". **", "." + Chr(13)+Chr(10) + "" ) cTexto := StrTran( cTexto, ":**", ":" + Chr(13)+Chr(10) + "" ) return cTexto ***************************************** Static Function Fnct_FormateaTexto( cTextoOriginal ) local cTextoFormateado cTextoFormateado := StrTran( cTextoOriginal, ". ", "." + CRLF ) return cTextoFormateado ***************************************** Static Function Fnct_ConsultarManual( cPregunta ) local cQuestionNorm, cRespuesta, cQuery, oResult, cTabla, cPromptManual, cIntencion local aPalabras, aPalabrasExpand, i, cWhereClause cQuestionNorm := Lower( AllTrim( cPregunta ) ) cIntencion := Fnct_DetectarIntencion( cPregunta ) if cIntencion="DESCONOCIDA" msgStop("No se encontraron coincidencias para esta consulta.","ERROR de conceptos") return .f. endif if !Empty( hCacheManual ) .AND. hb_HHasKey( hCacheManual, cQuestionNorm ) oRespuesta:VarPut( hCacheManual[cQuestionNorm] ) oRespuesta:Refresh() return .T. endif cRespuesta := "" if !_Conecta() return .F. endif cTabla := _cPrefijo + "_asistente_manual" aPalabras := hb_ATokens( Upper( AllTrim( cPregunta ) ), " " ) aPalabrasExpand := Fnct_ExpandirPorIntencion( aPalabras, cIntencion ) cWhereClause := "" for i := 1 to Len( aPalabrasExpand ) if !Empty( aPalabrasExpand[i] ) .AND. Len( aPalabrasExpand[i] ) >= 3 cWhereClause += "pregunta LIKE '%" + aPalabrasExpand[i] + "%' OR " endif next if Empty( cWhereClause ) cWhereClause := "1=0" else cWhereClause := Left( cWhereClause, Len( cWhereClause ) - 4 ) endif cQuery := "SELECT respuesta FROM " + cTabla + " WHERE " + cWhereClause + " LIMIT 1" TRY oResult := _oSqlConex:Query( cQuery ) if !Empty( oResult ) .AND. Len( oResult ) > 0 cRespuesta := Fnct_FormateaTexto( oResult:respuesta ) cRespuesta := Fnct_Utf8ToAnsi( cRespuesta ) cRespuesta := Fnct_LimpiarRespuesta( cRespuesta ) endif CATCH Fnct_MsgErrorBD("NoMySql", .F., "Error al consultar la base de datos", "Falló la consulta", "Error en servicio de Datos") _Close() return .F. END TRY _Close() if !Empty( cRespuesta ) hCacheManual[cQuestionNorm] := cRespuesta SaveCacheToFile( hCacheManual, "cache_manual.txt" ) oRespuesta:VarPut( cRespuesta ) oRespuesta:Refresh() return .T. endif cPromptManual := "Eres un experto en el software CUENTANUBE. " + ; "Responde ÚNICAMENTE basándote en el manual oficial. " + ; "Si no hay información clara, di: 'No encontré información en el manual'." cRespuesta := oRespuesta:GetText() if !Empty( cRespuesta ) hCacheManual[cQuestionNorm] := cRespuesta SaveCacheToFile( hCacheManual, "cache_manual.txt" ) endif return .T. ***************************************** Static Function Fnct_DetectarIntencion( cPregunta ) local cPreguntaNorm := Upper( AllTrim( cPregunta ) ) local cIntencion := "DESCONOCIDA" local aTokens := hb_ATokens( cPreguntaNorm, " " ) local i for i := 1 to Len( aTokens ) DO Case Case aTokens[i] == "AGREGAR" .OR. aTokens[i] == "INCLUIR" .OR. ; aTokens[i] == "REGISTRAR" .OR. aTokens[i] == "GRABAR" .OR. ; aTokens[i] == "INSERTAR" cIntencion := "AGREGAR" EXIT Case aTokens[i] == "BORRAR" .OR. aTokens[i] == "ELIMINAR" .OR. ; aTokens[i] == "QUITAR" .OR. aTokens[i] == "SUPRIMIR" cIntencion := "ELIMINAR" EXIT Case aTokens[i] == "MODIFICAR" .OR. aTokens[i] == "EDITAR" .OR. ; aTokens[i] == "CAMBIAR" .OR. aTokens[i] == "ACTUALIZAR" cIntencion := "MODIFICAR" EXIT Case aTokens[i] == "CONSULTAR" .OR. aTokens[i] == "VER" .OR. ; aTokens[i] == "BUSCAR" .OR. aTokens[i] == "MOSTRAR" cIntencion := "CONSULTAR" EXIT Case aTokens[i] == "LISTAR" .OR. aTokens[i] == "REVISAR" .OR. ; aTokens[i] == "EXAMINAR" .OR. aTokens[i] == "EXPLORAR" cIntencion := "LISTAR" EXIT endcase next return cIntencion ***************************************** Static Function Fnct_ExpandirPorIntencion( aPalabras, cIntencion ) local hSinonimos, aExpandida := {}, i, j, aSinonimos // Diccionario de sinónimos por intención hSinonimos := { ; "AGREGAR" => { "AGREGAR", "INCLUIR", "REGISTRAR", "GRABAR", "INSERTAR" }, ; "ELIMINAR" => { "BORRAR", "ELIMINAR", "QUITAR", "SUPRIMIR" }, ; "MODIFICAR" => { "MODIFICAR", "EDITAR", "CAMBIAR", "ACTUALIZAR" }, ; "CONSULTAR" => { "CONSULTAR", "VER", "BUSCAR", "MOSTRAR" }, ; "LISTAR" => { "LISTAR", "REVISAR", "EXAMINAR", "EXPLORAR" } ; } // Copiar palabras originales for i := 1 to Len( aPalabras ) AAdd( aExpandida, aPalabras[i] ) next // Agregar sinónimos si existen para la intención if hb_HHasKey( hSinonimos, cIntencion ) aSinonimos := hSinonimos[cIntencion] for j := 1 to Len( aSinonimos ) if AScan( aExpandida, aSinonimos[j] ) == 0 AAdd( aExpandida, aSinonimos[j] ) endif next endif return aExpandida ***************************************** Static Function Fnct_ExtraerContenidoDeepSeek( cJson ) local nIni, nFin, cTexto, i, cLog nIni := At( '"content":"', cJson ) if nIni > 0 nIni += Len( '"content":"' ) nFin := At( '"},"logprobs"', SubStr( cJson, nIni ) ) if nFin > 0 cTexto := SubStr( cJson, nIni, nFin - 1 ) cTexto := StrTran( cTexto, '\n', Chr(13)+Chr(10) ) return cTexto endif endif return "? No se pudo extraer el contenido." ***************************************** Static Function Fnct_ContieneVerbosAccion( cTexto ) local aVerbosProhibidos := { ; "crear", "hacer", "elaborar", "emitir", "dibujar", "generar", ; "construir", "desarrollar", "realizar", "producir", "ejecutar", ; "crea", "haz", "elabora", "emite", "dibuja", "genera", ; "construye", "desarrolla", "realiza", "produce", "ejecuta", ; "creame", "hazme", "elaborame", "emitame", "dibujame", "generame", ; "construyeme", "desarrollame", "realizame", "produceme", "ejecutame", ; "escribe", "redacta", "postea", "traduce", "elimina", "borra" ; } local cTextoLower := Lower( AllTrim( cTexto ) ) local i for i := 1 to Len( aVerbosProhibidos ) if aVerbosProhibidos[i] $ cTextoLower return .t. endif next return .f. ***************************************** Static Function Fnct_NormalizarRespuestaIA( cTexto ) local n, cNum, nUltimo := 0 // ?? Corrección de codificación UTF-8 mal interpretada cTexto := StrTran( cTexto, "á", "á" ) cTexto := StrTran( cTexto, "é", "é" ) cTexto := StrTran( cTexto, "í", "í" ) cTexto := StrTran( cTexto, "ó", "ó" ) cTexto := StrTran( cTexto, "ú", "ú" ) cTexto := StrTran( cTexto, "ñ", "ñ" ) cTexto := StrTran( cTexto, "Ã", "" ) // limpieza residual // ?? Limpieza de signos de interrogación corruptos cTexto := StrTran( cTexto, "¿", "¿" ) cTexto := StrTran( cTexto, "�", "¿" ) // ?? Limpieza de emojis mal codificados cTexto := StrTran( cTexto, "ðŸ˜", "" ) cTexto := StrTran( cTexto, "ðŸ‘", "" ) cTexto := StrTran( cTexto, "ðŸŠ", "" ) cTexto := StrTran( cTexto, "ðŸ", "" ) // ?? Limpieza extendida de emojis corruptos al final cTexto := StrTran( cTexto, "ðŸ¤", "" ) cTexto := StrTran( cTexto, "ðŸŒ", "" ) cTexto := StrTran( cTexto, "ðŸŽ", "" ) cTexto := StrTran( cTexto, "ðŸ", "" ) // limpieza residual // ?? Si el último carácter sigue siendo corrupto, lo eliminamos if Right( cTexto, 1 ) $ "ÂïðŸ" cTexto := Left( cTexto, Len( cTexto ) - 1 ) endif // ?? Limpieza de caracteres invisibles o corruptos cTexto := StrTran( cTexto, Chr(0), "" ) cTexto := StrTran( cTexto, Chr(11), "" ) cTexto := StrTran( cTexto, Chr(12), "" ) cTexto := StrTran( cTexto, Chr(13)+Chr(13), Chr(13) ) cTexto := StrTran( cTexto, Chr(10)+Chr(10), Chr(10) ) // ?? Insertar salto después de ":" si está pegado al "1." cTexto := StrTran( cTexto, ":1.", ":" + Chr(13)+Chr(10) + "1." ) // ?? Insertar saltos antes de cada número del 2 al 99 for n := 2 to 99 cNum := " " + AllTrim( Str( n ) ) + "." if At( cNum, cTexto ) > 0 // Evitar duplicar saltos si ya existen if ! ( SubStr( cTexto, At( cNum, cTexto ) - 2, 2 ) == Chr(13)+Chr(10) ) cTexto := StrTran( cTexto, cNum, Chr(13)+Chr(10) + AllTrim( Str( n ) ) + "." ) endif nUltimo := n endif next // ?? Insertar salto después del último número si hay texto adicional if nUltimo > 0 cNum := AllTrim( Str( nUltimo ) ) + "." if At( cNum, cTexto ) > 0 if Len( SubStr( cTexto, At( cNum, cTexto ) + Len( cNum ) ) ) > 0 cTexto := StrTran( cTexto, cNum, cNum + Chr(13)+Chr(10) ) endif endif endif // ?? Reformatear Markdown y guiones cTexto := StrTran( cTexto, "--", Chr(13)+Chr(10) + "--" ) cTexto := StrTran( cTexto, " - ", Chr(13)+Chr(10) + "- " ) cTexto := StrTran( cTexto, ". **", "." + Chr(13)+Chr(10) + "**" ) cTexto := StrTran( cTexto, ":**", ":" + Chr(13)+Chr(10) + "**" ) cTexto := StrTran( cTexto, "**", "" ) // ?? Limpieza final de carácter extraño al inicio if Left( cTexto, 1 ) $ Chr(0)+Chr(11)+Chr(12)+Chr(13)+Chr(10)+"Âï" cTexto := SubStr( cTexto, 2 ) endif return cTexto ***************************************** static Function Fnct_InsertarSaltosPorNumeracion( cTexto ) local n, cNum, nUltimo := 0 // Insertar salto después de ":" si está pegado al "1." cTexto := StrTran( cTexto, ":1.", ":" + Chr(13)+Chr(10) + "1." ) // Insertar saltos antes de cada número del 2 al 99 for n := 2 to 99 cNum := " " + AllTrim( Str( n ) ) + "." if At( cNum, cTexto ) > 0 cTexto := StrTran( cTexto, cNum, Chr(13)+Chr(10) + AllTrim( Str( n ) ) + "." ) nUltimo := n endif next // Insertar salto antes de cualquier texto que venga después del último número if nUltimo > 0 cTexto := StrTran( cTexto, AllTrim( Str( nUltimo ) ) + ".", AllTrim( Str( nUltimo ) ) + "." + Chr(13)+Chr(10) ) endif return cTexto