@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +50 -23
  2. package/NOTICE +32 -32
  3. package/README.md +117 -116
  4. package/atomic_io.py +94 -94
  5. package/bin/smart-tool.mjs +5 -1
  6. package/client_hooks.py +192 -192
  7. package/code_graph_java.mjs +49 -49
  8. package/code_graph_js.cjs +135 -135
  9. package/duplicates.py +313 -313
  10. package/embedding_cache.py +99 -99
  11. package/endpoint_sync.py +179 -179
  12. package/gateway.py +220 -220
  13. package/hook_decision.py +96 -96
  14. package/index_inventory.py +75 -75
  15. package/indexer.py +878 -878
  16. package/install.py +113 -11
  17. package/install_runtime.py +101 -35
  18. package/integration_metrics.py +82 -82
  19. package/local_embedder.py +172 -172
  20. package/model_defaults.py +70 -70
  21. package/package.json +35 -35
  22. package/project_insights_ui.py +38 -38
  23. package/project_monitor.py +204 -204
  24. package/project_store.py +220 -220
  25. package/projects_ui.py +146 -146
  26. package/requirements.txt +8 -8
  27. package/research_cache.py +171 -171
  28. package/research_metrics.py +110 -110
  29. package/router.py +582 -582
  30. package/secret_store.py +121 -121
  31. package/setup_ui.py +1011 -986
  32. package/smart_tool_daemon.py +3604 -3604
  33. package/version.py +1 -1
  34. package/web_adapters/browser/fetch_camoufox.py +69 -0
  35. package/web_adapters/node/ddg-search.mjs +89 -89
  36. package/web_adapters/node/open-websearch-adapter.mjs +77 -77
  37. package/web_adapters/node/package.json +12 -12
  38. package/web_adapters/node/wikipedia-search.mjs +44 -44
  39. package/web_search_adapters.py +193 -193
  40. package/web_search_external.py +288 -288
  41. package/web_search_health.py +88 -88
@@ -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