Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions components/embedding/jina/embedding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package jina

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"

"github.com/cloudwego/eino/components"
"github.com/cloudwego/eino/components/embedding"
)

// JinaEmbedder implements the eino Embedder interface for Jina AI
type JinaEmbedder struct {
config *JinaConfig
client *http.Client
}

// JinaConfig holds configuration for Jina embedder
type JinaConfig struct {
APIKey string `json:"api_key"`
Model string `json:"model"`
Task string `json:"task"` // e.g., "text-matching", "retrieval.query", "retrieval.passage"
BaseURL string `json:"base_url"`
Timeout time.Duration `json:"timeout"`
Dimensions *int `json:"dimensions"`
}

// JinaRequest represents the request body for Jina API
type JinaRequest struct {
Model string `json:"model"`
Task string `json:"task"`
Input []map[string]interface{} `json:"input"`
}

// JinaResponse represents the response from Jina API
type JinaResponse struct {
Object string `json:"object"`
Data []struct {
Object string `json:"object"`
Index int `json:"index"`
Embedding []float64 `json:"embedding"`
} `json:"data"`
Model string `json:"model"`
Usage struct {
TotalTokens int `json:"total_tokens"`
PromptTokens int `json:"prompt_tokens"`
} `json:"usage"`
}

// NewEmbedder creates a new Jina embedder instance
func NewEmbedder(ctx context.Context, config *JinaConfig) (*JinaEmbedder, error) {
if config.APIKey == "" {
return nil, fmt.Errorf("jina api key is required")
}

if config.Model == "" {
config.Model = "jina-embeddings-v4"
}

if config.Task == "" {
config.Task = "retrieval.passage"
}

if config.BaseURL == "" {
config.BaseURL = "https://api.jina.ai/v1"
}

if config.Timeout == 0 {
config.Timeout = 30 * time.Second
}

if config.Dimensions == nil {
dim := 2048
config.Dimensions = &dim
}

client := &http.Client{
Timeout: config.Timeout,
}

return &JinaEmbedder{
config: config,
client: client,
}, nil
}

// EmbedStrings embeds multiple strings using Jina API
func (j *JinaEmbedder) EmbedStrings(ctx context.Context, texts []string, opts ...embedding.Option) ([][]float64, error) {
if len(texts) == 0 {
return nil, fmt.Errorf("no texts provided")
}

// Convert texts to Jina API format
input := make([]map[string]interface{}, len(texts))
for i, text := range texts {
input[i] = map[string]interface{}{
"text": text,
}
}

request := JinaRequest{
Model: j.config.Model,
Task: j.config.Task,
Input: input,
}

requestBody, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, "POST", j.config.BaseURL+"/embeddings", bytes.NewBuffer(requestBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+j.config.APIKey)

resp, err := j.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("jina api error (status %d): %s", resp.StatusCode, string(body))
}

var jinaResp JinaResponse
if err := json.NewDecoder(resp.Body).Decode(&jinaResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

if len(jinaResp.Data) != len(texts) {
return nil, fmt.Errorf("response data length (%d) doesn't match input length (%d)", len(jinaResp.Data), len(texts))
}

result := make([][]float64, len(texts))
for i, data := range jinaResp.Data {
result[i] = data.Embedding
}

return result, nil
}

// EmbedString embeds a single string
func (j *JinaEmbedder) EmbedString(ctx context.Context, text string, opts ...embedding.Option) ([]float64, error) {
results, err := j.EmbedStrings(ctx, []string{text})
if err != nil {
return nil, err
}
return results[0], nil
}

// GetType returns the component type
func (j *JinaEmbedder) GetType() components.Component {
return components.ComponentOfEmbedding
}

// GetDimension returns the embedding dimension (if known)
func (j *JinaEmbedder) GetDimension() int {
if j.config.Dimensions != nil {
return *j.config.Dimensions
}
// Default dimension for jina-embeddings-v4
return 2048
}

// Close cleans up resources (no-op for HTTP client)
func (j *JinaEmbedder) Close() error {
return nil
}
26 changes: 26 additions & 0 deletions components/embedding/jina/examples/embedding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"context"
"fmt"

"github.com/cloudwego/eino-ext/components/embedding/jina"
)

func main() {
ctx := context.Background()
embedder, err := jina.NewEmbedder(ctx, &jina.JinaConfig{
APIKey: "jina_5bbcd9***",
})
if err != nil {
fmt.Println("Jina init error:", err)
return
}
embedding, err := embedder.EmbedStrings(ctx, []string{"hello world"})
if err != nil {
fmt.Println("Jina embed error:", err)
return
}
fmt.Println("Jina embedding:", embedding)
fmt.Println("Jina embedding length:", len(embedding[0]))
}
41 changes: 41 additions & 0 deletions components/embedding/jina/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module github.com/cloudwego/eino-ext/components/embedding/jina

go 1.23.0

require github.com/cloudwego/eino v0.5.3

require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eino-contrib/jsonschema v1.0.0 // indirect
github.com/getkin/kin-openapi v0.118.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.19.5 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/perimeterx/marshmallow v1.1.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
golang.org/x/arch v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
golang.org/x/sys v0.26.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading