mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
256 lines
6.6 KiB
Go
256 lines
6.6 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// OllamaClient handles communication with Ollama API
|
|
type OllamaClient struct {
|
|
baseURL string
|
|
model string
|
|
httpClient *http.Client
|
|
timeout time.Duration
|
|
username string
|
|
password string
|
|
}
|
|
|
|
// OllamaClientConfig holds configuration for OllamaClient
|
|
type OllamaClientConfig struct {
|
|
BaseURL string
|
|
Model string
|
|
Timeout time.Duration
|
|
MaxRetries int
|
|
Username string
|
|
Password string
|
|
}
|
|
|
|
// DefaultOllamaClientConfig returns default configuration
|
|
func DefaultOllamaClientConfig() OllamaClientConfig {
|
|
return OllamaClientConfig{
|
|
BaseURL: "http://localhost:11434",
|
|
Model: "qwen2.5:7b",
|
|
Timeout: 120 * time.Second,
|
|
MaxRetries: 0, // No retries by default
|
|
}
|
|
}
|
|
|
|
// OllamaGenerateRequest represents the request payload for Ollama API
|
|
type OllamaGenerateRequest struct {
|
|
Model string `json:"model"`
|
|
Prompt string `json:"prompt"`
|
|
Stream bool `json:"stream"`
|
|
}
|
|
|
|
// OllamaGenerateResponse represents the response from Ollama API
|
|
type OllamaGenerateResponse struct {
|
|
Response string `json:"response"`
|
|
Done bool `json:"done"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// NewOllamaClient creates a new Ollama client with default configuration
|
|
func NewOllamaClient(baseURL, model string) *OllamaClient {
|
|
config := DefaultOllamaClientConfig()
|
|
if baseURL != "" {
|
|
config.BaseURL = baseURL
|
|
}
|
|
if model != "" {
|
|
config.Model = model
|
|
}
|
|
return NewOllamaClientWithConfig(config)
|
|
}
|
|
|
|
// NewOllamaClientWithAuth creates a new Ollama client with authentication
|
|
func NewOllamaClientWithAuth(baseURL, model, username, password string) *OllamaClient {
|
|
config := DefaultOllamaClientConfig()
|
|
if baseURL != "" {
|
|
config.BaseURL = baseURL
|
|
}
|
|
if model != "" {
|
|
config.Model = model
|
|
}
|
|
config.Username = username
|
|
config.Password = password
|
|
return NewOllamaClientWithConfig(config)
|
|
}
|
|
|
|
// NewOllamaClientWithConfig creates a new Ollama client with custom configuration
|
|
func NewOllamaClientWithConfig(config OllamaClientConfig) *OllamaClient {
|
|
if config.BaseURL == "" {
|
|
config.BaseURL = DefaultOllamaClientConfig().BaseURL
|
|
}
|
|
if config.Model == "" {
|
|
config.Model = DefaultOllamaClientConfig().Model
|
|
}
|
|
if config.Timeout == 0 {
|
|
config.Timeout = DefaultOllamaClientConfig().Timeout
|
|
}
|
|
|
|
return &OllamaClient{
|
|
baseURL: config.BaseURL,
|
|
model: config.Model,
|
|
timeout: config.Timeout,
|
|
username: config.Username,
|
|
password: config.Password,
|
|
httpClient: &http.Client{
|
|
Timeout: config.Timeout,
|
|
},
|
|
}
|
|
}
|
|
|
|
const (
|
|
// MaxTextLength is the maximum text length allowed for translation (50KB)
|
|
MaxTextLength = 50 * 1024
|
|
// MinTextLength is the minimum text length (1 character)
|
|
MinTextLength = 1
|
|
)
|
|
|
|
// Translate translates text from Russian to the target locale using Ollama
|
|
// It accepts a context for cancellation and timeout control
|
|
func (c *OllamaClient) Translate(ctx context.Context, text, targetLocale string) (string, error) {
|
|
// Validate input
|
|
if err := c.validateInput(text, targetLocale); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Build translation prompt based on target locale
|
|
var targetLanguage string
|
|
switch targetLocale {
|
|
case "en":
|
|
targetLanguage = "English"
|
|
case "tt":
|
|
targetLanguage = "Tatar"
|
|
default:
|
|
return "", fmt.Errorf("unsupported target locale: %s. Supported: en, tt", targetLocale)
|
|
}
|
|
|
|
// Create a clear translation prompt
|
|
prompt := fmt.Sprintf(`Translate the following Russian text to %s. Return only the translation, without any explanations or additional text.
|
|
|
|
Russian text: %s
|
|
|
|
Translation:`, targetLanguage, text)
|
|
|
|
// Prepare request
|
|
reqBody := OllamaGenerateRequest{
|
|
Model: c.model,
|
|
Prompt: prompt,
|
|
Stream: false,
|
|
}
|
|
|
|
jsonData, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
// Make HTTP request with context
|
|
apiURL := fmt.Sprintf("%s/api/generate", c.baseURL)
|
|
req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Add basic authentication if credentials are provided
|
|
if c.username != "" && c.password != "" {
|
|
req.SetBasicAuth(c.username, c.password)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
// Check if error is due to context cancellation/timeout
|
|
if ctx.Err() != nil {
|
|
return "", fmt.Errorf("request cancelled or timed out: %w", ctx.Err())
|
|
}
|
|
return "", fmt.Errorf("failed to make request to Ollama: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
|
return "", fmt.Errorf("Ollama API returned status %d: %s", resp.StatusCode, string(bodyBytes))
|
|
}
|
|
|
|
// Parse response
|
|
var ollamaResp OllamaGenerateResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
|
|
return "", fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
if ollamaResp.Error != "" {
|
|
return "", fmt.Errorf("Ollama API error: %s", ollamaResp.Error)
|
|
}
|
|
|
|
if ollamaResp.Response == "" {
|
|
return "", fmt.Errorf("empty response from Ollama")
|
|
}
|
|
|
|
// Clean up the response (remove any extra whitespace)
|
|
translation := strings.TrimSpace(ollamaResp.Response)
|
|
|
|
return translation, nil
|
|
}
|
|
|
|
// SetModel changes the model used for translation
|
|
func (c *OllamaClient) SetModel(model string) error {
|
|
if model == "" {
|
|
return fmt.Errorf("model cannot be empty")
|
|
}
|
|
c.model = model
|
|
return nil
|
|
}
|
|
|
|
// SetBaseURL changes the base URL for Ollama API
|
|
func (c *OllamaClient) SetBaseURL(baseURL string) error {
|
|
if baseURL == "" {
|
|
return fmt.Errorf("base URL cannot be empty")
|
|
}
|
|
// Validate URL format
|
|
_, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid base URL format: %w", err)
|
|
}
|
|
c.baseURL = baseURL
|
|
return nil
|
|
}
|
|
|
|
// SetTimeout changes the HTTP client timeout
|
|
func (c *OllamaClient) SetTimeout(timeout time.Duration) {
|
|
if timeout <= 0 {
|
|
timeout = DefaultOllamaClientConfig().Timeout
|
|
}
|
|
c.timeout = timeout
|
|
c.httpClient.Timeout = timeout
|
|
}
|
|
|
|
// validateInput validates the input parameters
|
|
func (c *OllamaClient) validateInput(text, targetLocale string) error {
|
|
if text == "" {
|
|
return fmt.Errorf("text cannot be empty")
|
|
}
|
|
|
|
textLen := len([]rune(text)) // Count runes, not bytes
|
|
if textLen < MinTextLength {
|
|
return fmt.Errorf("text is too short (minimum %d characters)", MinTextLength)
|
|
}
|
|
if len(text) > MaxTextLength {
|
|
return fmt.Errorf("text is too long (maximum %d bytes, got %d)", MaxTextLength, len(text))
|
|
}
|
|
|
|
if targetLocale != "en" && targetLocale != "tt" {
|
|
return fmt.Errorf("unsupported target locale: %s. Supported: en, tt", targetLocale)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|