-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add vector service concept, and resource endpoint to handle vector se…
…arch This cannibalizes some of #23 but focusses on the read path: searching a configured vector store using the configured embedding engine.
- Loading branch information
Showing
12 changed files
with
446 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package plugin | ||
|
||
import ( | ||
"encoding/json" | ||
|
||
"github.com/grafana/grafana-plugin-sdk-go/backend" | ||
"github.com/grafana/llm/pkg/plugin/vector/embed" | ||
"github.com/grafana/llm/pkg/plugin/vector/store" | ||
) | ||
|
||
const openAIKey = "openAIKey" | ||
|
||
type Settings struct { | ||
OpenAIURL string `json:"openAIUrl"` | ||
OpenAIOrganizationID string `json:"openAIOrganizationId"` | ||
|
||
openAIKey string | ||
|
||
EmbeddingSettings embed.Settings `json:"embeddings"` | ||
VectorStoreSettings store.Settings `json:"vectorStore"` | ||
} | ||
|
||
func loadSettings(appSettings backend.AppInstanceSettings) Settings { | ||
settings := Settings{ | ||
OpenAIURL: "https://api.openai.com", | ||
} | ||
_ = json.Unmarshal(appSettings.JSONData, &settings) | ||
|
||
settings.openAIKey = appSettings.DecryptedSecureJSONData[openAIKey] | ||
settings.EmbeddingSettings.OpenAI.APIKey = settings.openAIKey | ||
return settings | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package embed | ||
|
||
import "context" | ||
|
||
type EmbedderType string | ||
|
||
const ( | ||
EmbedderOpenAI EmbedderType = "openai" | ||
) | ||
|
||
type Embedder interface { | ||
Embed(ctx context.Context, model string, text string) ([]float32, error) | ||
} | ||
|
||
type Settings struct { | ||
Type string `json:"type"` | ||
|
||
OpenAI openAISettings `json:"openai"` | ||
} | ||
|
||
// NewEmbedder creates a new embedder. | ||
func NewEmbedder(s Settings) (Embedder, error) { | ||
switch EmbedderType(s.Type) { | ||
case EmbedderOpenAI: | ||
return newOpenAIEmbedder(s.OpenAI), nil | ||
} | ||
return nil, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package embed | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/grafana/grafana-plugin-sdk-go/backend/log" | ||
) | ||
|
||
type openAISettings struct { | ||
URL string `json:"url"` | ||
APIKey string `json:"apiKey"` | ||
} | ||
|
||
type openAILLMClient struct { | ||
client *http.Client | ||
url string | ||
apiKey string | ||
} | ||
|
||
type openAIEmbeddingsRequest struct { | ||
Model string `json:"model"` | ||
Input string `json:"input"` | ||
} | ||
|
||
type openAIEmbeddingsResponse struct { | ||
Data []openAIEmbeddingData `json:"data"` | ||
} | ||
|
||
type openAIEmbeddingData struct { | ||
Embedding []float32 `json:"embedding"` | ||
} | ||
|
||
func (o *openAILLMClient) Embed(ctx context.Context, model string, payload string) ([]float32, error) { | ||
// TODO: ensure payload is under 8191 tokens, somehow. | ||
url := o.url | ||
if url == "" { | ||
url = "https://api.openai.com" | ||
} | ||
url = strings.TrimSuffix(url, "/") | ||
url = url + "/v1/embeddings" | ||
reqBody := openAIEmbeddingsRequest{ | ||
Model: model, | ||
Input: payload, | ||
} | ||
bodyJSON, err := json.Marshal(reqBody) | ||
if err != nil { | ||
return nil, fmt.Errorf("marshal request: %w", err) | ||
} | ||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyJSON)) | ||
if err != nil { | ||
return nil, fmt.Errorf("create request: %w", err) | ||
} | ||
req.Header.Set("Content-Type", "application/json") | ||
req.Header.Set("Authorization", "Bearer "+o.apiKey) | ||
resp, err := o.client.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("make request: %w", err) | ||
} | ||
defer func() { | ||
if err := resp.Body.Close(); err != nil { | ||
log.DefaultLogger.Warn("failed to close response body", "err", err) | ||
} | ||
}() | ||
if resp.StatusCode/100 != 2 { | ||
return nil, fmt.Errorf("got non-2xx status from OpenAI: %s", resp.Status) | ||
} | ||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024*2)) | ||
if err != nil { | ||
return nil, fmt.Errorf("read response body: %w", err) | ||
} | ||
var body openAIEmbeddingsResponse | ||
err = json.Unmarshal(respBody, &body) | ||
if err != nil { | ||
return nil, fmt.Errorf("unmarshal response body: %w", err) | ||
} | ||
return body.Data[0].Embedding, nil | ||
} | ||
|
||
// newOpenAIEmbedder creates a new Embedder using OpenAI's API. | ||
func newOpenAIEmbedder(settings openAISettings) Embedder { | ||
impl := openAILLMClient{ | ||
client: &http.Client{}, | ||
url: settings.URL, | ||
apiKey: settings.APIKey, | ||
} | ||
return &impl | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// package vector provides a service for searching vector embeddings. | ||
// It combines the embedding engine and the vector store. | ||
package vector | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/grafana/llm/pkg/plugin/vector/embed" | ||
"github.com/grafana/llm/pkg/plugin/vector/store" | ||
) | ||
|
||
type Collection struct { | ||
Name string `json:"name"` | ||
Dimension int `json:"dimension"` | ||
Model string `json:"model"` | ||
} | ||
|
||
type Service interface { | ||
Search(ctx context.Context, collection string, query string) ([]store.SearchResult, error) | ||
} | ||
|
||
type vectorService struct { | ||
embedder embed.Embedder | ||
store store.ReadVectorStore | ||
collectionConfig map[string]Collection | ||
} | ||
|
||
func NewService(embedSettings embed.Settings, storeSettings store.Settings) (Service, error) { | ||
em, err := embed.NewEmbedder(embedSettings) | ||
if err != nil { | ||
return nil, fmt.Errorf("new embedder: %w", err) | ||
} | ||
if em == nil { | ||
return nil, nil | ||
} | ||
st, err := store.NewReadVectorStore(storeSettings) | ||
if err != nil { | ||
return nil, fmt.Errorf("new vector store: %w", err) | ||
} | ||
if st == nil { | ||
return nil, nil | ||
} | ||
return &vectorService{ | ||
embedder: em, | ||
store: st, | ||
}, nil | ||
} | ||
|
||
func (g vectorService) Search(ctx context.Context, collection string, query string) ([]store.SearchResult, error) { | ||
// Determine which model was used to embed this collection. | ||
c := g.collectionConfig[collection] | ||
if c.Name == "" { | ||
return nil, fmt.Errorf("unknown collection %s", collection) | ||
} | ||
|
||
// Get the embedding for the search query. | ||
e, err := g.embedder.Embed(ctx, c.Model, query) | ||
if err != nil { | ||
return nil, fmt.Errorf("embed query: %w", err) | ||
} | ||
|
||
// Search the vector store for similar vectors. | ||
results, err := g.store.Search(ctx, collection, e, 10) | ||
if err != nil { | ||
return nil, fmt.Errorf("vector store search: %w", err) | ||
} | ||
|
||
return results, nil | ||
} |
Oops, something went wrong.