Files
HomeAgent/internal/memory/vector/provider_adapter.go
JianFeeeee 5aaae93367 feat(healthcheck): 内核状态快照报出内核版本号与 ONNX 模型启用状态
healthcheck_kernel 此前没有任何「ONNX 模型是否在用」的信息,只报「向量可用/不可用」,
分不清「统一多模态空间已加载」与「退回到词嵌入/TF-IDF 路径」;人格卡要求
「版本以运行时快照为准」,也缺一个可查字段(build.version 早就在,但没人知道)。

- KernelStatus 新增 onnx 段:enabled / provider / dim / fingerprint / modalities / reason。
  判据取 Loaded()(provider 真正打开且元数据合法),**不是**「配置里写了 provider」
  —— 后者在模型缺失 / 运行时缺失时也为真,报出去就是假绿。
- 未启用时 reason 给**具体原因**:未配置(说明会走回退路径)/ 打开失败的具体错误。
  homed 把「配置的 provider 名」与「打开失败原因」透传给 Agent,仅供状态报告。
- ProviderAdapter 新增 Modalities()(可选能力,按接口断言取用,不改公开契约)。
- healthcheck_kernel 的工具描述同步说明它回答这两件事。

**顺带修一个真实 panic**:collectKernelStatus 的 knowledge 是**接口**参数,
(*knowledge.Store)(nil) 塞进接口后 `ks != nil` 仍为真 → 调 List() 直接 panic,
而 healthcheck_kernel 正是走这条路径(panic 发生在工具 goroutine 里)。
GetKernelStatus 改为先按具体指针判空、再赋给接口;并加刻画测试钉住这个成因
(一旦不再 panic 说明参数形状已变,守卫与该测试应同步删除)。

验证:单测 4 例(已启用 / 打开失败 / 未配置 / 未加载)+ 刻画测试;
隔离实例 E2E 7/7:正例 provider=chineseclip → enabled=true、dim=512、模态 2;
反例 provider=nonexistent → enabled=false 且 reason 含具体错误与 provider 名,
真实对话仍通。
2026-09-12 14:56:25 +08:00

96 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package vector
import (
"context"
"sync"
"gitcode.com/JianFeeeee/HomeAgent/pkg/embedding"
)
// ProviderAdapter translates the public model-neutral embedding.Provider SPI
// to the small internal interface used by the existing memory consumers.
// Model selection, media decoding, preprocessing, and runtime details remain
// entirely inside the selected provider.
type ProviderAdapter struct {
provider embedding.Provider
info embedding.Info
mu sync.RWMutex
closed bool
}
// AdaptProvider validates and wraps a public provider for internal memory use.
func AdaptProvider(provider embedding.Provider) (*ProviderAdapter, error) {
info := provider.Info()
if err := embedding.ValidateInfo(info); err != nil {
return nil, err
}
return &ProviderAdapter{provider: provider, info: info}, nil
}
func (a *ProviderAdapter) VectorizeDense(text string) ([]float64, error) {
return a.embed(embedding.Input{
Modality: embedding.ModalityText,
Purpose: embedding.PurposeQuery,
Text: text,
})
}
func (a *ProviderAdapter) EmbedImageDense(data []byte, mime string) ([]float64, error) {
return a.embed(embedding.Input{
Modality: embedding.ModalityImage,
Purpose: embedding.PurposeDocument,
Data: data,
MIME: mime,
})
}
func (a *ProviderAdapter) embed(input embedding.Input) ([]float64, error) {
a.mu.RLock()
closed := a.closed
a.mu.RUnlock()
if closed {
return nil, context.Canceled
}
vec, err := a.provider.Embed(context.Background(), input)
if err != nil {
return nil, err
}
if err := embedding.ValidateVector(vec, a.info.Dimension); err != nil {
return nil, err
}
return vec, nil
}
func (a *ProviderAdapter) Fingerprint() string { return a.info.Fingerprint }
func (a *ProviderAdapter) Dim() int { return a.info.Dimension }
// Modalities 报告该空间支持的输入模态text/image/...)。
//
// 模态是**可选能力**MultimodalEmbedder 契约里没有它,状态查询按接口断言取用,
// 所以这里既不改公开接口,也不影响其它实现(核心也不硬编码任何模型名)。
func (a *ProviderAdapter) Modalities() []string {
out := make([]string, 0, len(a.info.Modalities))
for _, m := range a.info.Modalities {
out = append(out, string(m))
}
return out
}
func (a *ProviderAdapter) Loaded() bool {
a.mu.RLock()
defer a.mu.RUnlock()
return !a.closed
}
func (a *ProviderAdapter) Close() {
a.mu.Lock()
if a.closed {
a.mu.Unlock()
return
}
a.closed = true
a.mu.Unlock()
a.provider.Close()
}