velune-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- velune/__init__.py +5 -0
- velune/__main__.py +6 -0
- velune/cli/__init__.py +5 -0
- velune/cli/app.py +212 -0
- velune/cli/autocomplete.py +76 -0
- velune/cli/banner.py +98 -0
- velune/cli/commands/__init__.py +32 -0
- velune/cli/commands/ask.py +149 -0
- velune/cli/commands/base.py +16 -0
- velune/cli/commands/chat.py +188 -0
- velune/cli/commands/config.py +182 -0
- velune/cli/commands/daemon.py +85 -0
- velune/cli/commands/doctor.py +373 -0
- velune/cli/commands/init.py +160 -0
- velune/cli/commands/mcp.py +80 -0
- velune/cli/commands/memory.py +269 -0
- velune/cli/commands/models.py +462 -0
- velune/cli/commands/preflight.py +95 -0
- velune/cli/commands/run.py +171 -0
- velune/cli/commands/setup.py +182 -0
- velune/cli/commands/workspace.py +217 -0
- velune/cli/context.py +37 -0
- velune/cli/councilmodel_ui.py +171 -0
- velune/cli/display/council_view.py +240 -0
- velune/cli/display/memory_view.py +93 -0
- velune/cli/display/panels.py +35 -0
- velune/cli/display/progress.py +25 -0
- velune/cli/display/themes.py +21 -0
- velune/cli/main.py +15 -0
- velune/cli/model_selector.py +44 -0
- velune/cli/modes.py +86 -0
- velune/cli/pull_ui.py +118 -0
- velune/cli/registry.py +81 -0
- velune/cli/repl.py +1178 -0
- velune/cli/session_manager.py +69 -0
- velune/cli/slash_commands.py +37 -0
- velune/cognition/__init__.py +19 -0
- velune/cognition/arbitrator.py +216 -0
- velune/cognition/architecture.py +398 -0
- velune/cognition/council/__init__.py +47 -0
- velune/cognition/council/base.py +216 -0
- velune/cognition/council/challenger.py +70 -0
- velune/cognition/council/coder.py +79 -0
- velune/cognition/council/critic_agent.py +39 -0
- velune/cognition/council/critic_configs.py +111 -0
- velune/cognition/council/critics.py +41 -0
- velune/cognition/council/debate.py +44 -0
- velune/cognition/council/factory.py +140 -0
- velune/cognition/council/messages.py +53 -0
- velune/cognition/council/planner.py +119 -0
- velune/cognition/council/reviewer.py +72 -0
- velune/cognition/council/synthesizer.py +67 -0
- velune/cognition/council/tiers.py +181 -0
- velune/cognition/firewall.py +256 -0
- velune/cognition/module.py +38 -0
- velune/cognition/orchestrator.py +886 -0
- velune/cognition/personality.py +236 -0
- velune/cognition/style_resolver.py +62 -0
- velune/cognition/verification.py +201 -0
- velune/context/__init__.py +11 -0
- velune/context/extractive.py +94 -0
- velune/context/window.py +62 -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 +53 -0
- velune/core/errors/execution.py +26 -0
- velune/core/errors/memory.py +21 -0
- velune/core/errors/orchestration.py +26 -0
- velune/core/errors/provider.py +31 -0
- velune/core/event_loop.py +30 -0
- velune/core/logging.py +83 -0
- velune/core/runtime.py +106 -0
- velune/core/task_registry.py +120 -0
- velune/core/trace.py +80 -0
- velune/core/types/__init__.py +48 -0
- velune/core/types/agent.py +49 -0
- velune/core/types/context.py +39 -0
- velune/core/types/inference.py +35 -0
- velune/core/types/memory.py +39 -0
- velune/core/types/model.py +64 -0
- velune/core/types/provider.py +35 -0
- velune/core/types/repository.py +35 -0
- velune/core/types/task.py +56 -0
- velune/core/types/workspace.py +28 -0
- velune/daemon/client.py +13 -0
- velune/daemon/server.py +115 -0
- velune/daemon/transport.py +169 -0
- velune/events.py +194 -0
- velune/execution/__init__.py +22 -0
- velune/execution/benchmarker.py +311 -0
- velune/execution/cancellation.py +53 -0
- velune/execution/checkpointer.py +128 -0
- velune/execution/command_spec.py +140 -0
- velune/execution/diff_preview.py +172 -0
- velune/execution/executor.py +173 -0
- velune/execution/module.py +16 -0
- velune/execution/multi_diff.py +70 -0
- velune/execution/path_guard.py +19 -0
- velune/execution/planner.py +91 -0
- velune/execution/rollback.py +80 -0
- velune/execution/sandbox.py +257 -0
- velune/execution/validator.py +113 -0
- velune/hardware/__init__.py +1 -0
- velune/hardware/detector.py +162 -0
- velune/kernel/__init__.py +58 -0
- velune/kernel/bootstrap.py +107 -0
- velune/kernel/config.py +252 -0
- velune/kernel/health.py +54 -0
- velune/kernel/lifecycle.py +102 -0
- velune/kernel/module.py +15 -0
- velune/kernel/modules.py +23 -0
- velune/kernel/registry.py +93 -0
- velune/kernel/schemas.py +28 -0
- velune/main.py +9 -0
- velune/mcp/__init__.py +9 -0
- velune/mcp/client.py +113 -0
- velune/mcp/config.py +19 -0
- velune/mcp/server.py +90 -0
- velune/memory/__init__.py +28 -0
- velune/memory/lifecycle.py +154 -0
- velune/memory/module.py +94 -0
- velune/memory/prioritizer.py +65 -0
- velune/memory/storage/sqlite_manager.py +368 -0
- velune/memory/tiers/episodic.py +156 -0
- velune/memory/tiers/graph.py +282 -0
- velune/memory/tiers/lineage.py +367 -0
- velune/memory/tiers/semantic.py +198 -0
- velune/memory/tiers/working.py +165 -0
- velune/models/__init__.py +16 -0
- velune/models/module.py +18 -0
- velune/models/probes.py +182 -0
- velune/models/profile_cache.py +82 -0
- velune/models/profiler.py +105 -0
- velune/models/registry.py +201 -0
- velune/models/scorer.py +225 -0
- velune/models/specializations.py +196 -0
- velune/orchestration/__init__.py +15 -0
- velune/orchestration/module.py +14 -0
- velune/orchestration/role_assignments.py +78 -0
- velune/orchestration/schemas.py +99 -0
- velune/plugins/__init__.py +13 -0
- velune/plugins/hooks.py +49 -0
- velune/plugins/loader.py +95 -0
- velune/plugins/registry.py +54 -0
- velune/plugins/schemas.py +19 -0
- velune/providers/__init__.py +18 -0
- velune/providers/adapters/anthropic.py +231 -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 +203 -0
- velune/providers/adapters/llamacpp.py +202 -0
- velune/providers/adapters/lmstudio.py +173 -0
- velune/providers/adapters/ollama.py +186 -0
- velune/providers/adapters/openai.py +207 -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 +135 -0
- velune/providers/discovery/__init__.py +33 -0
- velune/providers/discovery/anthropic.py +77 -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 +87 -0
- velune/providers/discovery/google.py +95 -0
- velune/providers/discovery/gpu.py +109 -0
- velune/providers/discovery/groq.py +20 -0
- velune/providers/discovery/huggingface.py +67 -0
- velune/providers/discovery/lmstudio.py +80 -0
- velune/providers/discovery/ollama.py +165 -0
- velune/providers/discovery/openai.py +96 -0
- velune/providers/discovery/openrouter.py +113 -0
- velune/providers/discovery/scanner.py +114 -0
- velune/providers/discovery/together.py +114 -0
- velune/providers/discovery/xai.py +57 -0
- velune/providers/keystore.py +83 -0
- velune/providers/local_paths.py +49 -0
- velune/providers/local_resolver.py +208 -0
- velune/providers/module.py +15 -0
- velune/providers/ollama_manager.py +193 -0
- velune/providers/registry.py +173 -0
- velune/providers/router.py +82 -0
- velune/py.typed +0 -0
- velune/repository/__init__.py +21 -0
- velune/repository/analyzer.py +123 -0
- velune/repository/cognition.py +172 -0
- velune/repository/grapher.py +182 -0
- velune/repository/indexer.py +229 -0
- velune/repository/module.py +15 -0
- velune/repository/parser.py +378 -0
- velune/repository/project_type.py +293 -0
- velune/repository/scanner.py +177 -0
- velune/repository/schemas.py +102 -0
- velune/repository/tracker.py +233 -0
- velune/retrieval/__init__.py +27 -0
- velune/retrieval/graph.py +117 -0
- velune/retrieval/hybrid.py +250 -0
- velune/retrieval/keyword.py +113 -0
- velune/retrieval/module.py +19 -0
- velune/retrieval/reranker.py +68 -0
- velune/retrieval/schemas.py +59 -0
- velune/retrieval/vector.py +163 -0
- velune/telemetry/__init__.py +7 -0
- velune/telemetry/cognition.py +260 -0
- velune/telemetry/token_tracker.py +133 -0
- velune/tools/__init__.py +41 -0
- velune/tools/base/registry.py +84 -0
- velune/tools/base/tool.py +63 -0
- velune/tools/code/navigate.py +107 -0
- velune/tools/code/search.py +112 -0
- velune/tools/filesystem/read.py +75 -0
- velune/tools/filesystem/search.py +123 -0
- velune/tools/filesystem/write.py +160 -0
- velune/tools/git/history.py +185 -0
- velune/tools/git/operations.py +132 -0
- velune/tools/git/state.py +134 -0
- velune/tools/module.py +65 -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 +96 -0
- velune_cli-1.0.0.dist-info/METADATA +497 -0
- velune_cli-1.0.0.dist-info/RECORD +229 -0
- velune_cli-1.0.0.dist-info/WHEEL +4 -0
- velune_cli-1.0.0.dist-info/entry_points.txt +2 -0
- velune_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""BM25 Lexical retrieval layer for exact keyword matches."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from rank_bm25 import BM25Okapi
|
|
9
|
+
|
|
10
|
+
from velune.retrieval.schemas import RetrievalDocument, RetrievalHit, RetrievalSource
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("velune.retrieval.keyword")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BM25Retriever:
|
|
16
|
+
"""Retrieves context using BM25 exact keyword lexical indexing."""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self.documents: list[RetrievalDocument] = []
|
|
20
|
+
self.corpus: list[list[str]] = []
|
|
21
|
+
self.bm25: BM25Okapi | None = None
|
|
22
|
+
self._dirty: bool = False # True when corpus needs rebuild
|
|
23
|
+
self._rebuild_lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def index_size(self) -> int:
|
|
27
|
+
return len(self.documents)
|
|
28
|
+
|
|
29
|
+
def add_documents(self, docs: list[RetrievalDocument]) -> None:
|
|
30
|
+
"""Appends new documents to the lexical index corpus and marks the index as dirty."""
|
|
31
|
+
for doc in docs:
|
|
32
|
+
self.documents.append(doc)
|
|
33
|
+
self.corpus.append(self._tokenize(doc.content))
|
|
34
|
+
self._dirty = True
|
|
35
|
+
self.bm25 = None # Invalidate current index
|
|
36
|
+
logger.debug("BM25 index marked dirty: %d total documents", len(self.documents))
|
|
37
|
+
|
|
38
|
+
def add_documents_batch(self, docs: list[RetrievalDocument]) -> None:
|
|
39
|
+
"""More efficient batch add — tokenizes new documents and appends them to corpus."""
|
|
40
|
+
new_tokens = [self._tokenize(doc.content) for doc in docs]
|
|
41
|
+
self.documents.extend(docs)
|
|
42
|
+
self.corpus.extend(new_tokens)
|
|
43
|
+
self._dirty = True
|
|
44
|
+
self.bm25 = None
|
|
45
|
+
logger.debug("BM25 index marked dirty: %d total documents", len(self.documents))
|
|
46
|
+
|
|
47
|
+
def _ensure_index(self) -> None:
|
|
48
|
+
"""Rebuild BM25 index if dirty. Thread-safe."""
|
|
49
|
+
if not self._dirty or not self.corpus:
|
|
50
|
+
return
|
|
51
|
+
with self._rebuild_lock:
|
|
52
|
+
if not self._dirty: # Double-check after acquiring lock
|
|
53
|
+
return
|
|
54
|
+
start_time = time.time()
|
|
55
|
+
self.bm25 = BM25Okapi(self.corpus)
|
|
56
|
+
self._dirty = False
|
|
57
|
+
elapsed = time.time() - start_time
|
|
58
|
+
logger.debug("BM25 index rebuilt: %d documents, %.3fs", len(self.documents), elapsed)
|
|
59
|
+
|
|
60
|
+
def retrieve(self, query: str, top_k: int = 10, namespace: str | None = None) -> list[RetrievalHit]:
|
|
61
|
+
"""Queries the BM25 lexical index and scores candidates."""
|
|
62
|
+
if not self.documents:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
if self._dirty:
|
|
66
|
+
self._ensure_index()
|
|
67
|
+
|
|
68
|
+
if not self.bm25:
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
tokens = self._tokenize(query)
|
|
72
|
+
scores = self.bm25.get_scores(tokens)
|
|
73
|
+
|
|
74
|
+
# Pair scores with documents and index ranks
|
|
75
|
+
hits: list[RetrievalHit] = []
|
|
76
|
+
for i, score in enumerate(scores):
|
|
77
|
+
doc = self.documents[i]
|
|
78
|
+
|
|
79
|
+
# Match namespace filter if provided
|
|
80
|
+
if namespace and doc.namespace != namespace:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if score > 0.0: # Only capture positive keyword matches
|
|
84
|
+
hits.append(
|
|
85
|
+
RetrievalHit(
|
|
86
|
+
document=doc,
|
|
87
|
+
score=float(score),
|
|
88
|
+
source=RetrievalSource.LEXICAL,
|
|
89
|
+
rank=0
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Sort and return top candidates
|
|
94
|
+
hits.sort(key=lambda x: x.score, reverse=True)
|
|
95
|
+
|
|
96
|
+
# Trim list and apply proper sequential ranks
|
|
97
|
+
final_hits = hits[:top_k]
|
|
98
|
+
for idx, h in enumerate(final_hits):
|
|
99
|
+
h.rank = idx + 1
|
|
100
|
+
|
|
101
|
+
return final_hits
|
|
102
|
+
|
|
103
|
+
def _tokenize(self, text: str) -> list[str]:
|
|
104
|
+
"""Simplistic and quick alphanumeric tokenization ignoring standard case mappings."""
|
|
105
|
+
# Lowercase and split on non-alphanumeric boundaries
|
|
106
|
+
words = re.findall(r"\w+", text.lower())
|
|
107
|
+
|
|
108
|
+
# Remove extremely common short English stop words to filter noise
|
|
109
|
+
stop_words = {
|
|
110
|
+
"a", "an", "the", "and", "or", "but", "if", "then", "else",
|
|
111
|
+
"to", "of", "in", "for", "on", "with", "at", "by", "from", "is", "this", "that"
|
|
112
|
+
}
|
|
113
|
+
return [w for w in words if w not in stop_words and len(w) > 1]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from velune.kernel.bootstrap import RuntimeEnvironment, SubsystemModule
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _create_hybrid_retriever(env: RuntimeEnvironment):
|
|
5
|
+
from velune.retrieval.hybrid import HybridRetriever
|
|
6
|
+
velune_dir = env.workspace / ".velune"
|
|
7
|
+
vector_path = str(velune_dir / "qdrant_local_store")
|
|
8
|
+
semantic_tier = env.container.get("runtime.semantic_memory")
|
|
9
|
+
return HybridRetriever(location=vector_path, client=semantic_tier.client)
|
|
10
|
+
|
|
11
|
+
RETRIEVAL_MODULES = [
|
|
12
|
+
SubsystemModule(
|
|
13
|
+
name="retrieval",
|
|
14
|
+
factory=_create_hybrid_retriever,
|
|
15
|
+
container_key="runtime.retrieval",
|
|
16
|
+
lifecycle_key="retrieval",
|
|
17
|
+
dependencies=["runtime.semantic_memory"],
|
|
18
|
+
)
|
|
19
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Cross-encoder API reranking with lightweight fallback similarity scoring."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from velune.retrieval.schemas import RetrievalHit
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ContextReranker:
|
|
8
|
+
"""Re-scores candidate hits using lexical alignment and semantic keyword density."""
|
|
9
|
+
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
def rerank(self, query: str, hits: list[RetrievalHit]) -> list[RetrievalHit]:
|
|
14
|
+
"""Re-scores and re-ranks candidate hits based on similarity density."""
|
|
15
|
+
if not hits:
|
|
16
|
+
return []
|
|
17
|
+
|
|
18
|
+
scored_hits: list[RetrievalHit] = []
|
|
19
|
+
for hit in hits:
|
|
20
|
+
score = self._compute_alignment_score(query, hit.document.content)
|
|
21
|
+
|
|
22
|
+
# Combine the original retrieval score (BM25 or vector cosine) with our alignment score
|
|
23
|
+
# Vector cosine is between -1.0 and 1.0 (typically 0.4-0.9), BM25 can be larger.
|
|
24
|
+
# We scale the alignment score to a 0.0-1.0 range and merge.
|
|
25
|
+
merged_score = hit.score * 0.4 + score * 0.6
|
|
26
|
+
|
|
27
|
+
# Clone hit with new score
|
|
28
|
+
scored_hits.append(
|
|
29
|
+
RetrievalHit(
|
|
30
|
+
document=hit.document,
|
|
31
|
+
score=merged_score,
|
|
32
|
+
source=hit.source,
|
|
33
|
+
rank=0
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Sort by updated scores
|
|
38
|
+
scored_hits.sort(key=lambda x: x.score, reverse=True)
|
|
39
|
+
|
|
40
|
+
# Apply new ranks
|
|
41
|
+
for idx, h in enumerate(scored_hits):
|
|
42
|
+
h.rank = idx + 1
|
|
43
|
+
|
|
44
|
+
return scored_hits
|
|
45
|
+
|
|
46
|
+
def _compute_alignment_score(self, query: str, document_text: str) -> float:
|
|
47
|
+
"""Computes lexical term alignment and density score (Jaccard + keyword overlaps)."""
|
|
48
|
+
q_tokens = set(query.lower().split())
|
|
49
|
+
doc_tokens = set(document_text.lower().split())
|
|
50
|
+
|
|
51
|
+
if not q_tokens or not doc_tokens:
|
|
52
|
+
return 0.0
|
|
53
|
+
|
|
54
|
+
# Jaccard similarity index
|
|
55
|
+
intersection = q_tokens.intersection(doc_tokens)
|
|
56
|
+
union = q_tokens.union(doc_tokens)
|
|
57
|
+
jaccard = len(intersection) / len(union)
|
|
58
|
+
|
|
59
|
+
# Coverage fraction (how many query words appear in document)
|
|
60
|
+
coverage = len(intersection) / len(q_tokens)
|
|
61
|
+
|
|
62
|
+
# Exact substring matches (bonus weight for sequential phrase matches)
|
|
63
|
+
phrase_bonus = 0.0
|
|
64
|
+
if query.lower() in document_text.lower():
|
|
65
|
+
phrase_bonus = 0.3
|
|
66
|
+
|
|
67
|
+
# Composite score
|
|
68
|
+
return jaccard * 0.2 + coverage * 0.5 + phrase_bonus
|
|
@@ -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)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Vector retrieval layer using Qdrant client."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from qdrant_client import QdrantClient
|
|
6
|
+
from qdrant_client.http import models as qmodels
|
|
7
|
+
|
|
8
|
+
from velune.retrieval.schemas import RetrievalDocument, RetrievalHit, RetrievalSource
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("velune.retrieval.vector")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VectorRetriever:
|
|
14
|
+
"""Retrieves context from Qdrant vector database using dense embeddings."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, collection_name: str = "velune_symbols", location: str = ".velune/qdrant_local_store", client: QdrantClient | None = None) -> None:
|
|
17
|
+
self.collection_name = collection_name
|
|
18
|
+
self.location = location
|
|
19
|
+
if client is not None:
|
|
20
|
+
self.client = client
|
|
21
|
+
elif location.startswith(".") or "/" in location or "\\" in location:
|
|
22
|
+
self.client = QdrantClient(path=location)
|
|
23
|
+
else:
|
|
24
|
+
self.client = QdrantClient(location=location)
|
|
25
|
+
self._detected_dimension: int | None = None
|
|
26
|
+
|
|
27
|
+
# Proactively detect existing collection dimension if it exists
|
|
28
|
+
try:
|
|
29
|
+
collections = self.client.get_collections().collections
|
|
30
|
+
existing = any(c.name == self.collection_name for c in collections)
|
|
31
|
+
if existing:
|
|
32
|
+
info = self.client.get_collection(self.collection_name)
|
|
33
|
+
self._detected_dimension = info.config.params.vectors.size
|
|
34
|
+
logger.info("Collection '%s' initialized with dimension %d", self.collection_name, self._detected_dimension)
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def _ensure_collection_for_dimension(self, dim: int) -> None:
|
|
39
|
+
"""Create or verify collection for the given dimension."""
|
|
40
|
+
if self._detected_dimension == dim:
|
|
41
|
+
return # Already configured
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
collections = self.client.get_collections().collections
|
|
45
|
+
existing = next((c for c in collections if c.name == self.collection_name), None)
|
|
46
|
+
|
|
47
|
+
if existing:
|
|
48
|
+
# Check if dimension matches
|
|
49
|
+
info = self.client.get_collection(self.collection_name)
|
|
50
|
+
existing_dim = info.config.params.vectors.size
|
|
51
|
+
if existing_dim != dim:
|
|
52
|
+
logger.warning(
|
|
53
|
+
"Embedding dimension changed from %d to %d. "
|
|
54
|
+
"Recreating collection '%s'. Existing vectors lost.",
|
|
55
|
+
existing_dim, dim, self.collection_name
|
|
56
|
+
)
|
|
57
|
+
self.client.delete_collection(self.collection_name)
|
|
58
|
+
existing = None
|
|
59
|
+
|
|
60
|
+
if not existing:
|
|
61
|
+
self.client.create_collection(
|
|
62
|
+
collection_name=self.collection_name,
|
|
63
|
+
vectors_config=qmodels.VectorParams(
|
|
64
|
+
size=dim,
|
|
65
|
+
distance=qmodels.Distance.COSINE
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
logger.info("Collection '%s' initialized with dimension %d", self.collection_name, dim)
|
|
69
|
+
|
|
70
|
+
self._detected_dimension = dim
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error("Failed to ensure collection for dimension %d: %s", dim, e)
|
|
73
|
+
|
|
74
|
+
def upsert(self, doc: RetrievalDocument) -> None:
|
|
75
|
+
"""Inserts or updates a document with its embedding in Qdrant."""
|
|
76
|
+
if not doc.embedding:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
dim = len(doc.embedding)
|
|
80
|
+
self._ensure_collection_for_dimension(dim)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
self.client.upsert(
|
|
84
|
+
collection_name=self.collection_name,
|
|
85
|
+
points=[
|
|
86
|
+
qmodels.PointStruct(
|
|
87
|
+
id=hash(doc.id) % (2**63 - 1), # Map string ID to uint64
|
|
88
|
+
vector=doc.embedding, # exact length, no padding
|
|
89
|
+
payload={
|
|
90
|
+
"doc_id": doc.id,
|
|
91
|
+
"content": doc.content,
|
|
92
|
+
"namespace": doc.namespace,
|
|
93
|
+
**doc.metadata
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error("Failed to upsert document: %s", e)
|
|
100
|
+
|
|
101
|
+
def retrieve(self, query_vector: list[float], top_k: int = 10, namespace: str | None = None) -> list[RetrievalHit]:
|
|
102
|
+
"""Queries Qdrant vector spaces and returns matching document hits."""
|
|
103
|
+
if not self._detected_dimension:
|
|
104
|
+
try:
|
|
105
|
+
collections = self.client.get_collections().collections
|
|
106
|
+
existing = any(c.name == self.collection_name for c in collections)
|
|
107
|
+
if existing:
|
|
108
|
+
info = self.client.get_collection(self.collection_name)
|
|
109
|
+
self._detected_dimension = info.config.params.vectors.size
|
|
110
|
+
else:
|
|
111
|
+
return []
|
|
112
|
+
except Exception:
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
if self._detected_dimension and len(query_vector) != self._detected_dimension:
|
|
116
|
+
logger.error(
|
|
117
|
+
"Query dimension mismatch — skipping vector retrieval. "
|
|
118
|
+
"Query dimension %d != collection dimension %d.",
|
|
119
|
+
len(query_vector), self._detected_dimension
|
|
120
|
+
)
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
hits: list[RetrievalHit] = []
|
|
124
|
+
try:
|
|
125
|
+
# Build filters
|
|
126
|
+
query_filter = None
|
|
127
|
+
if namespace:
|
|
128
|
+
query_filter = qmodels.Filter(
|
|
129
|
+
must=[
|
|
130
|
+
qmodels.FieldCondition(
|
|
131
|
+
key="namespace",
|
|
132
|
+
match=qmodels.MatchValue(value=namespace)
|
|
133
|
+
)
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
results = self.client.query_points(
|
|
138
|
+
collection_name=self.collection_name,
|
|
139
|
+
query=query_vector,
|
|
140
|
+
query_filter=query_filter,
|
|
141
|
+
limit=top_k
|
|
142
|
+
).points
|
|
143
|
+
|
|
144
|
+
for rank, res in enumerate(results):
|
|
145
|
+
payload = res.payload or {}
|
|
146
|
+
doc = RetrievalDocument(
|
|
147
|
+
id=payload.get("doc_id", str(res.id)),
|
|
148
|
+
content=payload.get("content", ""),
|
|
149
|
+
namespace=payload.get("namespace", "default"),
|
|
150
|
+
metadata={k: v for k, v in payload.items() if k not in ("doc_id", "content", "namespace")}
|
|
151
|
+
)
|
|
152
|
+
hits.append(
|
|
153
|
+
RetrievalHit(
|
|
154
|
+
document=doc,
|
|
155
|
+
score=res.score,
|
|
156
|
+
source=RetrievalSource.VECTOR,
|
|
157
|
+
rank=rank + 1
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error("Failed to retrieve matching documents from Qdrant: %s", e)
|
|
162
|
+
|
|
163
|
+
return hits
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Cognitive Performance Analytics and Dynamic Model Routing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from velune.memory.storage.sqlite_manager import SQLiteManager
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("velune.telemetry.cognition")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CognitivePerformanceAnalytics:
|
|
17
|
+
"""Tracks and persists cognitive metrics like hallucination rates, rollbacks, and routes dynamically.
|
|
18
|
+
|
|
19
|
+
Routes all reads and writes through SQLiteManager to guarantee thread safety and eliminate lock contention.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, sqlite_manager: SQLiteManager | None = None, db_path: str | Path | None = None) -> None:
|
|
23
|
+
if sqlite_manager is not None:
|
|
24
|
+
self.sqlite_manager = sqlite_manager
|
|
25
|
+
self.db_path = sqlite_manager.db_path
|
|
26
|
+
else:
|
|
27
|
+
# Standalone mode (doctor command, tests)
|
|
28
|
+
if db_path is None:
|
|
29
|
+
self.db_path = Path(".velune") / "velune_cognitive_core.db"
|
|
30
|
+
else:
|
|
31
|
+
self.db_path = Path(db_path)
|
|
32
|
+
|
|
33
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
from velune.memory.storage.sqlite_manager import SQLiteManager
|
|
35
|
+
self.sqlite_manager = SQLiteManager(self.db_path)
|
|
36
|
+
|
|
37
|
+
self._init_db()
|
|
38
|
+
|
|
39
|
+
def _init_db(self) -> None:
|
|
40
|
+
script = """
|
|
41
|
+
CREATE TABLE IF NOT EXISTS cognitive_metrics (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
timestamp TEXT NOT NULL,
|
|
44
|
+
model_id TEXT NOT NULL,
|
|
45
|
+
task_type TEXT NOT NULL,
|
|
46
|
+
language TEXT,
|
|
47
|
+
directory TEXT,
|
|
48
|
+
hallucinated INTEGER NOT NULL, -- 0 or 1
|
|
49
|
+
rolled_back INTEGER NOT NULL, -- 0 or 1
|
|
50
|
+
token_count INTEGER NOT NULL,
|
|
51
|
+
execution_time_ms INTEGER NOT NULL,
|
|
52
|
+
success INTEGER NOT NULL -- 0 or 1
|
|
53
|
+
);
|
|
54
|
+
CREATE TABLE IF NOT EXISTS critic_performance_metrics (
|
|
55
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
|
+
task_id TEXT NOT NULL,
|
|
57
|
+
critic_role TEXT NOT NULL,
|
|
58
|
+
vote INTEGER NOT NULL, -- 1 for passed, 0 for objected
|
|
59
|
+
success INTEGER NOT NULL -- 1 for success, 0 for failed
|
|
60
|
+
);
|
|
61
|
+
CREATE TABLE IF NOT EXISTS debate_telemetry (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
timestamp TEXT NOT NULL,
|
|
64
|
+
turns_required INTEGER NOT NULL,
|
|
65
|
+
initial_objection_count INTEGER NOT NULL,
|
|
66
|
+
final_objection_count INTEGER NOT NULL,
|
|
67
|
+
converged INTEGER NOT NULL,
|
|
68
|
+
time_to_converge_ms INTEGER NOT NULL
|
|
69
|
+
);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS compression_metrics (
|
|
71
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
72
|
+
timestamp TEXT NOT NULL,
|
|
73
|
+
original_tokens INTEGER NOT NULL,
|
|
74
|
+
compressed_tokens INTEGER NOT NULL,
|
|
75
|
+
method TEXT NOT NULL,
|
|
76
|
+
latency_ms INTEGER NOT NULL
|
|
77
|
+
);
|
|
78
|
+
CREATE TABLE IF NOT EXISTS injection_attempts (
|
|
79
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
|
+
timestamp TEXT NOT NULL,
|
|
81
|
+
source TEXT NOT NULL,
|
|
82
|
+
pattern TEXT NOT NULL
|
|
83
|
+
);
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
self.sqlite_manager.execute_script(script)
|
|
87
|
+
except (TimeoutError, RuntimeError) as e:
|
|
88
|
+
logger.critical("Failed to initialize database schema: %s", e)
|
|
89
|
+
raise
|
|
90
|
+
|
|
91
|
+
def record_metrics(
|
|
92
|
+
self,
|
|
93
|
+
model_id: str,
|
|
94
|
+
task_type: str,
|
|
95
|
+
hallucinated: bool,
|
|
96
|
+
rolled_back: bool,
|
|
97
|
+
token_count: int,
|
|
98
|
+
execution_time_ms: int,
|
|
99
|
+
success: bool,
|
|
100
|
+
language: str | None = None,
|
|
101
|
+
directory: str | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Records a single cognitive invocation metric."""
|
|
104
|
+
query = """
|
|
105
|
+
INSERT INTO cognitive_metrics (
|
|
106
|
+
timestamp, model_id, task_type, language, directory,
|
|
107
|
+
hallucinated, rolled_back, token_count, execution_time_ms, success
|
|
108
|
+
)
|
|
109
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
110
|
+
"""
|
|
111
|
+
params = (
|
|
112
|
+
datetime.now(tz=UTC).isoformat(),
|
|
113
|
+
model_id,
|
|
114
|
+
task_type,
|
|
115
|
+
language,
|
|
116
|
+
directory,
|
|
117
|
+
1 if hallucinated else 0,
|
|
118
|
+
1 if rolled_back else 0,
|
|
119
|
+
token_count,
|
|
120
|
+
execution_time_ms,
|
|
121
|
+
1 if success else 0,
|
|
122
|
+
)
|
|
123
|
+
self.sqlite_manager.execute_write(query, params)
|
|
124
|
+
logger.debug("Recorded cognitive metrics for model %s", model_id)
|
|
125
|
+
|
|
126
|
+
def get_model_performance(self, model_id: str) -> dict[str, Any]:
|
|
127
|
+
"""Calculates performance aggregated KPIs for a specific model."""
|
|
128
|
+
query = """
|
|
129
|
+
SELECT
|
|
130
|
+
COUNT(*) as total_runs,
|
|
131
|
+
SUM(hallucinated) as total_hallucinations,
|
|
132
|
+
SUM(rolled_back) as total_rollbacks,
|
|
133
|
+
SUM(success) as total_successes,
|
|
134
|
+
AVG(token_count) as avg_tokens,
|
|
135
|
+
AVG(execution_time_ms) as avg_execution_time
|
|
136
|
+
FROM cognitive_metrics
|
|
137
|
+
WHERE model_id = ?
|
|
138
|
+
"""
|
|
139
|
+
rows = self.sqlite_manager.execute_read(query, (model_id,))
|
|
140
|
+
if not rows or rows[0]["total_runs"] == 0:
|
|
141
|
+
return {
|
|
142
|
+
"total_runs": 0,
|
|
143
|
+
"hallucination_rate": 0.0,
|
|
144
|
+
"rollback_ratio": 0.0,
|
|
145
|
+
"success_rate": 0.0,
|
|
146
|
+
"avg_tokens": 0.0,
|
|
147
|
+
"avg_execution_time": 0.0,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
row = rows[0]
|
|
151
|
+
total = row["total_runs"]
|
|
152
|
+
return {
|
|
153
|
+
"total_runs": total,
|
|
154
|
+
"hallucination_rate": round(float(row["total_hallucinations"] or 0) / total, 3),
|
|
155
|
+
"rollback_ratio": round(float(row["total_rollbacks"] or 0) / total, 3),
|
|
156
|
+
"success_rate": round(float(row["total_successes"] or 0) / total, 3),
|
|
157
|
+
"avg_tokens": round(float(row["avg_tokens"] or 0), 1),
|
|
158
|
+
"avg_execution_time": round(float(row["avg_execution_time"] or 0), 1),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
def record_critic_vote(
|
|
162
|
+
self,
|
|
163
|
+
task_id: str,
|
|
164
|
+
critic_role: str,
|
|
165
|
+
vote: bool,
|
|
166
|
+
success: bool,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Records a single critic vote and the eventual execution success."""
|
|
169
|
+
query = """
|
|
170
|
+
INSERT INTO critic_performance_metrics (task_id, critic_role, vote, success)
|
|
171
|
+
VALUES (?, ?, ?, ?)
|
|
172
|
+
"""
|
|
173
|
+
params = (task_id, critic_role, 1 if vote else 0, 1 if success else 0)
|
|
174
|
+
self.sqlite_manager.execute_write(query, params)
|
|
175
|
+
logger.debug("Recorded critic vote for task %s, role %s", task_id, critic_role)
|
|
176
|
+
|
|
177
|
+
def get_critic_weights(self) -> dict[str, float]:
|
|
178
|
+
"""Returns a static default weight map for council critics."""
|
|
179
|
+
return {
|
|
180
|
+
"scalability": 1.0,
|
|
181
|
+
"security": 1.0,
|
|
182
|
+
"performance": 1.0,
|
|
183
|
+
"maintainability": 1.0,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
def record_debate_outcome(
|
|
187
|
+
self,
|
|
188
|
+
turns_required: int,
|
|
189
|
+
initial_objection_count: int,
|
|
190
|
+
final_objection_count: int,
|
|
191
|
+
converged: bool,
|
|
192
|
+
time_to_converge_ms: int,
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Records a single debate's performance and convergence telemetry."""
|
|
195
|
+
query = """
|
|
196
|
+
INSERT INTO debate_telemetry (
|
|
197
|
+
timestamp, turns_required, initial_objection_count,
|
|
198
|
+
final_objection_count, converged, time_to_converge_ms
|
|
199
|
+
)
|
|
200
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
201
|
+
"""
|
|
202
|
+
params = (
|
|
203
|
+
datetime.now(tz=UTC).isoformat(),
|
|
204
|
+
turns_required,
|
|
205
|
+
initial_objection_count,
|
|
206
|
+
final_objection_count,
|
|
207
|
+
1 if converged else 0,
|
|
208
|
+
time_to_converge_ms,
|
|
209
|
+
)
|
|
210
|
+
self.sqlite_manager.execute_write(query, params)
|
|
211
|
+
logger.debug(
|
|
212
|
+
"Recorded debate outcome: turns=%d, converged=%s, initial_objections=%d",
|
|
213
|
+
turns_required,
|
|
214
|
+
converged,
|
|
215
|
+
initial_objection_count,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def record_compression(
|
|
219
|
+
self,
|
|
220
|
+
original_tokens: int,
|
|
221
|
+
compressed_tokens: int,
|
|
222
|
+
method: str,
|
|
223
|
+
latency_ms: int,
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Records a single context compression invocation's telemetry."""
|
|
226
|
+
query = """
|
|
227
|
+
INSERT INTO compression_metrics (
|
|
228
|
+
timestamp, original_tokens, compressed_tokens,
|
|
229
|
+
method, latency_ms
|
|
230
|
+
)
|
|
231
|
+
VALUES (?, ?, ?, ?, ?)
|
|
232
|
+
"""
|
|
233
|
+
params = (
|
|
234
|
+
datetime.now(tz=UTC).isoformat(),
|
|
235
|
+
original_tokens,
|
|
236
|
+
compressed_tokens,
|
|
237
|
+
method,
|
|
238
|
+
latency_ms,
|
|
239
|
+
)
|
|
240
|
+
self.sqlite_manager.execute_write(query, params)
|
|
241
|
+
logger.debug(
|
|
242
|
+
"Recorded compression metrics: original_tokens=%d, compressed_tokens=%d, method=%s",
|
|
243
|
+
original_tokens,
|
|
244
|
+
compressed_tokens,
|
|
245
|
+
method,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def record_injection_attempt(self, source: str, pattern: str) -> None:
|
|
249
|
+
"""Records a single blocked prompt injection attempt."""
|
|
250
|
+
query = """
|
|
251
|
+
INSERT INTO injection_attempts (timestamp, source, pattern)
|
|
252
|
+
VALUES (?, ?, ?)
|
|
253
|
+
"""
|
|
254
|
+
params = (
|
|
255
|
+
datetime.now(tz=UTC).isoformat(),
|
|
256
|
+
source,
|
|
257
|
+
pattern,
|
|
258
|
+
)
|
|
259
|
+
self.sqlite_manager.execute_write(query, params)
|
|
260
|
+
logger.warning("Recorded prompt injection attempt from source '%s' matching pattern: %s", source, pattern)
|