@allansantos-dev/smart-tool 0.9.0 → 0.9.2
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 +50 -23
- package/NOTICE +32 -32
- package/README.md +117 -116
- package/atomic_io.py +94 -94
- package/bin/smart-tool.mjs +5 -1
- package/client_hooks.py +192 -192
- package/code_graph_java.mjs +49 -49
- package/code_graph_js.cjs +135 -135
- package/duplicates.py +313 -313
- package/embedding_cache.py +99 -99
- package/endpoint_sync.py +179 -179
- package/gateway.py +220 -220
- package/hook_decision.py +96 -96
- package/index_inventory.py +75 -75
- package/indexer.py +878 -878
- package/install.py +113 -11
- package/install_runtime.py +101 -35
- package/integration_metrics.py +82 -82
- package/local_embedder.py +172 -172
- package/model_defaults.py +70 -70
- package/package.json +35 -35
- package/project_insights_ui.py +38 -38
- package/project_monitor.py +204 -204
- package/project_store.py +220 -220
- package/projects_ui.py +146 -146
- package/requirements.txt +8 -8
- package/research_cache.py +171 -171
- package/research_metrics.py +110 -110
- package/router.py +582 -582
- package/secret_store.py +121 -121
- package/setup_ui.py +1011 -986
- package/smart_tool_daemon.py +3604 -3604
- package/version.py +1 -1
- package/web_adapters/browser/fetch_camoufox.py +69 -0
- package/web_adapters/node/ddg-search.mjs +89 -89
- package/web_adapters/node/open-websearch-adapter.mjs +77 -77
- package/web_adapters/node/package.json +12 -12
- package/web_adapters/node/wikipedia-search.mjs +44 -44
- package/web_search_adapters.py +193 -193
- package/web_search_external.py +288 -288
- package/web_search_health.py +88 -88
package/web_search_health.py
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Histórico de sucesso/falha por Tier do web_search, lido de volta pelo router_model
|
|
3
|
-
antes de cada decisão — diferente de router-metrics.jsonl (router.py), que é write-only
|
|
4
|
-
e serve só de audit trail para o roteamento de smart_search.
|
|
5
|
-
"""
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import paths
|
|
9
|
-
import threading
|
|
10
|
-
import time
|
|
11
|
-
|
|
12
|
-
HEALTH_PATH = os.path.join(paths.DATA_DIR, "web-search-health.jsonl")
|
|
13
|
-
DEFAULT_WINDOW = 20
|
|
14
|
-
_MAX_LINES_SCANNED = 2000
|
|
15
|
-
_MAX_LINES_BEFORE_ROTATE = 5000
|
|
16
|
-
|
|
17
|
-
# Todo record()/rotação passa por aqui: o daemon é ThreadingHTTPServer, então
|
|
18
|
-
# múltiplas requisições concorrentes podem chamar record() na mesma linha de
|
|
19
|
-
# tempo — sem lock, escritas de threads diferentes se intercalam no mesmo
|
|
20
|
-
# arquivo e corrompem linhas JSONL.
|
|
21
|
-
_LOCK = threading.Lock()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def record(tier_name, success):
|
|
25
|
-
os.makedirs(os.path.dirname(HEALTH_PATH), exist_ok=True)
|
|
26
|
-
entry = {"ts": time.time(), "tier": tier_name, "success": bool(success)}
|
|
27
|
-
with _LOCK:
|
|
28
|
-
with open(HEALTH_PATH, "a", encoding="utf-8") as f:
|
|
29
|
-
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
30
|
-
_rotate_if_needed()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _rotate_if_needed():
|
|
34
|
-
# Já dentro de _LOCK (chamado só por record()). Mantém o arquivo limitado
|
|
35
|
-
# a _MAX_LINES_SCANNED linhas — sem isso, cada leitura futura reprocessa o
|
|
36
|
-
# arquivo inteiro por tempo indefinido de uso real.
|
|
37
|
-
try:
|
|
38
|
-
with open(HEALTH_PATH, "r", encoding="utf-8") as f:
|
|
39
|
-
lines = f.readlines()
|
|
40
|
-
except FileNotFoundError:
|
|
41
|
-
return
|
|
42
|
-
if len(lines) <= _MAX_LINES_BEFORE_ROTATE:
|
|
43
|
-
return
|
|
44
|
-
tmp_path = HEALTH_PATH + ".tmp"
|
|
45
|
-
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
46
|
-
f.writelines(lines[-_MAX_LINES_SCANNED:])
|
|
47
|
-
os.replace(tmp_path, HEALTH_PATH) # atômico — um crash a meio da rotação não zera o arquivo real
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _read_recent_records():
|
|
51
|
-
with _LOCK:
|
|
52
|
-
try:
|
|
53
|
-
with open(HEALTH_PATH, "r", encoding="utf-8") as f:
|
|
54
|
-
lines = f.readlines()
|
|
55
|
-
except FileNotFoundError:
|
|
56
|
-
return []
|
|
57
|
-
records = []
|
|
58
|
-
for line in lines[-_MAX_LINES_SCANNED:]:
|
|
59
|
-
line = line.strip()
|
|
60
|
-
if not line:
|
|
61
|
-
continue
|
|
62
|
-
try:
|
|
63
|
-
records.append(json.loads(line))
|
|
64
|
-
except json.JSONDecodeError:
|
|
65
|
-
continue
|
|
66
|
-
return records
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def recent_success_rate(tier_name, window=DEFAULT_WINDOW):
|
|
70
|
-
records = [r for r in _read_recent_records() if r.get("tier") == tier_name]
|
|
71
|
-
if not records:
|
|
72
|
-
return None
|
|
73
|
-
records = records[-window:]
|
|
74
|
-
successes = sum(1 for r in records if r.get("success"))
|
|
75
|
-
return successes / len(records)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def summary_for_tiers(tier_names, window=DEFAULT_WINDOW):
|
|
79
|
-
all_records = _read_recent_records()
|
|
80
|
-
summary = {}
|
|
81
|
-
for name in tier_names:
|
|
82
|
-
records = [r for r in all_records if r.get("tier") == name][-window:]
|
|
83
|
-
if not records:
|
|
84
|
-
summary[name] = None
|
|
85
|
-
else:
|
|
86
|
-
successes = sum(1 for r in records if r.get("success"))
|
|
87
|
-
summary[name] = successes / len(records)
|
|
88
|
-
return summary
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Histórico de sucesso/falha por Tier do web_search, lido de volta pelo router_model
|
|
3
|
+
antes de cada decisão — diferente de router-metrics.jsonl (router.py), que é write-only
|
|
4
|
+
e serve só de audit trail para o roteamento de smart_search.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import paths
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
HEALTH_PATH = os.path.join(paths.DATA_DIR, "web-search-health.jsonl")
|
|
13
|
+
DEFAULT_WINDOW = 20
|
|
14
|
+
_MAX_LINES_SCANNED = 2000
|
|
15
|
+
_MAX_LINES_BEFORE_ROTATE = 5000
|
|
16
|
+
|
|
17
|
+
# Todo record()/rotação passa por aqui: o daemon é ThreadingHTTPServer, então
|
|
18
|
+
# múltiplas requisições concorrentes podem chamar record() na mesma linha de
|
|
19
|
+
# tempo — sem lock, escritas de threads diferentes se intercalam no mesmo
|
|
20
|
+
# arquivo e corrompem linhas JSONL.
|
|
21
|
+
_LOCK = threading.Lock()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def record(tier_name, success):
|
|
25
|
+
os.makedirs(os.path.dirname(HEALTH_PATH), exist_ok=True)
|
|
26
|
+
entry = {"ts": time.time(), "tier": tier_name, "success": bool(success)}
|
|
27
|
+
with _LOCK:
|
|
28
|
+
with open(HEALTH_PATH, "a", encoding="utf-8") as f:
|
|
29
|
+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
30
|
+
_rotate_if_needed()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _rotate_if_needed():
|
|
34
|
+
# Já dentro de _LOCK (chamado só por record()). Mantém o arquivo limitado
|
|
35
|
+
# a _MAX_LINES_SCANNED linhas — sem isso, cada leitura futura reprocessa o
|
|
36
|
+
# arquivo inteiro por tempo indefinido de uso real.
|
|
37
|
+
try:
|
|
38
|
+
with open(HEALTH_PATH, "r", encoding="utf-8") as f:
|
|
39
|
+
lines = f.readlines()
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
return
|
|
42
|
+
if len(lines) <= _MAX_LINES_BEFORE_ROTATE:
|
|
43
|
+
return
|
|
44
|
+
tmp_path = HEALTH_PATH + ".tmp"
|
|
45
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
46
|
+
f.writelines(lines[-_MAX_LINES_SCANNED:])
|
|
47
|
+
os.replace(tmp_path, HEALTH_PATH) # atômico — um crash a meio da rotação não zera o arquivo real
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _read_recent_records():
|
|
51
|
+
with _LOCK:
|
|
52
|
+
try:
|
|
53
|
+
with open(HEALTH_PATH, "r", encoding="utf-8") as f:
|
|
54
|
+
lines = f.readlines()
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
return []
|
|
57
|
+
records = []
|
|
58
|
+
for line in lines[-_MAX_LINES_SCANNED:]:
|
|
59
|
+
line = line.strip()
|
|
60
|
+
if not line:
|
|
61
|
+
continue
|
|
62
|
+
try:
|
|
63
|
+
records.append(json.loads(line))
|
|
64
|
+
except json.JSONDecodeError:
|
|
65
|
+
continue
|
|
66
|
+
return records
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def recent_success_rate(tier_name, window=DEFAULT_WINDOW):
|
|
70
|
+
records = [r for r in _read_recent_records() if r.get("tier") == tier_name]
|
|
71
|
+
if not records:
|
|
72
|
+
return None
|
|
73
|
+
records = records[-window:]
|
|
74
|
+
successes = sum(1 for r in records if r.get("success"))
|
|
75
|
+
return successes / len(records)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def summary_for_tiers(tier_names, window=DEFAULT_WINDOW):
|
|
79
|
+
all_records = _read_recent_records()
|
|
80
|
+
summary = {}
|
|
81
|
+
for name in tier_names:
|
|
82
|
+
records = [r for r in all_records if r.get("tier") == name][-window:]
|
|
83
|
+
if not records:
|
|
84
|
+
summary[name] = None
|
|
85
|
+
else:
|
|
86
|
+
successes = sum(1 for r in records if r.get("success"))
|
|
87
|
+
summary[name] = successes / len(records)
|
|
88
|
+
return summary
|