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,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
|
+
]
|