added error for roboflow
Some checks failed
golangci-lint / lint (push) Failing after 24s
Run Go Tests / build (push) Failing after 0s
build / Build (push) Successful in 1m47s
Build and Push Docker Image / Build and push image (push) Successful in 2m29s

This commit is contained in:
2025-05-06 19:18:16 -07:00
parent c570cd506a
commit cb14535c1f
7 changed files with 590 additions and 212 deletions

View File

@@ -1,55 +1,18 @@
mode: set
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:15.13,26.12 7 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:26.12,29.17 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:29.17,31.4 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:35.2,41.45 5 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:41.45,43.3 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:45.2,45.35 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:22.31,23.48 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:23.48,25.3 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:28.28,37.2 3 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:39.38,41.21 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:41.21,43.3 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:43.8,44.34 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:44.34,47.4 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:48.3,48.51 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:50.2,50.13 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:53.81,62.2 3 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:26.26,30.2 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:32.44,62.55 13 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:62.55,63.46 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:63.46,66.74 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:66.74,68.5 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:74.2,74.11 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:78.39,80.2 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:33.53,35.2 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:37.67,58.19 5 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:58.19,60.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:62.5,63.19 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:63.19,65.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:67.5,72.19 5 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:72.19,74.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:75.5,78.72 3 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:78.72,80.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:82.5,82.35 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:82.35,84.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:86.5,86.52 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:25.57,27.2 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:29.77,32.19 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:32.19,34.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:35.5,43.19 5 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:43.19,45.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:48.5,48.49 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:48.49,50.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:53.5,58.19 4 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:58.19,60.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:62.5,67.19 4 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:67.19,69.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:70.5,74.72 3 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:74.72,76.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:79.5,80.53 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:80.53,82.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/robowflow.go:84.5,84.24 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:19.25,23.2 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:25.36,27.16 2 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:27.16,29.3 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:30.2,30.11 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:33.91,37.45 3 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:37.45,39.3 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:41.2,41.54 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:41.54,43.3 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:45.2,47.16 3 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:50.100,51.46 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:51.46,52.72 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:52.72,56.24 3 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:56.24,59.5 2 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:61.4,61.24 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:25.13,31.62 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:31.62,32.31 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/application.go:32.31,33.27 1 0
@@ -68,23 +31,67 @@ gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:32.4
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:36.4,36.80 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:36.80,38.5 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger/request.go:38.10,40.5 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:19.25,23.2 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:25.36,27.16 2 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:27.16,29.3 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:30.2,30.11 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:33.91,37.45 3 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:37.45,39.3 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:41.2,41.54 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:41.54,43.3 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:45.2,47.16 3 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:50.100,51.46 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:51.46,52.72 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:52.72,56.24 3 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:56.24,59.5 2 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/limiter/limiter.go:61.4,61.24 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:13.43,16.2 2 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:18.38,19.63 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:19.63,21.3 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:22.2,22.12 1 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:25.76,30.2 4 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response/response.go:32.100,35.2 2 1
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:15.13,26.12 7 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:26.12,29.17 3 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:29.17,31.4 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:35.2,41.45 5 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:41.45,43.3 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/cmd/junk2jive/main.go:45.2,45.35 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:38.28,42.2 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:45.65,49.61 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:49.61,53.3 3 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:56.2,56.64 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:56.64,59.3 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:62.2,63.16 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:63.16,66.3 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:68.2,72.16 3 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:72.16,75.3 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:78.2,79.38 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:83.77,90.16 4 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:90.16,92.3 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:95.2,100.16 4 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:100.16,102.3 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:103.2,106.38 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:106.38,109.3 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:112.2,113.69 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:113.69,115.3 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:117.2,117.23 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:121.62,123.50 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:123.50,125.3 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow/roboflow.go:126.2,126.16 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:22.31,23.48 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:23.48,25.3 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:28.28,37.2 3 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:39.38,41.21 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:41.21,43.3 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:43.8,44.34 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:44.34,47.4 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:48.3,48.51 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:50.2,50.13 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/config/config.go:53.81,62.2 3 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:33.53,35.2 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:37.67,58.19 5 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:58.19,60.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:62.5,63.19 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:63.19,65.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:67.5,72.19 5 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:72.19,74.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:75.5,78.72 3 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:78.72,80.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:82.5,82.35 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:82.35,84.6 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/services/openai.go:86.5,86.52 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:28.26,32.2 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:34.44,64.55 13 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:64.55,65.46 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:65.46,68.74 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:68.74,70.5 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:72.4,73.58 2 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:76.2,76.11 1 0
gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/router/router.go:80.39,82.2 1 0

