Hasta ahora crear una plantilla era escribir HTML con variables de Go a mano. Ahora se sube el documento que ya existe (.docx, .html, .txt o una foto del papel), se lee —docx con stdlib, imágenes por OCR— y la IA lo devuelve armado como plantilla con las variables del generador puestas donde iban los datos. No se guarda solo: el HTML cae en el editor para revisarlo, y si la IA devolvió sintaxis de plantilla inválida se avisa antes de guardar. PDF queda afuera por ahora; el mensaje de error dice qué hacer en su lugar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
112 lines
3.5 KiB
Go
112 lines
3.5 KiB
Go
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.
|
|
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)
|
|
}
|
|
modelo := config.ModelName
|
|
if modelo == "" {
|
|
return "", fmt.Errorf("la configuración de IA de %q no tiene modelo definido", servicio)
|
|
}
|
|
|
|
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))
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
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
|
|
}
|