953 lines
39 KiB
C#
953 lines
39 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using LanguageDetection;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using System.Diagnostics;
|
|
using libObsidean;
|
|
|
|
namespace GTPCorrgir
|
|
{
|
|
public class ApiSettings
|
|
{
|
|
public class ApiKeySection
|
|
{
|
|
public string OpenAI { get; set; }
|
|
public string Groq { get; set; }
|
|
public string Grok { get; set; }
|
|
public string Claude { get; set; }
|
|
}
|
|
|
|
public ApiKeySection ApiKeys { get; set; }
|
|
}
|
|
|
|
public class gtpask : IDisposable
|
|
{
|
|
private string _openAiApiKey;
|
|
private string _groqApiKey;
|
|
private string _grokApiKey;
|
|
private string _claudeApiKey;
|
|
private readonly HttpClient _httpClient;
|
|
private bool _disposed;
|
|
private readonly LanguageDetector _languageDetector;
|
|
private readonly Obsidean _markdownProcessor;
|
|
|
|
public Logger Log { get; }
|
|
|
|
private readonly Dictionary<string, string> _languageMap = new Dictionary<string, string>
|
|
{
|
|
{ "en", "English" },
|
|
{ "es", "Spanish" },
|
|
{ "it", "Italian" },
|
|
{ "pt", "Portuguese" }
|
|
};
|
|
|
|
public string IdiomaDetectado { get; private set; }
|
|
public string TextoACorregir { get; set; }
|
|
public string TextoCorregido { get; private set; }
|
|
public string TextodeSistema { get; private set; }
|
|
private const bool Simulacion = false;
|
|
|
|
public gtpask()
|
|
{
|
|
try
|
|
{
|
|
Log = new Logger();
|
|
_httpClient = new HttpClient();
|
|
_languageDetector = new LanguageDetector();
|
|
_languageDetector.AddLanguages("en", "es", "it", "pt");
|
|
_markdownProcessor = new Obsidean();
|
|
|
|
LoadApiKeys();
|
|
InitializeHttpClient();
|
|
_markdownProcessor.LeerPalabrasTecnicas();
|
|
|
|
Log.Log("gtpask initialized successfully");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error initializing gtpask: {ex.Message}");
|
|
throw new ApplicationException("Failed to initialize gtpask", ex);
|
|
}
|
|
}
|
|
|
|
private void LoadApiKeys()
|
|
{
|
|
try
|
|
{
|
|
string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appsettings.json");
|
|
if (!File.Exists(configPath))
|
|
{
|
|
throw new FileNotFoundException("Configuration file (appsettings.json) not found.");
|
|
}
|
|
|
|
string jsonContent = File.ReadAllText(configPath);
|
|
var settings = JsonConvert.DeserializeObject<ApiSettings>(jsonContent);
|
|
|
|
_openAiApiKey = settings?.ApiKeys?.OpenAI;
|
|
_groqApiKey = settings?.ApiKeys?.Groq;
|
|
_grokApiKey = settings?.ApiKeys?.Grok;
|
|
_claudeApiKey = settings?.ApiKeys?.Claude;
|
|
|
|
ValidateApiKeys();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error loading API keys: {ex.Message}");
|
|
throw new ApplicationException("Failed to load API keys", ex);
|
|
}
|
|
}
|
|
|
|
private void ValidateApiKeys()
|
|
{
|
|
var missingKeys = new List<string>();
|
|
|
|
if (string.IsNullOrEmpty(_openAiApiKey)) missingKeys.Add("OpenAI");
|
|
if (string.IsNullOrEmpty(_groqApiKey)) missingKeys.Add("Groq");
|
|
if (string.IsNullOrEmpty(_grokApiKey)) missingKeys.Add("Grok");
|
|
if (string.IsNullOrEmpty(_claudeApiKey)) missingKeys.Add("Claude");
|
|
|
|
if (missingKeys.Any())
|
|
{
|
|
string missingKeysStr = string.Join(", ", missingKeys);
|
|
throw new ApplicationException($"Missing API keys: {missingKeysStr}");
|
|
}
|
|
}
|
|
|
|
private void InitializeHttpClient()
|
|
{
|
|
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
|
_httpClient.DefaultRequestHeaders.Clear();
|
|
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
|
}
|
|
|
|
public async Task CorregirTexto()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(TextoACorregir))
|
|
{
|
|
Log.Log("No hay texto para corregir");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
Log.Log("Iniciando proceso de corrección");
|
|
Log.Log($"Texto original: {TextoACorregir}");
|
|
|
|
if (!DetectarIdioma())
|
|
{
|
|
throw new ApplicationException("No se pudo detectar el idioma del texto");
|
|
}
|
|
|
|
string textoMarcado = MarcarPalabrasTecnicas();
|
|
Log.Log($"Texto marcado: {textoMarcado}");
|
|
|
|
if (Simulacion)
|
|
{
|
|
await SimularCorreccion();
|
|
}
|
|
else
|
|
{
|
|
await ProcesarTextoConLLM(textoMarcado);
|
|
}
|
|
|
|
Log.Log($"Texto corregido: {TextoCorregido}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error en CorregirTexto: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private bool DetectarIdioma()
|
|
{
|
|
try
|
|
{
|
|
if (EsModoTraduccion())
|
|
{
|
|
IdiomaDetectado = ObtenerIdiomaObjetivo();
|
|
Log.Log($"Modo traducción: idioma objetivo establecido a {IdiomaDetectado}");
|
|
return true;
|
|
}
|
|
|
|
// Si el texto es solo una URL, usar español como predeterminado
|
|
if (EsSoloURL(TextoACorregir))
|
|
{
|
|
IdiomaDetectado = _languageMap["es"]; // Español como predeterminado
|
|
Log.Log($"Texto es URL, usando idioma predeterminado: {IdiomaDetectado}");
|
|
return true;
|
|
}
|
|
|
|
// Si el texto es muy corto, usar español como predeterminado
|
|
if (string.IsNullOrWhiteSpace(TextoACorregir) || TextoACorregir.Trim().Length < 10)
|
|
{
|
|
IdiomaDetectado = _languageMap["es"]; // Español como predeterminado
|
|
Log.Log($"Texto muy corto, usando idioma predeterminado: {IdiomaDetectado}");
|
|
return true;
|
|
}
|
|
|
|
string detectedLanguageCode = _languageDetector.Detect(TextoACorregir);
|
|
|
|
// Si no se detectó idioma o es desconocido, usar español como fallback
|
|
if (string.IsNullOrEmpty(detectedLanguageCode) || !_languageMap.ContainsKey(detectedLanguageCode))
|
|
{
|
|
IdiomaDetectado = _languageMap["es"]; // Español como fallback
|
|
Log.Log($"Idioma no detectado, usando fallback: {IdiomaDetectado}");
|
|
return true;
|
|
}
|
|
|
|
IdiomaDetectado = _languageMap[detectedLanguageCode];
|
|
Log.Log($"Idioma detectado: {IdiomaDetectado}");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error al detectar idioma: {ex.Message}");
|
|
// En caso de error, usar español como fallback
|
|
IdiomaDetectado = _languageMap["es"];
|
|
Log.Log($"Error en detección, usando fallback: {IdiomaDetectado}");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private bool EsModoTraduccion()
|
|
{
|
|
return Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Ingles ||
|
|
Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Italiano ||
|
|
Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Espanol ||
|
|
Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Portugues;
|
|
}
|
|
|
|
private string ObtenerIdiomaObjetivo()
|
|
{
|
|
return Opciones.Instance.modo switch
|
|
{
|
|
Opciones.modoDeUso.Traducir_a_Ingles => _languageMap["en"],
|
|
Opciones.modoDeUso.Traducir_a_Italiano => _languageMap["it"],
|
|
Opciones.modoDeUso.Traducir_a_Espanol => _languageMap["es"],
|
|
Opciones.modoDeUso.Traducir_a_Portugues => _languageMap["pt"],
|
|
_ => throw new ArgumentException("Modo de traducción no válido")
|
|
};
|
|
}
|
|
|
|
private string MarcarPalabrasTecnicas()
|
|
{
|
|
try
|
|
{
|
|
return _markdownProcessor.MarkTechnicalTerms_IgnoreCase(TextoACorregir);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error al marcar palabras técnicas: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task ProcesarTextoConLLM(string textoMarcado)
|
|
{
|
|
try
|
|
{
|
|
string respuestaLLM;
|
|
string respuestaCompleta = "";
|
|
|
|
// Si es el modo Claude Web Search, usar el método específico
|
|
if (Opciones.Instance.modo == Opciones.modoDeUso.ClaudeWebSearch)
|
|
{
|
|
var resultadoWebSearch = await CallClaudeWebSearchApiCompleto(textoMarcado);
|
|
respuestaLLM = resultadoWebSearch.texto;
|
|
respuestaCompleta = resultadoWebSearch.respuestaCompleta;
|
|
}
|
|
else
|
|
{
|
|
switch (Opciones.Instance.LLM)
|
|
{
|
|
case Opciones.LLM_a_Usar.OpenAI:
|
|
respuestaLLM = await CallOpenAiApi(textoMarcado);
|
|
break;
|
|
case Opciones.LLM_a_Usar.Ollama:
|
|
respuestaLLM = await CallOllamaApi(textoMarcado);
|
|
break;
|
|
case Opciones.LLM_a_Usar.Groq:
|
|
respuestaLLM = await CallGroqAiApi(textoMarcado);
|
|
break;
|
|
case Opciones.LLM_a_Usar.Grok:
|
|
respuestaLLM = await CallGrokApi(textoMarcado);
|
|
break;
|
|
case Opciones.LLM_a_Usar.Claude:
|
|
respuestaLLM = await CallClaudeApi(textoMarcado);
|
|
break;
|
|
default:
|
|
throw new ArgumentException("LLM no válido");
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(respuestaLLM))
|
|
{
|
|
throw new ApplicationException("No se recibió respuesta del LLM");
|
|
}
|
|
|
|
ProcesarRespuestaLLM(respuestaLLM, respuestaCompleta);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error al procesar texto con LLM: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private void ProcesarRespuestaLLM(string respuestaLLM, string respuestaCompleta)
|
|
{
|
|
// Para Claude Web Search, la respuesta ya viene en formato de texto final
|
|
if (Opciones.Instance.modo == Opciones.modoDeUso.ClaudeWebSearch)
|
|
{
|
|
// Claude Web Search ya devuelve el texto final con formato JSON interno,
|
|
// intentamos extraer JSON primero, si falla usamos el texto completo
|
|
string respuestaExtraida = ExtraerValorUnicoJSON(respuestaLLM);
|
|
if (respuestaExtraida == null)
|
|
{
|
|
// Si no se puede extraer JSON, usamos la respuesta completa
|
|
respuestaExtraida = respuestaLLM;
|
|
Log.Log("Claude Web Search: No se encontró JSON, usando respuesta completa");
|
|
}
|
|
|
|
respuestaExtraida = System.Text.RegularExpressions.Regex.Replace(respuestaExtraida, @"\*\*(.*?)\*\*", "$1");
|
|
respuestaExtraida = _markdownProcessor.RemoveTechnicalTermMarkers_IgnoreCase(respuestaExtraida).Trim('"');
|
|
respuestaExtraida = _markdownProcessor.RemoveDoubleBrackets(respuestaExtraida);
|
|
|
|
// Extraer y formatear enlaces de búsqueda web
|
|
string enlacesFormateados = ExtraerEnlacesWebSearch(respuestaCompleta);
|
|
|
|
// Para el modo Pregunta-Respuesta y Claude Web Search, combinar pregunta original con la respuesta y enlaces
|
|
TextoCorregido = $"{TextoACorregir}\n\n{respuestaExtraida}";
|
|
|
|
if (!string.IsNullOrEmpty(enlacesFormateados))
|
|
{
|
|
TextoCorregido += $"\n\n## Fuentes consultadas:\n{enlacesFormateados}";
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Para otros LLMs, procesamiento normal con JSON requerido
|
|
string respuestaExtraidaNormal = ExtraerValorUnicoJSON(respuestaLLM);
|
|
if (respuestaExtraidaNormal == null)
|
|
{
|
|
throw new ApplicationException("Error al extraer el texto corregido de la respuesta JSON");
|
|
}
|
|
|
|
respuestaExtraidaNormal = System.Text.RegularExpressions.Regex.Replace(respuestaExtraidaNormal, @"\*\*(.*?)\*\*", "$1");
|
|
respuestaExtraidaNormal = _markdownProcessor.RemoveTechnicalTermMarkers_IgnoreCase(respuestaExtraidaNormal).Trim('"');
|
|
respuestaExtraidaNormal = _markdownProcessor.RemoveDoubleBrackets(respuestaExtraidaNormal);
|
|
|
|
// Para el modo Pregunta-Respuesta, combinar pregunta original con la respuesta
|
|
if (Opciones.Instance.modo == Opciones.modoDeUso.PreguntaRespuesta)
|
|
{
|
|
TextoCorregido = $"{TextoACorregir}\n{respuestaExtraidaNormal}";
|
|
}
|
|
else
|
|
{
|
|
TextoCorregido = respuestaExtraidaNormal;
|
|
}
|
|
}
|
|
|
|
private string ExtraerEnlacesWebSearch(string respuestaCompleta)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(respuestaCompleta))
|
|
{
|
|
Log.Log("ExtraerEnlacesWebSearch: respuestaCompleta está vacía");
|
|
return "";
|
|
}
|
|
|
|
Log.Log($"ExtraerEnlacesWebSearch: Procesando respuesta de {respuestaCompleta.Length} caracteres");
|
|
|
|
var enlaces = new List<string>();
|
|
|
|
// Deserializar la respuesta completa para extraer los resultados de búsqueda
|
|
var data = JsonConvert.DeserializeObject<dynamic>(respuestaCompleta);
|
|
|
|
if (data?.content != null)
|
|
{
|
|
Log.Log($"ExtraerEnlacesWebSearch: Encontrados {data.content.Count} elementos de contenido");
|
|
|
|
foreach (var contentItem in data.content)
|
|
{
|
|
Log.Log($"ExtraerEnlacesWebSearch: Procesando elemento tipo: {contentItem.type}");
|
|
|
|
// Buscar elementos de tipo "web_search_tool_result"
|
|
if (contentItem.type == "web_search_tool_result" && contentItem.content != null)
|
|
{
|
|
Log.Log($"ExtraerEnlacesWebSearch: Encontrado web_search_tool_result con {contentItem.content.Count} resultados");
|
|
|
|
foreach (var searchResult in contentItem.content)
|
|
{
|
|
if (searchResult.type == "web_search_result")
|
|
{
|
|
string titulo = searchResult.title?.ToString() ?? "Sin título";
|
|
string url = searchResult.url?.ToString() ?? "";
|
|
string pageAge = searchResult.page_age?.ToString() ?? "";
|
|
|
|
Log.Log($"ExtraerEnlacesWebSearch: Resultado - Título: {titulo}, URL: {url}");
|
|
|
|
if (!string.IsNullOrEmpty(url))
|
|
{
|
|
string enlaceFormateado;
|
|
if (!string.IsNullOrEmpty(pageAge))
|
|
{
|
|
enlaceFormateado = $"* [{titulo}]({url}) - {pageAge}";
|
|
}
|
|
else
|
|
{
|
|
enlaceFormateado = $"* [{titulo}]({url})";
|
|
}
|
|
|
|
enlaces.Add(enlaceFormateado);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.Log("ExtraerEnlacesWebSearch: No se encontró contenido en la respuesta");
|
|
}
|
|
|
|
// Eliminar duplicados y devolver como string
|
|
var enlacesUnicos = enlaces.Distinct().ToList();
|
|
|
|
Log.Log($"ExtraerEnlacesWebSearch: Extraídos {enlacesUnicos.Count} enlaces únicos");
|
|
|
|
if (enlacesUnicos.Any())
|
|
{
|
|
return string.Join("\n", enlacesUnicos);
|
|
}
|
|
|
|
return "";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error al extraer enlaces de web search: {ex.Message}");
|
|
return "";
|
|
}
|
|
}
|
|
|
|
private async Task SimularCorreccion()
|
|
{
|
|
await Task.Delay(1000);
|
|
TextoCorregido = "Texto simulado de prueba";
|
|
Log.Log("Simulación completada");
|
|
}
|
|
|
|
public string ExtraerValorUnicoJSON(string input)
|
|
{
|
|
try
|
|
{
|
|
int startJson = input.IndexOf('{');
|
|
int endJson = input.LastIndexOf('}') + 1;
|
|
|
|
if (startJson == -1 || endJson == -1 || endJson <= startJson)
|
|
{
|
|
Log.Log("Formato JSON inválido en la respuesta");
|
|
return null;
|
|
}
|
|
|
|
string jsonString = input[startJson..endJson];
|
|
JObject jsonObject = JObject.Parse(jsonString);
|
|
var firstField = jsonObject.Properties().FirstOrDefault();
|
|
|
|
return firstField?.Value?.ToString();
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
Log.Log($"Error al procesar JSON: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private string CrearMensajeDeSistema()
|
|
{
|
|
return Opciones.Instance.modo switch
|
|
{
|
|
Opciones.modoDeUso.Corregir =>
|
|
"You are an engineer working in industrial automation. Your task is to review texts and rewrite them in a simple and concise manner. If you find words enclosed in double brackets [[like this]], preserve them exactly as they appear without any modifications. For all other technical terms, write them normally without adding any brackets or special formatting. Preserve any existing markdown language if present. Please rewrite the following text in " + IdiomaDetectado + " and respond in the following JSON format: { \"Rewritten_text\": \"Your text here\" }. Important: Do not add any new brackets to words that aren't already enclosed in double brackets.",
|
|
|
|
Opciones.modoDeUso.Ortografia =>
|
|
"Please check the following text for spelling errors and provide the corrected version. Do not change the meaning or structure of the sentences. If you find words enclosed in double brackets [[like this]], preserve them exactly as they appear. For all other words, only correct spelling mistakes while preserving technical terms. Preserve any existing markdown language if present, but do not introduce new markdown styling (such as bold or italics) for emphasis unless it was part of the original input. Please write in " + IdiomaDetectado + " and respond in the following JSON format: { \"Rewritten_text\": \"Your text here\" }.",
|
|
|
|
Opciones.modoDeUso.PreguntaRespuesta =>
|
|
"You are a helpful assistant specialized in industrial automation and technical topics. Please answer the user's question accurately and clearly. If the question contains words in double brackets [[like this]], preserve them exactly as they appear in your response. Please write your answer in " + IdiomaDetectado + " and respond in the following JSON format: { \"Reply_text\": \"Your answer here\" }.",
|
|
|
|
_ => "You are an engineer working specialized in industrial automation. If the question contains words in double brackets [[like this]], preserve them exactly as they appear. Please answer the following question in " + IdiomaDetectado + " and respond in the following JSON format: { \"Reply_text\": \"Your text here\" }."
|
|
};
|
|
}
|
|
|
|
private string CrearMensajeDeWebSearch()
|
|
{
|
|
return "You are a helpful assistant specialized in industrial automation and technical topics with web search capabilities. " +
|
|
"When users ask questions, you can use web search to find current and accurate information to provide comprehensive answers. " +
|
|
"If the question contains words in double brackets [[like this]], preserve them exactly as they appear in your response. " +
|
|
"Use the web search tool when you need to find recent information, current data, or specific technical details that might not be in your training data. " +
|
|
"Always provide well-researched, accurate answers and cite your sources when using web search results. " +
|
|
"Please write your answer in " + IdiomaDetectado + ". " +
|
|
"You can respond in JSON format like { \"Reply_text\": \"Your answer here\" } or provide a direct text response.";
|
|
}
|
|
|
|
private string CrearMensajeDeUsuario(string texto) =>
|
|
Opciones.Instance.modo switch
|
|
{
|
|
Opciones.modoDeUso.Corregir =>
|
|
$"Please rewrite and improve the following text to make it clearer and more concise the words inside brackets are technical words: \"{texto}\"",
|
|
|
|
Opciones.modoDeUso.Ortografia =>
|
|
$"Please check the following text for spelling errors and provide the corrected version. Do not change the meaning or structure of the sentences. Only correct any spelling mistakes you find on: \"{texto}\"",
|
|
|
|
Opciones.modoDeUso.PreguntaRespuesta =>
|
|
texto, // Para pregunta-respuesta, enviamos el texto directamente como la pregunta
|
|
|
|
Opciones.modoDeUso.ClaudeWebSearch =>
|
|
texto, // Para web search, enviamos la pregunta directamente
|
|
|
|
Opciones.modoDeUso.Traducir_a_Ingles =>
|
|
$"Please check the following text for spelling errors and provide the corrected version tranlated to English. Do not change the meaning or structure of the sentences. Only correct any spelling mistakes you find on: \"{texto}\"",
|
|
|
|
Opciones.modoDeUso.Traducir_a_Italiano =>
|
|
$"Please check the following text for spelling errors and provide the corrected version tranlated to Italian. Do not change the meaning or structure of the sentences. Only correct any spelling mistakes you find on: \"{texto}\"",
|
|
|
|
Opciones.modoDeUso.Traducir_a_Espanol =>
|
|
$"Please check the following text for spelling errors and provide the corrected version tranlated to Spanish. Do not change the meaning or structure of the sentences. Only correct any spelling mistakes you find on: \"{texto}\"",
|
|
|
|
Opciones.modoDeUso.Traducir_a_Portugues =>
|
|
$"Please check the following text for spelling errors and provide the corrected version tranlated to Portuguese. Do not change the meaning or structure of the sentences. Only correct any spelling mistakes you find on: \"{texto}\"",
|
|
|
|
_ => texto
|
|
};
|
|
|
|
private async Task<string> CallGrokApi(string input)
|
|
{
|
|
try
|
|
{
|
|
_httpClient.DefaultRequestHeaders.Clear();
|
|
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_grokApiKey}");
|
|
|
|
var requestData = new
|
|
{
|
|
messages = new[]
|
|
{
|
|
new { role = "system", content = CrearMensajeDeSistema() },
|
|
new { role = "user", content = CrearMensajeDeUsuario(input) }
|
|
},
|
|
model = "grok-beta",
|
|
stream = false,
|
|
temperature = 0
|
|
};
|
|
|
|
return await EnviarSolicitudLLM("https://api.x.ai/v1/chat/completions", requestData);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error en llamada a Grok API: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task<string> CallOllamaApi(string input)
|
|
{
|
|
try
|
|
{
|
|
var requestData = new
|
|
{
|
|
model = "llama3.2:latest",
|
|
messages = new[]
|
|
{
|
|
new { role = "system", content = CrearMensajeDeSistema() },
|
|
new { role = "user", content = CrearMensajeDeUsuario(input) }
|
|
},
|
|
stream = false
|
|
};
|
|
|
|
return await EnviarSolicitudLLM("http://127.0.0.1:11434/api/chat", requestData);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error en llamada a Ollama API: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task<string> CallOpenAiApi(string input)
|
|
{
|
|
try
|
|
{
|
|
_httpClient.DefaultRequestHeaders.Clear();
|
|
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_openAiApiKey}");
|
|
|
|
var requestData = new
|
|
{
|
|
model = "gpt-4o-mini",
|
|
messages = new[]
|
|
{
|
|
new { role = "system", content = CrearMensajeDeSistema() },
|
|
new { role = "user", content = CrearMensajeDeUsuario(input) }
|
|
}
|
|
};
|
|
|
|
return await EnviarSolicitudLLM("https://api.openai.com/v1/chat/completions", requestData);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error en llamada a OpenAI API: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task<string> CallGroqAiApi(string input)
|
|
{
|
|
try
|
|
{
|
|
_httpClient.DefaultRequestHeaders.Clear();
|
|
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_groqApiKey}");
|
|
|
|
var requestData = new
|
|
{
|
|
model = "llama-3.2-3b-preview",
|
|
messages = new[]
|
|
{
|
|
new { role = "system", content = CrearMensajeDeSistema() },
|
|
new { role = "user", content = CrearMensajeDeUsuario(input) }
|
|
},
|
|
max_tokens = 2048,
|
|
stream = false
|
|
};
|
|
|
|
return await EnviarSolicitudLLM("https://api.groq.com/openai/v1/chat/completions", requestData);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error en llamada a Groq API: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task<string> CallClaudeApi(string input)
|
|
{
|
|
try
|
|
{
|
|
_httpClient.DefaultRequestHeaders.Clear();
|
|
_httpClient.DefaultRequestHeaders.Add("x-api-key", _claudeApiKey);
|
|
_httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
|
|
|
|
var requestData = new
|
|
{
|
|
model = "claude-sonnet-4-20250514",
|
|
max_tokens = 4096,
|
|
temperature = 1,
|
|
system = CrearMensajeDeSistema(),
|
|
messages = new[]
|
|
{
|
|
new { role = "user", content = CrearMensajeDeUsuario(input) }
|
|
},
|
|
thinking = new
|
|
{
|
|
type = "enabled",
|
|
budget_tokens = 2048
|
|
}
|
|
};
|
|
|
|
return await EnviarSolicitudLLM("https://api.anthropic.com/v1/messages", requestData);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error en llamada a Claude API: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task<string> CallClaudeWebSearchApi(string input)
|
|
{
|
|
var resultado = await CallClaudeWebSearchApiCompleto(input);
|
|
return resultado.texto;
|
|
}
|
|
|
|
private async Task<(string texto, string respuestaCompleta)> CallClaudeWebSearchApiCompleto(string input)
|
|
{
|
|
try
|
|
{
|
|
_httpClient.DefaultRequestHeaders.Clear();
|
|
_httpClient.DefaultRequestHeaders.Add("x-api-key", _claudeApiKey);
|
|
_httpClient.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01");
|
|
_httpClient.DefaultRequestHeaders.Add("anthropic-beta", "web-search-2025-03-05");
|
|
|
|
var requestData = new
|
|
{
|
|
model = "claude-sonnet-4-20250514",
|
|
max_tokens = 4166,
|
|
temperature = 1,
|
|
system = CrearMensajeDeWebSearch(),
|
|
messages = new[]
|
|
{
|
|
new { role = "user", content = input } // Para web search, enviamos la pregunta directamente
|
|
},
|
|
tools = new[]
|
|
{
|
|
new
|
|
{
|
|
name = "web_search",
|
|
type = "web_search_20250305"
|
|
}
|
|
},
|
|
thinking = new
|
|
{
|
|
type = "enabled",
|
|
budget_tokens = 4123
|
|
}
|
|
};
|
|
|
|
var content = new StringContent(
|
|
JsonConvert.SerializeObject(requestData),
|
|
Encoding.UTF8,
|
|
"application/json"
|
|
);
|
|
|
|
Log.Log($"Enviando solicitud a https://api.anthropic.com/v1/messages");
|
|
Log.Log($"Datos de solicitud: {JsonConvert.SerializeObject(requestData)}");
|
|
|
|
using var response = await _httpClient.PostAsync("https://api.anthropic.com/v1/messages", content);
|
|
|
|
var responseContent = await response.Content.ReadAsStringAsync();
|
|
|
|
// Para Claude Web Search, filtrar campos encrypted_content largos del log
|
|
string logContent = responseContent;
|
|
if (responseContent.Contains("encrypted_content") || responseContent.Contains("signature"))
|
|
{
|
|
logContent = FiltrarEncryptedContentParaLog(responseContent);
|
|
}
|
|
|
|
Log.Log($"Respuesta recibida: {logContent}");
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
throw new HttpRequestException(
|
|
$"Error en la solicitud HTTP: {response.StatusCode} - {responseContent}"
|
|
);
|
|
}
|
|
|
|
var data = JsonConvert.DeserializeObject<dynamic>(responseContent);
|
|
|
|
if (data.content != null && data.content.Count > 0)
|
|
{
|
|
// Buscar el elemento con type = "text" en el array de content
|
|
foreach (var contentItem in data.content)
|
|
{
|
|
if (contentItem.type == "text")
|
|
{
|
|
return (contentItem.text.ToString(), responseContent);
|
|
}
|
|
}
|
|
// Si no encuentra un elemento con type="text", usar el primer elemento como fallback
|
|
if (data.content[0].text != null)
|
|
{
|
|
return (data.content[0].text.ToString(), responseContent);
|
|
}
|
|
}
|
|
|
|
throw new ApplicationException("No se encontró contenido en la respuesta de Claude Web Search");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error en llamada a Claude Web Search API: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task<string> EnviarSolicitudLLM(string endpoint, object requestData)
|
|
{
|
|
try
|
|
{
|
|
var content = new StringContent(
|
|
JsonConvert.SerializeObject(requestData),
|
|
Encoding.UTF8,
|
|
"application/json"
|
|
);
|
|
|
|
Log.Log($"Enviando solicitud a {endpoint}");
|
|
Log.Log($"Datos de solicitud: {JsonConvert.SerializeObject(requestData)}");
|
|
|
|
using var response = await _httpClient.PostAsync(endpoint, content);
|
|
|
|
var responseContent = await response.Content.ReadAsStringAsync();
|
|
|
|
// Para Claude Web Search, filtrar campos encrypted_content largos del log
|
|
string logContent = responseContent;
|
|
if (endpoint.Contains("anthropic") && (responseContent.Contains("encrypted_content") || responseContent.Contains("signature")))
|
|
{
|
|
logContent = FiltrarEncryptedContentParaLog(responseContent);
|
|
}
|
|
|
|
Log.Log($"Respuesta recibida: {logContent}");
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
throw new HttpRequestException(
|
|
$"Error en la solicitud HTTP: {response.StatusCode} - {responseContent}"
|
|
);
|
|
}
|
|
|
|
var data = JsonConvert.DeserializeObject<dynamic>(responseContent);
|
|
|
|
// Manejar diferentes formatos de respuesta según el LLM
|
|
if (endpoint.Contains("ollama"))
|
|
{
|
|
if (data.done == true && data.message != null)
|
|
{
|
|
return data.message.content;
|
|
}
|
|
throw new ApplicationException("Formato de respuesta de Ollama inválido");
|
|
}
|
|
else if (endpoint.Contains("anthropic"))
|
|
{
|
|
if (data.content != null && data.content.Count > 0)
|
|
{
|
|
// Buscar el elemento con type = "text" en el array de content
|
|
foreach (var contentItem in data.content)
|
|
{
|
|
if (contentItem.type == "text")
|
|
{
|
|
return contentItem.text;
|
|
}
|
|
}
|
|
// Si no encuentra un elemento con type="text", usar el primer elemento como fallback
|
|
if (data.content[0].text != null)
|
|
{
|
|
return data.content[0].text;
|
|
}
|
|
}
|
|
throw new ApplicationException("No se encontró contenido en la respuesta de Claude");
|
|
}
|
|
else // OpenAI, Groq, Grok
|
|
{
|
|
if (data.choices != null && data.choices.Count > 0)
|
|
{
|
|
return data.choices[0].message.content;
|
|
}
|
|
throw new ApplicationException("No se encontró contenido en la respuesta del LLM");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error al enviar solicitud a {endpoint}: {ex.Message}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private string FiltrarEncryptedContentParaLog(string responseContent)
|
|
{
|
|
try
|
|
{
|
|
// Usar regex para reemplazar campos encrypted_content largos con un placeholder
|
|
var regex = new System.Text.RegularExpressions.Regex(
|
|
@"""encrypted_content""\s*:\s*""[^""]{50,}""",
|
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase
|
|
);
|
|
|
|
string filtered = regex.Replace(responseContent, @"""encrypted_content"": ""[CONTENT_FILTERED_FOR_LOG]""");
|
|
|
|
// También filtrar encrypted_index si es muy largo
|
|
var regexIndex = new System.Text.RegularExpressions.Regex(
|
|
@"""encrypted_index""\s*:\s*""[^""]{50,}""",
|
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase
|
|
);
|
|
|
|
filtered = regexIndex.Replace(filtered, @"""encrypted_index"": ""[INDEX_FILTERED_FOR_LOG]""");
|
|
|
|
// Filtrar signature si es muy largo
|
|
var regexSignature = new System.Text.RegularExpressions.Regex(
|
|
@"""signature""\s*:\s*""[^""]{20,}""",
|
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase
|
|
);
|
|
|
|
filtered = regexSignature.Replace(filtered, @"""signature"": ""[SIGNATURE_FILTERED_FOR_LOG]""");
|
|
|
|
return filtered;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Log($"Error al filtrar encrypted_content: {ex.Message}");
|
|
// Si hay error al filtrar, devolver contenido original
|
|
return responseContent;
|
|
}
|
|
}
|
|
|
|
private bool EsSoloURL(string texto)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(texto))
|
|
return false;
|
|
|
|
texto = texto.Trim();
|
|
|
|
// Verificar si el texto es principalmente una URL
|
|
return texto.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
|
texto.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
|
|
texto.StartsWith("www.", StringComparison.OrdinalIgnoreCase) ||
|
|
(texto.Contains(".") && texto.Split(' ').Length == 1);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_httpClient?.Dispose();
|
|
}
|
|
|
|
_disposed = true;
|
|
}
|
|
}
|
|
|
|
~gtpask()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
}
|
|
|
|
// Clase auxiliar para manejar excepciones específicas de la aplicación
|
|
public class LLMException : Exception
|
|
{
|
|
public LLMException(string message) : base(message) { }
|
|
public LLMException(string message, Exception innerException) : base(message, innerException) { }
|
|
}
|
|
|
|
// Clase auxiliar para validación
|
|
public static class Validations
|
|
{
|
|
public static void ValidateNotNull(object value, string paramName)
|
|
{
|
|
if (value == null)
|
|
{
|
|
throw new ArgumentNullException(paramName);
|
|
}
|
|
}
|
|
|
|
public static void ValidateNotNullOrEmpty(string value, string paramName)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
throw new ArgumentException("Value cannot be null or empty", paramName);
|
|
}
|
|
}
|
|
}
|
|
} |