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.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +208 -0
- velune/cli/autocomplete.py +80 -0
- velune/cli/banner.py +60 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +175 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +228 -0
- velune/cli/commands/config.py +224 -0
- velune/cli/commands/daemon.py +88 -0
- velune/cli/commands/doctor.py +721 -0
- velune/cli/commands/init.py +170 -0
- velune/cli/commands/mcp.py +82 -0
- velune/cli/commands/memory.py +293 -0
- velune/cli/commands/models.py +683 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +270 -0
- velune/cli/commands/setup.py +184 -0
- velune/cli/commands/workspace.py +249 -0
- velune/cli/context.py +36 -0
- velune/cli/councilmodel_ui.py +199 -0
- velune/cli/display/council_view.py +254 -0
- velune/cli/display/memory_view.py +126 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +25 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +51 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +123 -0
- velune/cli/registry.py +80 -0
- velune/cli/rendering/__init__.py +5 -0
- velune/cli/rendering/error_panel.py +79 -0
- velune/cli/rendering/markdown.py +63 -0
- velune/cli/repl.py +1855 -0
- velune/cli/session_manager.py +71 -0
- velune/cli/slash_commands.py +37 -0
- velune/cli/theme.py +8 -0
- velune/cognition/__init__.py +23 -0
- velune/cognition/agents/__init__.py +7 -0
- velune/cognition/agents/coder.py +209 -0
- velune/cognition/agents/planner.py +156 -0
- velune/cognition/agents/reviewer.py +195 -0
- velune/cognition/arbitrator.py +220 -0
- velune/cognition/architecture.py +415 -0
- velune/cognition/budget.py +65 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +217 -0
- velune/cognition/council/challenger.py +74 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +43 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +46 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +56 -0
- velune/cognition/council/planner.py +124 -0
- velune/cognition/council/reviewer.py +74 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +188 -0
- velune/cognition/council_orchestrator.py +282 -0
- velune/cognition/firewall.py +354 -0
- velune/cognition/module.py +46 -0
- velune/cognition/orchestrator.py +1205 -0
- velune/cognition/personality.py +238 -0
- velune/cognition/state.py +104 -0
- velune/cognition/style_resolver.py +64 -0
- velune/cognition/verification.py +205 -0
- velune/context/__init__.py +28 -0
- velune/context/assembler.py +240 -0
- velune/context/budget.py +97 -0
- velune/context/extractive.py +95 -0
- velune/context/prompt_adaptation.py +480 -0
- velune/context/sections.py +99 -0
- velune/context/token_counter.py +134 -0
- velune/context/utilization.py +33 -0
- velune/context/window.py +63 -0
- velune/core/__init__.py +89 -0
- velune/core/background.py +5 -0
- velune/core/config/__init__.py +37 -0
- velune/core/errors/__init__.py +90 -0
- velune/core/errors/catalog.py +188 -0
- velune/core/errors/execution.py +31 -0
- velune/core/errors/memory.py +25 -0
- velune/core/errors/orchestration.py +31 -0
- velune/core/errors/provider.py +37 -0
- velune/core/event_loop.py +35 -0
- velune/core/logging.py +83 -0
- velune/core/paths.py +165 -0
- velune/core/runtime.py +113 -0
- velune/core/startup_profiler.py +56 -0
- velune/core/task_registry.py +117 -0
- velune/core/trace.py +83 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +53 -0
- velune/core/types/context.py +42 -0
- velune/core/types/inference.py +38 -0
- velune/core/types/memory.py +42 -0
- velune/core/types/model.py +70 -0
- velune/core/types/provider.py +62 -0
- velune/core/types/repository.py +38 -0
- velune/core/types/task.py +61 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +127 -0
- velune/daemon/transport.py +179 -0
- velune/events.py +204 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +315 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +130 -0
- velune/execution/command_spec.py +165 -0
- velune/execution/diff_preview.py +197 -0
- velune/execution/executor.py +181 -0
- velune/execution/module.py +18 -0
- velune/execution/multi_diff.py +67 -0
- velune/execution/path_guard.py +74 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +89 -0
- velune/execution/sandbox.py +268 -0
- velune/execution/validator.py +115 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +192 -0
- velune/kernel/__init__.py +55 -0
- velune/kernel/bootstrap.py +125 -0
- velune/kernel/config.py +426 -0
- velune/kernel/entrypoint.py +78 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +143 -0
- velune/kernel/module.py +17 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +96 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +115 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +624 -0
- velune/memory/__init__.py +32 -0
- velune/memory/compaction.py +506 -0
- velune/memory/embedding_pipeline.py +241 -0
- velune/memory/lifecycle.py +680 -0
- velune/memory/module.py +218 -0
- velune/memory/prioritizer.py +67 -0
- velune/memory/storage/episodic_schema.sql +53 -0
- velune/memory/storage/lancedb_store.py +282 -0
- velune/memory/storage/sqlite_manager.py +369 -0
- velune/memory/storage/sqlite_pool.py +149 -0
- velune/memory/tiers/episodic.py +588 -0
- velune/memory/tiers/graph.py +378 -0
- velune/memory/tiers/lineage.py +416 -0
- velune/memory/tiers/semantic.py +475 -0
- velune/memory/tiers/working.py +168 -0
- velune/memory/vitality.py +132 -0
- velune/models/__init__.py +15 -0
- velune/models/family.py +76 -0
- velune/models/module.py +20 -0
- velune/models/probes.py +192 -0
- velune/models/profile_cache.py +84 -0
- velune/models/profiler.py +108 -0
- velune/models/registry.py +251 -0
- velune/models/scorer.py +233 -0
- velune/models/specializations.py +205 -0
- velune/orchestration/__init__.py +19 -0
- velune/orchestration/engine.py +239 -0
- velune/orchestration/module.py +15 -0
- velune/orchestration/role_assignments.py +82 -0
- velune/orchestration/schemas.py +98 -0
- velune/plugins/__init__.py +20 -0
- velune/plugins/hooks.py +50 -0
- velune/plugins/loader.py +161 -0
- velune/plugins/registry.py +56 -0
- velune/plugins/schemas.py +21 -0
- velune/providers/__init__.py +23 -0
- velune/providers/adapters/anthropic.py +257 -0
- velune/providers/adapters/fireworks.py +115 -0
- velune/providers/adapters/google.py +234 -0
- velune/providers/adapters/groq.py +151 -0
- velune/providers/adapters/huggingface.py +210 -0
- velune/providers/adapters/llamacpp.py +208 -0
- velune/providers/adapters/lmstudio.py +175 -0
- velune/providers/adapters/ollama.py +233 -0
- velune/providers/adapters/openai.py +213 -0
- velune/providers/adapters/openrouter.py +81 -0
- velune/providers/adapters/together.py +134 -0
- velune/providers/adapters/xai.py +60 -0
- velune/providers/base.py +86 -0
- velune/providers/benchmarker.py +138 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +79 -0
- velune/providers/discovery/benchmarks.py +44 -0
- velune/providers/discovery/classifier.py +69 -0
- velune/providers/discovery/fireworks.py +95 -0
- velune/providers/discovery/gguf.py +88 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +117 -0
- velune/providers/discovery/groq.py +21 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +162 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +115 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/health.py +67 -0
- velune/providers/health_monitor.py +169 -0
- velune/providers/keystore.py +142 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +229 -0
- velune/providers/module.py +51 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +220 -0
- velune/providers/router.py +255 -0
- velune/providers/task_classifier.py +288 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +33 -0
- velune/repository/analyzer.py +127 -0
- velune/repository/ast_parser.py +822 -0
- velune/repository/blast_radius.py +298 -0
- velune/repository/boundary_classifier.py +295 -0
- velune/repository/cognition.py +316 -0
- velune/repository/grapher.py +179 -0
- velune/repository/import_graph.py +263 -0
- velune/repository/incremental_indexer.py +275 -0
- velune/repository/index_state.py +96 -0
- velune/repository/indexer.py +243 -0
- velune/repository/module.py +17 -0
- velune/repository/parser.py +474 -0
- velune/repository/project_type.py +300 -0
- velune/repository/rename_journal.py +287 -0
- velune/repository/scanner.py +193 -0
- velune/repository/schemas.py +102 -0
- velune/repository/symbol_registry.py +365 -0
- velune/repository/tracker.py +252 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/cache.py +110 -0
- velune/retrieval/fast_path.py +391 -0
- velune/retrieval/graph.py +124 -0
- velune/retrieval/hybrid.py +271 -0
- velune/retrieval/keyword.py +131 -0
- velune/retrieval/module.py +26 -0
- velune/retrieval/pipeline.py +303 -0
- velune/retrieval/reranker.py +102 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/slow_path.py +364 -0
- velune/retrieval/vector.py +203 -0
- velune/telemetry/__init__.py +59 -0
- velune/telemetry/cognition.py +267 -0
- velune/telemetry/cost_estimator.py +92 -0
- velune/telemetry/debug.py +304 -0
- velune/telemetry/doctor.py +244 -0
- velune/telemetry/logging.py +286 -0
- velune/telemetry/spans.py +277 -0
- velune/telemetry/token_tracker.py +140 -0
- velune/telemetry/usage_tracker.py +340 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +87 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +116 -0
- velune/tools/code/search.py +123 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +136 -0
- velune/tools/filesystem/write.py +163 -0
- velune/tools/git/history.py +177 -0
- velune/tools/git/operations.py +122 -0
- velune/tools/git/state.py +121 -0
- velune/tools/module.py +81 -0
- velune/tools/terminal/execute.py +72 -0
- velune/tools/terminal/history.py +47 -0
- velune/tools/web/fetch.py +55 -0
- velune/tools/web/validator.py +122 -0
- velune_cli-0.9.0.dist-info/METADATA +518 -0
- velune_cli-0.9.0.dist-info/RECORD +279 -0
- velune_cli-0.9.0.dist-info/WHEEL +4 -0
- velune_cli-0.9.0.dist-info/entry_points.txt +2 -0
- 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)
|