Files
soft_usite/pkg/services/ia_service.go
T
Lizandro GuarnizoandClaude Opus 5 e681f41639 feat(plantillas): subir el documento y que la IA lo devuelva como plantilla editable
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>
2026-08-17 18:39:11 -05:00

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
}