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.
Files changed (61) hide show
  1. agent/__init__.py +0 -0
  2. agent/cartographer.py +428 -0
  3. agent/chunk_quality.py +1119 -0
  4. agent/config.py +648 -0
  5. agent/config.yaml +1153 -0
  6. agent/eviction_advisor.py +447 -0
  7. agent/identifier_hint.py +68 -0
  8. agent/indexer/__init__.py +112 -0
  9. agent/indexer/_chunking.py +458 -0
  10. agent/indexer/_constants.py +105 -0
  11. agent/indexer/_core.py +878 -0
  12. agent/indexer/_types.py +157 -0
  13. agent/instance_registry.py +127 -0
  14. agent/llm_client.py +8 -0
  15. agent/model_cache.py +101 -0
  16. agent/prompt_templates.py +31 -0
  17. agent/searcher.py +848 -0
  18. agent/strategy_selector.py +299 -0
  19. agent/symbol_graph/__init__.py +131 -0
  20. agent/symbol_graph/_constants.py +366 -0
  21. agent/symbol_graph/_extraction.py +528 -0
  22. agent/symbol_graph/_graph.py +1878 -0
  23. agent/symbol_graph/_types.py +35 -0
  24. agent/templates/claude_md.md +65 -0
  25. agent/templates/claude_md_search_only.md +27 -0
  26. agent/templates/cursor_mcp.json.template +7 -0
  27. agent/templates/cursor_rules_header.txt +5 -0
  28. agent/templates/hook_no_double_recall.txt +1 -0
  29. agent/templates/mcp.json.template +8 -0
  30. agent/templates/session_start_guidance_default.txt +3 -0
  31. agent/templates/session_start_guidance_hooks_aware.txt +1 -0
  32. agent/templates/tool_loading_guidance_claude.txt +1 -0
  33. agent/templates/tool_loading_guidance_claude_search_only.txt +1 -0
  34. agent/templates/vscode_mcp.json.template +8 -0
  35. agent/tool_necessity_probe.py +177 -0
  36. agent/version_stamp.py +58 -0
  37. agent/watcher.py +515 -0
  38. agent/working_context_store/__init__.py +76 -0
  39. agent/working_context_store/_audit.py +46 -0
  40. agent/working_context_store/_encryption.py +75 -0
  41. agent/working_context_store/_store.py +1130 -0
  42. agent/working_context_store/_types.py +50 -0
  43. api.py +101 -0
  44. app/__init__.py +0 -0
  45. app/models.py +368 -0
  46. app/routes.py +451 -0
  47. app/service.py +1055 -0
  48. integrations/__init__.py +0 -0
  49. integrations/mcp_server/__init__.py +73 -0
  50. integrations/mcp_server/_dispatch.py +782 -0
  51. integrations/mcp_server/_schemas.py +484 -0
  52. integrations/mcp_server/_session.py +86 -0
  53. integrations/vscode_bridge.py +71 -0
  54. integrations/workspace_detect.py +259 -0
  55. main.py +2037 -0
  56. vectr-1.0.0.dist-info/METADATA +37 -0
  57. vectr-1.0.0.dist-info/RECORD +61 -0
  58. vectr-1.0.0.dist-info/WHEEL +5 -0
  59. vectr-1.0.0.dist-info/entry_points.txt +2 -0
  60. vectr-1.0.0.dist-info/licenses/LICENSE +21 -0
  61. vectr-1.0.0.dist-info/top_level.txt +5 -0
@@ -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
@@ -0,0 +1,8 @@
1
+ """Reports the configured LLM model name for API response metadata."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+
6
+
7
+ def get_model() -> str:
8
+ return os.getenv("LLM_MODEL", "claude-sonnet-4-6")
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")