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,303 @@
1
+ """Dual-path retrieval pipeline with fast and slow paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ from velune.context.budget import ContextBudget
12
+
13
+ logger = logging.getLogger("velune.retrieval.pipeline")
14
+
15
+
16
+ @dataclass
17
+ class TaskProfile:
18
+ """Task profile for retrieval decisions."""
19
+
20
+ task_type: str # CODING, REASONING, SUMMARIZATION, etc.
21
+ complexity: str # LOW, MEDIUM, HIGH
22
+ requires_long_context: bool = False
23
+ latency_sensitive: bool = False
24
+
25
+
26
+ @dataclass
27
+ class ContextChunk:
28
+ """A chunk of retrieved context."""
29
+
30
+ id: str
31
+ source: str # "semantic", "symbol", "episodic", "lineage"
32
+ content: str
33
+ metadata: dict[str, Any] = field(default_factory=dict)
34
+ relevance_score: float = 0.0
35
+ recency_score: float = 0.0
36
+ trust_score: float = 0.0
37
+ combined_score: float = 0.0
38
+
39
+
40
+ @dataclass
41
+ class RetrievedContext:
42
+ """Results from retrieval pipeline."""
43
+
44
+ chunks: list[ContextChunk]
45
+ total_tokens: int
46
+ fast_path_tokens: int
47
+ slow_path_tokens: int
48
+ retrieval_time_ms: float
49
+ fast_path_time_ms: float
50
+ slow_path_time_ms: float
51
+ cache_hit: bool = False
52
+ metadata: dict[str, Any] = field(default_factory=dict)
53
+
54
+
55
+ class RetrievalPipeline:
56
+ """Orchestrates dual-path retrieval (fast + slow based on complexity)."""
57
+
58
+ # Complexity thresholds
59
+ FAST_PATH_ONLY = {"LOW", "MEDIUM"}
60
+ FAST_AND_SLOW = {"HIGH"}
61
+
62
+ def __init__(
63
+ self,
64
+ fast_path_retriever: Any,
65
+ slow_path_retriever: Any,
66
+ reranker: Any,
67
+ cache: Any,
68
+ max_total_chunks: int = 20,
69
+ ) -> None:
70
+ """Initialize pipeline.
71
+
72
+ Parameters
73
+ ----------
74
+ fast_path_retriever:
75
+ FastPathRetriever instance
76
+ slow_path_retriever:
77
+ SlowPathRetriever instance
78
+ reranker:
79
+ CrossEncoderReranker instance
80
+ cache:
81
+ RetrievalCache instance
82
+ max_total_chunks:
83
+ Maximum chunks to return
84
+ """
85
+ self.fast_path = fast_path_retriever
86
+ self.slow_path = slow_path_retriever
87
+ self.reranker = reranker
88
+ self.cache = cache
89
+ self.max_total_chunks = max_total_chunks
90
+
91
+ async def retrieve(
92
+ self,
93
+ query: str,
94
+ budget: ContextBudget,
95
+ task_profile: TaskProfile,
96
+ workspace_root: str = "",
97
+ ) -> RetrievedContext:
98
+ """Retrieve context using dual-path strategy.
99
+
100
+ Algorithm:
101
+ 1. Check cache (< 1ms)
102
+ 2. Run fast path (< 200ms)
103
+ 3. If HIGH complexity: run slow path (< 2000ms)
104
+ 4. Merge and deduplicate results
105
+ 5. Rerank by combined score
106
+ 6. Fit to budget
107
+
108
+ Parameters
109
+ ----------
110
+ query:
111
+ Retrieval query
112
+ budget:
113
+ Token budget for retrieval
114
+ task_profile:
115
+ Task complexity and profile
116
+ workspace_root:
117
+ Workspace root for scoping
118
+
119
+ Returns
120
+ -------
121
+ RetrievedContext:
122
+ Retrieved chunks with metadata
123
+ """
124
+ start_time = time.perf_counter()
125
+
126
+ # Check cache first
127
+ cache_key = self._make_cache_key(query, task_profile)
128
+ cached = await self.cache.get(cache_key)
129
+ if cached:
130
+ logger.debug("Retrieval cache hit")
131
+ cached.cache_hit = True
132
+ cached.metadata["retrieval_time_ms"] = (time.perf_counter() - start_time) * 1000
133
+ return cached
134
+
135
+ fast_start = time.perf_counter()
136
+ slow_start = None
137
+ slow_duration = 0.0
138
+
139
+ # Always run fast path
140
+ logger.debug(f"Running fast path for query: {query[:50]}...")
141
+ fast_results = await self.fast_path.retrieve(
142
+ query=query,
143
+ budget=budget,
144
+ workspace_root=workspace_root,
145
+ )
146
+ fast_duration = (time.perf_counter() - fast_start) * 1000
147
+
148
+ # Conditionally run slow path
149
+ slow_results = []
150
+ if task_profile.complexity in self.FAST_AND_SLOW:
151
+ logger.debug(f"Running slow path (complexity={task_profile.complexity})")
152
+ slow_start = time.perf_counter()
153
+
154
+ # Start slow path in background, but wait for it
155
+ try:
156
+ slow_results = await asyncio.wait_for(
157
+ self.slow_path.retrieve(
158
+ query=query,
159
+ fast_results=fast_results,
160
+ budget=budget,
161
+ workspace_root=workspace_root,
162
+ ),
163
+ timeout=2.0,
164
+ )
165
+ except TimeoutError:
166
+ logger.warning("Slow path timed out (>2000ms)")
167
+ slow_results = []
168
+
169
+ slow_duration = (time.perf_counter() - slow_start) * 1000
170
+
171
+ # Merge results (deduplicate by source ID)
172
+ merged = self._merge_results(fast_results, slow_results)
173
+
174
+ # Rerank merged results
175
+ reranked = self.reranker.rerank(merged, query)
176
+
177
+ # Fit to budget
178
+ fitted = self._fit_to_budget(reranked, budget)
179
+
180
+ total_duration = (time.perf_counter() - start_time) * 1000
181
+
182
+ result = RetrievedContext(
183
+ chunks=fitted,
184
+ total_tokens=sum(c.metadata.get("tokens", 100) for c in fitted),
185
+ fast_path_tokens=sum(c.metadata.get("tokens", 100) for c in fast_results),
186
+ slow_path_tokens=sum(c.metadata.get("tokens", 100) for c in slow_results),
187
+ retrieval_time_ms=total_duration,
188
+ fast_path_time_ms=fast_duration,
189
+ slow_path_time_ms=slow_duration,
190
+ cache_hit=False,
191
+ metadata={
192
+ "query_length": len(query),
193
+ "task_complexity": task_profile.complexity,
194
+ "chunks_fast": len(fast_results),
195
+ "chunks_slow": len(slow_results),
196
+ "chunks_final": len(fitted),
197
+ "slow_path_ran": task_profile.complexity in self.FAST_AND_SLOW,
198
+ },
199
+ )
200
+
201
+ # Cache result
202
+ await self.cache.set(cache_key, result)
203
+
204
+ logger.info(
205
+ f"Retrieval complete: {len(fast_results)} fast + {len(slow_results)} slow "
206
+ f"→ {len(fitted)} final chunks ({total_duration:.0f}ms)"
207
+ )
208
+
209
+ return result
210
+
211
+ def _make_cache_key(self, query: str, task_profile: TaskProfile) -> str:
212
+ """Generate cache key from query and profile.
213
+
214
+ Parameters
215
+ ----------
216
+ query:
217
+ Query text
218
+ task_profile:
219
+ Task profile
220
+
221
+ Returns
222
+ -------
223
+ str:
224
+ Cache key
225
+ """
226
+ import hashlib
227
+
228
+ key_parts = [
229
+ query.lower().strip(),
230
+ task_profile.complexity,
231
+ task_profile.task_type,
232
+ ]
233
+ key_str = "|".join(key_parts)
234
+ return hashlib.sha256(key_str.encode()).hexdigest()
235
+
236
+ def _merge_results(
237
+ self,
238
+ fast_results: list[ContextChunk],
239
+ slow_results: list[ContextChunk],
240
+ ) -> list[ContextChunk]:
241
+ """Merge results from both paths, deduplicating by source ID.
242
+
243
+ Parameters
244
+ ----------
245
+ fast_results:
246
+ Results from fast path
247
+ slow_results:
248
+ Results from slow path
249
+
250
+ Returns
251
+ -------
252
+ list[ContextChunk]:
253
+ Merged and deduplicated results
254
+ """
255
+ merged: dict[str, ContextChunk] = {}
256
+
257
+ # Add fast results first (they're more reliable due to lower latency)
258
+ for chunk in fast_results:
259
+ merged[chunk.id] = chunk
260
+
261
+ # Add slow results (updating if better score)
262
+ for chunk in slow_results:
263
+ if chunk.id not in merged:
264
+ merged[chunk.id] = chunk
265
+ else:
266
+ # Keep the one with higher combined score
267
+ if chunk.combined_score > merged[chunk.id].combined_score:
268
+ merged[chunk.id] = chunk
269
+
270
+ return list(merged.values())
271
+
272
+ def _fit_to_budget(
273
+ self,
274
+ chunks: list[ContextChunk],
275
+ budget: ContextBudget,
276
+ ) -> list[ContextChunk]:
277
+ """Fit chunks to token budget.
278
+
279
+ Parameters
280
+ ----------
281
+ chunks:
282
+ Ranked chunks
283
+ budget:
284
+ Token budget
285
+
286
+ Returns
287
+ -------
288
+ list[ContextChunk]:
289
+ Chunks that fit in budget, sorted by relevance
290
+ """
291
+ selected: list[ContextChunk] = []
292
+ total_tokens = 0
293
+
294
+ for chunk in chunks[: self.max_total_chunks]:
295
+ chunk_tokens = chunk.metadata.get("tokens", 100)
296
+
297
+ if total_tokens + chunk_tokens > budget.retrieval_allocation:
298
+ break
299
+
300
+ selected.append(chunk)
301
+ total_tokens += chunk_tokens
302
+
303
+ return selected
@@ -0,0 +1,102 @@
1
+ """Cross-encoder style reranking (simple scoring for Phase 2a)."""
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.reranker")
10
+
11
+
12
+ class CrossEncoderReranker:
13
+ """Reranks retrieved chunks using combined scoring.
14
+
15
+ Phase 2a: Simple score formula (no ML model)
16
+ Phase 3+: Real cross-encoder when compute available
17
+ """
18
+
19
+ # Score weights
20
+ SEMANTIC_WEIGHT = 0.5
21
+ RECENCY_WEIGHT = 0.3
22
+ TRUST_WEIGHT = 0.2
23
+
24
+ # Thresholds
25
+ MIN_RECENCY_SECONDS = 300
26
+ MAX_RECENCY_SECONDS = 2592000
27
+
28
+ def __init__(self) -> None:
29
+ """Initialize reranker."""
30
+ pass
31
+
32
+ def rerank(
33
+ self,
34
+ chunks: list[Any],
35
+ query: str | None = None,
36
+ ) -> list[Any]:
37
+ """Rerank chunks by combined score."""
38
+ start_time = time.perf_counter()
39
+
40
+ for chunk in chunks:
41
+ chunk.combined_score = self._calculate_combined_score(chunk)
42
+
43
+ ranked = sorted(chunks, key=lambda c: c.combined_score, reverse=True)
44
+ deduplicated = self._deduplicate_by_content(ranked)
45
+
46
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
47
+ logger.debug(f"Reranking {len(chunks)} → {len(deduplicated)} in {elapsed_ms:.1f}ms")
48
+
49
+ return deduplicated
50
+
51
+ def _calculate_combined_score(self, chunk: Any) -> float:
52
+ """Calculate combined score."""
53
+ semantic = min(1.0, max(0.0, chunk.relevance_score or 0.0))
54
+ recency = self._calculate_recency_score(chunk)
55
+ trust = self._calculate_trust_score(chunk)
56
+
57
+ combined = (
58
+ (semantic * self.SEMANTIC_WEIGHT)
59
+ + (recency * self.RECENCY_WEIGHT)
60
+ + (trust * self.TRUST_WEIGHT)
61
+ )
62
+
63
+ return min(1.0, max(0.0, combined))
64
+
65
+ def _calculate_recency_score(self, chunk: Any) -> float:
66
+ """Calculate recency score."""
67
+ timestamp = chunk.metadata.get("timestamp")
68
+ if not timestamp:
69
+ return 0.5
70
+
71
+ now = time.time()
72
+ age_seconds = now - timestamp
73
+
74
+ if age_seconds < self.MIN_RECENCY_SECONDS:
75
+ return 1.0
76
+ if age_seconds > self.MAX_RECENCY_SECONDS:
77
+ return 0.0
78
+
79
+ age_range = self.MAX_RECENCY_SECONDS - self.MIN_RECENCY_SECONDS
80
+ score = 1.0 - ((age_seconds - self.MIN_RECENCY_SECONDS) / age_range)
81
+ return min(1.0, max(0.0, score))
82
+
83
+ def _calculate_trust_score(self, chunk: Any) -> float:
84
+ """Calculate trust score based on source."""
85
+ source_trust = {
86
+ "symbol": 0.9,
87
+ "import_graph": 0.85,
88
+ "lineage": 0.8,
89
+ "call_graph": 0.75,
90
+ "episodic": 0.7,
91
+ "semantic": 0.6,
92
+ }
93
+ return source_trust.get(chunk.source, 0.5)
94
+
95
+ def _deduplicate_by_content(self, chunks: list[Any]) -> list[Any]:
96
+ """Remove similar chunks keeping highest score."""
97
+ seen: dict[str, Any] = {}
98
+ for chunk in chunks:
99
+ content_sig = chunk.content[:100].lower()
100
+ if content_sig not in seen or chunk.combined_score > seen[content_sig].combined_score:
101
+ seen[content_sig] = chunk
102
+ return list(seen.values())
@@ -0,0 +1,59 @@
1
+ """Retrieval data contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from enum import StrEnum
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class RetrievalSource(StrEnum):
13
+ """Source of a retrieval hit."""
14
+
15
+ VECTOR = "vector"
16
+ LEXICAL = "lexical"
17
+ GRAPH = "graph"
18
+ MEMORY = "memory"
19
+
20
+
21
+ class RetrievalDocument(BaseModel):
22
+ """Document stored in retrieval indexes."""
23
+
24
+ id: str
25
+ content: str
26
+ metadata: dict[str, Any] = Field(default_factory=dict)
27
+ namespace: str = "default"
28
+ embedding: list[float] | None = None
29
+ created_at: datetime = Field(default_factory=lambda: datetime.now(tz=UTC))
30
+
31
+
32
+ class RetrievalHit(BaseModel):
33
+ """A ranked retrieval hit."""
34
+
35
+ document: RetrievalDocument
36
+ score: float
37
+ source: RetrievalSource
38
+ rank: int = 0
39
+
40
+
41
+ class RetrievalQuery(BaseModel):
42
+ """Query for hybrid retrieval."""
43
+
44
+ text: str
45
+ top_k: int = Field(default=10, ge=1, le=100)
46
+ namespace: str | None = None
47
+ filters: dict[str, Any] = Field(default_factory=dict)
48
+ vector_weight: float = Field(default=0.5, ge=0.0, le=1.0)
49
+ lexical_weight: float = Field(default=0.3, ge=0.0, le=1.0)
50
+ graph_weight: float = Field(default=0.2, ge=0.0, le=1.0)
51
+
52
+
53
+ class RetrievalResult(BaseModel):
54
+ """Retrieved items plus provenance metadata."""
55
+
56
+ query: RetrievalQuery
57
+ hits: list[RetrievalHit] = Field(default_factory=list)
58
+ strategy: str = "hybrid"
59
+ metadata: dict[str, Any] = Field(default_factory=dict)