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 }