@blxzer/cursor-trellis 0.1.3 → 0.2.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.
- package/CHANGELOG.md +58 -0
- package/README.md +45 -23
- package/dist/configurators/workflow.d.ts +1 -1
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +20 -9
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.1.3.json +9 -0
- package/dist/migrations/manifests/0.1.4.json +9 -0
- package/dist/templates/markdown/agents.md +1 -1
- package/dist/templates/markdown/index.d.ts +1 -0
- package/dist/templates/markdown/index.d.ts.map +1 -1
- package/dist/templates/markdown/index.js +1 -0
- package/dist/templates/markdown/index.js.map +1 -1
- package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +13 -29
- package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +0 -49
- package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +0 -85
- package/dist/templates/markdown/spec/guides/cursor-semantic-compliance.md.txt +7 -21
- package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +6 -10
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +7 -13
- package/dist/templates/shared-hooks/inject-retrieval-plan.py +42 -23
- package/dist/templates/shared-hooks/inject-subagent-context.py +6 -2
- package/dist/templates/shared-hooks/session-start.py +17 -9
- package/dist/templates/trellis/index.d.ts +7 -4
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +11 -7
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +2 -2
- package/dist/templates/trellis/scripts/common/config.py +1 -1
- package/dist/templates/trellis/scripts/common/parent_orchestration.py +27 -15
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +1 -1
- package/dist/templates/trellis/scripts/common/smart_search_resolve.py +28 -3
- package/dist/templates/trellis/scripts/common/task_gates.py +372 -73
- package/dist/templates/trellis/scripts/common/task_map.py +29 -1
- package/dist/templates/trellis/workflow.md +9 -53
- package/package.json +5 -44
- package/bin/smart-search.js +0 -79
- package/dist/templates/trellis/scripts/aggregate_retrieval_telemetry.py +0 -342
- package/dist/templates/trellis/scripts/batch_plan_envelope.py +0 -105
- package/vendor/smart-search/LICENSE +0 -21
- package/vendor/smart-search/README.md +0 -412
- package/vendor/smart-search/README.zh-CN.md +0 -430
- package/vendor/smart-search/npm/bin/smart-search.js +0 -63
- package/vendor/smart-search/npm/scripts/postinstall.js +0 -87
- package/vendor/smart-search/npm/scripts/resolve-prerelease-version.js +0 -108
- package/vendor/smart-search/npm/scripts/set-package-version.js +0 -35
- package/vendor/smart-search/npm/scripts/sync-python-version.js +0 -22
- package/vendor/smart-search/npm/scripts/test-wrapper-repair.js +0 -99
- package/vendor/smart-search/npm/scripts/test.js +0 -85
- package/vendor/smart-search/package.json +0 -42
- package/vendor/smart-search/pyproject.toml +0 -37
- package/vendor/smart-search/skills/smart-search-cli/SKILL.md +0 -387
- package/vendor/smart-search/skills/smart-search-cli/agents/openai.yaml +0 -3
- package/vendor/smart-search/skills/smart-search-cli/examples/batch-search.md +0 -98
- package/vendor/smart-search/skills/smart-search-cli/examples/evidence-gathering.md +0 -89
- package/vendor/smart-search/skills/smart-search-cli/references/cli-contract.md +0 -337
- package/vendor/smart-search/src/smart_search/__init__.py +0 -1
- package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/SKILL.md +0 -387
- package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/agents/openai.yaml +0 -3
- package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/examples/batch-search.md +0 -98
- package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/examples/evidence-gathering.md +0 -89
- package/vendor/smart-search/src/smart_search/assets/skills/smart-search-cli/references/cli-contract.md +0 -337
- package/vendor/smart-search/src/smart_search/cli.py +0 -2003
- package/vendor/smart-search/src/smart_search/config.py +0 -580
- package/vendor/smart-search/src/smart_search/logger.py +0 -43
- package/vendor/smart-search/src/smart_search/providers/__init__.py +0 -16
- package/vendor/smart-search/src/smart_search/providers/base.py +0 -41
- package/vendor/smart-search/src/smart_search/providers/context7.py +0 -141
- package/vendor/smart-search/src/smart_search/providers/exa.py +0 -206
- package/vendor/smart-search/src/smart_search/providers/jina.py +0 -136
- package/vendor/smart-search/src/smart_search/providers/openai_compatible.py +0 -370
- package/vendor/smart-search/src/smart_search/providers/zhipu.py +0 -143
- package/vendor/smart-search/src/smart_search/service.py +0 -2865
- package/vendor/smart-search/src/smart_search/sources.py +0 -429
- package/vendor/smart-search/src/smart_search/utils.py +0 -220
|
@@ -1,580 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
class Config:
|
|
7
|
-
_instance = None
|
|
8
|
-
_SETUP_COMMAND = (
|
|
9
|
-
"Run `smart-search setup`, or configure OPENAI_COMPATIBLE_API_URL plus "
|
|
10
|
-
"OPENAI_COMPATIBLE_API_KEY, then run `smart-search doctor --format json`."
|
|
11
|
-
)
|
|
12
|
-
_DEFAULT_MODEL = "grok-4.20-multi-agent-xhigh"
|
|
13
|
-
_DEFAULT_VALIDATION_LEVEL = "balanced"
|
|
14
|
-
_DEFAULT_FALLBACK_MODE = "auto"
|
|
15
|
-
_DEFAULT_MINIMUM_PROFILE = "standard"
|
|
16
|
-
_ALLOWED_VALIDATION_LEVELS = {"fast", "balanced", "strict"}
|
|
17
|
-
_ALLOWED_FALLBACK_MODES = {"auto", "off"}
|
|
18
|
-
_ALLOWED_MINIMUM_PROFILES = {"standard", "off"}
|
|
19
|
-
_CONFIG_KEYS = {
|
|
20
|
-
"OPENAI_COMPATIBLE_API_URL",
|
|
21
|
-
"OPENAI_COMPATIBLE_API_KEY",
|
|
22
|
-
"OPENAI_COMPATIBLE_MODEL",
|
|
23
|
-
"OPENAI_COMPATIBLE_STREAM",
|
|
24
|
-
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
25
|
-
"SMART_SEARCH_FALLBACK_MODE",
|
|
26
|
-
"SMART_SEARCH_MINIMUM_PROFILE",
|
|
27
|
-
"SMART_SEARCH_RESEARCH_PREFERRED_PROVIDERS",
|
|
28
|
-
"SMART_SEARCH_RESEARCH_DISABLED_PROVIDERS",
|
|
29
|
-
"EXA_API_KEY",
|
|
30
|
-
"EXA_BASE_URL",
|
|
31
|
-
"EXA_TIMEOUT_SECONDS",
|
|
32
|
-
"CONTEXT7_API_KEY",
|
|
33
|
-
"CONTEXT7_BASE_URL",
|
|
34
|
-
"CONTEXT7_TIMEOUT_SECONDS",
|
|
35
|
-
"ZHIPU_API_KEY",
|
|
36
|
-
"ZHIPU_API_URL",
|
|
37
|
-
"ZHIPU_SEARCH_ENGINE",
|
|
38
|
-
"ZHIPU_TIMEOUT_SECONDS",
|
|
39
|
-
"JINA_API_KEY",
|
|
40
|
-
"JINA_READER_API_URL",
|
|
41
|
-
"JINA_RESPOND_WITH",
|
|
42
|
-
"JINA_TIMEOUT_SECONDS",
|
|
43
|
-
"TAVILY_API_KEY",
|
|
44
|
-
"TAVILY_API_URL",
|
|
45
|
-
"TAVILY_ENABLED",
|
|
46
|
-
"TAVILY_TIMEOUT_SECONDS",
|
|
47
|
-
"FIRECRAWL_API_KEY",
|
|
48
|
-
"FIRECRAWL_API_URL",
|
|
49
|
-
"SMART_SEARCH_DEBUG",
|
|
50
|
-
"SMART_SEARCH_LOG_LEVEL",
|
|
51
|
-
"SMART_SEARCH_LOG_DIR",
|
|
52
|
-
"SMART_SEARCH_EVIDENCE_DIR",
|
|
53
|
-
"SMART_SEARCH_RETRY_MAX_ATTEMPTS",
|
|
54
|
-
"SMART_SEARCH_RETRY_MULTIPLIER",
|
|
55
|
-
"SMART_SEARCH_RETRY_MAX_WAIT",
|
|
56
|
-
"SMART_SEARCH_OUTPUT_CLEANUP",
|
|
57
|
-
"SMART_SEARCH_LOG_TO_FILE",
|
|
58
|
-
"SSL_VERIFY",
|
|
59
|
-
}
|
|
60
|
-
_LEGACY_CONFIG_KEYS: dict[str, str] = {}
|
|
61
|
-
|
|
62
|
-
def __new__(cls):
|
|
63
|
-
if cls._instance is None:
|
|
64
|
-
cls._instance = super().__new__(cls)
|
|
65
|
-
cls._instance._config_file = None
|
|
66
|
-
cls._instance._config_dir_source = None
|
|
67
|
-
cls._instance._cached_model = None
|
|
68
|
-
return cls._instance
|
|
69
|
-
|
|
70
|
-
@staticmethod
|
|
71
|
-
def _default_config_dir() -> Path:
|
|
72
|
-
if sys.platform.startswith("win"):
|
|
73
|
-
local_appdata = os.getenv("LOCALAPPDATA")
|
|
74
|
-
if local_appdata:
|
|
75
|
-
return Path(local_appdata).expanduser() / "smart-search"
|
|
76
|
-
return Path.home() / ".config" / "smart-search"
|
|
77
|
-
|
|
78
|
-
@staticmethod
|
|
79
|
-
def _legacy_windows_config_dir() -> Path:
|
|
80
|
-
return Path.home() / ".config" / "smart-search"
|
|
81
|
-
|
|
82
|
-
@staticmethod
|
|
83
|
-
def _config_dir_override_value() -> str:
|
|
84
|
-
return os.getenv("SMART_SEARCH_CONFIG_DIR") or ""
|
|
85
|
-
|
|
86
|
-
@staticmethod
|
|
87
|
-
def _same_config_dir(left: Path, right: Path) -> bool:
|
|
88
|
-
left_text = os.path.abspath(os.path.expanduser(str(left)))
|
|
89
|
-
right_text = os.path.abspath(os.path.expanduser(str(right)))
|
|
90
|
-
if sys.platform.startswith("win"):
|
|
91
|
-
left_text = left_text.replace("/", "\\").rstrip("\\").lower()
|
|
92
|
-
right_text = right_text.replace("/", "\\").rstrip("\\").lower()
|
|
93
|
-
else:
|
|
94
|
-
left_text = left_text.rstrip("/")
|
|
95
|
-
right_text = right_text.rstrip("/")
|
|
96
|
-
return left_text == right_text
|
|
97
|
-
|
|
98
|
-
@classmethod
|
|
99
|
-
def _config_dir_override_matches_default(cls) -> bool:
|
|
100
|
-
env_dir = cls._config_dir_override_value()
|
|
101
|
-
if not env_dir:
|
|
102
|
-
return False
|
|
103
|
-
return cls._same_config_dir(Path(env_dir).expanduser(), cls._default_config_dir())
|
|
104
|
-
|
|
105
|
-
@staticmethod
|
|
106
|
-
def _resolve_config_dir() -> tuple[Path, str]:
|
|
107
|
-
env_dir = os.getenv("SMART_SEARCH_CONFIG_DIR")
|
|
108
|
-
if env_dir:
|
|
109
|
-
return Path(env_dir).expanduser(), "environment"
|
|
110
|
-
default_dir = Config._default_config_dir()
|
|
111
|
-
if sys.platform.startswith("win"):
|
|
112
|
-
legacy_dir = Config._legacy_windows_config_dir()
|
|
113
|
-
if legacy_dir != default_dir and not (default_dir / "config.json").exists() and (legacy_dir / "config.json").exists():
|
|
114
|
-
return legacy_dir, "legacy_windows_home"
|
|
115
|
-
return default_dir, "default"
|
|
116
|
-
|
|
117
|
-
@staticmethod
|
|
118
|
-
def _safe_mkdir(p: Path) -> bool:
|
|
119
|
-
try:
|
|
120
|
-
p.mkdir(parents=True, exist_ok=True)
|
|
121
|
-
return True
|
|
122
|
-
except (PermissionError, OSError):
|
|
123
|
-
return False
|
|
124
|
-
|
|
125
|
-
@property
|
|
126
|
-
def config_file(self) -> Path:
|
|
127
|
-
if self._config_file is None:
|
|
128
|
-
config_dir, config_dir_source = self._resolve_config_dir()
|
|
129
|
-
ok = self._safe_mkdir(config_dir)
|
|
130
|
-
if config_dir_source == "default" and not ok:
|
|
131
|
-
cwd_dir = Path.cwd() / ".smart-search"
|
|
132
|
-
if self._safe_mkdir(cwd_dir):
|
|
133
|
-
config_dir = cwd_dir
|
|
134
|
-
config_dir_source = "cwd_fallback"
|
|
135
|
-
self._config_file = config_dir / "config.json"
|
|
136
|
-
self._config_dir_source = config_dir_source
|
|
137
|
-
return self._config_file
|
|
138
|
-
|
|
139
|
-
@property
|
|
140
|
-
def config_dir_source(self) -> str:
|
|
141
|
-
if self._config_file is None:
|
|
142
|
-
_ = self.config_file
|
|
143
|
-
return self._config_dir_source or "override"
|
|
144
|
-
|
|
145
|
-
def _load_config_file(self) -> dict:
|
|
146
|
-
try:
|
|
147
|
-
with open(self.config_file, 'r', encoding='utf-8') as f:
|
|
148
|
-
data = json.load(f)
|
|
149
|
-
return data if isinstance(data, dict) else {}
|
|
150
|
-
except (FileNotFoundError, PermissionError, OSError, json.JSONDecodeError):
|
|
151
|
-
return {}
|
|
152
|
-
|
|
153
|
-
def _save_config_file(self, config_data: dict) -> None:
|
|
154
|
-
try:
|
|
155
|
-
with open(self.config_file, 'w', encoding='utf-8') as f:
|
|
156
|
-
json.dump(config_data, f, ensure_ascii=False, indent=2)
|
|
157
|
-
except (IOError, PermissionError, OSError) as e:
|
|
158
|
-
hint = " (sandbox/CI 下可设 SMART_SEARCH_CONFIG_DIR 指向可写目录)" if isinstance(e, PermissionError) else ""
|
|
159
|
-
raise ValueError(f"无法保存配置文件: {str(e)}{hint}")
|
|
160
|
-
|
|
161
|
-
def _get_config_value(self, key: str, default: str | None = None) -> str | None:
|
|
162
|
-
env_value = os.getenv(key)
|
|
163
|
-
if env_value is not None:
|
|
164
|
-
return env_value
|
|
165
|
-
|
|
166
|
-
data = self._load_config_file()
|
|
167
|
-
value = data.get(key)
|
|
168
|
-
if value is None:
|
|
169
|
-
legacy_key = next((old for old, new in self._LEGACY_CONFIG_KEYS.items() if new == key), None)
|
|
170
|
-
if legacy_key:
|
|
171
|
-
value = data.get(legacy_key)
|
|
172
|
-
if value is None:
|
|
173
|
-
return default
|
|
174
|
-
return str(value)
|
|
175
|
-
|
|
176
|
-
def get_saved_config(self, masked: bool = True) -> dict:
|
|
177
|
-
data = self._load_config_file()
|
|
178
|
-
normalized: dict[str, str] = {}
|
|
179
|
-
for old_key, new_key in self._LEGACY_CONFIG_KEYS.items():
|
|
180
|
-
if old_key in data and new_key not in data:
|
|
181
|
-
normalized[new_key] = str(data[old_key])
|
|
182
|
-
for key, value in data.items():
|
|
183
|
-
if key in self._CONFIG_KEYS and value is not None:
|
|
184
|
-
normalized[key] = str(value)
|
|
185
|
-
if not masked:
|
|
186
|
-
return normalized
|
|
187
|
-
return {key: self._mask_if_secret(key, value) for key, value in normalized.items()}
|
|
188
|
-
|
|
189
|
-
def get_config_source(self, key: str) -> str:
|
|
190
|
-
if os.getenv(key) is not None:
|
|
191
|
-
return "environment"
|
|
192
|
-
data = self._load_config_file()
|
|
193
|
-
if key in data:
|
|
194
|
-
return "config_file"
|
|
195
|
-
legacy_key = next((old for old, new in self._LEGACY_CONFIG_KEYS.items() if new == key), None)
|
|
196
|
-
if legacy_key and legacy_key in data:
|
|
197
|
-
return "config_file"
|
|
198
|
-
return "default"
|
|
199
|
-
|
|
200
|
-
def get_config_sources(self) -> dict[str, str]:
|
|
201
|
-
return {key: self.get_config_source(key) for key in sorted(self._CONFIG_KEYS)}
|
|
202
|
-
|
|
203
|
-
def set_config_value(self, key: str, value: str) -> None:
|
|
204
|
-
key = key.strip().upper()
|
|
205
|
-
if key not in self._CONFIG_KEYS:
|
|
206
|
-
raise ValueError(f"Unsupported config key: {key}")
|
|
207
|
-
config_data = self._load_config_file()
|
|
208
|
-
config_data[key] = value
|
|
209
|
-
self._save_config_file(config_data)
|
|
210
|
-
if key in {
|
|
211
|
-
"OPENAI_COMPATIBLE_API_URL",
|
|
212
|
-
"OPENAI_COMPATIBLE_API_KEY",
|
|
213
|
-
"OPENAI_COMPATIBLE_MODEL",
|
|
214
|
-
"OPENAI_COMPATIBLE_STREAM",
|
|
215
|
-
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
216
|
-
"SMART_SEARCH_FALLBACK_MODE",
|
|
217
|
-
"SMART_SEARCH_MINIMUM_PROFILE",
|
|
218
|
-
}:
|
|
219
|
-
self._cached_model = None
|
|
220
|
-
|
|
221
|
-
def unset_config_value(self, key: str) -> None:
|
|
222
|
-
key = key.strip().upper()
|
|
223
|
-
if key not in self._CONFIG_KEYS:
|
|
224
|
-
raise ValueError(f"Unsupported config key: {key}")
|
|
225
|
-
config_data = self._load_config_file()
|
|
226
|
-
config_data.pop(key, None)
|
|
227
|
-
for old_key, new_key in self._LEGACY_CONFIG_KEYS.items():
|
|
228
|
-
if new_key == key:
|
|
229
|
-
config_data.pop(old_key, None)
|
|
230
|
-
self._save_config_file(config_data)
|
|
231
|
-
if key in {
|
|
232
|
-
"OPENAI_COMPATIBLE_API_URL",
|
|
233
|
-
"OPENAI_COMPATIBLE_API_KEY",
|
|
234
|
-
"OPENAI_COMPATIBLE_MODEL",
|
|
235
|
-
"OPENAI_COMPATIBLE_STREAM",
|
|
236
|
-
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
237
|
-
"SMART_SEARCH_FALLBACK_MODE",
|
|
238
|
-
"SMART_SEARCH_MINIMUM_PROFILE",
|
|
239
|
-
}:
|
|
240
|
-
self._cached_model = None
|
|
241
|
-
|
|
242
|
-
def config_path_info(self) -> dict:
|
|
243
|
-
return {
|
|
244
|
-
"ok": True,
|
|
245
|
-
"config_file": str(self.config_file),
|
|
246
|
-
"config_dir": str(self.config_file.parent),
|
|
247
|
-
"config_dir_source": self.config_dir_source,
|
|
248
|
-
"default_config_file": str(self._default_config_dir() / "config.json"),
|
|
249
|
-
"legacy_windows_config_file": str(self._legacy_windows_config_dir() / "config.json") if sys.platform.startswith("win") else "",
|
|
250
|
-
"legacy_windows_config_exists": (self._legacy_windows_config_dir() / "config.json").exists() if sys.platform.startswith("win") else False,
|
|
251
|
-
"config_dir_override_value": self._config_dir_override_value(),
|
|
252
|
-
"config_dir_override_matches_default": self._config_dir_override_matches_default(),
|
|
253
|
-
"evidence_dir_config_value": self.evidence_dir_config_value,
|
|
254
|
-
"resolved_evidence_dir": str(self.evidence_dir),
|
|
255
|
-
"exists": self.config_file.exists(),
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
@property
|
|
259
|
-
def debug_enabled(self) -> bool:
|
|
260
|
-
return (self._get_config_value("SMART_SEARCH_DEBUG", "false") or "false").lower() in ("true", "1", "yes")
|
|
261
|
-
|
|
262
|
-
@property
|
|
263
|
-
def retry_max_attempts(self) -> int:
|
|
264
|
-
return int(self._get_config_value("SMART_SEARCH_RETRY_MAX_ATTEMPTS", "3") or "3")
|
|
265
|
-
|
|
266
|
-
@property
|
|
267
|
-
def retry_multiplier(self) -> float:
|
|
268
|
-
return float(self._get_config_value("SMART_SEARCH_RETRY_MULTIPLIER", "1") or "1")
|
|
269
|
-
|
|
270
|
-
@property
|
|
271
|
-
def retry_max_wait(self) -> int:
|
|
272
|
-
return int(self._get_config_value("SMART_SEARCH_RETRY_MAX_WAIT", "10") or "10")
|
|
273
|
-
|
|
274
|
-
@property
|
|
275
|
-
def openai_compatible_api_url(self) -> str | None:
|
|
276
|
-
return self._get_config_value("OPENAI_COMPATIBLE_API_URL")
|
|
277
|
-
|
|
278
|
-
@property
|
|
279
|
-
def openai_compatible_api_key(self) -> str | None:
|
|
280
|
-
return self._get_config_value("OPENAI_COMPATIBLE_API_KEY")
|
|
281
|
-
|
|
282
|
-
@property
|
|
283
|
-
def openai_compatible_model(self) -> str:
|
|
284
|
-
model = self._get_config_value("OPENAI_COMPATIBLE_MODEL") or self._base_model_value()
|
|
285
|
-
return self.apply_model_suffix_for_url(model, self.openai_compatible_api_url or "")
|
|
286
|
-
|
|
287
|
-
@property
|
|
288
|
-
def openai_compatible_stream(self) -> bool:
|
|
289
|
-
return (self._get_config_value("OPENAI_COMPATIBLE_STREAM", "true") or "true").lower() in ("true", "1", "yes")
|
|
290
|
-
|
|
291
|
-
def _validated_enum(self, key: str, default: str, allowed: set[str]) -> str:
|
|
292
|
-
value = (self._get_config_value(key, default) or default).strip().lower()
|
|
293
|
-
if value not in allowed:
|
|
294
|
-
allowed_text = ", ".join(sorted(allowed))
|
|
295
|
-
raise ValueError(f"Invalid {key}: {value}. Supported values: {allowed_text}")
|
|
296
|
-
return value
|
|
297
|
-
|
|
298
|
-
def _enum_info(self, key: str, default: str, allowed: set[str]) -> tuple[str, str]:
|
|
299
|
-
value = (self._get_config_value(key, default) or default).strip().lower()
|
|
300
|
-
if value not in allowed:
|
|
301
|
-
allowed_text = ", ".join(sorted(allowed))
|
|
302
|
-
return value, f"Invalid {key}: {value}. Supported values: {allowed_text}"
|
|
303
|
-
return value, ""
|
|
304
|
-
|
|
305
|
-
@property
|
|
306
|
-
def validation_level(self) -> str:
|
|
307
|
-
return self._validated_enum(
|
|
308
|
-
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
309
|
-
self._DEFAULT_VALIDATION_LEVEL,
|
|
310
|
-
self._ALLOWED_VALIDATION_LEVELS,
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
@property
|
|
314
|
-
def fallback_mode(self) -> str:
|
|
315
|
-
return self._validated_enum(
|
|
316
|
-
"SMART_SEARCH_FALLBACK_MODE",
|
|
317
|
-
self._DEFAULT_FALLBACK_MODE,
|
|
318
|
-
self._ALLOWED_FALLBACK_MODES,
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
@property
|
|
322
|
-
def minimum_profile(self) -> str:
|
|
323
|
-
return self._validated_enum(
|
|
324
|
-
"SMART_SEARCH_MINIMUM_PROFILE",
|
|
325
|
-
self._DEFAULT_MINIMUM_PROFILE,
|
|
326
|
-
self._ALLOWED_MINIMUM_PROFILES,
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
def _csv_values(self, key: str) -> list[str]:
|
|
330
|
-
raw = self._get_config_value(key, "") or ""
|
|
331
|
-
values: list[str] = []
|
|
332
|
-
seen: set[str] = set()
|
|
333
|
-
for item in raw.split(","):
|
|
334
|
-
value = item.strip().lower()
|
|
335
|
-
if value and value not in seen:
|
|
336
|
-
seen.add(value)
|
|
337
|
-
values.append(value)
|
|
338
|
-
return values
|
|
339
|
-
|
|
340
|
-
@property
|
|
341
|
-
def research_preferred_providers(self) -> list[str]:
|
|
342
|
-
return self._csv_values("SMART_SEARCH_RESEARCH_PREFERRED_PROVIDERS")
|
|
343
|
-
|
|
344
|
-
@property
|
|
345
|
-
def research_disabled_providers(self) -> list[str]:
|
|
346
|
-
return self._csv_values("SMART_SEARCH_RESEARCH_DISABLED_PROVIDERS")
|
|
347
|
-
|
|
348
|
-
@property
|
|
349
|
-
def tavily_enabled(self) -> bool:
|
|
350
|
-
return (self._get_config_value("TAVILY_ENABLED", "true") or "true").lower() in ("true", "1", "yes")
|
|
351
|
-
|
|
352
|
-
@property
|
|
353
|
-
def tavily_api_url(self) -> str:
|
|
354
|
-
return self._get_config_value("TAVILY_API_URL", "https://api.tavily.com") or "https://api.tavily.com"
|
|
355
|
-
|
|
356
|
-
@property
|
|
357
|
-
def tavily_api_key(self) -> str | None:
|
|
358
|
-
return self._get_config_value("TAVILY_API_KEY")
|
|
359
|
-
|
|
360
|
-
@property
|
|
361
|
-
def tavily_timeout(self) -> float:
|
|
362
|
-
return float(self._get_config_value("TAVILY_TIMEOUT_SECONDS", "60") or "60")
|
|
363
|
-
|
|
364
|
-
@property
|
|
365
|
-
def firecrawl_api_url(self) -> str:
|
|
366
|
-
return self._get_config_value("FIRECRAWL_API_URL", "https://api.firecrawl.dev/v2") or "https://api.firecrawl.dev/v2"
|
|
367
|
-
|
|
368
|
-
@property
|
|
369
|
-
def firecrawl_api_key(self) -> str | None:
|
|
370
|
-
return self._get_config_value("FIRECRAWL_API_KEY")
|
|
371
|
-
|
|
372
|
-
@property
|
|
373
|
-
def log_level(self) -> str:
|
|
374
|
-
return (self._get_config_value("SMART_SEARCH_LOG_LEVEL", "INFO") or "INFO").upper()
|
|
375
|
-
|
|
376
|
-
@property
|
|
377
|
-
def log_dir(self) -> Path:
|
|
378
|
-
log_dir_str = self.log_dir_config_value
|
|
379
|
-
log_dir = Path(log_dir_str)
|
|
380
|
-
if log_dir.is_absolute():
|
|
381
|
-
return log_dir
|
|
382
|
-
|
|
383
|
-
return self.config_file.parent / log_dir
|
|
384
|
-
|
|
385
|
-
@property
|
|
386
|
-
def log_dir_config_value(self) -> str:
|
|
387
|
-
return self._get_config_value("SMART_SEARCH_LOG_DIR", "logs") or "logs"
|
|
388
|
-
|
|
389
|
-
@property
|
|
390
|
-
def evidence_dir(self) -> Path:
|
|
391
|
-
evidence_dir_str = self.evidence_dir_config_value
|
|
392
|
-
evidence_dir = Path(evidence_dir_str)
|
|
393
|
-
if evidence_dir.is_absolute():
|
|
394
|
-
return evidence_dir
|
|
395
|
-
|
|
396
|
-
return self.config_file.parent / evidence_dir
|
|
397
|
-
|
|
398
|
-
@property
|
|
399
|
-
def evidence_dir_config_value(self) -> str:
|
|
400
|
-
return self._get_config_value("SMART_SEARCH_EVIDENCE_DIR", "evidence") or "evidence"
|
|
401
|
-
|
|
402
|
-
@staticmethod
|
|
403
|
-
def apply_model_suffix_for_url(model: str, api_url: str) -> str:
|
|
404
|
-
if "openrouter" in api_url and ":online" not in model:
|
|
405
|
-
return f"{model}:online"
|
|
406
|
-
return model
|
|
407
|
-
|
|
408
|
-
def _base_model_value(self) -> str:
|
|
409
|
-
return self._DEFAULT_MODEL
|
|
410
|
-
|
|
411
|
-
@staticmethod
|
|
412
|
-
def _mask_api_key(key: str) -> str:
|
|
413
|
-
if not key or len(key) <= 8:
|
|
414
|
-
return "***"
|
|
415
|
-
return f"{key[:4]}{'*' * (len(key) - 8)}{key[-4:]}"
|
|
416
|
-
|
|
417
|
-
@classmethod
|
|
418
|
-
def _mask_if_secret(cls, key: str, value: str) -> str:
|
|
419
|
-
if "KEY" in key or "TOKEN" in key or "SECRET" in key:
|
|
420
|
-
return cls._mask_api_key(value)
|
|
421
|
-
return value
|
|
422
|
-
|
|
423
|
-
@property
|
|
424
|
-
def output_cleanup_enabled(self) -> bool:
|
|
425
|
-
return (self._get_config_value("SMART_SEARCH_OUTPUT_CLEANUP", "true") or "true").lower() in ("true", "1", "yes")
|
|
426
|
-
|
|
427
|
-
@property
|
|
428
|
-
def log_to_file_enabled(self) -> bool:
|
|
429
|
-
return (self._get_config_value("SMART_SEARCH_LOG_TO_FILE", "false") or "false").lower() in ("true", "1", "yes")
|
|
430
|
-
|
|
431
|
-
@property
|
|
432
|
-
def ssl_verify_enabled(self) -> bool:
|
|
433
|
-
return (self._get_config_value("SSL_VERIFY", "true") or "true").lower() not in ("false", "0", "no")
|
|
434
|
-
|
|
435
|
-
@property
|
|
436
|
-
def exa_api_key(self) -> str | None:
|
|
437
|
-
return self._get_config_value("EXA_API_KEY")
|
|
438
|
-
|
|
439
|
-
@property
|
|
440
|
-
def exa_base_url(self) -> str:
|
|
441
|
-
return self._get_config_value("EXA_BASE_URL", "https://api.exa.ai") or "https://api.exa.ai"
|
|
442
|
-
|
|
443
|
-
@property
|
|
444
|
-
def exa_timeout(self) -> float:
|
|
445
|
-
return float(self._get_config_value("EXA_TIMEOUT_SECONDS", "60") or "60")
|
|
446
|
-
|
|
447
|
-
@property
|
|
448
|
-
def context7_api_key(self) -> str | None:
|
|
449
|
-
return self._get_config_value("CONTEXT7_API_KEY")
|
|
450
|
-
|
|
451
|
-
@property
|
|
452
|
-
def context7_base_url(self) -> str:
|
|
453
|
-
return self._get_config_value("CONTEXT7_BASE_URL", "https://context7.com") or "https://context7.com"
|
|
454
|
-
|
|
455
|
-
@property
|
|
456
|
-
def context7_timeout(self) -> float:
|
|
457
|
-
return float(self._get_config_value("CONTEXT7_TIMEOUT_SECONDS", "60") or "60")
|
|
458
|
-
|
|
459
|
-
@property
|
|
460
|
-
def zhipu_api_key(self) -> str | None:
|
|
461
|
-
return self._get_config_value("ZHIPU_API_KEY")
|
|
462
|
-
|
|
463
|
-
@property
|
|
464
|
-
def zhipu_api_url(self) -> str:
|
|
465
|
-
return self._get_config_value("ZHIPU_API_URL", "https://open.bigmodel.cn/api") or "https://open.bigmodel.cn/api"
|
|
466
|
-
|
|
467
|
-
@property
|
|
468
|
-
def zhipu_search_engine(self) -> str:
|
|
469
|
-
return self._get_config_value("ZHIPU_SEARCH_ENGINE", "search_std") or "search_std"
|
|
470
|
-
|
|
471
|
-
@property
|
|
472
|
-
def zhipu_timeout(self) -> float:
|
|
473
|
-
return float(self._get_config_value("ZHIPU_TIMEOUT_SECONDS", "60") or "60")
|
|
474
|
-
|
|
475
|
-
@property
|
|
476
|
-
def jina_api_key(self) -> str | None:
|
|
477
|
-
return self._get_config_value("JINA_API_KEY")
|
|
478
|
-
|
|
479
|
-
@property
|
|
480
|
-
def jina_reader_api_url(self) -> str:
|
|
481
|
-
return self._get_config_value("JINA_READER_API_URL", "https://r.jina.ai") or "https://r.jina.ai"
|
|
482
|
-
|
|
483
|
-
@property
|
|
484
|
-
def jina_respond_with(self) -> str:
|
|
485
|
-
return self._get_config_value("JINA_RESPOND_WITH", "") or ""
|
|
486
|
-
|
|
487
|
-
@property
|
|
488
|
-
def jina_timeout(self) -> float:
|
|
489
|
-
return float(self._get_config_value("JINA_TIMEOUT_SECONDS", "60") or "60")
|
|
490
|
-
|
|
491
|
-
def get_config_info(self) -> dict:
|
|
492
|
-
config_parameter_errors: list[str] = []
|
|
493
|
-
explicit_main_configured = bool(
|
|
494
|
-
self.openai_compatible_api_url and self.openai_compatible_api_key
|
|
495
|
-
)
|
|
496
|
-
if explicit_main_configured:
|
|
497
|
-
config_status = "ok: 配置完整"
|
|
498
|
-
else:
|
|
499
|
-
config_status = f"config_error: {self._SETUP_COMMAND}"
|
|
500
|
-
|
|
501
|
-
validation_level, validation_error = self._enum_info(
|
|
502
|
-
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
503
|
-
self._DEFAULT_VALIDATION_LEVEL,
|
|
504
|
-
self._ALLOWED_VALIDATION_LEVELS,
|
|
505
|
-
)
|
|
506
|
-
fallback_mode, fallback_error = self._enum_info(
|
|
507
|
-
"SMART_SEARCH_FALLBACK_MODE",
|
|
508
|
-
self._DEFAULT_FALLBACK_MODE,
|
|
509
|
-
self._ALLOWED_FALLBACK_MODES,
|
|
510
|
-
)
|
|
511
|
-
minimum_profile, minimum_error = self._enum_info(
|
|
512
|
-
"SMART_SEARCH_MINIMUM_PROFILE",
|
|
513
|
-
self._DEFAULT_MINIMUM_PROFILE,
|
|
514
|
-
self._ALLOWED_MINIMUM_PROFILES,
|
|
515
|
-
)
|
|
516
|
-
config_parameter_errors.extend(error for error in (validation_error, fallback_error, minimum_error) if error)
|
|
517
|
-
if config_parameter_errors and config_status.startswith("ok:"):
|
|
518
|
-
config_status = f"config_error: {'; '.join(config_parameter_errors)}"
|
|
519
|
-
|
|
520
|
-
return {
|
|
521
|
-
"OPENAI_COMPATIBLE_API_URL": self.openai_compatible_api_url or "未配置",
|
|
522
|
-
"OPENAI_COMPATIBLE_API_KEY": self._mask_api_key(self.openai_compatible_api_key) if self.openai_compatible_api_key else "未配置",
|
|
523
|
-
"OPENAI_COMPATIBLE_MODEL": self.openai_compatible_model,
|
|
524
|
-
"OPENAI_COMPATIBLE_STREAM": self.openai_compatible_stream,
|
|
525
|
-
"SMART_SEARCH_VALIDATION_LEVEL": validation_level,
|
|
526
|
-
"SMART_SEARCH_FALLBACK_MODE": fallback_mode,
|
|
527
|
-
"SMART_SEARCH_MINIMUM_PROFILE": minimum_profile,
|
|
528
|
-
"SMART_SEARCH_RESEARCH_PREFERRED_PROVIDERS": ",".join(self.research_preferred_providers),
|
|
529
|
-
"SMART_SEARCH_RESEARCH_DISABLED_PROVIDERS": ",".join(self.research_disabled_providers),
|
|
530
|
-
"SMART_SEARCH_DEBUG": self.debug_enabled,
|
|
531
|
-
"SMART_SEARCH_LOG_LEVEL": self.log_level,
|
|
532
|
-
"SMART_SEARCH_LOG_DIR": self.log_dir_config_value,
|
|
533
|
-
"SMART_SEARCH_EVIDENCE_DIR": self.evidence_dir_config_value,
|
|
534
|
-
"SMART_SEARCH_RETRY_MAX_ATTEMPTS": self.retry_max_attempts,
|
|
535
|
-
"SMART_SEARCH_RETRY_MULTIPLIER": self.retry_multiplier,
|
|
536
|
-
"SMART_SEARCH_RETRY_MAX_WAIT": self.retry_max_wait,
|
|
537
|
-
"TAVILY_API_URL": self.tavily_api_url,
|
|
538
|
-
"TAVILY_ENABLED": self.tavily_enabled,
|
|
539
|
-
"TAVILY_API_KEY": self._mask_api_key(self.tavily_api_key) if self.tavily_api_key else "未配置",
|
|
540
|
-
"TAVILY_TIMEOUT_SECONDS": self.tavily_timeout,
|
|
541
|
-
"FIRECRAWL_API_URL": self.firecrawl_api_url,
|
|
542
|
-
"FIRECRAWL_API_KEY": self._mask_api_key(self.firecrawl_api_key) if self.firecrawl_api_key else "未配置",
|
|
543
|
-
"SMART_SEARCH_OUTPUT_CLEANUP": self.output_cleanup_enabled,
|
|
544
|
-
"SMART_SEARCH_LOG_TO_FILE": self.log_to_file_enabled,
|
|
545
|
-
"SSL_VERIFY": self.ssl_verify_enabled,
|
|
546
|
-
"EXA_API_KEY": self._mask_api_key(self.exa_api_key) if self.exa_api_key else "未配置",
|
|
547
|
-
"EXA_BASE_URL": self.exa_base_url,
|
|
548
|
-
"EXA_TIMEOUT_SECONDS": self.exa_timeout,
|
|
549
|
-
"CONTEXT7_API_KEY": self._mask_api_key(self.context7_api_key) if self.context7_api_key else "未配置",
|
|
550
|
-
"CONTEXT7_BASE_URL": self.context7_base_url,
|
|
551
|
-
"CONTEXT7_TIMEOUT_SECONDS": self.context7_timeout,
|
|
552
|
-
"ZHIPU_API_KEY": self._mask_api_key(self.zhipu_api_key) if self.zhipu_api_key else "未配置",
|
|
553
|
-
"ZHIPU_API_URL": self.zhipu_api_url,
|
|
554
|
-
"ZHIPU_SEARCH_ENGINE": self.zhipu_search_engine,
|
|
555
|
-
"ZHIPU_TIMEOUT_SECONDS": self.zhipu_timeout,
|
|
556
|
-
"JINA_API_KEY": self._mask_api_key(self.jina_api_key) if self.jina_api_key else "未配置",
|
|
557
|
-
"JINA_READER_API_URL": self.jina_reader_api_url,
|
|
558
|
-
"JINA_RESPOND_WITH": self.jina_respond_with,
|
|
559
|
-
"JINA_TIMEOUT_SECONDS": self.jina_timeout,
|
|
560
|
-
"primary_api_mode": "chat-completions" if (self.openai_compatible_api_url and self.openai_compatible_api_key) else "未配置",
|
|
561
|
-
"primary_api_mode_source": "config_file" if explicit_main_configured else "default",
|
|
562
|
-
"config_file": str(self.config_file),
|
|
563
|
-
"config_dir": str(self.config_file.parent),
|
|
564
|
-
"config_dir_source": self.config_dir_source,
|
|
565
|
-
"default_config_file": str(self._default_config_dir() / "config.json"),
|
|
566
|
-
"legacy_windows_config_file": str(self._legacy_windows_config_dir() / "config.json") if sys.platform.startswith("win") else "",
|
|
567
|
-
"legacy_windows_config_exists": (self._legacy_windows_config_dir() / "config.json").exists() if sys.platform.startswith("win") else False,
|
|
568
|
-
"config_dir_override_value": self._config_dir_override_value(),
|
|
569
|
-
"config_dir_override_matches_default": self._config_dir_override_matches_default(),
|
|
570
|
-
"log_dir_config_value": self.log_dir_config_value,
|
|
571
|
-
"resolved_log_dir": str(self.log_dir),
|
|
572
|
-
"evidence_dir_config_value": self.evidence_dir_config_value,
|
|
573
|
-
"resolved_evidence_dir": str(self.evidence_dir),
|
|
574
|
-
"file_logging_enabled": self.debug_enabled or self.log_to_file_enabled,
|
|
575
|
-
"config_sources": self.get_config_sources(),
|
|
576
|
-
"config_parameter_errors": config_parameter_errors,
|
|
577
|
-
"config_status": config_status
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
config = Config()
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import os
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
from .config import config
|
|
5
|
-
|
|
6
|
-
logger = logging.getLogger("smart_search")
|
|
7
|
-
logger.setLevel(getattr(logging, config.log_level))
|
|
8
|
-
logger.addHandler(logging.NullHandler())
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _file_logging_enabled() -> bool:
|
|
12
|
-
return config.debug_enabled or config.log_to_file_enabled
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _configure_file_logging() -> None:
|
|
16
|
-
if not _file_logging_enabled():
|
|
17
|
-
return
|
|
18
|
-
if any(isinstance(handler, logging.FileHandler) for handler in logger.handlers):
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
log_dir = config.log_dir
|
|
22
|
-
log_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
-
log_file = log_dir / f"smart_search_{datetime.now().strftime('%Y%m%d')}.log"
|
|
24
|
-
|
|
25
|
-
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
|
26
|
-
file_handler.setLevel(getattr(logging, config.log_level))
|
|
27
|
-
|
|
28
|
-
formatter = logging.Formatter(
|
|
29
|
-
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
30
|
-
datefmt="%Y-%m-%d %H:%M:%S",
|
|
31
|
-
)
|
|
32
|
-
file_handler.setFormatter(formatter)
|
|
33
|
-
logger.addHandler(file_handler)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
_configure_file_logging()
|
|
37
|
-
|
|
38
|
-
async def log_info(ctx, message: str, is_debug: bool = False):
|
|
39
|
-
if is_debug:
|
|
40
|
-
logger.info(message)
|
|
41
|
-
|
|
42
|
-
if ctx:
|
|
43
|
-
await ctx.info(message)
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from .base import BaseSearchProvider, SearchResult
|
|
2
|
-
from .context7 import Context7Provider
|
|
3
|
-
from .openai_compatible import OpenAICompatibleSearchProvider
|
|
4
|
-
from .exa import ExaSearchProvider
|
|
5
|
-
from .jina import JinaReaderProvider
|
|
6
|
-
from .zhipu import ZhipuWebSearchProvider
|
|
7
|
-
|
|
8
|
-
__all__ = [
|
|
9
|
-
"BaseSearchProvider",
|
|
10
|
-
"SearchResult",
|
|
11
|
-
"Context7Provider",
|
|
12
|
-
"OpenAICompatibleSearchProvider",
|
|
13
|
-
"ExaSearchProvider",
|
|
14
|
-
"JinaReaderProvider",
|
|
15
|
-
"ZhipuWebSearchProvider",
|
|
16
|
-
]
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Dict, List
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class SearchResult:
|
|
6
|
-
def __init__(
|
|
7
|
-
self,
|
|
8
|
-
title: str,
|
|
9
|
-
url: str,
|
|
10
|
-
snippet: str,
|
|
11
|
-
source: str = "",
|
|
12
|
-
published_date: str = "",
|
|
13
|
-
):
|
|
14
|
-
self.title = title
|
|
15
|
-
self.url = url
|
|
16
|
-
self.snippet = snippet
|
|
17
|
-
self.source = source
|
|
18
|
-
self.published_date = published_date
|
|
19
|
-
|
|
20
|
-
def to_dict(self) -> Dict[str, str]:
|
|
21
|
-
return {
|
|
22
|
-
"title": self.title,
|
|
23
|
-
"url": self.url,
|
|
24
|
-
"snippet": self.snippet,
|
|
25
|
-
"source": self.source,
|
|
26
|
-
"published_date": self.published_date,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class BaseSearchProvider(ABC):
|
|
31
|
-
def __init__(self, api_url: str, api_key: str):
|
|
32
|
-
self.api_url = api_url
|
|
33
|
-
self.api_key = api_key
|
|
34
|
-
|
|
35
|
-
@abstractmethod
|
|
36
|
-
async def search(self, query: str, max_results: int = 5) -> List[SearchResult]:
|
|
37
|
-
pass
|
|
38
|
-
|
|
39
|
-
@abstractmethod
|
|
40
|
-
def get_provider_name(self) -> str:
|
|
41
|
-
pass
|