2bbf42b741
Remove Agent Desk users, roles, login sessions, tokens, and local permission persistence. Expose the backend as an embeddable ai-agent module with host-provided subject lookup and operation authorization callbacks, and complete the frontend/backend repository split.
248 lines
5.9 KiB
Go
248 lines
5.9 KiB
Go
package vectordb
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/qdrant/go-client/qdrant"
|
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
|
|
)
|
|
|
|
type QdrantProvider struct {
|
|
client *qdrant.Client
|
|
}
|
|
|
|
func NewQdrantProvider(cfg *config.QdrantVectorDBConfig) (*QdrantProvider, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("vectordb config is nil")
|
|
}
|
|
|
|
host := cfg.Host
|
|
if host == "" {
|
|
host = "localhost"
|
|
}
|
|
|
|
port := cfg.GrpcPort
|
|
if port <= 0 {
|
|
port = 6334
|
|
}
|
|
|
|
client, err := qdrant.NewClient(&qdrant.Config{
|
|
Host: host,
|
|
Port: port,
|
|
APIKey: cfg.APIKey,
|
|
UseTLS: cfg.UseTLS,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create qdrant client: %w", err)
|
|
}
|
|
|
|
return &QdrantProvider{client: client}, nil
|
|
}
|
|
|
|
func (p *QdrantProvider) Close() error {
|
|
if p.client != nil {
|
|
return p.client.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *QdrantProvider) CreateCollection(ctx context.Context, name string, dimension int) error {
|
|
err := p.client.CreateCollection(ctx, &qdrant.CreateCollection{
|
|
CollectionName: name,
|
|
VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
|
|
Size: uint64(dimension),
|
|
Distance: qdrant.Distance_Cosine,
|
|
}),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create collection %s: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *QdrantProvider) DeleteCollection(ctx context.Context, name string) error {
|
|
err := p.client.DeleteCollection(ctx, name)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete collection %s: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *QdrantProvider) GetCollection(ctx context.Context, name string) (*CollectionInfo, error) {
|
|
info, err := p.client.GetCollectionInfo(ctx, name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get collection %s: %w", name, err)
|
|
}
|
|
|
|
status := info.GetStatus().String()
|
|
pointCount := int(info.GetPointsCount())
|
|
|
|
dimension := 0
|
|
if info.Config != nil && info.Config.Params != nil {
|
|
vectorsConfig := info.Config.Params.VectorsConfig
|
|
if vectorsConfig != nil {
|
|
params := vectorsConfig.GetParams()
|
|
if params != nil {
|
|
dimension = int(params.Size)
|
|
}
|
|
}
|
|
}
|
|
|
|
return &CollectionInfo{
|
|
Name: name,
|
|
Dimension: dimension,
|
|
PointCount: pointCount,
|
|
Status: status,
|
|
}, nil
|
|
}
|
|
|
|
func (p *QdrantProvider) ListCollections(ctx context.Context) ([]string, error) {
|
|
collections, err := p.client.ListCollections(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list collections: %w", err)
|
|
}
|
|
|
|
return collections, nil
|
|
}
|
|
|
|
func (p *QdrantProvider) UpsertVectors(ctx context.Context, collectionName string, vectors []Vector) error {
|
|
if len(vectors) == 0 {
|
|
return nil
|
|
}
|
|
|
|
points := make([]*qdrant.PointStruct, 0, len(vectors))
|
|
for _, v := range vectors {
|
|
points = append(points, &qdrant.PointStruct{
|
|
Id: qdrant.NewID(v.ID),
|
|
Vectors: qdrant.NewVectors(v.Vector...),
|
|
Payload: qdrant.NewValueMap(v.Payload.ToMap()),
|
|
})
|
|
}
|
|
|
|
_, err := p.client.Upsert(ctx, &qdrant.UpsertPoints{
|
|
CollectionName: collectionName,
|
|
Points: points,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to upsert vectors to collection %s: %w", collectionName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *QdrantProvider) DeleteVectors(ctx context.Context, collectionName string, ids []string) error {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
|
|
pointIDs := make([]*qdrant.PointId, 0, len(ids))
|
|
for _, id := range ids {
|
|
pointIDs = append(pointIDs, qdrant.NewID(id))
|
|
}
|
|
|
|
_, err := p.client.Delete(ctx, &qdrant.DeletePoints{
|
|
CollectionName: collectionName,
|
|
Points: &qdrant.PointsSelector{
|
|
PointsSelectorOneOf: &qdrant.PointsSelector_Points{
|
|
Points: &qdrant.PointsIdsList{
|
|
Ids: pointIDs,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete vectors from collection %s: %w", collectionName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *QdrantProvider) Search(ctx context.Context, req *SearchRequest) ([]SearchResult, error) {
|
|
filter := p.buildFilter(req.Filter)
|
|
|
|
results, err := p.client.Query(ctx, &qdrant.QueryPoints{
|
|
CollectionName: req.CollectionName,
|
|
Query: qdrant.NewQuery(req.Vector...),
|
|
Limit: qdrant.PtrOf(uint64(req.TopK)),
|
|
ScoreThreshold: &req.ScoreThreshold,
|
|
Filter: filter,
|
|
WithPayload: qdrant.NewWithPayload(true),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to search collection %s: %w", req.CollectionName, err)
|
|
}
|
|
|
|
searchResults := make([]SearchResult, 0, len(results))
|
|
for _, r := range results {
|
|
payload := make(map[string]any)
|
|
if r.Payload != nil {
|
|
for k, v := range r.Payload {
|
|
payload[k] = p.extractPayloadValue(v)
|
|
}
|
|
}
|
|
|
|
id := ""
|
|
if r.Id != nil {
|
|
id = r.Id.GetUuid()
|
|
}
|
|
|
|
searchResults = append(searchResults, SearchResult{
|
|
ID: id,
|
|
Score: r.Score,
|
|
Payload: ChunkPayloadFromMap(payload),
|
|
})
|
|
}
|
|
|
|
return searchResults, nil
|
|
}
|
|
|
|
func (p *QdrantProvider) buildFilter(filter *SearchFilter) *qdrant.Filter {
|
|
if filter == nil {
|
|
return nil
|
|
}
|
|
|
|
must := make([]*qdrant.Condition, 0, 2)
|
|
if len(filter.KnowledgeBaseIDs) > 0 {
|
|
must = append(must, qdrant.NewMatchInts("knowledge_base_id", filter.KnowledgeBaseIDs...))
|
|
}
|
|
if len(filter.DocumentIDs) > 0 {
|
|
must = append(must, qdrant.NewMatchInts("document_id", filter.DocumentIDs...))
|
|
}
|
|
if len(must) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return &qdrant.Filter{Must: must}
|
|
}
|
|
|
|
func (p *QdrantProvider) extractPayloadValue(v *qdrant.Value) interface{} {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
|
|
switch val := v.Kind.(type) {
|
|
case *qdrant.Value_StringValue:
|
|
return val.StringValue
|
|
case *qdrant.Value_IntegerValue:
|
|
return val.IntegerValue
|
|
case *qdrant.Value_DoubleValue:
|
|
return val.DoubleValue
|
|
case *qdrant.Value_BoolValue:
|
|
return val.BoolValue
|
|
case *qdrant.Value_ListValue:
|
|
list := make([]interface{}, 0, len(val.ListValue.Values))
|
|
for _, item := range val.ListValue.Values {
|
|
list = append(list, p.extractPayloadValue(item))
|
|
}
|
|
return list
|
|
case *qdrant.Value_StructValue:
|
|
m := make(map[string]interface{})
|
|
for k, v := range val.StructValue.Fields {
|
|
m[k] = p.extractPayloadValue(v)
|
|
}
|
|
return m
|
|
default:
|
|
return nil
|
|
}
|
|
}
|