argo-search 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ query_rewriter.py — 查询改写引擎(v2.5 新增)
4
+
5
+ 原理:
6
+ 1. 复用 clarify.py 的歧义词库 + 品牌碰撞检测
7
+ 2. 高置信度消歧时,向查询追加领域关键词
8
+ 3. 混合语言查询时,提取英文部分作为备选改写
9
+ 4. 返回 (original, rewritten, confidence, reason)
10
+
11
+ 用法:
12
+ from query_rewriter import rewrite_query
13
+ result = rewrite_query("苹果股价")
14
+ # → rewritten="苹果股价 Apple 公司 AAPL 股票行情", confidence=0.85
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ from typing import Any
21
+
22
+
23
+ # ── 消歧改写映射 ──────────────────────────────────────────────────────────────
24
+ # 格式: (歧义词, 领域) → 追加的关键词
25
+ # 当歧义词被高置信度消歧后,将这些关键词追加到查询中
26
+
27
+ REWRITE_MAP: dict[tuple[str, str], str] = {
28
+ # 科技公司/产品
29
+ ("苹果", "finance"): "Apple 公司 AAPL 股票行情",
30
+ ("苹果", "tech"): "Apple 公司 iPhone Mac iOS",
31
+ ("Python", "tech"): "Python 编程语言 pip 库 框架",
32
+ ("Java", "tech"): "Java 编程语言 JDK Spring JVM",
33
+ ("Rust", "tech"): "Rust 编程语言 cargo crate 所有权",
34
+ ("小米", "tech"): "小米科技 手机 MIUI HyperOS",
35
+ ("小米", "auto"): "小米汽车 SU7",
36
+ ("华为", "tech"): "华为技术 鸿蒙 5G 芯片 Mate",
37
+ ("特斯拉", "auto"): "Tesla 电动汽车 Model FSD",
38
+ ("蔚来", "auto"): "蔚来汽车 NIO 换电 ET5 ES6",
39
+ ("理想", "auto"): "理想汽车 Li Auto 增程 L系列 MEGA",
40
+ ("小鹏", "auto"): "小鹏汽车 XPeng P7 G6 智驾",
41
+ ("芯片", "tech"): "半导体芯片 IC GPU 光刻机",
42
+ ("Transformer", "tech"): "Transformer 模型 AI 深度学习 attention NLP",
43
+ ("GPT", "tech"): "GPT 大语言模型 OpenAI ChatGPT GPT-4",
44
+ ("RAG", "tech"): "RAG 检索增强生成 向量 embedding 知识库",
45
+ ("Gemini", "tech"): "Google Gemini AI 大模型 多模态",
46
+ ("Claude", "tech"): "Claude Anthropic AI 助手 大模型",
47
+ ("Docker", "tech"): "Docker 容器化 镜像 compose Kubernetes 部署",
48
+ ("Go", "tech"): "Go 编程语言 Golang 并发 goroutine channel",
49
+ ("Redis", "tech"): "Redis 缓存数据库 内存 键值 持久化",
50
+ ("Swift", "tech"): "Swift 编程语言 iOS macOS SwiftUI",
51
+ ("Kotlin", "tech"): "Kotlin 编程语言 Android JVM JetBrains",
52
+ ("Cursor", "tech"): "Cursor AI 代码编辑器 VSCode Composer",
53
+ ("Notion", "tech"): "Notion 协作工具 笔记 数据库 模板",
54
+ ("飞书", "tech"): "飞书 字节跳动 协作 OKR 文档",
55
+ ("抖音", "tech"): "抖音 TikTok 短视频 直播 字节跳动",
56
+ ("微信", "tech"): "微信 WeChat 小程序 公众号 支付 腾讯",
57
+ ("Anthropic", "tech"): "Anthropic AI 公司 Claude 大模型",
58
+ ("OpenAI", "tech"): "OpenAI AI 公司 GPT ChatGPT",
59
+
60
+ # 金融
61
+ ("茅台", "finance"): "贵州茅台 600519 白酒 股票行情",
62
+ ("比特币", "finance"): "Bitcoin BTC 加密货币 区块链 挖矿",
63
+ ("A股", "finance"): "A股市场 上证 深证 创业板 科创板",
64
+ ("ETF", "finance"): "ETF 交易所交易基金 指数基金 净值",
65
+ ("量化", "finance"): "量化投资 量化交易 对冲 因子 Alpha 回测",
66
+ ("期权", "finance"): "期权 金融衍生品 认购 认沽 行权 波动率",
67
+ ("基金", "finance"): "公募基金 私募基金 净值 回撤 基金经理",
68
+ ("新能源", "auto"): "新能源汽车 电动车 充电桩 续航",
69
+ ("新能源", "energy"): "新能源 光伏 风电 储能 碳中和",
70
+ ("Swift", "finance"): "Swift 国际结算 银行 转账",
71
+
72
+ # 学术
73
+ ("Python", "nature"): "Python 蟒蛇 爬行动物 宠物",
74
+ ("特斯拉", "science"): "尼古拉·特斯拉 交流电 发明 无线电",
75
+ }
76
+
77
+ # 品牌碰撞 → 自动追加的域名限定词
78
+ BRAND_REWRITE: dict[str, str] = {
79
+ "Apple": "Apple Inc 公司",
80
+ "Amazon": "Amazon 电商 AWS",
81
+ "小米": "小米科技 公司",
82
+ "华为": "华为技术 公司",
83
+ "特斯拉": "Tesla 公司",
84
+ "字节": "字节跳动 ByteDance",
85
+ }
86
+
87
+
88
+ # ── 辅助函数 ──────────────────────────────────────────────────────────────────
89
+
90
+ _RE_CHINESE = re.compile(r"[\u4e00-\u9fff]")
91
+ _RE_ENGLISH_WORD = re.compile(r"\b[a-zA-Z]{2,}\b")
92
+
93
+
94
+ def _has_chinese(text: str) -> bool:
95
+ return bool(_RE_CHINESE.search(text))
96
+
97
+
98
+ def _has_english(text: str) -> bool:
99
+ return bool(_RE_ENGLISH_WORD.search(text))
100
+
101
+
102
+ def _extract_english_terms(text: str) -> list[str]:
103
+ """提取英文术语(用于混合语言查询的拆分改写)。"""
104
+ return [w.lower() for w in _RE_ENGLISH_WORD.findall(text) if len(w) > 2]
105
+
106
+
107
+ # ── 主改写函数 ─────────────────────────────────────────────────────────────────
108
+
109
+ def rewrite_query(query: str, min_confidence: float = 0.7) -> dict[str, Any]:
110
+ """改写查询,追加领域关键词以提升搜索质量。
111
+
112
+ Args:
113
+ query: 原始查询
114
+ min_confidence: 消歧置信度阈值,低于此值不做改写
115
+
116
+ Returns:
117
+ {
118
+ "original": str, # 原始查询
119
+ "rewritten": str | None, # 改写后的查询(无改写时为 None)
120
+ "confidence": float, # 改写置信度 (0.0-1.0)
121
+ "reason": str, # 改写原因
122
+ "type": str, # 改写类型: disambiguate / split / direct
123
+ }
124
+ """
125
+ # 延迟导入避免循环依赖
126
+ from clarify import AMBIGUOUS_TERMS, BRAND_COLLISIONS
127
+
128
+ rewritten_parts: list[str] = []
129
+ reasons: list[str] = []
130
+ total_confidence = 0.0
131
+ match_count = 0
132
+
133
+ # ── 策略 1:歧义消解改写 ──
134
+ query_lower = query.lower()
135
+ for term, info in AMBIGUOUS_TERMS.items():
136
+ # 在查询中定位歧义词(大小写不敏感)
137
+ term_lower = term.lower()
138
+ if term_lower not in query_lower and not (
139
+ term.isascii()
140
+ and re.search(r"\b" + re.escape(term) + r"\b", query, re.I)
141
+ ):
142
+ continue
143
+
144
+ best_domain = None
145
+ best_conf = 0.0
146
+ best_weight = 0.0
147
+
148
+ for meaning in info["meanings"]:
149
+ domain = meaning["domain"]
150
+ keywords = info.get("disambiguation_keywords", {}).get(domain, [])
151
+ # 大小写不敏感匹配
152
+ match_count_kw = sum(
153
+ 1 for kw in keywords if kw.lower() in query_lower
154
+ )
155
+
156
+ # 跨域强信号:金融/汽车/学术术语对任何领域都有效
157
+ strong_bonus = 0.0
158
+ is_auto_signal = False
159
+ if re.search(
160
+ r"(SU7|Model\s*[SY3X]|ET5|ES6|ES8|P7|G6|G9|L\d|MEGA|AION|X9|FSD|充电桩|续航|换电)",
161
+ query, re.I
162
+ ):
163
+ is_auto_signal = True
164
+
165
+ if re.search(
166
+ r"(股价|财报|市值|基金|ETF|净值|涨跌|K线|分红|营收|利润|ROE|研报)",
167
+ query
168
+ ):
169
+ strong_bonus = 0.25
170
+ elif is_auto_signal:
171
+ strong_bonus = 0.35
172
+ elif re.search(
173
+ r"(论文|paper|arxiv|API|SDK|框架|库|编程|代码|编译|部署|开源|repo)",
174
+ query, re.I
175
+ ):
176
+ strong_bonus = 0.2
177
+ elif domain == "energy" and re.search(
178
+ r"(光伏|风电|储能|碳中和|太阳能|风能)", query
179
+ ):
180
+ strong_bonus = 0.25
181
+
182
+ # 汽车信号:自动优先 auto 域
183
+ if is_auto_signal and domain == "auto":
184
+ strong_bonus = max(strong_bonus, 0.6)
185
+ match_count_kw += 5 # 虚拟关键词匹配,确保 auto 域胜出
186
+
187
+ weight = meaning["weight"] + match_count_kw * 0.12 + strong_bonus
188
+ # 强信号对置信度的贡献更大(0.7 vs 关键词 0.12)
189
+ conf = min(
190
+ 0.5 + match_count_kw * 0.22 + strong_bonus * 0.7, 0.95
191
+ )
192
+
193
+ if conf > best_conf or (conf == best_conf and weight > best_weight):
194
+ best_domain = domain
195
+ best_conf = conf
196
+ best_weight = weight
197
+
198
+ if best_domain and best_conf >= min_confidence:
199
+ rewrite_key = (term, best_domain)
200
+ append = REWRITE_MAP.get(rewrite_key)
201
+ if append:
202
+ rewritten_parts.append(append)
203
+ reasons.append(f"「{term}」→ {best_domain} 领域")
204
+ total_confidence += best_conf
205
+ match_count += 1
206
+
207
+ # ── 策略 2:混合语言拆分 ──
208
+ if _has_chinese(query) and _has_english(query):
209
+ eng_terms = _extract_english_terms(query)
210
+ technical = any(
211
+ t
212
+ in {
213
+ "api", "sdk", "http", "json", "xml", "css", "html", "sql",
214
+ "npm", "pip", "git", "ssh", "rest", "graphql", "grpc",
215
+ "react", "vue", "node", "python", "java", "rust", "go",
216
+ "docker", "kubernetes", "linux", "nginx", "redis",
217
+ "async", "await", "thread", "process", "async",
218
+ }
219
+ for t in eng_terms
220
+ )
221
+ if technical and match_count == 0:
222
+ appended = " ".join(eng_terms)
223
+ rewritten_parts.append(appended)
224
+ reasons.append(f"混合语言拆分:{appended}")
225
+ if not total_confidence:
226
+ total_confidence = 0.6
227
+ match_count += 1
228
+
229
+ # ── 构建结果 ──
230
+ if not rewritten_parts:
231
+ return {
232
+ "original": query,
233
+ "rewritten": None,
234
+ "confidence": 0.0,
235
+ "reason": "无需改写",
236
+ "type": "direct",
237
+ }
238
+
239
+ avg_confidence = round(total_confidence / match_count, 2)
240
+ rewritten = query + " " + " ".join(rewritten_parts)
241
+
242
+ return {
243
+ "original": query,
244
+ "rewritten": rewritten.strip(),
245
+ "confidence": avg_confidence,
246
+ "reason": ";".join(reasons),
247
+ "type": "disambiguate",
248
+ }
249
+
250
+
251
+ # ── CLI 测试 ──────────────────────────────────────────────────────────────────
252
+
253
+ if __name__ == "__main__":
254
+ import json
255
+ import sys
256
+
257
+ tests = sys.argv[1:] if len(sys.argv) > 1 else [
258
+ "苹果股价",
259
+ "Python 吞苹果 兼容吗",
260
+ "小米 su7 价格",
261
+ "特斯拉 FSD 最新进展",
262
+ "芯片 光刻机 ASML",
263
+ "茅台财报 2025",
264
+ "Transformer attention 机制论文",
265
+ "量化 基金 回测 因子",
266
+ "新能源 光伏 储能",
267
+ "Rust 编程 入门",
268
+ ]
269
+
270
+ for q in tests:
271
+ result = rewrite_query(q)
272
+ if result["rewritten"]:
273
+ print(f"原始:{q}")
274
+ print(f"改写:{result['rewritten']}")
275
+ print(f"置信度:{result['confidence']} | {result['reason']}")
276
+ else:
277
+ print(f"原始:{q} → {result['reason']}")
278
+ print()
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ quota.py — Unified Search v2 配额管理器
4
+
5
+ 增强(v2):
6
+ - 成本追踪(cost_tier × cost_per_call)
7
+ - 预算模式感知
8
+ - 配额 + 成本联合决策
9
+
10
+ 追踪各引擎 API 的配额消耗、错误率、成本,
11
+ 用于路由决策时的配额感知惩罚。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import time
18
+ import threading
19
+ from pathlib import Path
20
+ from typing import Optional
21
+
22
+ # ── 路径 ──────────────────────────────────────────────────────────────────────
23
+
24
+ SKILL_DIR = Path(__file__).parent.parent
25
+ BACKENDS_DIR = SKILL_DIR / "backends"
26
+ QUOTA_PROFILES_PATH = BACKENDS_DIR / "quota_profiles.json"
27
+ QUOTA_STATE_DIR = Path.home() / ".cache" / "unified-search"
28
+ QUOTA_STATE_PATH = QUOTA_STATE_DIR / "quota.json"
29
+
30
+
31
+ class QuotaManager:
32
+ """配额追踪与消耗速率计算(v2)。"""
33
+
34
+ def __init__(self):
35
+ self._lock = threading.Lock()
36
+ self._profiles: dict = {}
37
+ self._state: dict = {}
38
+ self._load_profiles()
39
+ self._load_state()
40
+
41
+ def _load_profiles(self) -> None:
42
+ if QUOTA_PROFILES_PATH.exists():
43
+ try:
44
+ self._profiles = json.loads(QUOTA_PROFILES_PATH.read_text())
45
+ except (json.JSONDecodeError, OSError):
46
+ self._profiles = {}
47
+
48
+ def _load_state(self) -> None:
49
+ if QUOTA_STATE_PATH.exists():
50
+ try:
51
+ self._state = json.loads(QUOTA_STATE_PATH.read_text())
52
+ except (json.JSONDecodeError, OSError):
53
+ self._state = {}
54
+
55
+ def _save_state(self) -> None:
56
+ QUOTA_STATE_DIR.mkdir(parents=True, exist_ok=True)
57
+ QUOTA_STATE_PATH.write_text(
58
+ json.dumps(self._state, ensure_ascii=False, indent=2)
59
+ )
60
+
61
+ def record(self, engine: str, success: bool = True, credits: int = 1) -> None:
62
+ """记录一次 API 调用。"""
63
+ with self._lock:
64
+ if engine not in self._state:
65
+ self._state[engine] = {
66
+ "used": 0, "limit": 0, "calls": [],
67
+ "errors": 0, "last_reset": time.time(), "total_cost": 0.0,
68
+ }
69
+ self._state[engine]["used"] += credits
70
+ self._state[engine]["calls"].append(time.time())
71
+ if not success:
72
+ self._state[engine]["errors"] += 1
73
+ # 累加成本
74
+ cost = self.get_cost_per_call(engine)
75
+ self._state[engine]["total_cost"] = self._state[engine].get("total_cost", 0.0) + cost
76
+
77
+ # 只保留最近 1 小时的时间戳
78
+ cutoff = time.time() - 3600
79
+ self._state[engine]["calls"] = [
80
+ t for t in self._state[engine]["calls"] if t > cutoff
81
+ ]
82
+ self._save_state()
83
+
84
+ def get_remaining_ratio(self, engine: str) -> float:
85
+ """获取配额剩余比例。无限配额返回 1.0。"""
86
+ profile = self._profiles.get(engine, {})
87
+ state = self._state.get(engine, {})
88
+ limit = profile.get("limit")
89
+ if limit is None:
90
+ return 1.0
91
+ used = state.get("used", 0)
92
+ period = profile.get("period", "day")
93
+ last_reset = state.get("last_reset", 0)
94
+ now = time.time()
95
+
96
+ # 按周期重置
97
+ if period == "month" and now - last_reset > 30 * 86400:
98
+ state["used"] = 0
99
+ state["last_reset"] = now
100
+ self._save_state()
101
+ used = 0
102
+ elif period == "day" and now - last_reset > 86400:
103
+ state["used"] = 0
104
+ state["last_reset"] = now
105
+ self._save_state()
106
+ used = 0
107
+ return max(0.0, (limit - used) / limit)
108
+
109
+ def get_current_rpm(self, engine: str) -> float:
110
+ """获取最近 1 分钟的调用速率。"""
111
+ state = self._state.get(engine, {})
112
+ now = time.time()
113
+ return len([t for t in state.get("calls", []) if now - t < 60])
114
+
115
+ def get_error_rate(self, engine: str) -> float:
116
+ """获取最近 1 小时的错误率。"""
117
+ state = self._state.get(engine, {})
118
+ total = len(state.get("calls", []))
119
+ if total == 0:
120
+ return 0.0
121
+ return state.get("errors", 0) / total
122
+
123
+ def is_available(self, engine: str, mode: str = "auto") -> bool:
124
+ """检查引擎是否可用(配额未耗尽且未触发限频 + 预算模式)。"""
125
+ qr = self.get_remaining_ratio(engine)
126
+ if qr <= 0:
127
+ return False
128
+
129
+ profile = self._profiles.get(engine, {})
130
+ qps = profile.get("qps")
131
+ if qps is not None:
132
+ rpm = self.get_current_rpm(engine)
133
+ if rpm >= qps * 60:
134
+ return False
135
+
136
+ # budget 模式禁用付费引擎
137
+ if mode in ("fast", "budget"):
138
+ cost_tier = profile.get("cost_tier", "free")
139
+ if cost_tier == "paid":
140
+ return False
141
+
142
+ return True
143
+
144
+ def get_cost_per_call(self, engine: str) -> float:
145
+ """获取单次调用的成本(单位:美元)。"""
146
+ profile = self._profiles.get(engine, {})
147
+ credits = profile.get("credits_per_search", 1)
148
+ cost = profile.get("cost_per_call", 0.0)
149
+ return credits * cost
150
+
151
+ def get_total_cost(self, engine: str) -> float:
152
+ """获取引擎累计成本。"""
153
+ return self._state.get(engine, {}).get("total_cost", 0.0)
154
+
155
+ def get_stats(self) -> dict:
156
+ """获取所有引擎的配额统计。"""
157
+ stats = {}
158
+ for engine in self._profiles:
159
+ if engine.startswith("_") or not isinstance(self._profiles[engine], dict):
160
+ continue
161
+ profile = self._profiles[engine]
162
+ stats[engine] = {
163
+ "remaining_ratio": round(self.get_remaining_ratio(engine), 2),
164
+ "rpm": self.get_current_rpm(engine),
165
+ "error_rate": round(self.get_error_rate(engine), 3),
166
+ "available": self.is_available(engine),
167
+ "cost_per_call": self.get_cost_per_call(engine),
168
+ "total_cost": round(self.get_total_cost(engine), 6),
169
+ "used": self._state.get(engine, {}).get("used", 0),
170
+ "limit": profile.get("limit", "∞"),
171
+ "cost_tier": profile.get("cost_tier", "free"),
172
+ }
173
+ return stats
174
+
175
+
176
+ # ── 模块级单例 ─────────────────────────────────────────────────────────────────
177
+
178
+ _manager: Optional[QuotaManager] = None
179
+
180
+
181
+ def get_quota_manager() -> QuotaManager:
182
+ global _manager
183
+ if _manager is None:
184
+ _manager = QuotaManager()
185
+ return _manager
186
+
187
+
188
+ # ── CLI ────────────────────────────────────────────────────────────────────────
189
+
190
+ if __name__ == "__main__":
191
+ import sys
192
+ mgr = get_quota_manager()
193
+ if len(sys.argv) > 1 and sys.argv[1] == "stats":
194
+ print(json.dumps(mgr.get_stats(), ensure_ascii=False, indent=2))
195
+ else:
196
+ print("用法: python3 quota.py stats")