mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 01:18:08 +00:00
问题:cmd/homed 里 `case "onnx": qwen.New(modelDir)` 把模型适配写进了核心, `type=onnx` 名义上是格式、实际写死了一个模型家族;2117 行 Qwen 专属代码 (BPE、chat template、M-RoPE、Vision_gN 命名)住在内核树里,还带着一对 `//go:build onnxruntime` 的 stub。加任何新模型都要改内核。 现在核心只认一个模型无关的公共契约(pkg/embedding): - 输入是不透明的 Data+MIME,解码/预处理/时序分组全归 provider - 能力是数据(Info.Modalities),不是接口方法——新增模态无需改核心接口 - 不支持的模态返回 embedding.ErrUnsupportedModality(可 errors.Is 识别) - 按名字注册,重复注册 panic;Options 是 provider 私有命名空间,核心不解释 改动: - 新增 pkg/embedding:Modality/Purpose/Input/Info/Provider/Config + 注册表 (Open 校验 Info,ValidateVector 在入库前拦下维度错与非有限值) - providers/qwen3vl:Qwen 实现整体移出内核(git mv),实现公共 SPI 并自注册 - internal/memory/vector:新增 ProviderAdapter(公共 SPI → 内部小接口); ErrModalityUnsupported 改为公共哨兵别名;删除 VideoEmbedder 可选接口 (那正是「核心为每个新模态长方法」的坏味道) - http embedder 也变成普通 provider(注册名 http) - cmd/homed:删除 qwen import 与 onnx/http 分支,改为按 provider 名打开 + 透传 options.*;provider 打开失败只警告并禁用多模态检索,不影响启动 - config:multimodal_space.type/onnx./http.* → provider + options.* - 删除 internal/memory/qwen(整体搬迁) 测试: - pkg/embedding:注册表隔离/未知名字/非法 Info 自动关闭/ValidateVector - vector:适配器原样透传字节与 MIME、维度错被拦、Close 幂等且停止使用、 两个哨兵 errors.Is 互通 - providers/qwen3vl:新增公共 SPI 全链路集成测试(Open→Info→Embed→ 未知模态哨兵),并明确断言 Info 不声明 video 已知未完成(不得当作已验证): - 视频冻结回归 TestEmbedderVideoMatchesONNXReference **显式跳过**:Go 侧 video 模板缺少 processor 按时间组插入的字面时间戳文本 (<0.0 seconds>/<1.0 seconds>),同一输入 Python seq=1190(1152+38)、 Go 只有 22 个文本 token。时间戳也占 M-RoPE 位置,故现有 M-RoPE 自洽断言 通过不能证明与官方实现一致。修复属 provider 内部工作。 - 视觉侧三档已导出并逐档校验通过(cos 1.000000119/1.000000119/1.000000000) 验证:go build ./... ;go vet -tags onnxruntime ./... ; go test -short ./internal/memory/... ./internal/agent/core/... ./internal/sdk/... ./pkg/... ;onnxruntime 下 providers/qwen3vl 全绿(视频为显式 skip)
173 lines
5.1 KiB
Go
173 lines
5.1 KiB
Go
// Package embedding defines the public SPI for dense multimodal embedding providers.
|
|
//
|
|
// The HomeAgent core depends only on this package. Model runtimes, tokenizers,
|
|
// preprocessing, media decoding, and model-specific configuration belong in
|
|
// provider packages registered with Register.
|
|
package embedding
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Modality identifies the semantic kind of an embedding input. Providers may
|
|
// support additional modality strings; the constants are the common values.
|
|
type Modality string
|
|
|
|
const (
|
|
ModalityText Modality = "text"
|
|
ModalityImage Modality = "image"
|
|
ModalityAudio Modality = "audio"
|
|
ModalityVideo Modality = "video"
|
|
)
|
|
|
|
// Purpose tells a provider how the vector will be used. Providers whose model
|
|
// distinguishes query and document prompts can map this value accordingly.
|
|
type Purpose string
|
|
|
|
const (
|
|
PurposeQuery Purpose = "query"
|
|
PurposeDocument Purpose = "document"
|
|
)
|
|
|
|
// Input is the model-neutral request passed to a provider.
|
|
//
|
|
// Data is deliberately opaque to the core. MIME describes the encoding; the
|
|
// selected provider owns decoding, frame sampling, preprocessing, and all
|
|
// other model-specific interpretation. Text is used for textual inputs.
|
|
type Input struct {
|
|
Modality Modality
|
|
Purpose Purpose
|
|
Text string
|
|
Data []byte
|
|
MIME string
|
|
Metadata map[string]string
|
|
}
|
|
|
|
// Info describes one vector space. Fingerprint must change whenever vectors
|
|
// cease to be comparable with vectors produced by a previous provider build.
|
|
type Info struct {
|
|
Dimension int
|
|
Fingerprint string
|
|
Modalities []Modality
|
|
}
|
|
|
|
// Provider is the public Go extension point for a dense multimodal vector
|
|
// space. Implementations must be safe for concurrent Embed calls unless their
|
|
// factory documents otherwise and serializes internally.
|
|
type Provider interface {
|
|
Embed(context.Context, Input) ([]float64, error)
|
|
Info() Info
|
|
Close()
|
|
}
|
|
|
|
// Config contains provider-owned options. The core does not interpret option
|
|
// names or values; it only passes core.memory.multimodal_space.options.*
|
|
// through after stripping the prefix.
|
|
type Config struct {
|
|
Options map[string]string
|
|
}
|
|
|
|
// Factory constructs a provider instance.
|
|
type Factory func(Config) (Provider, error)
|
|
|
|
var (
|
|
// ErrUnsupportedModality means this vector space has no native encoder for
|
|
// the requested modality. Callers must not substitute another model's vector.
|
|
ErrUnsupportedModality = errors.New("embedding: unsupported modality")
|
|
|
|
registryMu sync.RWMutex
|
|
registry = make(map[string]Factory)
|
|
)
|
|
|
|
// Register makes a provider factory available under name. It is normally
|
|
// called from a provider package's init function. Duplicate names panic so a
|
|
// build cannot silently select whichever package initialized last.
|
|
func Register(name string, factory Factory) {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
panic("embedding: register empty provider name")
|
|
}
|
|
if factory == nil {
|
|
panic("embedding: register nil factory for " + name)
|
|
}
|
|
registryMu.Lock()
|
|
defer registryMu.Unlock()
|
|
if _, exists := registry[name]; exists {
|
|
panic("embedding: provider already registered: " + name)
|
|
}
|
|
registry[name] = factory
|
|
}
|
|
|
|
// Open constructs a registered provider and validates its vector-space identity.
|
|
func Open(name string, cfg Config) (Provider, error) {
|
|
name = strings.TrimSpace(name)
|
|
registryMu.RLock()
|
|
factory := registry[name]
|
|
registryMu.RUnlock()
|
|
if factory == nil {
|
|
return nil, fmt.Errorf("embedding: unknown provider %q (available: %s)", name, strings.Join(Names(), ", "))
|
|
}
|
|
provider, err := factory(cloneConfig(cfg))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("embedding: open provider %q: %w", name, err)
|
|
}
|
|
if provider == nil {
|
|
return nil, fmt.Errorf("embedding: provider %q returned nil", name)
|
|
}
|
|
if err := ValidateInfo(provider.Info()); err != nil {
|
|
provider.Close()
|
|
return nil, fmt.Errorf("embedding: provider %q: %w", name, err)
|
|
}
|
|
return provider, nil
|
|
}
|
|
|
|
// Names returns registered provider names in deterministic order.
|
|
func Names() []string {
|
|
registryMu.RLock()
|
|
defer registryMu.RUnlock()
|
|
names := make([]string, 0, len(registry))
|
|
for name := range registry {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
// ValidateInfo checks the stable identity required by vector persistence.
|
|
func ValidateInfo(info Info) error {
|
|
if info.Dimension <= 0 {
|
|
return fmt.Errorf("invalid dimension %d", info.Dimension)
|
|
}
|
|
if strings.TrimSpace(info.Fingerprint) == "" {
|
|
return errors.New("empty fingerprint")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateVector rejects malformed provider output before it reaches storage.
|
|
func ValidateVector(vec []float64, dimension int) error {
|
|
if len(vec) != dimension {
|
|
return fmt.Errorf("embedding: vector dimension %d, want %d", len(vec), dimension)
|
|
}
|
|
for i, value := range vec {
|
|
if math.IsNaN(value) || math.IsInf(value, 0) {
|
|
return fmt.Errorf("embedding: vector value %d is not finite", i)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cloneConfig(cfg Config) Config {
|
|
out := Config{Options: make(map[string]string, len(cfg.Options))}
|
|
for key, value := range cfg.Options {
|
|
out.Options[key] = value
|
|
}
|
|
return out
|
|
}
|