velune-cli 0.9.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 (279) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +208 -0
  5. velune/cli/autocomplete.py +80 -0
  6. velune/cli/banner.py +60 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +175 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +228 -0
  11. velune/cli/commands/config.py +224 -0
  12. velune/cli/commands/daemon.py +88 -0
  13. velune/cli/commands/doctor.py +721 -0
  14. velune/cli/commands/init.py +170 -0
  15. velune/cli/commands/mcp.py +82 -0
  16. velune/cli/commands/memory.py +293 -0
  17. velune/cli/commands/models.py +683 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +270 -0
  20. velune/cli/commands/setup.py +184 -0
  21. velune/cli/commands/workspace.py +249 -0
  22. velune/cli/context.py +36 -0
  23. velune/cli/councilmodel_ui.py +199 -0
  24. velune/cli/display/council_view.py +254 -0
  25. velune/cli/display/memory_view.py +126 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +25 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +51 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +123 -0
  33. velune/cli/registry.py +80 -0
  34. velune/cli/rendering/__init__.py +5 -0
  35. velune/cli/rendering/error_panel.py +79 -0
  36. velune/cli/rendering/markdown.py +63 -0
  37. velune/cli/repl.py +1855 -0
  38. velune/cli/session_manager.py +71 -0
  39. velune/cli/slash_commands.py +37 -0
  40. velune/cli/theme.py +8 -0
  41. velune/cognition/__init__.py +23 -0
  42. velune/cognition/agents/__init__.py +7 -0
  43. velune/cognition/agents/coder.py +209 -0
  44. velune/cognition/agents/planner.py +156 -0
  45. velune/cognition/agents/reviewer.py +195 -0
  46. velune/cognition/arbitrator.py +220 -0
  47. velune/cognition/architecture.py +415 -0
  48. velune/cognition/budget.py +65 -0
  49. velune/cognition/council/__init__.py +47 -0
  50. velune/cognition/council/base.py +217 -0
  51. velune/cognition/council/challenger.py +74 -0
  52. velune/cognition/council/coder.py +79 -0
  53. velune/cognition/council/critic_agent.py +43 -0
  54. velune/cognition/council/critic_configs.py +111 -0
  55. velune/cognition/council/critics.py +41 -0
  56. velune/cognition/council/debate.py +46 -0
  57. velune/cognition/council/factory.py +140 -0
  58. velune/cognition/council/messages.py +56 -0
  59. velune/cognition/council/planner.py +124 -0
  60. velune/cognition/council/reviewer.py +74 -0
  61. velune/cognition/council/synthesizer.py +67 -0
  62. velune/cognition/council/tiers.py +188 -0
  63. velune/cognition/council_orchestrator.py +282 -0
  64. velune/cognition/firewall.py +354 -0
  65. velune/cognition/module.py +46 -0
  66. velune/cognition/orchestrator.py +1205 -0
  67. velune/cognition/personality.py +238 -0
  68. velune/cognition/state.py +104 -0
  69. velune/cognition/style_resolver.py +64 -0
  70. velune/cognition/verification.py +205 -0
  71. velune/context/__init__.py +28 -0
  72. velune/context/assembler.py +240 -0
  73. velune/context/budget.py +97 -0
  74. velune/context/extractive.py +95 -0
  75. velune/context/prompt_adaptation.py +480 -0
  76. velune/context/sections.py +99 -0
  77. velune/context/token_counter.py +134 -0
  78. velune/context/utilization.py +33 -0
  79. velune/context/window.py +63 -0
  80. velune/core/__init__.py +89 -0
  81. velune/core/background.py +5 -0
  82. velune/core/config/__init__.py +37 -0
  83. velune/core/errors/__init__.py +90 -0
  84. velune/core/errors/catalog.py +188 -0
  85. velune/core/errors/execution.py +31 -0
  86. velune/core/errors/memory.py +25 -0
  87. velune/core/errors/orchestration.py +31 -0
  88. velune/core/errors/provider.py +37 -0
  89. velune/core/event_loop.py +35 -0
  90. velune/core/logging.py +83 -0
  91. velune/core/paths.py +165 -0
  92. velune/core/runtime.py +113 -0
  93. velune/core/startup_profiler.py +56 -0
  94. velune/core/task_registry.py +117 -0
  95. velune/core/trace.py +83 -0
  96. velune/core/types/__init__.py +48 -0
  97. velune/core/types/agent.py +53 -0
  98. velune/core/types/context.py +42 -0
  99. velune/core/types/inference.py +38 -0
  100. velune/core/types/memory.py +42 -0
  101. velune/core/types/model.py +70 -0
  102. velune/core/types/provider.py +62 -0
  103. velune/core/types/repository.py +38 -0
  104. velune/core/types/task.py +61 -0
  105. velune/core/types/workspace.py +28 -0
  106. velune/daemon/client.py +13 -0
  107. velune/daemon/server.py +127 -0
  108. velune/daemon/transport.py +179 -0
  109. velune/events.py +204 -0
  110. velune/execution/__init__.py +22 -0
  111. velune/execution/benchmarker.py +315 -0
  112. velune/execution/cancellation.py +53 -0
  113. velune/execution/checkpointer.py +130 -0
  114. velune/execution/command_spec.py +165 -0
  115. velune/execution/diff_preview.py +197 -0
  116. velune/execution/executor.py +181 -0
  117. velune/execution/module.py +18 -0
  118. velune/execution/multi_diff.py +67 -0
  119. velune/execution/path_guard.py +74 -0
  120. velune/execution/planner.py +91 -0
  121. velune/execution/rollback.py +89 -0
  122. velune/execution/sandbox.py +268 -0
  123. velune/execution/validator.py +115 -0
  124. velune/hardware/__init__.py +1 -0
  125. velune/hardware/detector.py +192 -0
  126. velune/kernel/__init__.py +55 -0
  127. velune/kernel/bootstrap.py +125 -0
  128. velune/kernel/config.py +426 -0
  129. velune/kernel/entrypoint.py +78 -0
  130. velune/kernel/health.py +54 -0
  131. velune/kernel/lifecycle.py +143 -0
  132. velune/kernel/module.py +17 -0
  133. velune/kernel/modules.py +23 -0
  134. velune/kernel/registry.py +96 -0
  135. velune/kernel/schemas.py +28 -0
  136. velune/main.py +9 -0
  137. velune/mcp/__init__.py +9 -0
  138. velune/mcp/client.py +115 -0
  139. velune/mcp/config.py +19 -0
  140. velune/mcp/server.py +624 -0
  141. velune/memory/__init__.py +32 -0
  142. velune/memory/compaction.py +506 -0
  143. velune/memory/embedding_pipeline.py +241 -0
  144. velune/memory/lifecycle.py +680 -0
  145. velune/memory/module.py +218 -0
  146. velune/memory/prioritizer.py +67 -0
  147. velune/memory/storage/episodic_schema.sql +53 -0
  148. velune/memory/storage/lancedb_store.py +282 -0
  149. velune/memory/storage/sqlite_manager.py +369 -0
  150. velune/memory/storage/sqlite_pool.py +149 -0
  151. velune/memory/tiers/episodic.py +588 -0
  152. velune/memory/tiers/graph.py +378 -0
  153. velune/memory/tiers/lineage.py +416 -0
  154. velune/memory/tiers/semantic.py +475 -0
  155. velune/memory/tiers/working.py +168 -0
  156. velune/memory/vitality.py +132 -0
  157. velune/models/__init__.py +15 -0
  158. velune/models/family.py +76 -0
  159. velune/models/module.py +20 -0
  160. velune/models/probes.py +192 -0
  161. velune/models/profile_cache.py +84 -0
  162. velune/models/profiler.py +108 -0
  163. velune/models/registry.py +251 -0
  164. velune/models/scorer.py +233 -0
  165. velune/models/specializations.py +205 -0
  166. velune/orchestration/__init__.py +19 -0
  167. velune/orchestration/engine.py +239 -0
  168. velune/orchestration/module.py +15 -0
  169. velune/orchestration/role_assignments.py +82 -0
  170. velune/orchestration/schemas.py +98 -0
  171. velune/plugins/__init__.py +20 -0
  172. velune/plugins/hooks.py +50 -0
  173. velune/plugins/loader.py +161 -0
  174. velune/plugins/registry.py +56 -0
  175. velune/plugins/schemas.py +21 -0
  176. velune/providers/__init__.py +23 -0
  177. velune/providers/adapters/anthropic.py +257 -0
  178. velune/providers/adapters/fireworks.py +115 -0
  179. velune/providers/adapters/google.py +234 -0
  180. velune/providers/adapters/groq.py +151 -0
  181. velune/providers/adapters/huggingface.py +210 -0
  182. velune/providers/adapters/llamacpp.py +208 -0
  183. velune/providers/adapters/lmstudio.py +175 -0
  184. velune/providers/adapters/ollama.py +233 -0
  185. velune/providers/adapters/openai.py +213 -0
  186. velune/providers/adapters/openrouter.py +81 -0
  187. velune/providers/adapters/together.py +134 -0
  188. velune/providers/adapters/xai.py +60 -0
  189. velune/providers/base.py +86 -0
  190. velune/providers/benchmarker.py +138 -0
  191. velune/providers/discovery/__init__.py +33 -0
  192. velune/providers/discovery/anthropic.py +79 -0
  193. velune/providers/discovery/benchmarks.py +44 -0
  194. velune/providers/discovery/classifier.py +69 -0
  195. velune/providers/discovery/fireworks.py +95 -0
  196. velune/providers/discovery/gguf.py +88 -0
  197. velune/providers/discovery/google.py +95 -0
  198. velune/providers/discovery/gpu.py +117 -0
  199. velune/providers/discovery/groq.py +21 -0
  200. velune/providers/discovery/huggingface.py +67 -0
  201. velune/providers/discovery/lmstudio.py +80 -0
  202. velune/providers/discovery/ollama.py +162 -0
  203. velune/providers/discovery/openai.py +96 -0
  204. velune/providers/discovery/openrouter.py +113 -0
  205. velune/providers/discovery/scanner.py +115 -0
  206. velune/providers/discovery/together.py +114 -0
  207. velune/providers/discovery/xai.py +57 -0
  208. velune/providers/health.py +67 -0
  209. velune/providers/health_monitor.py +169 -0
  210. velune/providers/keystore.py +142 -0
  211. velune/providers/local_paths.py +49 -0
  212. velune/providers/local_resolver.py +229 -0
  213. velune/providers/module.py +51 -0
  214. velune/providers/ollama_manager.py +193 -0
  215. velune/providers/registry.py +220 -0
  216. velune/providers/router.py +255 -0
  217. velune/providers/task_classifier.py +288 -0
  218. velune/py.typed +0 -0
  219. velune/repository/__init__.py +33 -0
  220. velune/repository/analyzer.py +127 -0
  221. velune/repository/ast_parser.py +822 -0
  222. velune/repository/blast_radius.py +298 -0
  223. velune/repository/boundary_classifier.py +295 -0
  224. velune/repository/cognition.py +316 -0
  225. velune/repository/grapher.py +179 -0
  226. velune/repository/import_graph.py +263 -0
  227. velune/repository/incremental_indexer.py +275 -0
  228. velune/repository/index_state.py +96 -0
  229. velune/repository/indexer.py +243 -0
  230. velune/repository/module.py +17 -0
  231. velune/repository/parser.py +474 -0
  232. velune/repository/project_type.py +300 -0
  233. velune/repository/rename_journal.py +287 -0
  234. velune/repository/scanner.py +193 -0
  235. velune/repository/schemas.py +102 -0
  236. velune/repository/symbol_registry.py +365 -0
  237. velune/repository/tracker.py +252 -0
  238. velune/retrieval/__init__.py +27 -0
  239. velune/retrieval/cache.py +110 -0
  240. velune/retrieval/fast_path.py +391 -0
  241. velune/retrieval/graph.py +124 -0
  242. velune/retrieval/hybrid.py +271 -0
  243. velune/retrieval/keyword.py +131 -0
  244. velune/retrieval/module.py +26 -0
  245. velune/retrieval/pipeline.py +303 -0
  246. velune/retrieval/reranker.py +102 -0
  247. velune/retrieval/schemas.py +59 -0
  248. velune/retrieval/slow_path.py +364 -0
  249. velune/retrieval/vector.py +203 -0
  250. velune/telemetry/__init__.py +59 -0
  251. velune/telemetry/cognition.py +267 -0
  252. velune/telemetry/cost_estimator.py +92 -0
  253. velune/telemetry/debug.py +304 -0
  254. velune/telemetry/doctor.py +244 -0
  255. velune/telemetry/logging.py +286 -0
  256. velune/telemetry/spans.py +277 -0
  257. velune/telemetry/token_tracker.py +140 -0
  258. velune/telemetry/usage_tracker.py +340 -0
  259. velune/tools/__init__.py +41 -0
  260. velune/tools/base/registry.py +87 -0
  261. velune/tools/base/tool.py +63 -0
  262. velune/tools/code/navigate.py +116 -0
  263. velune/tools/code/search.py +123 -0
  264. velune/tools/filesystem/read.py +75 -0
  265. velune/tools/filesystem/search.py +136 -0
  266. velune/tools/filesystem/write.py +163 -0
  267. velune/tools/git/history.py +177 -0
  268. velune/tools/git/operations.py +122 -0
  269. velune/tools/git/state.py +121 -0
  270. velune/tools/module.py +81 -0
  271. velune/tools/terminal/execute.py +72 -0
  272. velune/tools/terminal/history.py +47 -0
  273. velune/tools/web/fetch.py +55 -0
  274. velune/tools/web/validator.py +122 -0
  275. velune_cli-0.9.0.dist-info/METADATA +518 -0
  276. velune_cli-0.9.0.dist-info/RECORD +279 -0
  277. velune_cli-0.9.0.dist-info/WHEEL +4 -0
  278. velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
  279. velune_cli-0.9.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,364 @@
