refactor(ai): align facade with AX naming
All checks were successful
Security Scan / security (push) Successful in 10s
Test / test (push) Successful in 1m2s

Add usage-example comments and rename short internal helpers to clearer names.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 06:06:38 +00:00
parent f9e2948176
commit d6c360813a
4 changed files with 45 additions and 56 deletions

View file

@ -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

View file

@ -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{

View file

@ -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))
}

View file

@ -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 ""
}