vectr 1.0.0__py3-none-any.whl
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.
- agent/__init__.py +0 -0
- agent/cartographer.py +428 -0
- agent/chunk_quality.py +1119 -0
- agent/config.py +648 -0
- agent/config.yaml +1153 -0
- agent/eviction_advisor.py +447 -0
- agent/identifier_hint.py +68 -0
- agent/indexer/__init__.py +112 -0
- agent/indexer/_chunking.py +458 -0
- agent/indexer/_constants.py +105 -0
- agent/indexer/_core.py +878 -0
- agent/indexer/_types.py +157 -0
- agent/instance_registry.py +127 -0
- agent/llm_client.py +8 -0
- agent/model_cache.py +101 -0
- agent/prompt_templates.py +31 -0
- agent/searcher.py +848 -0
- agent/strategy_selector.py +299 -0
- agent/symbol_graph/__init__.py +131 -0
- agent/symbol_graph/_constants.py +366 -0
- agent/symbol_graph/_extraction.py +528 -0
- agent/symbol_graph/_graph.py +1878 -0
- agent/symbol_graph/_types.py +35 -0
- agent/templates/claude_md.md +65 -0
- agent/templates/claude_md_search_only.md +27 -0
- agent/templates/cursor_mcp.json.template +7 -0
- agent/templates/cursor_rules_header.txt +5 -0
- agent/templates/hook_no_double_recall.txt +1 -0
- agent/templates/mcp.json.template +8 -0
- agent/templates/session_start_guidance_default.txt +3 -0
- agent/templates/session_start_guidance_hooks_aware.txt +1 -0
- agent/templates/tool_loading_guidance_claude.txt +1 -0
- agent/templates/tool_loading_guidance_claude_search_only.txt +1 -0
- agent/templates/vscode_mcp.json.template +8 -0
- agent/tool_necessity_probe.py +177 -0
- agent/version_stamp.py +58 -0
- agent/watcher.py +515 -0
- agent/working_context_store/__init__.py +76 -0
- agent/working_context_store/_audit.py +46 -0
- agent/working_context_store/_encryption.py +75 -0
- agent/working_context_store/_store.py +1130 -0
- agent/working_context_store/_types.py +50 -0
- api.py +101 -0
- app/__init__.py +0 -0
- app/models.py +368 -0
- app/routes.py +451 -0
- app/service.py +1055 -0
- integrations/__init__.py +0 -0
- integrations/mcp_server/__init__.py +73 -0
- integrations/mcp_server/_dispatch.py +782 -0
- integrations/mcp_server/_schemas.py +484 -0
- integrations/mcp_server/_session.py +86 -0
- integrations/vscode_bridge.py +71 -0
- integrations/workspace_detect.py +259 -0
- main.py +2037 -0
- vectr-1.0.0.dist-info/METADATA +37 -0
- vectr-1.0.0.dist-info/RECORD +61 -0
- vectr-1.0.0.dist-info/WHEEL +5 -0
- vectr-1.0.0.dist-info/entry_points.txt +2 -0
- vectr-1.0.0.dist-info/licenses/LICENSE +21 -0
- vectr-1.0.0.dist-info/top_level.txt +5 -0
agent/indexer/_types.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Chunk dataclass, EmbedProvider protocol, embed provider implementations."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from agent.config import (
|
|
9
|
+
EMBEDDING_DEFAULT_MODEL as _EMBEDDING_DEFAULT_MODEL,
|
|
10
|
+
EMBEDDING_MAX_SEQ_LENGTH as _EMBEDDING_MAX_SEQ_LENGTH,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Chunk dataclass
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CodeChunk:
|
|
20
|
+
chunk_id: str
|
|
21
|
+
content: str
|
|
22
|
+
file_path: str
|
|
23
|
+
language: str
|
|
24
|
+
node_type: str
|
|
25
|
+
start_line: int
|
|
26
|
+
end_line: int
|
|
27
|
+
symbol_name: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Embedding provider protocol
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
class EmbedProvider(Protocol):
|
|
35
|
+
def embed(self, texts: list[str]) -> list[list[float]]: ...
|
|
36
|
+
|
|
37
|
+
def embed_query(self, texts: list[str]) -> list[list[float]]:
|
|
38
|
+
"""Embed search-query text. Distinct from `embed()` (document/indexing side)
|
|
39
|
+
because some asymmetric embedding models require a different, model-
|
|
40
|
+
registered prompt for queries than for the passages they're matched
|
|
41
|
+
against. Providers with no such distinction may just delegate to
|
|
42
|
+
`embed()`.
|
|
43
|
+
"""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LocalEmbedProvider:
|
|
48
|
+
"""Uses sentence-transformers (no API key). Default model: config.yaml embedding.default_model
|
|
49
|
+
(UPG-EMBEDDER-SWAP-GRANITE)."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, model_name: str = _EMBEDDING_DEFAULT_MODEL) -> None:
|
|
52
|
+
from agent.model_cache import load_with_offline_preference, suppress_model_load_noise
|
|
53
|
+
suppress_model_load_noise()
|
|
54
|
+
import torch
|
|
55
|
+
from sentence_transformers import SentenceTransformer
|
|
56
|
+
cache_dir = Path.home() / ".cache" / "vectr" / "models"
|
|
57
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
# UPG-RERANKER-HF-NETWORK: prefer an offline (local_files_only) load
|
|
59
|
+
# when this model is already fully cached, so a warm daemon start
|
|
60
|
+
# never makes live huggingface.co calls (HEAD/GET on config.json,
|
|
61
|
+
# tokenizer_config.json, repo tree listings, ...) just to re-confirm
|
|
62
|
+
# a cache it already has. Falls back to a network-enabled load on a
|
|
63
|
+
# genuine cache miss (first run) so that UX is unchanged.
|
|
64
|
+
self._model = load_with_offline_preference(
|
|
65
|
+
# torch_dtype float32 is load-bearing on CPU (UPG-EMBED-CPU-DTYPE):
|
|
66
|
+
# transformers honors the checkpoint's declared dtype, and a model
|
|
67
|
+
# shipping bfloat16 weights (the current default does) runs through
|
|
68
|
+
# software-emulated bf16 matmuls on CPU — measured 12x slower than
|
|
69
|
+
# float32 for identical inputs. On CPU float32 is strictly correct;
|
|
70
|
+
# revisit only if a GPU/MPS device path is ever added.
|
|
71
|
+
lambda local_only: SentenceTransformer(
|
|
72
|
+
model_name,
|
|
73
|
+
cache_folder=str(cache_dir),
|
|
74
|
+
trust_remote_code=True,
|
|
75
|
+
device="cpu",
|
|
76
|
+
local_files_only=local_only,
|
|
77
|
+
model_kwargs={"torch_dtype": torch.float32},
|
|
78
|
+
),
|
|
79
|
+
model_name,
|
|
80
|
+
str(cache_dir),
|
|
81
|
+
)
|
|
82
|
+
# Cap the encoder's sequence length (config.yaml embedding.max_seq_length).
|
|
83
|
+
# A long-context model's own config otherwise wins — the current default
|
|
84
|
+
# declares 8192, and encoding long chunks at full length on CPU has
|
|
85
|
+
# quadratic-attention cost that stalls a full-corpus index for hours.
|
|
86
|
+
# Only ever lowers: a model that already declares less keeps its own cap.
|
|
87
|
+
current_cap = getattr(self._model, "max_seq_length", None)
|
|
88
|
+
if current_cap is None or current_cap > _EMBEDDING_MAX_SEQ_LENGTH:
|
|
89
|
+
self._model.max_seq_length = _EMBEDDING_MAX_SEQ_LENGTH
|
|
90
|
+
# Asymmetric embedding models (arctic-embed and others) register a "query"
|
|
91
|
+
# prompt in their sentence-transformers config that must be prepended to
|
|
92
|
+
# search queries but NOT to the documents/chunks being indexed. Detected from
|
|
93
|
+
# the loaded model itself (never hardcoded) so symmetric models — which
|
|
94
|
+
# register no such prompt — embed queries and documents identically, unchanged.
|
|
95
|
+
self._has_query_prompt = "query" in getattr(self._model, "prompts", {})
|
|
96
|
+
|
|
97
|
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
|
98
|
+
embeddings = self._model.encode(
|
|
99
|
+
texts,
|
|
100
|
+
convert_to_numpy=True,
|
|
101
|
+
normalize_embeddings=True,
|
|
102
|
+
show_progress_bar=False,
|
|
103
|
+
)
|
|
104
|
+
return embeddings.tolist()
|
|
105
|
+
|
|
106
|
+
def embed_query(self, texts: list[str]) -> list[list[float]]:
|
|
107
|
+
if not self._has_query_prompt:
|
|
108
|
+
return self.embed(texts)
|
|
109
|
+
embeddings = self._model.encode(
|
|
110
|
+
texts,
|
|
111
|
+
prompt_name="query",
|
|
112
|
+
convert_to_numpy=True,
|
|
113
|
+
normalize_embeddings=True,
|
|
114
|
+
show_progress_bar=False,
|
|
115
|
+
)
|
|
116
|
+
return embeddings.tolist()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class VoyageEmbedProvider:
|
|
120
|
+
"""Uses Voyage AI code embedding model (requires VOYAGE_API_KEY)."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, model_name: str = "voyage-code-2") -> None:
|
|
123
|
+
import voyageai # type: ignore
|
|
124
|
+
self._client = voyageai.Client()
|
|
125
|
+
self._model = model_name
|
|
126
|
+
|
|
127
|
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
|
128
|
+
result = self._client.embed(texts, model=self._model)
|
|
129
|
+
return result.embeddings
|
|
130
|
+
|
|
131
|
+
def embed_query(self, texts: list[str]) -> list[list[float]]:
|
|
132
|
+
return self.embed(texts)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class OpenAIEmbedProvider:
|
|
136
|
+
"""Uses OpenAI embedding model (requires OPENAI_API_KEY)."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, model_name: str = "text-embedding-3-small") -> None:
|
|
139
|
+
from openai import OpenAI
|
|
140
|
+
self._client = OpenAI()
|
|
141
|
+
self._model = model_name
|
|
142
|
+
|
|
143
|
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
|
144
|
+
response = self._client.embeddings.create(input=texts, model=self._model)
|
|
145
|
+
return [item.embedding for item in response.data]
|
|
146
|
+
|
|
147
|
+
def embed_query(self, texts: list[str]) -> list[list[float]]:
|
|
148
|
+
return self.embed(texts)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_embed_provider(model_spec: str) -> EmbedProvider:
|
|
152
|
+
"""Factory: parse VECTR_EMBED_MODEL and return the right provider."""
|
|
153
|
+
if model_spec.startswith("voyage"):
|
|
154
|
+
return VoyageEmbedProvider(model_spec)
|
|
155
|
+
if model_spec.startswith("openai/"):
|
|
156
|
+
return OpenAIEmbedProvider(model_spec.split("/", 1)[1])
|
|
157
|
+
return LocalEmbedProvider(model_spec)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""~/.vectr/instances.json — per-workspace daemon registry.
|
|
2
|
+
|
|
3
|
+
Tracks all running vectr daemons so multiple IDE windows can each
|
|
4
|
+
get their own vectr instance on its own port.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import socket
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
REGISTRY_PATH = Path.home() / ".vectr" / "instances.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def workspace_hash(path: str) -> str:
|
|
20
|
+
"""sha256(absolute_workspace_path)[:12] — same prefix used for cache dirs."""
|
|
21
|
+
return hashlib.sha256(path.encode()).hexdigest()[:12]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_pid_alive(pid: int) -> bool:
|
|
25
|
+
try:
|
|
26
|
+
os.kill(pid, 0)
|
|
27
|
+
return True
|
|
28
|
+
except ProcessLookupError:
|
|
29
|
+
return False
|
|
30
|
+
except PermissionError:
|
|
31
|
+
return True # process exists, we lack permission to signal it
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _port_is_free(port: int) -> bool:
|
|
35
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
36
|
+
try:
|
|
37
|
+
s.bind(("127.0.0.1", port))
|
|
38
|
+
return True
|
|
39
|
+
except OSError:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class InstanceRegistry:
|
|
44
|
+
def __init__(self, registry_path: Path = REGISTRY_PATH) -> None:
|
|
45
|
+
self._path = registry_path
|
|
46
|
+
|
|
47
|
+
def _read(self) -> dict[str, Any]:
|
|
48
|
+
if not self._path.exists():
|
|
49
|
+
return {}
|
|
50
|
+
try:
|
|
51
|
+
return json.loads(self._path.read_text())
|
|
52
|
+
except (json.JSONDecodeError, OSError):
|
|
53
|
+
return {}
|
|
54
|
+
|
|
55
|
+
def _write(self, data: dict[str, Any]) -> None:
|
|
56
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
tmp = self._path.with_suffix(".tmp")
|
|
58
|
+
tmp.write_text(json.dumps(data, indent=2))
|
|
59
|
+
os.rename(tmp, self._path)
|
|
60
|
+
|
|
61
|
+
def prune_dead(self) -> None:
|
|
62
|
+
"""Remove entries whose PIDs are no longer alive."""
|
|
63
|
+
data = self._read()
|
|
64
|
+
live = {k: v for k, v in data.items() if _is_pid_alive(v["pid"])}
|
|
65
|
+
if len(live) != len(data):
|
|
66
|
+
self._write(live)
|
|
67
|
+
|
|
68
|
+
def get(self, ws_hash: str) -> dict[str, Any] | None:
|
|
69
|
+
return self._read().get(ws_hash)
|
|
70
|
+
|
|
71
|
+
def list_all(self) -> dict[str, Any]:
|
|
72
|
+
return self._read()
|
|
73
|
+
|
|
74
|
+
def register(
|
|
75
|
+
self,
|
|
76
|
+
ws_hash: str,
|
|
77
|
+
workspace: str,
|
|
78
|
+
port: int,
|
|
79
|
+
pid: int,
|
|
80
|
+
extra_roots: list[str] | None = None,
|
|
81
|
+
code_workspace_file: str | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
data = self._read()
|
|
84
|
+
data[ws_hash] = {
|
|
85
|
+
"workspace": workspace,
|
|
86
|
+
"port": port,
|
|
87
|
+
"pid": pid,
|
|
88
|
+
"started_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
89
|
+
# Extra roots + the originating .code-workspace file (if any) are
|
|
90
|
+
# recorded here so `vectr status` can show what this instance
|
|
91
|
+
# actually serves without re-deriving it from CLI args it never
|
|
92
|
+
# saw (UPG-CLI-STATUS-MODE).
|
|
93
|
+
"extra_roots": extra_roots or [],
|
|
94
|
+
"code_workspace_file": code_workspace_file,
|
|
95
|
+
}
|
|
96
|
+
self._write(data)
|
|
97
|
+
|
|
98
|
+
def unregister(self, ws_hash: str) -> None:
|
|
99
|
+
data = self._read()
|
|
100
|
+
data.pop(ws_hash, None)
|
|
101
|
+
self._write(data)
|
|
102
|
+
|
|
103
|
+
def find_free_port(self, ws_hash: str, preferred_port: int) -> int:
|
|
104
|
+
"""Allocate a port per spec algorithm:
|
|
105
|
+
|
|
106
|
+
1. If ws_hash has a live entry → return its port (caller detects no-op).
|
|
107
|
+
2. If ws_hash has a dead entry → try to reuse its previous port first
|
|
108
|
+
(avoids rewriting .mcp.json).
|
|
109
|
+
3. Scan from preferred_port upward until a free port binds (up to 100 tries).
|
|
110
|
+
"""
|
|
111
|
+
entry = self._read().get(ws_hash)
|
|
112
|
+
|
|
113
|
+
if entry is not None:
|
|
114
|
+
pid, port = entry["pid"], entry["port"]
|
|
115
|
+
if _is_pid_alive(pid):
|
|
116
|
+
return port # caller should treat as no-op
|
|
117
|
+
if _port_is_free(port):
|
|
118
|
+
return port
|
|
119
|
+
|
|
120
|
+
for offset in range(100):
|
|
121
|
+
candidate = preferred_port + offset
|
|
122
|
+
if _port_is_free(candidate):
|
|
123
|
+
return candidate
|
|
124
|
+
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
f"No free port found in range {preferred_port}–{preferred_port + 99}"
|
|
127
|
+
)
|
agent/llm_client.py
ADDED
agent/model_cache.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Shared local-model-cache helpers for embedder/reranker loading.
|
|
2
|
+
|
|
3
|
+
Both the embedder (agent/indexer/_types.py:LocalEmbedProvider) and the
|
|
4
|
+
reranker (agent/searcher.py:_Reranker) load a Hugging Face model that, in
|
|
5
|
+
the common case, is already fully present in vectr's local model cache
|
|
6
|
+
(``~/.cache/vectr/models``). Without an explicit offline hint,
|
|
7
|
+
``sentence_transformers`` still performs live network calls against
|
|
8
|
+
huggingface.co (HEAD/GET on config.json, tokenizer_config.json, repo tree
|
|
9
|
+
listings, ...) to check for a newer revision before falling back to the
|
|
10
|
+
cache — costing seconds of network-dependent latency on every daemon
|
|
11
|
+
start and failing hard on an air-gapped or proxied machine despite a
|
|
12
|
+
complete local cache (UPG-RERANKER-HF-NETWORK).
|
|
13
|
+
|
|
14
|
+
``load_with_offline_preference`` is the one entry point both load sites
|
|
15
|
+
use: it checks whether the model is already cached via
|
|
16
|
+
``huggingface_hub``'s own cache-lookup API (never re-deriving the HF
|
|
17
|
+
``models--org--name`` on-disk layout by hand), prefers an offline
|
|
18
|
+
(``local_files_only=True``) load when it is, and always falls back to a
|
|
19
|
+
normal network-enabled load — either on a genuine cache miss (first run)
|
|
20
|
+
or if the offline load raises anyway (an incomplete/corrupted cache
|
|
21
|
+
entry) — so first-run UX is never broken by this change.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Callable, TypeVar
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def suppress_model_load_noise() -> None:
|
|
31
|
+
"""Silence tqdm download/fetch progress bars and INFO-level logging that
|
|
32
|
+
sentence-transformers' underlying huggingface_hub/transformers
|
|
33
|
+
dependencies print on model load (UPG-CLI-SMALL-UX): implementation
|
|
34
|
+
noise to a CLI user watching `vectr watch` index a workspace in the
|
|
35
|
+
foreground, and clutter in the daemon's own log file for `start`/
|
|
36
|
+
`restart` (whose stdout/stderr are redirected there, not the terminal,
|
|
37
|
+
but a user tailing the log while `vectr status` reports "still
|
|
38
|
+
loading" hits the same noise).
|
|
39
|
+
|
|
40
|
+
Uses each library's own official suppression API rather than setting
|
|
41
|
+
environment variables, since some versions of these libraries only read
|
|
42
|
+
the env var once at import time — calling the API instead works
|
|
43
|
+
regardless of import order or of which module happens to import
|
|
44
|
+
sentence_transformers first. Best-effort: never raises, so an older or
|
|
45
|
+
newer dependency version missing one of these functions never breaks
|
|
46
|
+
model loading.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
from huggingface_hub.utils import disable_progress_bars
|
|
50
|
+
disable_progress_bars()
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
try:
|
|
54
|
+
from transformers.utils import logging as _hf_logging
|
|
55
|
+
_hf_logging.set_verbosity_error()
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_model_cached(model_name: str, cache_dir: str) -> bool:
|
|
61
|
+
"""Best-effort check for whether ``model_name`` already has a snapshot in
|
|
62
|
+
``cache_dir`` (vectr's Hugging Face cache root).
|
|
63
|
+
|
|
64
|
+
Probes for ``config.json`` via ``huggingface_hub.try_to_load_from_cache`` —
|
|
65
|
+
every HF model repo vectr loads (sentence-transformers embedders and
|
|
66
|
+
cross-encoder rerankers alike) ships one. This is a proxy for "this
|
|
67
|
+
repo's snapshot already exists locally", not a guarantee every auxiliary
|
|
68
|
+
file is present; ``load_with_offline_preference`` below still falls back
|
|
69
|
+
to a network-enabled load if the offline attempt raises anyway.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
from huggingface_hub import try_to_load_from_cache
|
|
73
|
+
except Exception:
|
|
74
|
+
return False
|
|
75
|
+
try:
|
|
76
|
+
hit = try_to_load_from_cache(model_name, "config.json", cache_dir=cache_dir)
|
|
77
|
+
except Exception:
|
|
78
|
+
return False
|
|
79
|
+
return isinstance(hit, str)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_with_offline_preference(
|
|
83
|
+
build_fn: Callable[[bool], T], model_name: str, cache_dir: str
|
|
84
|
+
) -> T:
|
|
85
|
+
"""Instantiate a Hugging-Face-backed model, preferring an offline load
|
|
86
|
+
(no network calls at all) when ``model_name`` is already present in
|
|
87
|
+
``cache_dir``, and falling back to a normal network-enabled load either
|
|
88
|
+
on a genuine cache miss or if the offline load raises anyway.
|
|
89
|
+
|
|
90
|
+
``build_fn(local_files_only)`` must construct and return the model,
|
|
91
|
+
passing ``local_files_only`` straight through to the underlying
|
|
92
|
+
``from_pretrained`` call (e.g. via ``SentenceTransformer(...,
|
|
93
|
+
local_files_only=local_files_only)`` /
|
|
94
|
+
``CrossEncoder(..., local_files_only=local_files_only)``).
|
|
95
|
+
"""
|
|
96
|
+
if is_model_cached(model_name, cache_dir):
|
|
97
|
+
try:
|
|
98
|
+
return build_fn(True)
|
|
99
|
+
except Exception:
|
|
100
|
+
pass # incomplete/corrupted cache entry — fall through to network
|
|
101
|
+
return build_fn(False)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Loader for vectr's bundled prompt/config templates (UPG-PROMPTS-AS-DATA).
|
|
2
|
+
|
|
3
|
+
`main.py`'s IDE-integration writers (CLAUDE.md, session-start guidance,
|
|
4
|
+
`.mcp.json` variants, the Cursor rules frontmatter, the hook
|
|
5
|
+
no-double-recall line) used to embed these as module-level Python string
|
|
6
|
+
constants. They are plain text/markdown/JSON content with `.format()`
|
|
7
|
+
placeholders, not code, so they live in `agent/templates/` and are loaded
|
|
8
|
+
here instead — keeping prompt copy editable without touching Python and
|
|
9
|
+
letting non-engineers review/tune it as data.
|
|
10
|
+
|
|
11
|
+
Resolved via ``importlib.resources`` (not a ``cwd``-relative ``open()``) so
|
|
12
|
+
this works identically whether vectr runs from the repository checkout or
|
|
13
|
+
as an installed wheel (global binary), and results are cached since these
|
|
14
|
+
files are read repeatedly (every `vectr init` / IDE-config write) but never
|
|
15
|
+
change within a process lifetime.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib.resources as _ilr
|
|
20
|
+
from functools import cache
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@cache
|
|
24
|
+
def load_template(name: str) -> str:
|
|
25
|
+
"""Read and return the exact text content of `agent/templates/<name>`.
|
|
26
|
+
|
|
27
|
+
Caching is safe because these are static, packaged files — not
|
|
28
|
+
per-workspace or per-request state.
|
|
29
|
+
"""
|
|
30
|
+
resource = _ilr.files("agent").joinpath("templates").joinpath(name)
|
|
31
|
+
return resource.read_text(encoding="utf-8")
|