ljsysfurry commited on
Commit
3b1cd72
·
verified ·
1 Parent(s): 5f93b79

Upload agentframe/embed/provider.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. agentframe/embed/provider.py +112 -0
agentframe/embed/provider.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Embedding Provider: 文本 → 向量 (可插拔)"""
2
+ import hashlib
3
+ import numpy as np
4
+ from abc import ABC, abstractmethod
5
+
6
+
7
+ class EmbeddingProvider(ABC):
8
+ """文本嵌入抽象接口"""
9
+
10
+ @abstractmethod
11
+ def embed(self, text: str) -> np.ndarray:
12
+ """文本 → 向量"""
13
+ raise NotImplementedError
14
+
15
+ @abstractmethod
16
+ def dim(self) -> int:
17
+ raise NotImplementedError
18
+
19
+
20
+ class HashEmbedding(EmbeddingProvider):
21
+ """
22
+ 本地哈希嵌入 (零依赖, 默认实现)
23
+ =================================
24
+ 基于字符 n-gram 特征哈希: 确定性、可复现、无语义但可区分相似文本.
25
+ 生产环境可替换为真实 embedding API (见 ApiEmbedding).
26
+
27
+ 特点:
28
+ - 相同文本 → 相同向量
29
+ - 相似文本 (共享 n-gram) → 相似向量
30
+ - 维度固定 (默认 576, 与 MLA latent 对齐)
31
+ """
32
+
33
+ def __init__(self, dim: int = 576, ngram: int = 3, seed: int = 42):
34
+ self._dim = dim
35
+ self.ngram = ngram
36
+ rng = np.random.default_rng(seed)
37
+ # 每维一个随机投影方向, 用于特征哈希
38
+ self._proj = rng.normal(0, 1, (dim,)).astype(np.float32)
39
+
40
+ def dim(self) -> int:
41
+ return self._dim
42
+
43
+ def _ngrams(self, text: str) -> list:
44
+ """提取字符 n-gram (含 unicode 感知)"""
45
+ if not text:
46
+ return []
47
+ grams = []
48
+ for i in range(len(text) - self.ngram + 1):
49
+ grams.append(text[i:i + self.ngram])
50
+ return grams
51
+
52
+ def embed(self, text: str) -> np.ndarray:
53
+ vec = np.zeros(self._dim, dtype=np.float32)
54
+ grams = self._ngrams(text)
55
+ if not grams:
56
+ return vec
57
+ for g in grams:
58
+ # 特征哈希: 确定性映射到维度 + 符号
59
+ h = int(hashlib.md5(g.encode("utf-8")).hexdigest(), 16)
60
+ idx = h % self._dim
61
+ sign = 1.0 if (h >> 32) % 2 == 0 else -1.0
62
+ vec[idx] += sign
63
+ # 归一化 (单位向量, 余弦相似度)
64
+ norm = np.linalg.norm(vec)
65
+ if norm > 1e-8:
66
+ vec /= norm
67
+ return vec
68
+
69
+
70
+ class ApiEmbedding(EmbeddingProvider):
71
+ """
72
+ 远程 API 嵌入 (OpenAI 兼容 /embeddings 端点)
73
+ 需配置: base_url + api_key + model
74
+ """
75
+
76
+ def __init__(self, base_url: str, api_key: str,
77
+ model: str = "text-embedding-3-small", dim: int = 1536):
78
+ self.base_url = base_url.rstrip("/")
79
+ self.api_key = api_key
80
+ self.model = model
81
+ self._dim = dim
82
+
83
+ def dim(self) -> int:
84
+ return self._dim
85
+
86
+ def embed(self, text: str) -> np.ndarray:
87
+ import json
88
+ import urllib.request
89
+ data = json.dumps({
90
+ "model": self.model,
91
+ "input": text,
92
+ }).encode()
93
+ req = urllib.request.Request(
94
+ f"{self.base_url}/embeddings", data=data, headers={
95
+ "Content-Type": "application/json",
96
+ "Authorization": f"Bearer {self.api_key}",
97
+ })
98
+ try:
99
+ with urllib.request.urlopen(req, timeout=30) as resp:
100
+ r = json.loads(resp.read())
101
+ emb = r["data"][0]["embedding"]
102
+ return np.asarray(emb, dtype=np.float32)
103
+ except Exception:
104
+ # API 失败时回退哈希嵌入
105
+ return HashEmbedding(self._dim).embed(text)
106
+
107
+
108
+ def create_embedding(kind: str = "hash", **kwargs) -> EmbeddingProvider:
109
+ """工厂: hash | api"""
110
+ if kind == "api":
111
+ return ApiEmbedding(**kwargs)
112
+ return HashEmbedding(**kwargs)