From d6c360813a8b6558c7b8284a4d1cd8fd1c42111e Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 06:06:38 +0000 Subject: [PATCH] refactor(ai): align facade with AX naming Add usage-example comments and rename short internal helpers to clearer names. Co-Authored-By: Virgil --- ai/ai.go | 12 +++-------- ai/metrics.go | 53 +++++++++++++++++++++++----------------------- ai/metrics_test.go | 10 ++++----- ai/rag.go | 26 ++++++++++------------- 4 files changed, 45 insertions(+), 56 deletions(-) diff --git a/ai/ai.go b/ai/ai.go index 29cc20e..378911a 100644 --- a/ai/ai.go +++ b/ai/ai.go @@ -1,11 +1,5 @@ -// Package ai provides the unified AI package for the core CLI. +// Package ai provides the AI facade for the core CLI. // -// It composes functionality from pkg/rag (vector search) and pkg/agentic -// (task management) into a single public API surface. New AI features -// should be added here; existing packages remain importable but pkg/ai -// is the canonical entry point. -// -// Sub-packages composed: -// - pkg/rag: Qdrant vector database + Ollama embeddings -// - pkg/agentic: Task queue client and context building +// Example: ai.QueryRAGForTask(ai.TaskInfo{Title: "Investigate build failure", Description: "CI compile step fails"}) +// Example: ai.Record(ai.Event{Type: "security.scan", Repo: "wailsapp/wails"}) package ai diff --git a/ai/metrics.go b/ai/metrics.go index e2427d3..c9bf330 100644 --- a/ai/metrics.go +++ b/ai/metrics.go @@ -18,7 +18,7 @@ import ( // metricsMu protects concurrent file writes in Record. var metricsMu sync.Mutex -// Event represents a recorded AI/security metric event. +// Event records AI and security activity in ~/.core/ai/metrics/YYYY-MM-DD.jsonl. type Event struct { Type string `json:"type"` Timestamp time.Time `json:"timestamp"` @@ -42,8 +42,7 @@ func metricsFilePath(dir string, t time.Time) string { return filepath.Join(dir, t.Format("2006-01-02")+".jsonl") } -// Record appends an event to the daily JSONL file at -// ~/.core/ai/metrics/YYYY-MM-DD.jsonl. +// Record(ai.Event{Type: "security.scan", Repo: "wailsapp/wails"}) appends the event to the daily JSONL log. func Record(event Event) (err error) { if event.Timestamp.IsZero() { event.Timestamp = time.Now() @@ -85,7 +84,7 @@ func Record(event Event) (err error) { return nil } -// ReadEvents reads events from JSONL files within the given time range. +// ReadEvents(time.Now().Add(-24 * time.Hour)) reads the recent daily JSONL files and silently skips any missing days. func ReadEvents(since time.Time) ([]Event, error) { dir, err := metricsDir() if err != nil { @@ -137,48 +136,48 @@ func readMetricsFile(path string, since time.Time) ([]Event, error) { return events, nil } -// Summary aggregates events into counts by type, repo, and agent. +// Summary(ai.ReadEvents(time.Now().Add(-24 * time.Hour))) aggregates counts by type, repo, and agent. func Summary(events []Event) map[string]any { - byType := make(map[string]int) - byRepo := make(map[string]int) - byAgent := make(map[string]int) + byTypeCounts := make(map[string]int) + byRepoCounts := make(map[string]int) + byAgentCounts := make(map[string]int) for _, ev := range events { - byType[ev.Type]++ + byTypeCounts[ev.Type]++ if ev.Repo != "" { - byRepo[ev.Repo]++ + byRepoCounts[ev.Repo]++ } if ev.AgentID != "" { - byAgent[ev.AgentID]++ + byAgentCounts[ev.AgentID]++ } } - recent := make([]Event, len(events)) - copy(recent, events) - sort.SliceStable(recent, func(i, j int) bool { - return recent[i].Timestamp.After(recent[j].Timestamp) + recentEvents := make([]Event, len(events)) + copy(recentEvents, events) + sort.SliceStable(recentEvents, func(i, j int) bool { + return recentEvents[i].Timestamp.After(recentEvents[j].Timestamp) }) - if len(recent) > 10 { - recent = recent[:10] + if len(recentEvents) > 10 { + recentEvents = recentEvents[:10] } return map[string]any{ "total": len(events), - "by_type": sortedMap(byType), - "by_repo": sortedMap(byRepo), - "by_agent": sortedMap(byAgent), - "events": briefEvents(recent), + "by_type": sortedCountPairs(byTypeCounts), + "by_repo": sortedCountPairs(byRepoCounts), + "by_agent": sortedCountPairs(byAgentCounts), + "events": compactEvents(recentEvents), } } -// sortedMap returns a slice of key-count pairs sorted by count descending. -func sortedMap(m map[string]int) []map[string]any { +// sortedCountPairs returns a slice of key-count pairs sorted by count descending. +func sortedCountPairs(counts map[string]int) []map[string]any { type entry struct { key string count int } - entries := make([]entry, 0, len(m)) - for k, v := range m { + entries := make([]entry, 0, len(counts)) + for k, v := range counts { entries = append(entries, entry{k, v}) } @@ -193,8 +192,8 @@ func sortedMap(m map[string]int) []map[string]any { return result } -// briefEvents converts events into the compact shape used by metrics_query. -func briefEvents(events []Event) []map[string]any { +// compactEvents converts events into the compact shape used by metrics_query. +func compactEvents(events []Event) []map[string]any { result := make([]map[string]any, len(events)) for i, ev := range events { item := map[string]any{ diff --git a/ai/metrics_test.go b/ai/metrics_test.go index 260f3cc..4952f12 100644 --- a/ai/metrics_test.go +++ b/ai/metrics_test.go @@ -283,18 +283,18 @@ func TestSummary_Good_RecentEventsLimit(t *testing.T) { } } -// --- sortedMap --- +// --- sortedCountPairs --- -func TestSortedMap_Good_Empty(t *testing.T) { - result := sortedMap(map[string]int{}) +func TestSortedCountPairs_Good_Empty(t *testing.T) { + result := sortedCountPairs(map[string]int{}) if len(result) != 0 { t.Errorf("expected empty slice, got %d entries", len(result)) } } -func TestSortedMap_Good_Ordering(t *testing.T) { +func TestSortedCountPairs_Good_Ordering(t *testing.T) { m := map[string]int{"a": 1, "b": 3, "c": 2} - result := sortedMap(m) + result := sortedCountPairs(m) if len(result) != 3 { t.Fatalf("expected 3 entries, got %d", len(result)) } diff --git a/ai/rag.go b/ai/rag.go index d89c8c5..f23b164 100644 --- a/ai/rag.go +++ b/ai/rag.go @@ -13,35 +13,31 @@ var ( runRAGQuery = rag.Query ) -// TaskInfo carries the minimal task data needed for RAG queries, -// avoiding a direct dependency on pkg/agentic (which imports pkg/ai). +// TaskInfo carries the minimal task data needed for RAG queries. type TaskInfo struct { Title string Description string } -// QueryRAGForTask queries Qdrant for documentation relevant to a task. -// It builds a query from the task title and description, queries with -// sensible defaults, and returns formatted context or an empty string -// when the backing services are unavailable. +// QueryRAGForTask(TaskInfo{Title: "Investigate build failure", Description: "CI compile step fails"}) returns formatted RAG context, or "" when dependencies are unavailable. func QueryRAGForTask(task TaskInfo) string { - query := task.Title + " " + task.Description + queryText := task.Title + " " + task.Description // Truncate to 500 runes to keep the embedding focused. - runes := []rune(query) + runes := []rune(queryText) if len(runes) > 500 { - query = string(runes[:500]) + queryText = string(runes[:500]) } - qdrantCfg := rag.DefaultQdrantConfig() - qdrantClient, err := newQdrantClient(qdrantCfg) + qdrantConfig := rag.DefaultQdrantConfig() + qdrantClient, err := newQdrantClient(qdrantConfig) if err != nil { return "" } defer func() { _ = qdrantClient.Close() }() - ollamaCfg := rag.DefaultOllamaConfig() - ollamaClient, err := newOllamaClient(ollamaCfg) + ollamaConfig := rag.DefaultOllamaConfig() + ollamaClient, err := newOllamaClient(ollamaConfig) if err != nil { return "" } @@ -49,13 +45,13 @@ func QueryRAGForTask(task TaskInfo) string { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - queryCfg := rag.QueryConfig{ + queryConfig := rag.QueryConfig{ Collection: "hostuk-docs", Limit: 3, Threshold: 0.5, } - results, err := runRAGQuery(ctx, qdrantClient, ollamaClient, query, queryCfg) + results, err := runRAGQuery(ctx, qdrantClient, ollamaClient, queryText, queryConfig) if err != nil { return "" }