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.
Files changed (229) hide show
  1. velune/__init__.py +5 -0
  2. velune/__main__.py +6 -0
  3. velune/cli/__init__.py +5 -0
  4. velune/cli/app.py +212 -0
  5. velune/cli/autocomplete.py +76 -0
  6. velune/cli/banner.py +98 -0
  7. velune/cli/commands/__init__.py +32 -0
  8. velune/cli/commands/ask.py +149 -0
  9. velune/cli/commands/base.py +16 -0
  10. velune/cli/commands/chat.py +188 -0
  11. velune/cli/commands/config.py +182 -0
  12. velune/cli/commands/daemon.py +85 -0
  13. velune/cli/commands/doctor.py +373 -0
  14. velune/cli/commands/init.py +160 -0
  15. velune/cli/commands/mcp.py +80 -0
  16. velune/cli/commands/memory.py +269 -0
  17. velune/cli/commands/models.py +462 -0
  18. velune/cli/commands/preflight.py +95 -0
  19. velune/cli/commands/run.py +171 -0
  20. velune/cli/commands/setup.py +182 -0
  21. velune/cli/commands/workspace.py +217 -0
  22. velune/cli/context.py +37 -0
  23. velune/cli/councilmodel_ui.py +171 -0
  24. velune/cli/display/council_view.py +240 -0
  25. velune/cli/display/memory_view.py +93 -0
  26. velune/cli/display/panels.py +35 -0
  27. velune/cli/display/progress.py +25 -0
  28. velune/cli/display/themes.py +21 -0
  29. velune/cli/main.py +15 -0
  30. velune/cli/model_selector.py +44 -0
  31. velune/cli/modes.py +86 -0
  32. velune/cli/pull_ui.py +118 -0
  33. velune/cli/registry.py +81 -0
  34. velune/cli/repl.py +1178 -0
  35. velune/cli/session_manager.py +69 -0
  36. velune/cli/slash_commands.py +37 -0
  37. velune/cognition/__init__.py +19 -0
  38. velune/cognition/arbitrator.py +216 -0
  39. velune/cognition/architecture.py +398 -0
  40. velune/cognition/council/__init__.py +47 -0
  41. velune/cognition/council/base.py +216 -0
  42. velune/cognition/council/challenger.py +70 -0
  43. velune/cognition/council/coder.py +79 -0
  44. velune/cognition/council/critic_agent.py +39 -0
  45. velune/cognition/council/critic_configs.py +111 -0
  46. velune/cognition/council/critics.py +41 -0
  47. velune/cognition/council/debate.py +44 -0
  48. velune/cognition/council/factory.py +140 -0
  49. velune/cognition/council/messages.py +53 -0
  50. velune/cognition/council/planner.py +119 -0
  51. velune/cognition/council/reviewer.py +72 -0
  52. velune/cognition/council/synthesizer.py +67 -0
  53. velune/cognition/council/tiers.py +181 -0
  54. velune/cognition/firewall.py +256 -0
  55. velune/cognition/module.py +38 -0
  56. velune/cognition/orchestrator.py +886 -0
  57. velune/cognition/personality.py +236 -0
  58. velune/cognition/style_resolver.py +62 -0
  59. velune/cognition/verification.py +201 -0
  60. velune/context/__init__.py +11 -0
  61. velune/context/extractive.py +94 -0
  62. velune/context/window.py +62 -0
  63. velune/core/__init__.py +89 -0
  64. velune/core/background.py +5 -0
  65. velune/core/config/__init__.py +37 -0
  66. velune/core/errors/__init__.py +53 -0
  67. velune/core/errors/execution.py +26 -0
  68. velune/core/errors/memory.py +21 -0
  69. velune/core/errors/orchestration.py +26 -0
  70. velune/core/errors/provider.py +31 -0
  71. velune/core/event_loop.py +30 -0
  72. velune/core/logging.py +83 -0
  73. velune/core/runtime.py +106 -0
  74. velune/core/task_registry.py +120 -0
  75. velune/core/trace.py +80 -0
  76. velune/core/types/__init__.py +48 -0
  77. velune/core/types/agent.py +49 -0
  78. velune/core/types/context.py +39 -0
  79. velune/core/types/inference.py +35 -0
  80. velune/core/types/memory.py +39 -0
  81. velune/core/types/model.py +64 -0
  82. velune/core/types/provider.py +35 -0
  83. velune/core/types/repository.py +35 -0
  84. velune/core/types/task.py +56 -0
  85. velune/core/types/workspace.py +28 -0
  86. velune/daemon/client.py +13 -0
  87. velune/daemon/server.py +115 -0
  88. velune/daemon/transport.py +169 -0
  89. velune/events.py +194 -0
  90. velune/execution/__init__.py +22 -0
  91. velune/execution/benchmarker.py +311 -0
  92. velune/execution/cancellation.py +53 -0
  93. velune/execution/checkpointer.py +128 -0
  94. velune/execution/command_spec.py +140 -0
  95. velune/execution/diff_preview.py +172 -0
  96. velune/execution/executor.py +173 -0
  97. velune/execution/module.py +16 -0
  98. velune/execution/multi_diff.py +70 -0
  99. velune/execution/path_guard.py +19 -0
  100. velune/execution/planner.py +91 -0
  101. velune/execution/rollback.py +80 -0
  102. velune/execution/sandbox.py +257 -0
  103. velune/execution/validator.py +113 -0
  104. velune/hardware/__init__.py +1 -0
  105. velune/hardware/detector.py +162 -0
  106. velune/kernel/__init__.py +58 -0
  107. velune/kernel/bootstrap.py +107 -0
  108. velune/kernel/config.py +252 -0
  109. velune/kernel/health.py +54 -0
  110. velune/kernel/lifecycle.py +102 -0
  111. velune/kernel/module.py +15 -0
  112. velune/kernel/modules.py +23 -0
  113. velune/kernel/registry.py +93 -0
  114. velune/kernel/schemas.py +28 -0
  115. velune/main.py +9 -0
  116. velune/mcp/__init__.py +9 -0
  117. velune/mcp/client.py +113 -0
  118. velune/mcp/config.py +19 -0
  119. velune/mcp/server.py +90 -0
  120. velune/memory/__init__.py +28 -0
  121. velune/memory/lifecycle.py +154 -0
  122. velune/memory/module.py +94 -0
  123. velune/memory/prioritizer.py +65 -0
  124. velune/memory/storage/sqlite_manager.py +368 -0
  125. velune/memory/tiers/episodic.py +156 -0
  126. velune/memory/tiers/graph.py +282 -0
  127. velune/memory/tiers/lineage.py +367 -0
  128. velune/memory/tiers/semantic.py +198 -0
  129. velune/memory/tiers/working.py +165 -0
  130. velune/models/__init__.py +16 -0
  131. velune/models/module.py +18 -0
  132. velune/models/probes.py +182 -0
  133. velune/models/profile_cache.py +82 -0
  134. velune/models/profiler.py +105 -0
  135. velune/models/registry.py +201 -0
  136. velune/models/scorer.py +225 -0
  137. velune/models/specializations.py +196 -0
  138. velune/orchestration/__init__.py +15 -0
  139. velune/orchestration/module.py +14 -0
  140. velune/orchestration/role_assignments.py +78 -0
  141. velune/orchestration/schemas.py +99 -0
  142. velune/plugins/__init__.py +13 -0
  143. velune/plugins/hooks.py +49 -0
  144. velune/plugins/loader.py +95 -0
  145. velune/plugins/registry.py +54 -0
  146. velune/plugins/schemas.py +19 -0
  147. velune/providers/__init__.py +18 -0
  148. velune/providers/adapters/anthropic.py +231 -0
  149. velune/providers/adapters/fireworks.py +115 -0
  150. velune/providers/adapters/google.py +234 -0
  151. velune/providers/adapters/groq.py +151 -0
  152. velune/providers/adapters/huggingface.py +203 -0
  153. velune/providers/adapters/llamacpp.py +202 -0
  154. velune/providers/adapters/lmstudio.py +173 -0
  155. velune/providers/adapters/ollama.py +186 -0
  156. velune/providers/adapters/openai.py +207 -0
  157. velune/providers/adapters/openrouter.py +81 -0
  158. velune/providers/adapters/together.py +134 -0
  159. velune/providers/adapters/xai.py +60 -0
  160. velune/providers/base.py +86 -0
  161. velune/providers/benchmarker.py +135 -0
  162. velune/providers/discovery/__init__.py +33 -0
  163. velune/providers/discovery/anthropic.py +77 -0
  164. velune/providers/discovery/benchmarks.py +44 -0
  165. velune/providers/discovery/classifier.py +69 -0
  166. velune/providers/discovery/fireworks.py +95 -0
  167. velune/providers/discovery/gguf.py +87 -0
  168. velune/providers/discovery/google.py +95 -0
  169. velune/providers/discovery/gpu.py +109 -0
  170. velune/providers/discovery/groq.py +20 -0
  171. velune/providers/discovery/huggingface.py +67 -0
  172. velune/providers/discovery/lmstudio.py +80 -0
  173. velune/providers/discovery/ollama.py +165 -0
  174. velune/providers/discovery/openai.py +96 -0
  175. velune/providers/discovery/openrouter.py +113 -0
  176. velune/providers/discovery/scanner.py +114 -0
  177. velune/providers/discovery/together.py +114 -0
  178. velune/providers/discovery/xai.py +57 -0
  179. velune/providers/keystore.py +83 -0
  180. velune/providers/local_paths.py +49 -0
  181. velune/providers/local_resolver.py +208 -0
  182. velune/providers/module.py +15 -0
  183. velune/providers/ollama_manager.py +193 -0
  184. velune/providers/registry.py +173 -0
  185. velune/providers/router.py +82 -0
  186. velune/py.typed +0 -0
  187. velune/repository/__init__.py +21 -0
  188. velune/repository/analyzer.py +123 -0
  189. velune/repository/cognition.py +172 -0
  190. velune/repository/grapher.py +182 -0
  191. velune/repository/indexer.py +229 -0
  192. velune/repository/module.py +15 -0
  193. velune/repository/parser.py +378 -0
  194. velune/repository/project_type.py +293 -0
  195. velune/repository/scanner.py +177 -0
  196. velune/repository/schemas.py +102 -0
  197. velune/repository/tracker.py +233 -0
  198. velune/retrieval/__init__.py +27 -0
  199. velune/retrieval/graph.py +117 -0
  200. velune/retrieval/hybrid.py +250 -0
  201. velune/retrieval/keyword.py +113 -0
  202. velune/retrieval/module.py +19 -0
  203. velune/retrieval/reranker.py +68 -0
  204. velune/retrieval/schemas.py +59 -0
  205. velune/retrieval/vector.py +163 -0
  206. velune/telemetry/__init__.py +7 -0
  207. velune/telemetry/cognition.py +260 -0
  208. velune/telemetry/token_tracker.py +133 -0
  209. velune/tools/__init__.py +41 -0
  210. velune/tools/base/registry.py +84 -0
  211. velune/tools/base/tool.py +63 -0
  212. velune/tools/code/navigate.py +107 -0
  213. velune/tools/code/search.py +112 -0
  214. velune/tools/filesystem/read.py +75 -0
  215. velune/tools/filesystem/search.py +123 -0
  216. velune/tools/filesystem/write.py +160 -0
  217. velune/tools/git/history.py +185 -0
  218. velune/tools/git/operations.py +132 -0
  219. velune/tools/git/state.py +134 -0
  220. velune/tools/module.py +65 -0
  221. velune/tools/terminal/execute.py +72 -0
  222. velune/tools/terminal/history.py +47 -0
  223. velune/tools/web/fetch.py +55 -0
  224. velune/tools/web/validator.py +96 -0
  225. velune_cli-1.0.0.dist-info/METADATA +497 -0
  226. velune_cli-1.0.0.dist-info/RECORD +229 -0
  227. velune_cli-1.0.0.dist-info/WHEEL +4 -0
  228. velune_cli-1.0.0.dist-info/entry_points.txt +2 -0
  229. 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,7 @@
1
+ """Telemetry package for Velune CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from velune.telemetry.cognition import CognitivePerformanceAnalytics
6
+
7
+ __all__ = ["CognitivePerformanceAnalytics"]
@@ -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)