1
+ """Slow path retriever: deep graph traversal (< 2000ms, HIGH complexity only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ from typing import Any
8
+
9
+ logger = logging.getLogger("velune.retrieval.slow_path")
10
+
11
+
12
+ class SlowPathRetriever:
13
+ """Slow path using deep graph traversal and contextual search."""
14
+
15
+ # Traversal limits
16
+ CALLS_DEPTH = 2
17
+ IMPORTS_DEPTH = 1
18
+ TARGET_LATENCY_MS = 2000
19
+
20
+ def __init__(
21
+ self,
22
+ fast_path_retriever: Any,
23
+ import_graph: Any,
24
+ call_graph: Any,
25
+ episodic_memory: Any,
26
+ lineage_memory: Any,
27
+ ) -> None:
28
+ """Initialize slow path retriever.
29
+
30
+ Parameters
31
+ ----------
32
+ fast_path_retriever:
33
+ FastPathRetriever instance (to reuse results)
34
+ import_graph:
35
+ Import graph for traversal
36
+ call_graph:
37
+ Call graph for traversal
38
+ episodic_memory:
39
+ Episodic memory for context
40
+ lineage_memory:
41
+ Lineage memory for decisions/failures
42
+ """
43
+ self.fast_path = fast_path_retriever
44
+ self.import_graph = import_graph
45
+ self.call_graph = call_graph
46
+ self.episodic_memory = episodic_memory
47
+ self.lineage_memory = lineage_memory
48
+
49
+ async def retrieve(
50
+ self,
51
+ query: str,
52
+ fast_results: list[Any],
53
+ budget: Any,
54
+ workspace_root: str = "",
55
+ ) -> list[Any]:
56
+ """Run slow path retrieval (< 2000ms).
57
+
58
+ Steps:
59
+ 1. Identify high-blast-radius nodes from fast path
60
+ 2. Traverse CALLS edges (depth 2)
61
+ 3. Traverse IMPORTS edges (depth 1)
62
+ 4. Search episodic memory for sessions mentioning these nodes
63
+ 5. Retrieve lineage decisions/failures
64
+ 6. Return new chunks not in fast path
65
+
66
+ Parameters
67
+ ----------
68
+ query:
69
+ Query text (for logging/context)
70
+ fast_results:
71
+ Results from fast path (starting points)
72
+ budget:
73
+ Context budget
74
+ workspace_root:
75
+ Workspace root for scoping
76
+
77
+ Returns
78
+ -------
79
+ list[ContextChunk]:
80
+ Additional chunks from slow path
81
+ """
82
+ start_time = time.perf_counter()
83
+
84
+ chunks: list[Any] = []
85
+
86
+ # Step 1: Identify high-blast-radius nodes
87
+ blast_radius_nodes = self._identify_high_blast_radius_nodes(fast_results)
88
+ logger.debug(f"Identified {len(blast_radius_nodes)} high-blast-radius nodes")
89
+
90
+ # Step 2: Traverse CALLS edges
91
+ call_graph_results = await self._traverse_call_graph(blast_radius_nodes)
92
+ chunks.extend(call_graph_results)
93
+
94
+ # Step 3: Traverse IMPORTS edges
95
+ import_graph_results = await self._traverse_import_graph(blast_radius_nodes)
96
+ chunks.extend(import_graph_results)
97
+
98
+ # Step 4: Search episodic memory
99
+ episodic_results = await self._search_episodic_context(blast_radius_nodes, workspace_root)
100
+ chunks.extend(episodic_results)
101
+
102
+ # Step 5: Retrieve lineage decisions
103
+ lineage_results = await self._retrieve_lineage_context(blast_radius_nodes)
104
+ chunks.extend(lineage_results)
105
+
106
+ # Filter: keep only chunks not already in fast_results
107
+ fast_ids = {r.id for r in fast_results}
108
+ filtered_chunks = [c for c in chunks if c.id not in fast_ids]
109
+
110
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
111
+ logger.debug(f"Slow path complete: {len(filtered_chunks)} new chunks in {elapsed_ms:.0f}ms")
112
+
113
+ if elapsed_ms > self.TARGET_LATENCY_MS:
114
+ logger.warning(
115
+ f"Slow path exceeded target latency: {elapsed_ms:.0f}ms > {self.TARGET_LATENCY_MS}ms"
116
+ )
117
+
118
+ return filtered_chunks
119
+
120
+ def _identify_high_blast_radius_nodes(self, fast_results: list[Any]) -> list[str]:
121
+ """Identify high-blast-radius nodes from fast path results.
122
+
123
+ High-blast-radius: files/symbols that many other nodes depend on.
124
+
125
+ Parameters
126
+ ----------
127
+ fast_results:
128
+ Fast path results
129
+
130
+ Returns
131
+ -------
132
+ list[str]:
133
+ Node IDs to traverse from
134
+ """
135
+ nodes = []
136
+
137
+ for result in fast_results:
138
+ # Extract node ID from metadata
139
+ node_id = result.metadata.get("symbol_id") or result.metadata.get("path")
140
+ if node_id:
141
+ nodes.append(node_id)
142
+
143
+ return nodes[:10] # Limit to top 10
144
+
145
+ async def _traverse_call_graph(self, start_nodes: list[str]) -> list[Any]:
146
+ """Traverse call graph from starting nodes.
147
+
148
+ Parameters
149
+ ----------
150
+ start_nodes:
151
+ Starting node IDs
152
+
153
+ Returns
154
+ -------
155
+ list[ContextChunk]:
156
+ Chunks from call graph traversal
157
+ """
158
+ from velune.retrieval.pipeline import ContextChunk
159
+
160
+ chunks = []
161
+
162
+ try:
163
+ # Traverse from each node
164
+ for node_id in start_nodes:
165
+ # Get callers (who calls this function)
166
+ callers = await self.call_graph.get_neighbors(
167
+ node=node_id,
168
+ depth=self.CALLS_DEPTH,
169
+ direction="incoming", # Who calls this
170
+ )
171
+
172
+ # Add as chunks
173
+ for caller in callers:
174
+ chunk = ContextChunk(
175
+ id=f"callgraph_{caller.get('id')}",
176
+ source="call_graph",
177
+ content=f"Function: {caller.get('name')}\nCalls: {node_id}",
178
+ metadata={
179
+ "node_id": caller.get("id"),
180
+ "tokens": 50,
181
+ },
182
+ relevance_score=0.3,
183
+ )
184
+ chunks.append(chunk)
185
+
186
+ logger.debug(f"Call graph traversal found {len(chunks)} results")
187
+ return chunks
188
+
189
+ except Exception as e:
190
+ logger.debug(f"Call graph traversal failed: {e}")
191
+ return []
192
+
193
+ async def _traverse_import_graph(self, start_nodes: list[str]) -> list[Any]:
194
+ """Traverse import graph from starting nodes.
195
+
196
+ Parameters
197
+ ----------
198
+ start_nodes:
199
+ Starting node IDs (file paths)
200
+
201
+ Returns
202
+ -------
203
+ list[ContextChunk]:
204
+ Chunks from import graph traversal
205
+ """
206
+ from velune.retrieval.pipeline import ContextChunk
207
+
208
+ chunks = []
209
+
210
+ try:
211
+ # Traverse from each node
212
+ for node_id in start_nodes:
213
+ # Get import neighbors
214
+ neighbors = await self.import_graph.get_neighbors(
215
+ node=node_id,
216
+ depth=self.IMPORTS_DEPTH,
217
+ edge_type="imports",
218
+ )
219
+
220
+ # Add as chunks
221
+ for neighbor in neighbors:
222
+ chunk = ContextChunk(
223
+ id=f"imports_{neighbor.get('id')}",
224
+ source="import_graph",
225
+ content=f"Module: {neighbor.get('path')}\nImports: {node_id}",
226
+ metadata={
227
+ "node_id": neighbor.get("id"),
228
+ "path": neighbor.get("path"),
229
+ "tokens": 50,
230
+ },
231
+ relevance_score=0.25,
232
+ )
233
+ chunks.append(chunk)
234
+
235
+ logger.debug(f"Import graph traversal found {len(chunks)} results")
236
+ return chunks
237
+
238
+ except Exception as e:
239
+ logger.debug(f"Import graph traversal failed: {e}")
240
+ return []
241
+
242
+ async def _search_episodic_context(
243
+ self,
244
+ node_ids: list[str],
245
+ workspace_root: str,
246
+ ) -> list[Any]:
247
+ """Search episodic memory for sessions mentioning these nodes.
248
+
249
+ Parameters
250
+ ----------
251
+ node_ids:
252
+ Node IDs to search for
253
+ workspace_root:
254
+ Workspace root for scoping
255
+
256
+ Returns
257
+ -------
258
+ list[ContextChunk]:
259
+ Chunks from episodic memory
260
+ """
261
+ from velune.retrieval.pipeline import ContextChunk
262
+
263
+ chunks = []
264
+
265
+ try:
266
+ # Search for sessions mentioning these nodes
267
+ for node_id in node_ids:
268
+ # Convert node ID to searchable form
269
+ search_term = node_id.split("/")[-1] # Get filename
270
+
271
+ # LIKE search in episodic memory
272
+ results = await self.episodic_memory.search_turns(
273
+ pattern=f"%{search_term}%",
274
+ limit=3,
275
+ )
276
+
277
+ # Add as chunks
278
+ for result in results:
279
+ chunk = ContextChunk(
280
+ id=f"episodic_{result.get('turn_id')}",
281
+ source="episodic",
282
+ content=result.get("content", ""),
283
+ metadata={
284
+ "turn_id": result.get("turn_id"),
285
+ "session_id": result.get("session_id"),
286
+ "tokens": len(result.get("content", "")) // 4,
287
+ },
288
+ relevance_score=0.35,
289
+ )
290
+ chunks.append(chunk)
291
+
292
+ logger.debug(f"Episodic context search found {len(chunks)} results")
293
+ return chunks
294
+
295
+ except Exception as e:
296
+ logger.debug(f"Episodic context search failed: {e}")
297
+ return []
298
+
299
+ async def _retrieve_lineage_context(self, node_ids: list[str]) -> list[Any]:
300
+ """Retrieve lineage decisions/failures related to these nodes.
301
+
302
+ Parameters
303
+ ----------
304
+ node_ids:
305
+ Node IDs to search for
306
+
307
+ Returns
308
+ -------
309
+ list[ContextChunk]:
310
+ Chunks from lineage memory
311
+ """
312
+ from velune.retrieval.pipeline import ContextChunk
313
+
314
+ chunks = []
315
+
316
+ try:
317
+ # Search lineage for decisions/failures
318
+ for node_id in node_ids:
319
+ # Get related decisions
320
+ decisions = await self.lineage_memory.get_decisions(
321
+ node_id=node_id,
322
+ limit=2,
323
+ )
324
+
325
+ for decision in decisions:
326
+ chunk = ContextChunk(
327
+ id=f"lineage_decision_{decision.get('id')}",
328
+ source="lineage",
329
+ content=f"Decision: {decision.get('summary')}\nReason: {decision.get('reasoning')}",
330
+ metadata={
331
+ "decision_id": decision.get("id"),
332
+ "node_id": node_id,
333
+ "tokens": len(decision.get("reasoning", "")) // 4,
334
+ },
335
+ relevance_score=0.45,
336
+ )
337
+ chunks.append(chunk)
338
+
339
+ # Get related failures
340
+ failures = await self.lineage_memory.get_failures(
341
+ node_id=node_id,
342
+ limit=2,
343
+ )
344
+
345
+ for failure in failures:
346
+ chunk = ContextChunk(
347
+ id=f"lineage_failure_{failure.get('id')}",
348
+ source="lineage",
349
+ content=f"Failure: {failure.get('summary')}\nMitigation: {failure.get('mitigation')}",
350
+ metadata={
351
+ "failure_id": failure.get("id"),
352
+ "node_id": node_id,
353
+ "tokens": len(failure.get("mitigation", "")) // 4,
354
+ },
355
+ relevance_score=0.4,
356
+ )
357
+ chunks.append(chunk)
358
+
359
+ logger.debug(f"Lineage context retrieval found {len(chunks)} results")
360
+ return chunks
361
+
362
+ except Exception as e:
363
+ logger.debug(f"Lineage context retrieval failed: {e}")
364
+ return []
@@ -0,0 +1,203 @@
1
+ """Vector retrieval layer using Qdrant client.
2
+
3
+ The Qdrant client and its compiled backend are resolved lazily so that wiring
4
+ this retriever during bootstrap costs nothing. A ``client_provider`` callable
5
+ is preferred over a concrete ``client``: it lets us share the semantic tier's
6
+ single Qdrant connection (two clients on the same local path would deadlock the
7
+ store) without forcing that connection to open at startup.
8
+ """
9
+
10
+ import logging
11
+ from collections.abc import Callable
12
+ from typing import Any
13
+
14
+ from velune.retrieval.schemas import RetrievalDocument, RetrievalHit, RetrievalSource
15
+
16
+ logger = logging.getLogger("velune.retrieval.vector")
17
+
18
+
19
+ def _qmodels() -> Any:
20
+ """Lazily import qdrant http models (defers the qdrant_client import)."""
21
+ from qdrant_client.http import models as qmodels
22
+
23
+ return qmodels
24
+
25
+
26
+ class VectorRetriever:
27
+ """Retrieves context from Qdrant vector database using dense embeddings."""
28
+
29
+ def __init__(
30
+ self,
31
+ collection_name: str = "velune_symbols",
32
+ location: str = ".velune/qdrant_local_store",
33
+ client: Any | None = None,
34
+ client_provider: Callable[[], Any] | None = None,
35
+ ) -> None:
36
+ self.collection_name = collection_name
37
+ self.location = location
38
+ self._client = client
39
+ self._client_provider = client_provider
40
+ self._client_resolved = client is not None
41
+ self._detected_dimension: int | None = None
42
+
43
+ @property
44
+ def client(self) -> Any:
45
+ """Resolve the Qdrant client on first use (provider > location)."""
46
+ if not self._client_resolved:
47
+ self._client_resolved = True
48
+ if self._client_provider is not None:
49
+ self._client = self._client_provider()
50
+ else:
51
+ from qdrant_client import QdrantClient
52
+
53
+ loc = self.location
54
+ if loc.startswith(".") or "/" in loc or "\\" in loc:
55
+ self._client = QdrantClient(path=loc)
56
+ else:
57
+ self._client = QdrantClient(location=loc)
58
+ return self._client
59
+
60
+ def _ensure_collection_for_dimension(self, dim: int) -> None:
61
+ """Create or verify collection for the given dimension."""
62
+ if self._detected_dimension == dim:
63
+ return # Already configured
64
+
65
+ client = self.client
66
+ if client is None:
67
+ return # Degraded mode — no vector backend available
68
+ qmodels = _qmodels()
69
+
70
+ try:
71
+ collections = client.get_collections().collections
72
+ existing = next((c for c in collections if c.name == self.collection_name), None)
73
+
74
+ if existing:
75
+ # Check if dimension matches
76
+ info = client.get_collection(self.collection_name)
77
+ existing_dim = info.config.params.vectors.size
78
+ if existing_dim != dim:
79
+ logger.warning(
80
+ "Embedding dimension changed from %d to %d. "
81
+ "Recreating collection '%s'. Existing vectors lost.",
82
+ existing_dim,
83
+ dim,
84
+ self.collection_name,
85
+ )
86
+ client.delete_collection(self.collection_name)
87
+ existing = None
88
+
89
+ if not existing:
90
+ client.create_collection(
91
+ collection_name=self.collection_name,
92
+ vectors_config=qmodels.VectorParams(size=dim, distance=qmodels.Distance.COSINE),
93
+ )
94
+ logger.info(
95
+ "Collection '%s' initialized with dimension %d", self.collection_name, dim
96
+ )
97
+
98
+ self._detected_dimension = dim
99
+ except Exception as e:
100
+ logger.error("Failed to ensure collection for dimension %d: %s", dim, e)
101
+
102
+ def upsert(self, doc: RetrievalDocument) -> None:
103
+ """Inserts or updates a document with its embedding in Qdrant."""
104
+ if not doc.embedding:
105
+ return
106
+
107
+ dim = len(doc.embedding)
108
+ self._ensure_collection_for_dimension(dim)
109
+
110
+ client = self.client
111
+ if client is None:
112
+ return
113
+ qmodels = _qmodels()
114
+ try:
115
+ client.upsert(
116
+ collection_name=self.collection_name,
117
+ points=[
118
+ qmodels.PointStruct(
119
+ id=hash(doc.id) % (2**63 - 1), # Map string ID to uint64
120
+ vector=doc.embedding, # exact length, no padding
121
+ payload={
122
+ "doc_id": doc.id,
123
+ "content": doc.content,
124
+ "namespace": doc.namespace,
125
+ **doc.metadata,
126
+ },
127
+ )
128
+ ],
129
+ )
130
+ except Exception as e:
131
+ logger.error("Failed to upsert document: %s", e)
132
+
133
+ def retrieve(
134
+ self, query_vector: list[float], top_k: int = 10, namespace: str | None = None
135
+ ) -> list[RetrievalHit]:
136
+ """Queries Qdrant vector spaces and returns matching document hits."""
137
+ client = self.client
138
+ if client is None:
139
+ return [] # Degraded mode — no vector backend available
140
+ qmodels = _qmodels()
141
+
142
+ if not self._detected_dimension:
143
+ try:
144
+ collections = client.get_collections().collections
145
+ existing = any(c.name == self.collection_name for c in collections)
146
+ if existing:
147
+ info = client.get_collection(self.collection_name)
148
+ self._detected_dimension = info.config.params.vectors.size
149
+ else:
150
+ return []
151
+ except Exception:
152
+ return []
153
+
154
+ if self._detected_dimension and len(query_vector) != self._detected_dimension:
155
+ logger.error(
156
+ "Query dimension mismatch — skipping vector retrieval. "
157
+ "Query dimension %d != collection dimension %d.",
158
+ len(query_vector),
159
+ self._detected_dimension,
160
+ )
161
+ return []
162
+
163
+ hits: list[RetrievalHit] = []
164
+ try:
165
+ # Build filters
166
+ query_filter = None
167
+ if namespace:
168
+ query_filter = qmodels.Filter(
169
+ must=[
170
+ qmodels.FieldCondition(
171
+ key="namespace", match=qmodels.MatchValue(value=namespace)
172
+ )
173
+ ]
174
+ )
175
+
176
+ results = client.query_points(
177
+ collection_name=self.collection_name,
178
+ query=query_vector,
179
+ query_filter=query_filter,
180
+ limit=top_k,
181
+ ).points
182
+
183
+ for rank, res in enumerate(results):
184
+ payload = res.payload or {}
185
+ doc = RetrievalDocument(
186
+ id=payload.get("doc_id", str(res.id)),
187
+ content=payload.get("content", ""),
188
+ namespace=payload.get("namespace", "default"),
189
+ metadata={
190
+ k: v
191
+ for k, v in payload.items()
192
+ if k not in ("doc_id", "content", "namespace")
193
+ },
194
+ )
195
+ hits.append(
196
+ RetrievalHit(
197
+ document=doc, score=res.score, source=RetrievalSource.VECTOR, rank=rank + 1
198
+ )
199
+ )
200
+ except Exception as e:
201
+ logger.error("Failed to retrieve matching documents from Qdrant: %s", e)
202
+
203
+ return hits
@@ -0,0 +1,59 @@
1
+ """Telemetry package for Velune CLI.
2
+
3
+ Provides structured logging, span tracking, and usage analytics.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from velune.telemetry.cognition import CognitivePerformanceAnalytics
9
+ from velune.telemetry.debug import (
10
+ DebugTimer,
11
+ PipelineMetrics,
12
+ RoutingDecision,
13
+ TokenCounter,
14
+ debug_timer,
15
+ log_debug_info,
16
+ )
17
+ from velune.telemetry.doctor import (
18
+ get_telemetry_status,
19
+ print_provider_health_report,
20
+ print_telemetry_report,
21
+ )
22
+ from velune.telemetry.logging import bind_context, clear_context, configure_logging, context_scope
23
+ from velune.telemetry.spans import (
24
+ SpanContext,
25
+ async_span,
26
+ create_run_id,
27
+ create_span_id,
28
+ get_current_run_id,
29
+ get_current_span_id,
30
+ span,
31
+ )
32
+ from velune.telemetry.usage_tracker import SessionUsageTracker, UsageSummary, get_tracker
33
+
34
+ __all__ = [
35
+ "CognitivePerformanceAnalytics",
36
+ "configure_logging",
37
+ "bind_context",
38
+ "clear_context",
39
+ "context_scope",
40
+ "SpanContext",
41
+ "create_run_id",
42
+ "create_span_id",
43
+ "get_current_run_id",
44
+ "get_current_span_id",
45
+ "span",
46
+ "async_span",
47
+ "SessionUsageTracker",
48
+ "get_tracker",
49
+ "UsageSummary",
50
+ "DebugTimer",
51
+ "debug_timer",
52
+ "TokenCounter",
53
+ "RoutingDecision",
54
+ "PipelineMetrics",
55
+ "log_debug_info",
56
+ "print_telemetry_report",
57
+ "get_telemetry_status",
58
+ "print_provider_health_report",
59
+ ]