mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-17 05:59:36 +00:00
feat(leann-phase2): implement hybrid vector storage and graph-based search
- [x] Add AST-aware code chunking for Go, Python, and TypeScript using tree-sitter - [x] Implement LEANN-inspired hybrid vector storage with hub detection and selective embedding storage (60-80% savings) - [x] Add observation relationship graph with CSR format and edge detection (file overlap, semantic similarity, temporal, concept) - [x] Implement graph-aware search with two-level traversal and relationship-based ranking - [x] Add auto-tuning system for dynamic hub threshold adjustment based on query performance - [x] Add comprehensive metrics tracking for vector storage, queries, latency, and graph traversals - [x] Update configuration system with graph and hybrid storage settings - [x] Add graph stats and vector metrics endpoints to worker service - [x] Enhance UI sidebar with advanced metrics display and graph visualization - [x] Optimize struct field alignment throughout codebase for memory efficiency - [x] Update documentation with LEANN Phase 2 features and performance benefits - [x] Add tree-sitter dependency for AST parsing
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "8fe9ea5-dirty",
|
||||
"version": "v0.10.5-1-g7ab4b07-dirty",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "8fe9ea5-dirty",
|
||||
"version": "v0.10.5-1-g7ab4b07-dirty",
|
||||
"dependencies": {
|
||||
"vis-data": "^7.1.9",
|
||||
"vis-network": "^9.1.9",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mnemonic-dashboard",
|
||||
"version": "8fe9ea5-dirty",
|
||||
"version": "v0.10.5-1-g7ab4b07-dirty",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Stats, SelfCheckResponse } from '@/types'
|
||||
import ProjectFilter from './ProjectFilter.vue'
|
||||
import { useGraphMetrics } from '@/composables'
|
||||
|
||||
const props = defineProps<{
|
||||
stats: Stats | null
|
||||
@@ -18,12 +19,21 @@ defineEmits<{
|
||||
|
||||
// Collapse state - persisted in localStorage
|
||||
const isCollapsed = ref(localStorage.getItem('sidebar-collapsed') === 'true')
|
||||
const metricsExpanded = ref(localStorage.getItem('metrics-expanded') === 'true')
|
||||
|
||||
// Graph metrics composable
|
||||
const { graphStats, vectorMetrics, loading: metricsLoading, refresh: refreshMetrics } = useGraphMetrics()
|
||||
|
||||
function toggleCollapse() {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
localStorage.setItem('sidebar-collapsed', String(isCollapsed.value))
|
||||
}
|
||||
|
||||
function toggleMetrics() {
|
||||
metricsExpanded.value = !metricsExpanded.value
|
||||
localStorage.setItem('metrics-expanded', String(metricsExpanded.value))
|
||||
}
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'
|
||||
if (n >= 1000) return (n / 1000).toFixed(1) + 'K'
|
||||
@@ -205,6 +215,99 @@ function getStatusColor(status: string): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Metrics -->
|
||||
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50">
|
||||
<button
|
||||
@click="toggleMetrics"
|
||||
class="w-full flex items-center justify-between p-4 hover:bg-slate-700/30 transition-colors rounded-lg"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-chart-line text-violet-400" />
|
||||
<h3 class="text-sm font-semibold text-white">Advanced Metrics</h3>
|
||||
</div>
|
||||
<i
|
||||
:class="[
|
||||
'fas text-slate-400 transition-transform duration-200',
|
||||
metricsExpanded ? 'fa-chevron-up' : 'fa-chevron-down'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<Transition name="expand">
|
||||
<div v-show="metricsExpanded" class="px-4 pb-4 space-y-4">
|
||||
<!-- Loading State -->
|
||||
<div v-if="metricsLoading" class="text-center py-4">
|
||||
<i class="fas fa-spinner fa-spin text-slate-400" />
|
||||
<p class="text-slate-500 text-sm mt-2">Loading metrics...</p>
|
||||
</div>
|
||||
|
||||
<!-- Graph Stats -->
|
||||
<div v-else-if="graphStats?.enabled">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-xs text-slate-400 uppercase tracking-wide">Graph</span>
|
||||
<button
|
||||
@click="refreshMetrics"
|
||||
class="text-xs text-violet-400 hover:text-violet-300 transition-colors"
|
||||
title="Refresh metrics"
|
||||
>
|
||||
<i class="fas fa-sync-alt" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Nodes</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(graphStats.nodeCount) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Edges</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(graphStats.edgeCount) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Avg Degree</span>
|
||||
<span class="text-white font-medium">{{ graphStats.avgDegree.toFixed(1) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Max Degree</span>
|
||||
<span class="text-white font-medium">{{ graphStats.maxDegree }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vector Metrics -->
|
||||
<div v-if="vectorMetrics?.enabled" class="mt-4 pt-4 border-t border-slate-700/50">
|
||||
<div class="text-xs text-slate-400 uppercase tracking-wide mb-2">Vector Storage</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Savings</span>
|
||||
<span class="text-green-400 font-medium">
|
||||
{{ vectorMetrics.storage.savingsPercent.toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Queries</span>
|
||||
<span class="text-white font-medium">{{ formatNumber(vectorMetrics.queries.total) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Cache Hit</span>
|
||||
<span class="text-cyan-400 font-medium">
|
||||
{{ (vectorMetrics.cache.hitRate * 100).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-slate-400 text-sm">Avg Latency</span>
|
||||
<span class="text-white font-medium text-xs">{{ vectorMetrics.latency.avg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disabled State -->
|
||||
<div v-else class="text-slate-500 text-sm py-2">
|
||||
{{ graphStats?.message || 'Metrics not available' }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- Session Info -->
|
||||
<div v-if="stats" class="bg-slate-800/50 rounded-lg p-4 border border-slate-700/50">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
@@ -260,6 +363,30 @@ function getStatusColor(status: string): string {
|
||||
>
|
||||
<i class="fas fa-search text-cyan-400" />
|
||||
</div>
|
||||
|
||||
<!-- Metrics indicator -->
|
||||
<div
|
||||
v-if="graphStats?.enabled"
|
||||
class="bg-slate-800/50 rounded-lg p-3 border border-slate-700/50 flex justify-center"
|
||||
:title="`${graphStats.nodeCount} nodes, ${graphStats.edgeCount} edges`"
|
||||
>
|
||||
<i class="fas fa-chart-line text-violet-400" />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.expand-enter-active,
|
||||
.expand-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.expand-enter-from,
|
||||
.expand-leave-to {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,3 +3,4 @@ export { useStats } from './useStats'
|
||||
export { useTimeline } from './useTimeline'
|
||||
export { useUpdate } from './useUpdate'
|
||||
export { useHealth } from './useHealth'
|
||||
export { useGraphMetrics } from './useGraphMetrics'
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import type { GraphStats, VectorMetrics } from '@/types'
|
||||
import { fetchGraphStats, fetchVectorMetrics } from '@/utils/api'
|
||||
|
||||
export function useGraphMetrics() {
|
||||
const graphStats = ref<GraphStats | null>(null)
|
||||
const vectorMetrics = ref<VectorMetrics | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const refresh = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// Fetch both in parallel
|
||||
const [graph, vector] = await Promise.all([
|
||||
fetchGraphStats(),
|
||||
fetchVectorMetrics()
|
||||
])
|
||||
|
||||
graphStats.value = graph
|
||||
vectorMetrics.value = vector
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch metrics'
|
||||
console.error('[GraphMetrics] Error:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
return {
|
||||
graphStats,
|
||||
vectorMetrics,
|
||||
loading,
|
||||
error,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
@@ -63,3 +63,58 @@ export interface SelfCheckResponse {
|
||||
uptime: string
|
||||
components: ComponentHealth[]
|
||||
}
|
||||
|
||||
export interface GraphStats {
|
||||
enabled: boolean
|
||||
nodeCount: number
|
||||
edgeCount: number
|
||||
avgDegree: number
|
||||
maxDegree: number
|
||||
minDegree: number
|
||||
medianDegree: number
|
||||
edgeTypes: Record<string, number>
|
||||
config: {
|
||||
maxHops: number
|
||||
branchFactor: number
|
||||
edgeWeight: number
|
||||
rebuildIntervalMin: number
|
||||
}
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface VectorMetrics {
|
||||
enabled: boolean
|
||||
queries: {
|
||||
total: number
|
||||
hubOnly: number
|
||||
hybrid: number
|
||||
onDemand: number
|
||||
graph: number
|
||||
}
|
||||
latency: {
|
||||
avg: string
|
||||
p50: string
|
||||
p95: string
|
||||
p99: string
|
||||
avgHub: string
|
||||
avgRecompute: string
|
||||
}
|
||||
storage: {
|
||||
totalDocuments: number
|
||||
hubDocuments: number
|
||||
storedEmbeddings: number
|
||||
savingsPercent: number
|
||||
recomputedTotal: number
|
||||
}
|
||||
cache: {
|
||||
hits: number
|
||||
misses: number
|
||||
hitRate: number
|
||||
}
|
||||
graph: {
|
||||
traversals: number
|
||||
avgDepth: number
|
||||
}
|
||||
uptime: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
+9
-1
@@ -1,4 +1,4 @@
|
||||
import type { Observation, UserPrompt, SessionSummary, Stats, FeedItem, ObservationFeedItem, PromptFeedItem, SummaryFeedItem, RelationWithDetails, RelationGraph, RelationStats } from '@/types'
|
||||
import type { Observation, UserPrompt, SessionSummary, Stats, FeedItem, ObservationFeedItem, PromptFeedItem, SummaryFeedItem, RelationWithDetails, RelationGraph, RelationStats, GraphStats, VectorMetrics } from '@/types'
|
||||
|
||||
const API_BASE = '/api'
|
||||
const DEFAULT_TIMEOUT = 10000 // 10 seconds
|
||||
@@ -164,3 +164,11 @@ export async function fetchRelatedObservations(observationId: number, minConfide
|
||||
export async function fetchRelationStats(signal?: AbortSignal): Promise<RelationStats> {
|
||||
return fetchWithRetry<RelationStats>(`${API_BASE}/relations/stats`, { signal })
|
||||
}
|
||||
|
||||
export async function fetchGraphStats(signal?: AbortSignal): Promise<GraphStats> {
|
||||
return fetchWithRetry<GraphStats>(`${API_BASE}/graph/stats`, { signal })
|
||||
}
|
||||
|
||||
export async function fetchVectorMetrics(signal?: AbortSignal): Promise<VectorMetrics> {
|
||||
return fetchWithRetry<VectorMetrics>(`${API_BASE}/vector/metrics`, { signal })
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usehealth.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/composables/usetypes.ts","./src/composables/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.ts","./src/types/relation.ts","./src/types/summary.ts","./src/utils/api.ts","./src/utils/formatters.ts","./src/app.vue","./src/components/badge.vue","./src/components/card.vue","./src/components/filtertabs.vue","./src/components/header.vue","./src/components/iconbox.vue","./src/components/observationcard.vue","./src/components/projectfilter.vue","./src/components/promptcard.vue","./src/components/relationgraph.vue","./src/components/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/timeline.vue"],"version":"5.7.3"}
|
||||
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usegraphmetrics.ts","./src/composables/usehealth.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/composables/usetypes.ts","./src/composables/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.ts","./src/types/relation.ts","./src/types/summary.ts","./src/utils/api.ts","./src/utils/formatters.ts","./src/app.vue","./src/components/badge.vue","./src/components/card.vue","./src/components/filtertabs.vue","./src/components/header.vue","./src/components/iconbox.vue","./src/components/observationcard.vue","./src/components/projectfilter.vue","./src/components/promptcard.vue","./src/components/relationgraph.vue","./src/components/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/timeline.vue"],"version":"5.7.3"}
|
||||
Reference in New Issue
Block a user