View File

@@ -7,14 +7,16 @@ import (
"io"
"net/http"
"os"
"encoding/base64"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response"
"github.com/joho/godotenv"
)
type Ollama struct {
OllamaKey string
Message string
Image string
OllamaKey string
OllamaEndpoint string
Image base64.Encoding
}
// ChatMessage represents a message in the chat completion request
@@ -36,69 +38,81 @@ type ChatCompletionRequest struct {
Files []FileInfo `json:"files,omitempty"`
}
func init() {
// Load .env file if it exists
_ = godotenv.Load()
}
func NewOllama() *Ollama {
// Get API endpoint from environment with default fallback
endpoint := os.Getenv("OLLAMA_API_ENDPOINT")
if endpoint == "" {
endpoint = "http://localhost:3000/api/chat/completions"
}
return &Ollama{
OllamaKey: os.Getenv("OLLAMA_API_KEY"),
OllamaKey: os.Getenv("OLLAMA_API_KEY"),
OllamaEndpoint: endpoint,
}
}
func (o *Ollama) SendRequest(userMessage string, fileID string) (string, error) {
// Prepare request body
reqBody := ChatCompletionRequest{
Model: "gemma3:12b",
Messages: []ChatMessage{
{Role: "user", Content: userMessage},
},
}
// Prepare request body
reqBody := ChatCompletionRequest{
Model: "gemma3:12b",
Messages: []ChatMessage{
{Role: "user", Content: userMessage},
},
}
// Add file if fileID is provided
if fileID != "" {
reqBody.Files = []FileInfo{
{Type: "file", ID: fileID},
}
}
// Add file if fileID is provided
if fileID != "" {
reqBody.Files = []FileInfo{
{Type: "file", ID: fileID},
}
}
// Marshal the request body to JSON
jsonBody, err := json.Marshal(reqBody)
if err != nil {
response.RespondWithError(nil, nil, http.StatusInternalServerError, "Error marshalling request", err)
return "", fmt.Errorf("error marshalling request: %w", err)
}
// Marshal the request body to JSON
jsonBody, err := json.Marshal(reqBody)
if err != nil {
// Remove the call to RespondWithError and just return the error
return "", fmt.Errorf("error marshalling request: %w", err)
}
// Create the HTTP request
req, err := http.NewRequest("POST", "http://localhost:3000/api/chat/completions", bytes.NewBuffer(jsonBody))
if err != nil {
response.RespondWithError(nil, nil, http.StatusInternalServerError, "Error creating request", err)
return "", fmt.Errorf("error creating request: %w", err)
}
// Create the HTTP request
req, err := http.NewRequest("POST", o.OllamaEndpoint, bytes.NewBuffer(jsonBody))
if err != nil {
// Remove the call to RespondWithError and just return the error
return "", fmt.Errorf("error creating request: %w", err)
}
// Add headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+o.OllamaKey)
// Add headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+o.OllamaKey)
// Send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
response.RespondWithError(nil, nil, http.StatusInternalServerError, "Error sending request", err)
return "", fmt.Errorf("error sending request: %w", err)
}
defer resp.Body.Close()
// Send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
// Remove the call to RespondWithError and just return the error
return "", fmt.Errorf("error sending request: %w", err)
}
defer resp.Body.Close()
// Read the response
body, err := io.ReadAll(resp.Body)
if err != nil {
response.RespondWithError(nil, nil, http.StatusInternalServerError, "Error reading response", err)
return "", fmt.Errorf("error reading response: %w", err)
}
// Read the response
body, err := io.ReadAll(resp.Body)
if err != nil {
// Remove the call to RespondWithError and just return the error
return "", fmt.Errorf("error reading response: %w", err)
}
// Check for non-200 status code
if resp.StatusCode != http.StatusOK {
response.RespondWithError(nil, nil, http.StatusInternalServerError, "API request failed", err)
return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
// Check for non-200 status code
if resp.StatusCode != http.StatusOK {
// Remove the call to RespondWithError and just return the error
return "", fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
return string(body), nil
return string(body), nil
}
func OllamaRequest(w http.ResponseWriter, r *http.Request) {
@@ -110,7 +124,7 @@ func OllamaRequest(w http.ResponseWriter, r *http.Request) {
err := json.NewDecoder(r.Body).Decode(&requestData)
if err != nil {
response.RespondWithError(w, r, http.StatusBadRequest, "Invalid request body", err)
response.RespondWithError(w, r, http.StatusBadRequest, "Invalid request body", err)
return
}
@@ -118,13 +132,12 @@ func OllamaRequest(w http.ResponseWriter, r *http.Request) {
ollama := NewOllama()
// Send request to Ollama API
apiResponse, err := ollama.SendRequest(requestData.Message, requestData.FileID)
if err != nil {
response.RespondWithError(w, r, http.StatusInternalServerError, "Error sending request to Ollama API", err)
http.Error(w, fmt.Sprintf("Error from Ollama API: %v", err), http.StatusInternalServerError)
return
}
apiResponse, err := ollama.SendRequest(requestData.Message, requestData.FileID)
if err != nil {
response.RespondWithError(w, r, http.StatusInternalServerError, "Error sending request to Ollama API", err)
return
}
// Return response
response.RespondWithJSON(w, http.StatusOK, map[string]string{"response": apiResponse})
// Return response
response.RespondWithJSON(w, http.StatusOK, map[string]string{"response": apiResponse})
}

View File

@@ -0,0 +1,315 @@
package ollama
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response"
)
// Mock HTTP client for testing
type mockHTTPClient struct {
DoFunc func(req *http.Request) (*http.Response, error)
}
func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
return m.DoFunc(req)
}
// Helper function to patch the HTTP client for testing
func patchHTTPClient(client interface{}) func() {
originalClient := http.DefaultClient
http.DefaultClient = client.(*http.Client)
return func() {
http.DefaultClient = originalClient
}
}
// TestNewOllama tests the NewOllama function
func TestNewOllama(t *testing.T) {
// Save original env and restore after test
originalKey := os.Getenv("OLLAMA_API_KEY")
defer os.Setenv("OLLAMA_API_KEY", originalKey)
testKey := "test-api-key"
os.Setenv("OLLAMA_API_KEY", testKey)
ollama := NewOllama()
if ollama == nil {
t.Fatal("Expected NewOllama to return a non-nil value")
}
if ollama.OllamaKey != testKey {
t.Errorf("Expected OllamaKey to be %q, got %q", testKey, ollama.OllamaKey)
}
}
// TestSendRequest tests the SendRequest method
func TestSendRequest(t *testing.T) {
tests := []struct {
name string
userMessage string
fileID string
setupMock func() *mockHTTPClient
expectedResult string
expectError bool
}{
{
name: "Successful request without file",
userMessage: "Hello, world!",
fileID: "",
setupMock: func() *mockHTTPClient {
return &mockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
// Verify request has correct content and headers
if req.Header.Get("Content-Type") != "application/json" {
t.Errorf("Expected Content-Type header to be application/json")
}
if req.Header.Get("Authorization") != "Bearer test-key" {
t.Errorf("Expected Authorization header to be Bearer test-key")
}
// Check request body
body, _ := io.ReadAll(req.Body)
var reqBody ChatCompletionRequest
if err := json.Unmarshal(body, &reqBody); err != nil {
t.Errorf("Failed to unmarshal request body: %v", err)
}
if reqBody.Model != "gemma3:12b" {
t.Errorf("Expected model to be gemma3:12b, got %s", reqBody.Model)
}
if len(reqBody.Messages) != 1 || reqBody.Messages[0].Content != "Hello, world!" {
t.Errorf("Expected message content to be 'Hello, world!'")
}
if len(reqBody.Files) != 0 {
t.Errorf("Expected no files, got %d", len(reqBody.Files))
}
// Return successful response
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(`{"response": "Hello from Ollama!"}`)),
}, nil
},
}
},
expectedResult: `{"response": "Hello from Ollama!"}`,
expectError: false,
},
{
name: "Successful request with file",
userMessage: "Analyze this file",
fileID: "file123",
setupMock: func() *mockHTTPClient {
return &mockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
// Check request body to ensure file was included
body, _ := io.ReadAll(req.Body)
var reqBody ChatCompletionRequest
json.Unmarshal(body, &reqBody)
if len(reqBody.Files) != 1 || reqBody.Files[0].ID != "file123" {
t.Errorf("Expected file ID to be file123")
}
// Return successful response
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(`{"response": "File analysis complete"}`)),
}, nil
},
}
},
expectedResult: `{"response": "File analysis complete"}`,
expectError: false,
},
{
name: "HTTP error response",
userMessage: "Hello, world!",
fileID: "",
setupMock: func() *mockHTTPClient {
return &mockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(bytes.NewBufferString(`{"error": "Bad request"}`)),
}, nil
},
}
},
expectedResult: "",
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create mock client
mockClient := tc.setupMock()
// Create a custom HTTP client and replace the default
client := &http.Client{
Transport: &mockTransport{mockClient: mockClient},
}
// Initialize ollama with test key
ollama := &Ollama{OllamaKey: "test-key"}
// Mock response.RespondWithError to prevent nil pointer dereference
originalRespondWithError := response.RespondWithError
response.RespondWithError = func(w http.ResponseWriter, r *http.Request, status int, message string, err error) {}
defer func() { response.RespondWithError = originalRespondWithError }()
// Call the function
result, err := ollama.SendRequest(tc.userMessage, tc.fileID)
// Check results
if tc.expectError && err == nil {
t.Error("Expected an error but got none")
}
if !tc.expectError && err != nil {
t.Errorf("Did not expect an error but got: %v", err)
}
if !tc.expectError && result != tc.expectedResult {
t.Errorf("Expected result %q, got %q", tc.expectedResult, result)
}
})
}
}
// Custom transport that uses our mockClient
type mockTransport struct {
mockClient *mockHTTPClient
}
func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return m.mockClient.Do(req)
}
// TestOllamaRequest tests the OllamaRequest HTTP handler
func TestOllamaRequest(t *testing.T) {
tests := []struct {
name string
requestBody string
setupMock func() (*Ollama, func())
expectedStatusCode int
expectedResponse string
}{
{
name: "Valid request",
requestBody: `{"message": "Hello", "fileId": ""}`,
setupMock: func() (*Ollama, func()) {
// Create a mock Ollama that returns a successful response
mockOllama := &Ollama{}
// Save the original NewOllama function and replace it
originalNewOllama := NewOllama
NewOllama = func() *Ollama {
return mockOllama
}
// Save the original SendRequest method and replace it
originalSendRequest := mockOllama.SendRequest
mockOllama.SendRequest = func(userMessage, fileID string) (string, error) {
return `{"content": "Hello from Ollama!"}`, nil
}
// Return the mock and a cleanup function
return mockOllama, func() {
NewOllama = originalNewOllama
mockOllama.SendRequest = originalSendRequest
}
},
expectedStatusCode: http.StatusOK,
expectedResponse: `{"response":{"content": "Hello from Ollama!"}}`,
},
{
name: "Invalid JSON request",
requestBody: `{"message": "Hello"`, // Missing closing brace
setupMock: func() (*Ollama, func()) {
mockOllama := &Ollama{}
originalNewOllama := NewOllama
NewOllama = func() *Ollama {
return mockOllama
}
return mockOllama, func() {
NewOllama = originalNewOllama
}
},
expectedStatusCode: http.StatusBadRequest,
expectedResponse: "", // We don't care about the exact error message
},
{
name: "API error",
requestBody: `{"message": "Error", "fileId": ""}`,
setupMock: func() (*Ollama, func()) {
mockOllama := &Ollama{}
originalNewOllama := NewOllama
NewOllama = func() *Ollama {
return mockOllama
}
originalSendRequest := mockOllama.SendRequest
mockOllama.SendRequest = func(userMessage, fileID string) (string, error) {
return "", &httpError{message: "API error", statusCode: http.StatusInternalServerError}
}
return mockOllama, func() {
NewOllama = originalNewOllama
mockOllama.SendRequest = originalSendRequest
}
},
expectedStatusCode: http.StatusInternalServerError,
expectedResponse: "", // We don't care about the exact error message
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup mock
_, cleanup := tc.setupMock()
defer cleanup()
// Create request and response recorder
req := httptest.NewRequest("POST", "/api/ollama", strings.NewReader(tc.requestBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
// Call the handler
OllamaRequest(w, req)
// Check status code
if w.Code != tc.expectedStatusCode {
t.Errorf("Expected status code %d, got %d", tc.expectedStatusCode, w.Code)
}
// For successful responses, check the response body
if tc.expectedStatusCode == http.StatusOK {
// Remove whitespace for comparison
expectedNoSpace := strings.ReplaceAll(tc.expectedResponse, " ", "")
actualNoSpace := strings.ReplaceAll(w.Body.String(), " ", "")
actualNoSpace = strings.ReplaceAll(actualNoSpace, "\n", "")
if expectedNoSpace != actualNoSpace {
t.Errorf("Expected response %q, got %q", tc.expectedResponse, w.Body.String())
}
}
})
}
}
// Helper error type for testing
type httpError struct {
message string
statusCode int
}
func (e *httpError) Error() string {
return e.message
}

View File

@@ -0,0 +1,127 @@
package roboflow
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response"
)
// ImageRequest represents the incoming JSON request with image data
type ImageRequest struct {
Image string `json:"image"` // Base64 encoded image
}
// RoboflowResponse represents the structured response from Roboflow API
type RoboflowResponse struct {
Predictions []struct {
Class string `json:"class"`
Confidence float64 `json:"confidence"`
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
} `json:"predictions"`
}
// Service represents a Roboflow service
type Service struct {
apiKey string
}
// NewService creates a new Roboflow service instance
func NewService() *Service {
return &Service{
apiKey: os.Getenv("ROBOFLOW_API_KEY"),
}
}
// HandleImageRequest processes the JSON image request
func HandleImageRequest(w http.ResponseWriter, r *http.Request) {
// Decode the JSON request
var req ImageRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.RespondWithError(w, r, http.StatusBadRequest, "Invalid JSON request: %v", err)
http.Error(w, "Invalid JSON request: "+err.Error(), http.StatusBadRequest)
return
}
// Check if image was provided
if err := json.Unmarshal([]byte(req.Image), &req); err != nil {
response.RespondWithError(w, r, http.StatusBadRequest, "No image provided in request", err)
return
}
// Decode base64 image
imageData, err := base64.StdEncoding.DecodeString(req.Image)
if err != nil {
response.RespondWithError(w, r, http.StatusBadRequest, "Invalid base64 image: %v ", err)
return
}
service := NewService()
// Process the image with Roboflow
responser, err := service.AnalyzeImage(imageData)
if err != nil {
response.RespondWithError(w, r, http.StatusInternalServerError, "Error processing image: %v", err)
return
}
// Return JSON response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(responser)
}
// AnalyzeImage sends image bytes to Roboflow API
func (s *Service) AnalyzeImage(imageData []byte) (*RoboflowResponse, error) {
// Base64 encode the image data
base64Data := base64.StdEncoding.EncodeToString(imageData)
// Create the request
url := fmt.Sprintf("https://serverless.roboflow.com/taco-puuof/1?api_key=%s", s.apiKey)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBufferString(base64Data))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Set appropriate headers
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// Send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// Check response status
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
// Parse the response
var response RoboflowResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return &response, nil
}
// GetDetectedObjects extracts the class names from the response
func GetDetectedObjects(response *RoboflowResponse) []string {
var objects []string
for _, prediction := range response.Predictions {
objects = append(objects, prediction.Class)
}
return objects
}

