@allansantos-dev/smart-tool 0.0.0-stage → 0.9.0

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 (77) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +32 -0
  4. package/README.md +116 -3
  5. package/atomic_io.py +94 -0
  6. package/autostart.py +199 -0
  7. package/bin/smart-tool.mjs +82 -0
  8. package/browser_service_client.py +232 -0
  9. package/capabilities.py +63 -0
  10. package/client_hooks.py +192 -0
  11. package/code_graph.py +403 -0
  12. package/code_graph_java.mjs +49 -0
  13. package/code_graph_js.cjs +135 -0
  14. package/config.py +241 -0
  15. package/daemon_launcher.py +165 -0
  16. package/directory_watch.py +131 -0
  17. package/document_text.py +69 -0
  18. package/duplicates.py +313 -0
  19. package/embedding_cache.py +99 -0
  20. package/endpoint_sync.py +179 -0
  21. package/folder_picker.py +73 -0
  22. package/freshness.py +278 -0
  23. package/gateway.py +220 -0
  24. package/hook_decision.py +96 -0
  25. package/hooks/pretool_router.py +45 -0
  26. package/index_inventory.py +75 -0
  27. package/index_profile.py +167 -0
  28. package/index_scope.py +458 -0
  29. package/index_views.py +121 -0
  30. package/indexer.py +878 -0
  31. package/install.py +251 -0
  32. package/install_runtime.py +249 -0
  33. package/integration_metrics.py +82 -0
  34. package/local_embedder.py +172 -0
  35. package/model_adapters/__init__.py +0 -0
  36. package/model_adapters/openai_compatible.py +57 -0
  37. package/model_catalog.py +74 -0
  38. package/model_client.py +38 -0
  39. package/model_defaults.py +70 -0
  40. package/package.json +35 -6
  41. package/paths.py +6 -0
  42. package/project_identity.py +42 -0
  43. package/project_insights_ui.py +38 -0
  44. package/project_maps_ui.py +96 -0
  45. package/project_monitor.py +204 -0
  46. package/project_scheduler.py +61 -0
  47. package/project_storage.py +165 -0
  48. package/project_store.py +220 -0
  49. package/projects_ui.py +146 -0
  50. package/requirements.txt +8 -0
  51. package/research_cache.py +171 -0
  52. package/research_metrics.py +110 -0
  53. package/router.py +582 -0
  54. package/secret_store.py +121 -0
  55. package/setup_ui.py +986 -0
  56. package/site_memory.py +53 -0
  57. package/smart_tool_daemon.py +3604 -0
  58. package/tray.py +245 -0
  59. package/update_check.py +22 -0
  60. package/usage_meter.py +124 -0
  61. package/version.py +18 -0
  62. package/view_compare.py +68 -0
  63. package/web_adapters/browser/browser_service.py +302 -0
  64. package/web_adapters/browser/crawl4ai_search.py +236 -0
  65. package/web_adapters/browser/requirements.txt +2 -0
  66. package/web_adapters/node/adapter-common.mjs +40 -0
  67. package/web_adapters/node/ddg-search.mjs +89 -0
  68. package/web_adapters/node/open-websearch-adapter.mjs +77 -0
  69. package/web_adapters/node/package-lock.json +2720 -0
  70. package/web_adapters/node/package.json +12 -0
  71. package/web_adapters/node/wikipedia-search.mjs +44 -0
  72. package/web_document_graph.py +71 -0
  73. package/web_fetch.py +242 -0
  74. package/web_search_adapters.py +193 -0
  75. package/web_search_ddgs.py +42 -0
  76. package/web_search_external.py +288 -0
  77. package/web_search_health.py +88 -0
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python3
2
+ """Trilha de calibração da memória (cosseno + rerank) e do teto de pesquisa:
3
+ `site_memory_similarity_threshold` pré-filtra tópicos, o reranker confirma o
4
+ reaproveitamento, e `research_max_iterations` limita o retorno por rodada. Write-only, como
5
+ `router-metrics.jsonl` — nada no daemon lê isto de volta; quem lê é
6
+ `.token-guard/analyze_research.py`.
7
+
8
+ Grava só o que a calibração usa: textos de consulta/tópico truncados e números. Nunca
9
+ snippets nem páginas, que são conteúdo de terceiros e não ajudam a decidir o valor.
10
+ """
11
+ import json
12
+ import os
13
+ import paths
14
+ import threading
15
+ import time
16
+
17
+ import atomic_io
18
+
19
+ METRICS_PATH = os.path.join(paths.DATA_DIR, "research-metrics.jsonl")
20
+ METRICS_MAX_BYTES = 2 * 1024 * 1024
21
+ MAX_TEXT_CHARS = 200
22
+ MAX_PAIRS = 10
23
+
24
+ # O daemon é multi-thread (fan-out síncrono e jobs de pesquisa em pools próprios): sem
25
+ # lock, dois appends grandes podem se intercalar na mesma linha.
26
+ _LOCK = threading.Lock()
27
+
28
+
29
+ def _short(text):
30
+ text = str(text or "")
31
+ if len(text) > MAX_TEXT_CHARS:
32
+ return text[:MAX_TEXT_CHARS] + "..."
33
+ return text
34
+
35
+
36
+ def _append(record):
37
+ record = {"ts": time.time(), **record}
38
+ try:
39
+ line = json.dumps(record, ensure_ascii=False) + "\n"
40
+ with _LOCK:
41
+ os.makedirs(os.path.dirname(METRICS_PATH), exist_ok=True)
42
+ atomic_io.rotate(METRICS_PATH, METRICS_MAX_BYTES)
43
+ with open(METRICS_PATH, "a", encoding="utf-8") as f:
44
+ f.write(line)
45
+ except (OSError, TypeError, ValueError):
46
+ # Métrica é para calibrar depois: disco cheio ou arquivo travado não pode derrubar
47
+ # uma pesquisa.
48
+ pass
49
+
50
+
51
+ def log_site_memory(query, threshold, scored_topics, recalled, rerank_scores=None, rerank_threshold=None):
52
+ """Uma correlação da memória do dia. `scored_topics`: `[(tópico, similaridade)]` de
53
+ todos os tópicos comparados — só os `MAX_PAIRS` mais altos vão pro disco, que são os
54
+ que decidem onde o limiar corta."""
55
+ try:
56
+ ranked = sorted(scored_topics, key=lambda pair: pair[1], reverse=True)
57
+ record = {
58
+ "kind": "site_memory",
59
+ "query": _short(query),
60
+ "threshold": threshold,
61
+ "topics": len(ranked),
62
+ "matched": sum(1 for _, score in ranked if score >= threshold),
63
+ "recalled": recalled,
64
+ "pairs": [
65
+ {
66
+ "topic": _short(topic), "score": round(float(score), 4),
67
+ **({"rerank_score": round(float(rerank_scores[topic]), 4)}
68
+ if rerank_scores is not None and topic in rerank_scores else {}),
69
+ }
70
+ for topic, score in ranked[:MAX_PAIRS]
71
+ ],
72
+ }
73
+ if rerank_scores is not None:
74
+ record["rerank_threshold"] = rerank_threshold
75
+ record["rerank_matched"] = sum(
76
+ score >= rerank_threshold for score in rerank_scores.values()
77
+ )
78
+ except Exception:
79
+ return
80
+ _append(record)
81
+
82
+
83
+ def log_research_job(topic, max_iterations, rounds, stopped_by, sources, elapsed_s):
84
+ """Um job `depth='research'` concluído. `rounds` já traz, por rodada, quanto ela
85
+ acrescentou (`new_urls`, `findings`) — é a curva de retorno marginal que decide o
86
+ teto."""
87
+ try:
88
+ record = {
89
+ "kind": "research",
90
+ "topic": _short(topic),
91
+ "max_iterations": max_iterations,
92
+ "stopped_by": stopped_by,
93
+ "sources": sources,
94
+ "elapsed_s": round(elapsed_s, 1),
95
+ "rounds": [
96
+ {
97
+ "query": _short(r.get("query")),
98
+ "results": r.get("results_count", 0),
99
+ "new_urls": r.get("new_urls", 0),
100
+ "findings": r.get("findings", 0),
101
+ "gaps": r.get("gaps", 0),
102
+ "continue": bool(r.get("continue")),
103
+ "reason": _short(r.get("reason")),
104
+ }
105
+ for r in rounds
106
+ ],
107
+ }
108
+ except Exception:
109
+ return
110
+ _append(record)