Se agregó la opción "Claude Web Search" al menú y se implementó la lógica para manejar las solicitudes a la API de Claude, incluyendo la extracción de enlaces de búsqueda. Se mejoró la detección de idioma y se estableció español como predeterminado en casos específicos. Además, se optimizó el procesamiento de respuestas para el modo Claude Web Search.

This commit is contained in:
Miguel 2025-06-17 12:40:29 +02:00
parent a48e64f372
commit a8aca9a82a
4 changed files with 412 additions and 77 deletions

View File

@ -81,6 +81,7 @@ namespace GTPCorrgir
case Opciones.modoDeUso.Corregir:
case Opciones.modoDeUso.Ortografia:
case Opciones.modoDeUso.PreguntaRespuesta:
case Opciones.modoDeUso.ClaudeWebSearch:
case Opciones.modoDeUso.Traducir_a_Espanol:
case Opciones.modoDeUso.Traducir_a_Ingles:
case Opciones.modoDeUso.Traducir_a_Italiano:
@ -155,9 +156,9 @@ namespace GTPCorrgir
$"Corrección en: {Math.Round(stopwatch.ElapsedMilliseconds / 1000.0, 1)} s");
if (Opciones.Instance.modo == Opciones.modoDeUso.Corregir || Opciones.Instance.modo == Opciones.modoDeUso.Ortografia ||
Opciones.Instance.modo == Opciones.modoDeUso.PreguntaRespuesta || Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Espanol ||
Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Ingles || Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Italiano ||
Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Portugues)
Opciones.Instance.modo == Opciones.modoDeUso.PreguntaRespuesta || Opciones.Instance.modo == Opciones.modoDeUso.ClaudeWebSearch ||
Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Espanol || Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Ingles ||
Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Italiano || Opciones.Instance.modo == Opciones.modoDeUso.Traducir_a_Portugues)
{
if (Opciones.Instance.FuncionesOpcionales == Opciones.funcionesOpcionales.MostrarPopUp)
{

View File

@ -47,6 +47,7 @@ namespace GTPCorrgir
new MenuOption { DisplayName = "Revisar ortografía", Value = Opciones.modoDeUso.Ortografia },
new MenuOption { DisplayName = "Chat", Value = Opciones.modoDeUso.Chat },
new MenuOption { DisplayName = "Pregunta-Respuesta", Value = Opciones.modoDeUso.PreguntaRespuesta },
new MenuOption { DisplayName = "Claude Web Search", Value = Opciones.modoDeUso.ClaudeWebSearch },
new MenuOption { DisplayName = "Traducir a inglés", Value = Opciones.modoDeUso.Traducir_a_Ingles },
new MenuOption { DisplayName = "Traducir a italiano", Value = Opciones.modoDeUso.Traducir_a_Italiano },
new MenuOption { DisplayName = "Traducir a español", Value = Opciones.modoDeUso.Traducir_a_Espanol },
@ -167,6 +168,12 @@ namespace GTPCorrgir
var modeValue = (Opciones.modoDeUso)selectedMode.Value;
var llmValue = (Opciones.LLM_a_Usar)selectedLLM.Value;
// Si se selecciona Claude Web Search, forzar el uso de Claude
if (modeValue == Opciones.modoDeUso.ClaudeWebSearch)
{
llmValue = Opciones.LLM_a_Usar.Claude;
}
Opciones.Instance.modo = modeValue;
Opciones.Instance.LLM = llmValue;

View File

@ -38,6 +38,7 @@ namespace GTPCorrgir
Traducir_a_Portugues,
OCRaTexto,
Menu,
ClaudeWebSearch,
}
public Dictionary<LLM_a_Usar, string> nombreLLM = new Dictionary<LLM_a_Usar, string>
@ -130,6 +131,8 @@ namespace GTPCorrgir
Opciones.Instance.modo = Opciones.modoDeUso.Traducir_a_Portugues;
if (arg.Contains("OCRaTexto"))
Opciones.Instance.modo = Opciones.modoDeUso.OCRaTexto;
if (arg.Contains("ClaudeWebSearch"))
Opciones.Instance.modo = Opciones.modoDeUso.ClaudeWebSearch;
if (arg.Contains("AutoCopy"))
Opciones.Instance.AutoCopy = true;
if (arg.Contains("Menu"))

344
gtpask.cs
View File

@ -176,16 +176,43 @@ namespace GTPCorrgir
return true;
}
string detectedLanguageCode = _languageDetector.Detect(TextoACorregir);
IdiomaDetectado = _languageMap.GetValueOrDefault(detectedLanguageCode, "Desconocido");
// 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 IdiomaDetectado != "Desconocido";
return true;
}
catch (Exception ex)
{
Log.Log($"Error al detectar idioma: {ex.Message}");
return false;
// En caso de error, usar español como fallback
IdiomaDetectado = _languageMap["es"];
Log.Log($"Error en detección, usando fallback: {IdiomaDetectado}");
return true;
}
}
@ -227,7 +254,17 @@ namespace GTPCorrgir
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:
@ -248,13 +285,14 @@ namespace GTPCorrgir
default:
throw new ArgumentException("LLM no válido");
}
}
if (string.IsNullOrEmpty(respuestaLLM))
{
throw new ApplicationException("No se recibió respuesta del LLM");
}
ProcesarRespuestaLLM(respuestaLLM);
ProcesarRespuestaLLM(respuestaLLM, respuestaCompleta);
}
catch (Exception ex)
{
@ -263,26 +301,141 @@ namespace GTPCorrgir
}
}
private void ProcesarRespuestaLLM(string respuestaLLM)
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)
{
throw new ApplicationException("Error al extraer el texto corregido de la respuesta JSON");
// 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{respuestaExtraida}";
TextoCorregido = $"{TextoACorregir}\n{respuestaExtraidaNormal}";
}
else
{
TextoCorregido = respuestaExtraida;
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 "";
}
}
@ -336,6 +489,17 @@ namespace GTPCorrgir
};
}
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
{
@ -348,6 +512,9 @@ namespace GTPCorrgir
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}\"",
@ -503,6 +670,103 @@ namespace GTPCorrgir
}
}
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
@ -519,7 +783,15 @@ namespace GTPCorrgir
using var response = await _httpClient.PostAsync(endpoint, content);
var responseContent = await response.Content.ReadAsStringAsync();
Log.Log($"Respuesta recibida: {responseContent}");
// 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)
{
@ -575,6 +847,58 @@ namespace GTPCorrgir
}
}
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);