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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8c3ab79e88
commit
e681f41639
@@ -0,0 +1,111 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user