Files
CyberStrikeAI/internal/knowledge/rerank_http.go
T
Sycun 35508eb614 feat(pi): add the Pi agent runtime with native workflow and retrieval engines
Introduce an isolated Node worker that hosts the Pi SDK while Go keeps
business state, permissions, MCP execution, evidence and delivery checks.
The worker is published per source fingerprint, streams events back to Go
and supports tool callbacks, cancellation, session resume and per-model
transport settings.

Agent dispatch, tool search, plan/task tools and migration controls are
wired on top of it: large tool catalogs load through Pi tool_search, the
conversation task board keeps its existing tools, and legacy mode/route
values still resolve. Workflow gains a native DAG with versioned JSON
checkpoints, and knowledge retrieval moves to native documents, splitter,
HTTP embeddings, vector search and reranking.
2026-11-12 17:55:33 +08:00

226 lines
5.7 KiB
Go

package knowledge
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"cyberstrike-ai/internal/config"
"go.uber.org/zap"
)
// HTTPReranker calls a hosted rerank API (DashScope or Cohere-compatible).
type HTTPReranker struct {
provider string
model string
baseURL string
apiKey string
client *http.Client
logger *zap.Logger
}
// NewHTTPReranker builds a rerank client from knowledge retrieval config; openAI supplies fallback credentials.
func NewHTTPReranker(rc *config.RerankConfig, openAI *config.OpenAIConfig, logger *zap.Logger) (*HTTPReranker, error) {
if rc == nil {
return nil, fmt.Errorf("rerank config is nil")
}
baseURL := strings.TrimSpace(rc.BaseURL)
apiKey := strings.TrimSpace(rc.APIKey)
if openAI != nil {
if baseURL == "" {
baseURL = strings.TrimSpace(openAI.BaseURL)
}
if apiKey == "" {
apiKey = strings.TrimSpace(openAI.APIKey)
}
}
if apiKey == "" {
return nil, fmt.Errorf("rerank api_key is required")
}
provider := rc.ProviderEffective(baseURL)
model := rc.ModelEffective(provider)
return &HTTPReranker{
provider: provider,
model: model,
baseURL: strings.TrimSuffix(baseURL, "/"),
apiKey: apiKey,
client: &http.Client{Timeout: 60 * time.Second},
logger: logger,
}, nil
}
func (r *HTTPReranker) Rerank(ctx context.Context, query string, docs []*Document) ([]*Document, error) {
if r == nil {
return docs, nil
}
q := strings.TrimSpace(query)
if q == "" || len(docs) == 0 {
return docs, nil
}
if len(docs) == 1 {
return docs, nil
}
texts := make([]string, 0, len(docs))
for _, d := range docs {
if d == nil {
texts = append(texts, "")
continue
}
texts = append(texts, d.Content)
}
var order []int
var err error
switch r.provider {
case "dashscope":
order, err = r.rerankDashScope(ctx, q, texts, len(docs))
default:
order, err = r.rerankCohere(ctx, q, texts, len(docs))
}
if err != nil {
return nil, err
}
out := make([]*Document, 0, len(order))
for _, idx := range order {
if idx < 0 || idx >= len(docs) || docs[idx] == nil {
continue
}
out = append(out, docs[idx])
}
if len(out) == 0 {
return docs, nil
}
return out, nil
}
func (r *HTTPReranker) rerankCohere(ctx context.Context, query string, documents []string, topN int) ([]int, error) {
url := r.cohereRerankURL()
body := map[string]any{
"model": r.model,
"query": query,
"documents": documents,
"top_n": topN,
}
raw, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+r.apiKey)
resp, err := r.client.Do(req)
if err != nil {
return nil, fmt.Errorf("rerank request: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("rerank http %d: %s", resp.StatusCode, truncateForRerankLog(string(respBody)))
}
var parsed struct {
Results []struct {
Index int `json:"index"`
} `json:"results"`
}
if err := json.Unmarshal(respBody, &parsed); err != nil {
return nil, fmt.Errorf("rerank decode: %w", err)
}
order := make([]int, 0, len(parsed.Results))
for _, row := range parsed.Results {
order = append(order, row.Index)
}
return order, nil
}
func (r *HTTPReranker) rerankDashScope(ctx context.Context, query string, documents []string, topN int) ([]int, error) {
url := r.dashscopeRerankURL()
body := map[string]any{
"model": r.model,
"input": map[string]any{
"query": query,
"documents": documents,
},
"parameters": map[string]any{
"return_documents": false,
"top_n": topN,
},
}
raw, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+r.apiKey)
resp, err := r.client.Do(req)
if err != nil {
return nil, fmt.Errorf("dashscope rerank: %w", err)
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("dashscope rerank http %d: %s", resp.StatusCode, truncateForRerankLog(string(respBody)))
}
var parsed struct {
Output struct {
Results []struct {
Index int `json:"index"`
} `json:"results"`
} `json:"output"`
}
if err := json.Unmarshal(respBody, &parsed); err != nil {
return nil, fmt.Errorf("dashscope rerank decode: %w", err)
}
order := make([]int, 0, len(parsed.Output.Results))
for _, row := range parsed.Output.Results {
order = append(order, row.Index)
}
return order, nil
}
func (r *HTTPReranker) cohereRerankURL() string {
base := r.baseURL
if base == "" {
base = "https://api.cohere.com"
}
if strings.HasSuffix(base, "/v1") {
return base + "/rerank"
}
return base + "/v1/rerank"
}
func (r *HTTPReranker) dashscopeRerankURL() string {
base := strings.TrimSpace(r.baseURL)
if base == "" {
return "https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank"
}
if strings.Contains(base, "/api/v1/services/rerank") {
return base
}
if strings.Contains(base, "dashscope.aliyuncs.com") || strings.Contains(base, "compatible-mode") {
return "https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank"
}
return strings.TrimSuffix(base, "/")
}
func truncateForRerankLog(s string) string {
s = strings.TrimSpace(s)
if len(s) > 512 {
return s[:512] + "..."
}
return s
}
var _ DocumentReranker = (*HTTPReranker)(nil)