View File

@@ -14,6 +14,7 @@ import (
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/logger"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/ollama"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/response"
"gitea.miguelmuniz.com/rogueking/junk2jive-server/internal/roboflow"
)
// Router encapsulates the Chi router and its dependencies
@@ -69,7 +70,7 @@ func SetupRouter(origins []string) *Router {
})
subRouter.Post("/text", ollama.OllamaRequest)
// subRouter.Post("/visual", handlers.VisualAIHandler())
subRouter.Post("/visual", roboflow.HandleImageRequest)
})
})
return &r

View File

@@ -1,85 +0,0 @@
package services
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
type RoboflowService struct {
apiKey string
}
type RoboflowResponse struct {
Predictions []struct {
Class string `json:"class"`
Confidence float64 `json:"confidence"`
} `json:"predictions"`
}
func NewRoboflowService(apiKey string) *RoboflowService {
return &RoboflowService{apiKey: apiKey}
}
func (s *RoboflowService) DetectObjects(imagePath string) ([]string, error) {
// Open the file
file, err := os.Open(imagePath)
if err != nil {
return nil, err
}
defer file.Close()
// Create a new multipart writer
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Create a form file field
part, err := writer.CreateFormFile("file", filepath.Base(imagePath))
if err != nil {
return nil, err
}
// Copy the file content to the form field
if _, err = io.Copy(part, file); err != nil {
return nil, err
}
// Close the writer to finalize the form
writer.Close()
// Create the request
url := fmt.Sprintf("https://detect.roboflow.com/taco-puuof/1?api_key=%s&confidence=60&overlap=30", s.apiKey)
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
// Send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Parse the response
var response RoboflowResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, err
}
// Extract object classes
var objects []string
for _, prediction := range response.Predictions {
objects = append(objects, prediction.Class)
}
return objects, nil
}

BIN
j2znlrcs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB