refactor(ai): align facade with AX naming
Add usage-example comments and rename short internal helpers to clearer names. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
f9e2948176
commit
d6c360813a
4 changed files with 45 additions and 56 deletions
12
ai/ai.go
12
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
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
26
ai/rag.go
26
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 ""
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue