@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/local_embedder.py
CHANGED
|
@@ -1,172 +1,172 @@
|
|
|
1
|
-
"""Embedder local do mcp-memory (sidecar granite em ~/.mcp-memory/embedding.properties) como reserva sem modelo no gateway.
|
|
2
|
-
|
|
3
|
-
Medido em 2026-10-05 no gabarito do rerank (42 alvos de código, sem rerank, docs/spikes/local_embedder_spike.py):
|
|
4
|
-
granite-embedding-107m-multilingual local 42/35/41 (no pool / 1º / top 5), MRR@5 0,892; gateway
|
|
5
|
-
text-embedding-3-small do gateway 42/35/40, 0,885; só lexical 38/29/36, 0,767.
|
|
6
|
-
"""
|
|
7
|
-
import http.client
|
|
8
|
-
import json
|
|
9
|
-
import os
|
|
10
|
-
import threading
|
|
11
|
-
import time
|
|
12
|
-
import urllib.parse
|
|
13
|
-
|
|
14
|
-
import config
|
|
15
|
-
import indexer
|
|
16
|
-
|
|
17
|
-
PREFIX = "local:"
|
|
18
|
-
PROPERTIES = os.path.join(os.path.expanduser("~"), ".mcp-memory", "embedding.properties")
|
|
19
|
-
BATCH = 16
|
|
20
|
-
INFO_TTL_S = 60
|
|
21
|
-
MAX_ATTEMPTS = 3
|
|
22
|
-
REQUEST_TIMEOUT_S = 60
|
|
23
|
-
_lock = threading.Lock()
|
|
24
|
-
_info = {"at": 0.0, "value": None}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def url():
|
|
28
|
-
"""Sidecar base URL; None when absent or `tei.url=off`. localhost becomes 127.0.0.1: on Windows ::1 is tried
|
|
29
|
-
first and a sidecar bound to IPv4 only costs ~2 s per connection (measured)."""
|
|
30
|
-
try:
|
|
31
|
-
with open(PROPERTIES, encoding="utf-8") as stream:
|
|
32
|
-
for line in stream:
|
|
33
|
-
key, _, value = line.strip().partition("=")
|
|
34
|
-
value = value.strip()
|
|
35
|
-
if key.strip() == "tei.url":
|
|
36
|
-
if not value or value.lower() == "off":
|
|
37
|
-
return None
|
|
38
|
-
parsed = urllib.parse.urlsplit(value.rstrip("/"))
|
|
39
|
-
if parsed.hostname == "localhost":
|
|
40
|
-
parsed = parsed._replace(netloc=parsed.netloc.replace("localhost", "127.0.0.1", 1))
|
|
41
|
-
return urllib.parse.urlunsplit(parsed)
|
|
42
|
-
except OSError:
|
|
43
|
-
return None
|
|
44
|
-
return None
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def configured():
|
|
48
|
-
return url() is not None
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _connection(base, timeout):
|
|
52
|
-
parsed = urllib.parse.urlsplit(base)
|
|
53
|
-
cls = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection
|
|
54
|
-
return cls(parsed.hostname, parsed.port, timeout=timeout)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _request(conn, method, path, body=None):
|
|
58
|
-
headers = {"Content-Type": "application/json"} if body is not None else {}
|
|
59
|
-
conn.request(method, path, body=json.dumps(body).encode("utf-8") if body is not None else None, headers=headers)
|
|
60
|
-
response = conn.getresponse()
|
|
61
|
-
data = response.read()
|
|
62
|
-
if response.status >= 400:
|
|
63
|
-
raise ConnectionError(f"local embedder answered HTTP {response.status}")
|
|
64
|
-
return json.loads(data)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _probe(base, timeout):
|
|
68
|
-
conn = _connection(base, timeout)
|
|
69
|
-
try:
|
|
70
|
-
data = _request(conn, "GET", "/info")
|
|
71
|
-
finally:
|
|
72
|
-
conn.close()
|
|
73
|
-
if not data.get("model_id") or int(data.get("dimension") or 0) <= 0:
|
|
74
|
-
raise ValueError("local embedder has no model_id/dimension in /info")
|
|
75
|
-
return {"model": PREFIX + data["model_id"], "dimensions": int(data["dimension"]), "url": base,
|
|
76
|
-
"sha256": data.get("model_sha256")}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def info(timeout=1.5):
|
|
80
|
-
"""Sidecar /info cached for INFO_TTL_S; None when not configured or not answering. The probe runs under the lock
|
|
81
|
-
so a slow failing probe never overwrites a newer good answer."""
|
|
82
|
-
with _lock:
|
|
83
|
-
if time.monotonic() - _info["at"] < INFO_TTL_S:
|
|
84
|
-
return _info["value"]
|
|
85
|
-
base, value = url(), None
|
|
86
|
-
if base:
|
|
87
|
-
try:
|
|
88
|
-
value = _probe(base, timeout)
|
|
89
|
-
except (OSError, ValueError, http.client.HTTPException):
|
|
90
|
-
value = None
|
|
91
|
-
_info.update(at=time.monotonic(), value=value)
|
|
92
|
-
return value
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def is_local(model):
|
|
96
|
-
return bool(model) and model.startswith(PREFIX)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def resolve(configured_model, current=None, mode=None):
|
|
100
|
-
"""Embedding model for the index, following config `local_models` (fallback | off | prefer).
|
|
101
|
-
|
|
102
|
-
An index already built with the local model is kept while the sidecar is briefly down, so a restart of the
|
|
103
|
-
sidecar never triggers a rebuild to lexical and back."""
|
|
104
|
-
mode = mode or config.local_models_mode()
|
|
105
|
-
if mode == "off":
|
|
106
|
-
return configured_model or indexer.LEXICAL_MODEL
|
|
107
|
-
if configured_model and mode == "fallback":
|
|
108
|
-
return configured_model
|
|
109
|
-
available = info()
|
|
110
|
-
if available:
|
|
111
|
-
return available["model"]
|
|
112
|
-
if is_local(current):
|
|
113
|
-
return current
|
|
114
|
-
return configured_model or indexer.LEXICAL_MODEL
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def _left(deadline):
|
|
118
|
-
if deadline is None:
|
|
119
|
-
return REQUEST_TIMEOUT_S
|
|
120
|
-
left = deadline - time.monotonic()
|
|
121
|
-
if left <= 0:
|
|
122
|
-
raise TimeoutError("Deadline exceeded before local embeddings finished.")
|
|
123
|
-
return min(REQUEST_TIMEOUT_S, left)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def embed(model, texts, deadline=None, cancel_check=None, on_retry=None):
|
|
127
|
-
base = url()
|
|
128
|
-
if not base:
|
|
129
|
-
raise RuntimeError(f"mcp-memory local embedder is not configured ({PROPERTIES}).")
|
|
130
|
-
# Fresh /info before and after: /embed does not echo the model, and a cached answer could let vectors of a
|
|
131
|
-
# different model reach the persistent vector cache under this model's contract.
|
|
132
|
-
try:
|
|
133
|
-
before = _probe(base, _left(deadline))
|
|
134
|
-
except (OSError, ValueError, http.client.HTTPException) as exc:
|
|
135
|
-
raise RuntimeError(f"mcp-memory local embedder is not responding ({base}): {exc}") from None
|
|
136
|
-
if before["model"] != model:
|
|
137
|
-
raise indexer.IndexCompatibilityError(
|
|
138
|
-
f"The local embedder is now {before['model']}; the index was built with {model} and must be rebuilt.")
|
|
139
|
-
vectors = []
|
|
140
|
-
conn = _connection(base, _left(deadline))
|
|
141
|
-
try:
|
|
142
|
-
for start in range(0, len(texts), BATCH):
|
|
143
|
-
part = texts[start:start + BATCH]
|
|
144
|
-
for attempt in range(1, MAX_ATTEMPTS + 1):
|
|
145
|
-
if cancel_check:
|
|
146
|
-
cancel_check()
|
|
147
|
-
conn.timeout = _left(deadline)
|
|
148
|
-
try:
|
|
149
|
-
got = _request(conn, "POST", "/embed", {"inputs": part})
|
|
150
|
-
break
|
|
151
|
-
except (OSError, http.client.HTTPException) as exc:
|
|
152
|
-
conn.close()
|
|
153
|
-
conn = _connection(base, _left(deadline))
|
|
154
|
-
if attempt == MAX_ATTEMPTS:
|
|
155
|
-
raise RuntimeError(f"Local embedder failed after {MAX_ATTEMPTS} attempts: {exc}") from None
|
|
156
|
-
if on_retry:
|
|
157
|
-
on_retry(attempt + 1, len(part))
|
|
158
|
-
delay = attempt
|
|
159
|
-
if deadline is not None and time.monotonic() + delay >= deadline:
|
|
160
|
-
raise TimeoutError("Not enough time left to retry the local embedder.") from None
|
|
161
|
-
time.sleep(delay)
|
|
162
|
-
if not isinstance(got, list) or len(got) != len(part):
|
|
163
|
-
raise indexer.IndexVectorError("Local embedder returned a vector count different from the input.")
|
|
164
|
-
for vector in got:
|
|
165
|
-
indexer.validate_vector(vector, before["dimensions"])
|
|
166
|
-
vectors += got
|
|
167
|
-
finally:
|
|
168
|
-
conn.close()
|
|
169
|
-
after = _probe(base, _left(deadline))
|
|
170
|
-
if (after["model"], after.get("sha256")) != (before["model"], before.get("sha256")):
|
|
171
|
-
raise indexer.IndexCompatibilityError("The local embedder changed models during the call; try again.")
|
|
172
|
-
return vectors
|
|
1
|
+
"""Embedder local do mcp-memory (sidecar granite em ~/.mcp-memory/embedding.properties) como reserva sem modelo no gateway.
|
|
2
|
+
|
|
3
|
+
Medido em 2026-10-05 no gabarito do rerank (42 alvos de código, sem rerank, docs/spikes/local_embedder_spike.py):
|
|
4
|
+
granite-embedding-107m-multilingual local 42/35/41 (no pool / 1º / top 5), MRR@5 0,892; gateway
|
|
5
|
+
text-embedding-3-small do gateway 42/35/40, 0,885; só lexical 38/29/36, 0,767.
|
|
6
|
+
"""
|
|
7
|
+
import http.client
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import urllib.parse
|
|
13
|
+
|
|
14
|
+
import config
|
|
15
|
+
import indexer
|
|
16
|
+
|
|
17
|
+
PREFIX = "local:"
|
|
18
|
+
PROPERTIES = os.path.join(os.path.expanduser("~"), ".mcp-memory", "embedding.properties")
|
|
19
|
+
BATCH = 16
|
|
20
|
+
INFO_TTL_S = 60
|
|
21
|
+
MAX_ATTEMPTS = 3
|
|
22
|
+
REQUEST_TIMEOUT_S = 60
|
|
23
|
+
_lock = threading.Lock()
|
|
24
|
+
_info = {"at": 0.0, "value": None}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def url():
|
|
28
|
+
"""Sidecar base URL; None when absent or `tei.url=off`. localhost becomes 127.0.0.1: on Windows ::1 is tried
|
|
29
|
+
first and a sidecar bound to IPv4 only costs ~2 s per connection (measured)."""
|
|
30
|
+
try:
|
|
31
|
+
with open(PROPERTIES, encoding="utf-8") as stream:
|
|
32
|
+
for line in stream:
|
|
33
|
+
key, _, value = line.strip().partition("=")
|
|
34
|
+
value = value.strip()
|
|
35
|
+
if key.strip() == "tei.url":
|
|
36
|
+
if not value or value.lower() == "off":
|
|
37
|
+
return None
|
|
38
|
+
parsed = urllib.parse.urlsplit(value.rstrip("/"))
|
|
39
|
+
if parsed.hostname == "localhost":
|
|
40
|
+
parsed = parsed._replace(netloc=parsed.netloc.replace("localhost", "127.0.0.1", 1))
|
|
41
|
+
return urllib.parse.urlunsplit(parsed)
|
|
42
|
+
except OSError:
|
|
43
|
+
return None
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def configured():
|
|
48
|
+
return url() is not None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _connection(base, timeout):
|
|
52
|
+
parsed = urllib.parse.urlsplit(base)
|
|
53
|
+
cls = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection
|
|
54
|
+
return cls(parsed.hostname, parsed.port, timeout=timeout)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _request(conn, method, path, body=None):
|
|
58
|
+
headers = {"Content-Type": "application/json"} if body is not None else {}
|
|
59
|
+
conn.request(method, path, body=json.dumps(body).encode("utf-8") if body is not None else None, headers=headers)
|
|
60
|
+
response = conn.getresponse()
|
|
61
|
+
data = response.read()
|
|
62
|
+
if response.status >= 400:
|
|
63
|
+
raise ConnectionError(f"local embedder answered HTTP {response.status}")
|
|
64
|
+
return json.loads(data)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _probe(base, timeout):
|
|
68
|
+
conn = _connection(base, timeout)
|
|
69
|
+
try:
|
|
70
|
+
data = _request(conn, "GET", "/info")
|
|
71
|
+
finally:
|
|
72
|
+
conn.close()
|
|
73
|
+
if not data.get("model_id") or int(data.get("dimension") or 0) <= 0:
|
|
74
|
+
raise ValueError("local embedder has no model_id/dimension in /info")
|
|
75
|
+
return {"model": PREFIX + data["model_id"], "dimensions": int(data["dimension"]), "url": base,
|
|
76
|
+
"sha256": data.get("model_sha256")}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def info(timeout=1.5):
|
|
80
|
+
"""Sidecar /info cached for INFO_TTL_S; None when not configured or not answering. The probe runs under the lock
|
|
81
|
+
so a slow failing probe never overwrites a newer good answer."""
|
|
82
|
+
with _lock:
|
|
83
|
+
if time.monotonic() - _info["at"] < INFO_TTL_S:
|
|
84
|
+
return _info["value"]
|
|
85
|
+
base, value = url(), None
|
|
86
|
+
if base:
|
|
87
|
+
try:
|
|
88
|
+
value = _probe(base, timeout)
|
|
89
|
+
except (OSError, ValueError, http.client.HTTPException):
|
|
90
|
+
value = None
|
|
91
|
+
_info.update(at=time.monotonic(), value=value)
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_local(model):
|
|
96
|
+
return bool(model) and model.startswith(PREFIX)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def resolve(configured_model, current=None, mode=None):
|
|
100
|
+
"""Embedding model for the index, following config `local_models` (fallback | off | prefer).
|
|
101
|
+
|
|
102
|
+
An index already built with the local model is kept while the sidecar is briefly down, so a restart of the
|
|
103
|
+
sidecar never triggers a rebuild to lexical and back."""
|
|
104
|
+
mode = mode or config.local_models_mode()
|
|
105
|
+
if mode == "off":
|
|
106
|
+
return configured_model or indexer.LEXICAL_MODEL
|
|
107
|
+
if configured_model and mode == "fallback":
|
|
108
|
+
return configured_model
|
|
109
|
+
available = info()
|
|
110
|
+
if available:
|
|
111
|
+
return available["model"]
|
|
112
|
+
if is_local(current):
|
|
113
|
+
return current
|
|
114
|
+
return configured_model or indexer.LEXICAL_MODEL
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _left(deadline):
|
|
118
|
+
if deadline is None:
|
|
119
|
+
return REQUEST_TIMEOUT_S
|
|
120
|
+
left = deadline - time.monotonic()
|
|
121
|
+
if left <= 0:
|
|
122
|
+
raise TimeoutError("Deadline exceeded before local embeddings finished.")
|
|
123
|
+
return min(REQUEST_TIMEOUT_S, left)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def embed(model, texts, deadline=None, cancel_check=None, on_retry=None):
|
|
127
|
+
base = url()
|
|
128
|
+
if not base:
|
|
129
|
+
raise RuntimeError(f"mcp-memory local embedder is not configured ({PROPERTIES}).")
|
|
130
|
+
# Fresh /info before and after: /embed does not echo the model, and a cached answer could let vectors of a
|
|
131
|
+
# different model reach the persistent vector cache under this model's contract.
|
|
132
|
+
try:
|
|
133
|
+
before = _probe(base, _left(deadline))
|
|
134
|
+
except (OSError, ValueError, http.client.HTTPException) as exc:
|
|
135
|
+
raise RuntimeError(f"mcp-memory local embedder is not responding ({base}): {exc}") from None
|
|
136
|
+
if before["model"] != model:
|
|
137
|
+
raise indexer.IndexCompatibilityError(
|
|
138
|
+
f"The local embedder is now {before['model']}; the index was built with {model} and must be rebuilt.")
|
|
139
|
+
vectors = []
|
|
140
|
+
conn = _connection(base, _left(deadline))
|
|
141
|
+
try:
|
|
142
|
+
for start in range(0, len(texts), BATCH):
|
|
143
|
+
part = texts[start:start + BATCH]
|
|
144
|
+
for attempt in range(1, MAX_ATTEMPTS + 1):
|
|
145
|
+
if cancel_check:
|
|
146
|
+
cancel_check()
|
|
147
|
+
conn.timeout = _left(deadline)
|
|
148
|
+
try:
|
|
149
|
+
got = _request(conn, "POST", "/embed", {"inputs": part})
|
|
150
|
+
break
|
|
151
|
+
except (OSError, http.client.HTTPException) as exc:
|
|
152
|
+
conn.close()
|
|
153
|
+
conn = _connection(base, _left(deadline))
|
|
154
|
+
if attempt == MAX_ATTEMPTS:
|
|
155
|
+
raise RuntimeError(f"Local embedder failed after {MAX_ATTEMPTS} attempts: {exc}") from None
|
|
156
|
+
if on_retry:
|
|
157
|
+
on_retry(attempt + 1, len(part))
|
|
158
|
+
delay = attempt
|
|
159
|
+
if deadline is not None and time.monotonic() + delay >= deadline:
|
|
160
|
+
raise TimeoutError("Not enough time left to retry the local embedder.") from None
|
|
161
|
+
time.sleep(delay)
|
|
162
|
+
if not isinstance(got, list) or len(got) != len(part):
|
|
163
|
+
raise indexer.IndexVectorError("Local embedder returned a vector count different from the input.")
|
|
164
|
+
for vector in got:
|
|
165
|
+
indexer.validate_vector(vector, before["dimensions"])
|
|
166
|
+
vectors += got
|
|
167
|
+
finally:
|
|
168
|
+
conn.close()
|
|
169
|
+
after = _probe(base, _left(deadline))
|
|
170
|
+
if (after["model"], after.get("sha256")) != (before["model"], before.get("sha256")):
|
|
171
|
+
raise indexer.IndexCompatibilityError("The local embedder changed models during the call; try again.")
|
|
172
|
+
return vectors
|
package/model_defaults.py
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
"""Preenche modelos recomendados somente após consultar o catálogo do gateway configurado."""
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
|
|
5
|
-
import config
|
|
6
|
-
import model_catalog
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# Matched as the last path/dot segment of the catalog id, so "gpt-4o-mini" also finds "openai/gpt-4o-mini" (OpenRouter)
|
|
10
|
-
# and "openai.gpt-4o-mini" (LiteLLM aliases). Router: small, no reasoning (it runs before tool calls).
|
|
11
|
-
RECOMMENDED_MODELS = {
|
|
12
|
-
"router_model": ("chat", "gpt-4o-mini"),
|
|
13
|
-
"scope_model": ("chat", "gpt-5-mini"),
|
|
14
|
-
"research_model": ("chat", "gpt-oss-20b"),
|
|
15
|
-
"embedding_model": ("embedding", "text-embedding-3-small"),
|
|
16
|
-
"rerank_model": ("rerank", "rerank-3-5"),
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _matching(catalog, name):
|
|
21
|
-
return next((model_id for model_id in sorted(catalog) if re.split(r"[/.:]", model_id)[-1] == name
|
|
22
|
-
or model_id.endswith(("/" + name, "." + name)) or model_id == name), None)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def ensure_recommended_models(key_ready):
|
|
26
|
-
"""Preenche só campos vazios e só com IDs presentes no catálogo permitido.
|
|
27
|
-
|
|
28
|
-
Retorna os campos ainda vazios e uma explicação curta para a tela de setup/tray.
|
|
29
|
-
Um arquivo corrompido ou uma seleção manual nunca é substituído.
|
|
30
|
-
"""
|
|
31
|
-
corrupted = config.corrupted_reason()
|
|
32
|
-
if corrupted:
|
|
33
|
-
return {"status": "config_corrupt", "missing": list(RECOMMENDED_MODELS),
|
|
34
|
-
"message": corrupted}
|
|
35
|
-
|
|
36
|
-
cfg = config.load_config()
|
|
37
|
-
missing = [field for field in RECOMMENDED_MODELS if not cfg.get(field)]
|
|
38
|
-
if not key_ready:
|
|
39
|
-
return {"status": "needs_key", "missing": missing,
|
|
40
|
-
"message": "Model gateway unavailable; check the base URL and the API key."}
|
|
41
|
-
if not missing:
|
|
42
|
-
return {"status": "ready", "missing": [], "message": "Models configured."}
|
|
43
|
-
|
|
44
|
-
try:
|
|
45
|
-
available = {mode: set(model_catalog.list_models(mode)) for mode in
|
|
46
|
-
{RECOMMENDED_MODELS[field][0] for field in missing}}
|
|
47
|
-
except Exception:
|
|
48
|
-
return {"status": "catalog_unavailable", "missing": missing,
|
|
49
|
-
"message": "Catalog unavailable. Check the base URL, the API key and the connection to the gateway."}
|
|
50
|
-
|
|
51
|
-
choices = {field: found for field in missing
|
|
52
|
-
for mode, name in [RECOMMENDED_MODELS[field]]
|
|
53
|
-
for found in [_matching(available[mode], name)] if found}
|
|
54
|
-
if choices:
|
|
55
|
-
def fill_empty(current):
|
|
56
|
-
for field, model_id in choices.items():
|
|
57
|
-
if not current.get(field):
|
|
58
|
-
current[field] = model_id
|
|
59
|
-
|
|
60
|
-
try:
|
|
61
|
-
cfg = config.update_config(fill_empty)
|
|
62
|
-
except (config.ConfigCorruptedError, OSError):
|
|
63
|
-
return {"status": "config_corrupt", "missing": missing,
|
|
64
|
-
"message": "Could not save the configuration. Open Smart Tool to check the file."}
|
|
65
|
-
|
|
66
|
-
missing = [field for field in RECOMMENDED_MODELS if not cfg.get(field)]
|
|
67
|
-
if missing:
|
|
68
|
-
return {"status": "needs_models", "missing": missing,
|
|
69
|
-
"message": "Some recommended models are not in this gateway's catalog. Choose them in the settings."}
|
|
70
|
-
return {"status": "ready", "missing": [], "message": "Recommended models configured."}
|
|
1
|
+
"""Preenche modelos recomendados somente após consultar o catálogo do gateway configurado."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import config
|
|
6
|
+
import model_catalog
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Matched as the last path/dot segment of the catalog id, so "gpt-4o-mini" also finds "openai/gpt-4o-mini" (OpenRouter)
|
|
10
|
+
# and "openai.gpt-4o-mini" (LiteLLM aliases). Router: small, no reasoning (it runs before tool calls).
|
|
11
|
+
RECOMMENDED_MODELS = {
|
|
12
|
+
"router_model": ("chat", "gpt-4o-mini"),
|
|
13
|
+
"scope_model": ("chat", "gpt-5-mini"),
|
|
14
|
+
"research_model": ("chat", "gpt-oss-20b"),
|
|
15
|
+
"embedding_model": ("embedding", "text-embedding-3-small"),
|
|
16
|
+
"rerank_model": ("rerank", "rerank-3-5"),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _matching(catalog, name):
|
|
21
|
+
return next((model_id for model_id in sorted(catalog) if re.split(r"[/.:]", model_id)[-1] == name
|
|
22
|
+
or model_id.endswith(("/" + name, "." + name)) or model_id == name), None)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ensure_recommended_models(key_ready):
|
|
26
|
+
"""Preenche só campos vazios e só com IDs presentes no catálogo permitido.
|
|
27
|
+
|
|
28
|
+
Retorna os campos ainda vazios e uma explicação curta para a tela de setup/tray.
|
|
29
|
+
Um arquivo corrompido ou uma seleção manual nunca é substituído.
|
|
30
|
+
"""
|
|
31
|
+
corrupted = config.corrupted_reason()
|
|
32
|
+
if corrupted:
|
|
33
|
+
return {"status": "config_corrupt", "missing": list(RECOMMENDED_MODELS),
|
|
34
|
+
"message": corrupted}
|
|
35
|
+
|
|
36
|
+
cfg = config.load_config()
|
|
37
|
+
missing = [field for field in RECOMMENDED_MODELS if not cfg.get(field)]
|
|
38
|
+
if not key_ready:
|
|
39
|
+
return {"status": "needs_key", "missing": missing,
|
|
40
|
+
"message": "Model gateway unavailable; check the base URL and the API key."}
|
|
41
|
+
if not missing:
|
|
42
|
+
return {"status": "ready", "missing": [], "message": "Models configured."}
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
available = {mode: set(model_catalog.list_models(mode)) for mode in
|
|
46
|
+
{RECOMMENDED_MODELS[field][0] for field in missing}}
|
|
47
|
+
except Exception:
|
|
48
|
+
return {"status": "catalog_unavailable", "missing": missing,
|
|
49
|
+
"message": "Catalog unavailable. Check the base URL, the API key and the connection to the gateway."}
|
|
50
|
+
|
|
51
|
+
choices = {field: found for field in missing
|
|
52
|
+
for mode, name in [RECOMMENDED_MODELS[field]]
|
|
53
|
+
for found in [_matching(available[mode], name)] if found}
|
|
54
|
+
if choices:
|
|
55
|
+
def fill_empty(current):
|
|
56
|
+
for field, model_id in choices.items():
|
|
57
|
+
if not current.get(field):
|
|
58
|
+
current[field] = model_id
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
cfg = config.update_config(fill_empty)
|
|
62
|
+
except (config.ConfigCorruptedError, OSError):
|
|
63
|
+
return {"status": "config_corrupt", "missing": missing,
|
|
64
|
+
"message": "Could not save the configuration. Open Smart Tool to check the file."}
|
|
65
|
+
|
|
66
|
+
missing = [field for field in RECOMMENDED_MODELS if not cfg.get(field)]
|
|
67
|
+
if missing:
|
|
68
|
+
return {"status": "needs_models", "missing": missing,
|
|
69
|
+
"message": "Some recommended models are not in this gateway's catalog. Choose them in the settings."}
|
|
70
|
+
return {"status": "ready", "missing": [], "message": "Recommended models configured."}
|
package/package.json
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@allansantos-dev/smart-tool",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Local MCP server that gives coding agents (Claude Code, Codex) cheaper, sharper tools than their built-in search. Windows.",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"author": "Allan Santos",
|
|
7
|
-
"homepage": "https://allansantos-dv.github.io/smart-tool/",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/AllanSantos-DV/smart-tool-dist.git"
|
|
11
|
-
},
|
|
12
|
-
"keywords": [
|
|
13
|
-
"mcp",
|
|
14
|
-
"claude-code",
|
|
15
|
-
"codex",
|
|
16
|
-
"semantic-search",
|
|
17
|
-
"web-search",
|
|
18
|
-
"coding-agent"
|
|
19
|
-
],
|
|
20
|
-
"bin": {
|
|
21
|
-
"smart-tool": "bin/smart-tool.mjs"
|
|
22
|
-
},
|
|
23
|
-
"os": [
|
|
24
|
-
"win32"
|
|
25
|
-
],
|
|
26
|
-
"cpu": [
|
|
27
|
-
"x64"
|
|
28
|
-
],
|
|
29
|
-
"engines": {
|
|
30
|
-
"node": ">=18"
|
|
31
|
-
},
|
|
32
|
-
"publishConfig": {
|
|
33
|
-
"access": "public"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@allansantos-dev/smart-tool",
|
|
3
|
+
"version": "0.9.2",
|
|
4
|
+
"description": "Local MCP server that gives coding agents (Claude Code, Codex) cheaper, sharper tools than their built-in search. Windows.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Allan Santos",
|
|
7
|
+
"homepage": "https://allansantos-dv.github.io/smart-tool/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/AllanSantos-DV/smart-tool-dist.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"mcp",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"codex",
|
|
16
|
+
"semantic-search",
|
|
17
|
+
"web-search",
|
|
18
|
+
"coding-agent"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"smart-tool": "bin/smart-tool.mjs"
|
|
22
|
+
},
|
|
23
|
+
"os": [
|
|
24
|
+
"win32"
|
|
25
|
+
],
|
|
26
|
+
"cpu": [
|
|
27
|
+
"x64"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|