fix(plantillas): "Leer con IA" fallaba con la config de IA que ya está en producción
Escribí una segunda implementación de "llamar al proveedor" en vez de usar la que ya existe (callAI, la del bot de Telegram). Esa copia no hablaba Anthropic —se lo mandaba todo al endpoint de OpenAI— y, si la config tenía la URL base vacía (lo normal: la vista dice que es opcional), armaba una URL relativa que ni siquiera es una petición válida. Cualquiera de las dos cosas terminaba en el 502. Ahora usa callAI, que despacha por proveedor y completa la URL base. El error del proveedor queda en el log del servidor y el mensaje llega entero a la vista; si la respuesta no trae mensaje, la vista muestra el status para distinguir un fallo de la app de uno del proxy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c4b0305112
commit
ac47769c27
+20
-91
@@ -1,111 +1,40 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
)
|
||||
|
||||
// CompletarTextoIA hace una llamada simple (sin streaming, sin tools) al
|
||||
// proveedor configurado para el servicio dado y devuelve el texto de la
|
||||
// respuesta. Es la contraparte no-streaming de GeneraTextoStream, para los
|
||||
// casos en que el backend necesita el resultado completo antes de seguir.
|
||||
// CompletarTextoIA hace una llamada simple (sin tools, sin streaming) al
|
||||
// proveedor configurado para el servicio dado y devuelve el texto.
|
||||
//
|
||||
// Reusa callAI, que es el mismo despachador del bot de Telegram: sabe hablar
|
||||
// Anthropic y OpenAI-compatible, y completa la URL base cuando la config la
|
||||
// tiene vacía. Escribir una segunda implementación acá fue un error: no
|
||||
// soportaba Anthropic y armaba una URL relativa cuando faltaba la base, así
|
||||
// que fallaba con la config que ya estaba en producción.
|
||||
func CompletarTextoIA(servicio, sistema, usuario string) (string, error) {
|
||||
config, err := models.GetAiConfigForService(servicio)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no hay configuración de IA activa para %q; configurá una en /app/ai-config", servicio)
|
||||
return "", fmt.Errorf("no hay configuración de IA activa; configurá una en /app/ai-config")
|
||||
}
|
||||
modelo := config.ModelName
|
||||
if modelo == "" {
|
||||
return "", fmt.Errorf("la configuración de IA de %q no tiene modelo definido", servicio)
|
||||
if strings.TrimSpace(config.ModelName) == "" {
|
||||
return "", fmt.Errorf("la configuración de IA %q no tiene modelo definido (/app/ai-config)", config.Nombre)
|
||||
}
|
||||
|
||||
provider := strings.ToLower(config.Provider)
|
||||
clave := config.ClaveEnClaro()
|
||||
baseURL := strings.TrimRight(config.BaseURL, "/")
|
||||
|
||||
var endpoint string
|
||||
var cuerpo []byte
|
||||
if provider == "gemini" {
|
||||
endpoint = fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent?key=%s", modelo, clave)
|
||||
cuerpo, _ = json.Marshal(map[string]any{
|
||||
"contents": []map[string]any{
|
||||
{"parts": []map[string]string{{"text": sistema + "\n\n" + usuario}}},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
endpoint = strings.TrimSuffix(baseURL, "/v1") + "/v1/chat/completions"
|
||||
cuerpo, _ = json.Marshal(map[string]any{
|
||||
"model": modelo,
|
||||
"messages": []map[string]string{
|
||||
{"role": "system", "content": sistema},
|
||||
{"role": "user", "content": usuario},
|
||||
},
|
||||
"stream": false,
|
||||
})
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", endpoint, bytes.NewReader(cuerpo))
|
||||
msg, _, err := callAI(config, []agentMessage{
|
||||
{Role: "system", Content: sistema},
|
||||
{Role: "user", Content: usuario},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if provider != "gemini" && clave != "" {
|
||||
if provider == "ollama" && clave != "ollama" {
|
||||
req.SetBasicAuth("ollama", clave)
|
||||
} else if provider != "ollama" {
|
||||
req.Header.Set("Authorization", "Bearer "+clave)
|
||||
}
|
||||
return "", fmt.Errorf("%s (%s): %w", config.Nombre, config.Provider, err)
|
||||
}
|
||||
|
||||
// Generar una plantilla entera es lento; el timeout es alto a propósito.
|
||||
resp, err := (&http.Client{Timeout: 180 * time.Second}).Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no se pudo conectar al proveedor de IA: %w", err)
|
||||
texto, _ := msg.Content.(string)
|
||||
if strings.TrimSpace(texto) == "" {
|
||||
return "", fmt.Errorf("%s no devolvió texto", config.Nombre)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("el proveedor de IA respondió %d: %s", resp.StatusCode, recortar(strings.TrimSpace(string(raw)), 300))
|
||||
}
|
||||
|
||||
if provider == "gemini" {
|
||||
var out struct {
|
||||
Candidates []struct {
|
||||
Content struct {
|
||||
Parts []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"parts"`
|
||||
} `json:"content"`
|
||||
} `json:"candidates"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return "", fmt.Errorf("respuesta inesperada del proveedor: %s", recortar(strings.TrimSpace(string(raw)), 300))
|
||||
}
|
||||
if len(out.Candidates) == 0 || len(out.Candidates[0].Content.Parts) == 0 {
|
||||
return "", fmt.Errorf("el proveedor de IA no devolvió texto")
|
||||
}
|
||||
return out.Candidates[0].Content.Parts[0].Text, nil
|
||||
}
|
||||
|
||||
var out struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return "", fmt.Errorf("respuesta inesperada del proveedor: %s", recortar(strings.TrimSpace(string(raw)), 300))
|
||||
}
|
||||
if len(out.Choices) == 0 {
|
||||
return "", fmt.Errorf("el proveedor de IA no devolvió texto")
|
||||
}
|
||||
return out.Choices[0].Message.Content, nil
|
||||
return texto, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user