Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PineconeCategorizer implements VectorCategorizer via Pinecone query API.
|
||||
// When Embedder is set (admin AI role "vectorization" / env fallback), queries
|
||||
// send an explicit vector; otherwise Text is used (Pinecone integrated inference).
|
||||
// ASSUMPTION: when not configured, Enabled() is false and callers skip vector categorize.
|
||||
type PineconeCategorizer struct {
|
||||
APIKey string
|
||||
Host string
|
||||
Namespace string
|
||||
HTTPClient *http.Client
|
||||
Embedder Embedder
|
||||
}
|
||||
|
||||
func NewPineconeCategorizer(apiKey, host, namespace string) *PineconeCategorizer {
|
||||
return &PineconeCategorizer{
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
Host: strings.TrimRight(strings.TrimSpace(host), "/"),
|
||||
Namespace: namespace,
|
||||
HTTPClient: &http.Client{Timeout: 20 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PineconeCategorizer) Enabled() bool {
|
||||
return p != nil && p.APIKey != "" && p.Host != ""
|
||||
}
|
||||
|
||||
type pineconeQueryRequest struct {
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
TopK int `json:"topK"`
|
||||
IncludeMetadata bool `json:"includeMetadata"`
|
||||
Vector []float32 `json:"vector,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type pineconeQueryResponse struct {
|
||||
Matches []struct {
|
||||
ID string `json:"id"`
|
||||
Score float64 `json:"score"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
} `json:"matches"`
|
||||
}
|
||||
|
||||
func (p *PineconeCategorizer) SuggestCategory(ctx context.Context, companyID, productText string, candidates []string) (string, error) {
|
||||
if !p.Enabled() {
|
||||
return "", errors.New("pinecone not configured")
|
||||
}
|
||||
productText = SanitizeText(productText)
|
||||
if productText == "" {
|
||||
return "", errors.New("empty product text")
|
||||
}
|
||||
_ = companyID
|
||||
_ = candidates
|
||||
|
||||
reqBody := pineconeQueryRequest{
|
||||
Namespace: p.Namespace,
|
||||
TopK: 1,
|
||||
IncludeMetadata: true,
|
||||
}
|
||||
if p.Embedder != nil {
|
||||
vecs, err := p.Embedder.Embed(ctx, []string{productText})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(vecs) == 0 || len(vecs[0]) == 0 {
|
||||
return "", errors.New("empty embedding")
|
||||
}
|
||||
reqBody.Vector = vecs[0]
|
||||
} else {
|
||||
reqBody.Text = productText
|
||||
}
|
||||
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.Host+"/query", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Api-Key", p.APIKey)
|
||||
|
||||
res, err := p.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
raw, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if res.StatusCode >= 400 {
|
||||
return "", errors.New(TruncateError(errors.New("pinecone query failed")))
|
||||
}
|
||||
var parsed pineconeQueryResponse
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(parsed.Matches) == 0 {
|
||||
return "", errors.New("no pinecone matches")
|
||||
}
|
||||
m := parsed.Matches[0].Metadata
|
||||
if m != nil {
|
||||
if name, ok := m["category"].(string); ok && strings.TrimSpace(name) != "" {
|
||||
return SanitizeOutput(name), nil
|
||||
}
|
||||
if name, ok := m["name"].(string); ok && strings.TrimSpace(name) != "" {
|
||||
return SanitizeOutput(name), nil
|
||||
}
|
||||
}
|
||||
return SanitizeOutput(parsed.Matches[0].ID), nil
|
||||
}
|
||||
|
||||
// NoopVectorCategorizer is the default when Pinecone is unset.
|
||||
type NoopVectorCategorizer struct{}
|
||||
|
||||
func (NoopVectorCategorizer) Enabled() bool { return false }
|
||||
|
||||
func (NoopVectorCategorizer) SuggestCategory(context.Context, string, string, []string) (string, error) {
|
||||
return "", errors.New("vector categorizer disabled")
|
||||
}
|
||||
Reference in New Issue
Block a user