Ctrl K
ComunidadGuías…

internal/sessionquery/sessionquery.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Package sessionquery reads the task ledger back.
//
// The ledger is the best record this product keeps and it was write-only: reading it
// meant one admin call for one task run whose ID you already had. Nobody could ask
// when something last worked, or what happened the last three times a person asked
// for it.
package sessionquery

import (
	"errors"
	"strings"
	"time"

	"github.com/yeomyeonggeori/blueclaw/internal/task"
)

const (
	defaultLimit = 20
	maximumLimit = 100
	// Reading a run's events is a query per run, so the scan is bounded by how many
	// queries one search may issue rather than by how much a person ever ran.
	maximumScannedTaskRuns = 200
)

type Store interface {
	ListTaskRunByPersonID(personID string) []task.TaskRun
	ListTaskEvent(taskRunID string) []task.TaskEvent
}

type Request struct {
	RequesterPersonID string
	Text              string
	ConversationID    string
	Limit             int
}

type Match struct {
	TaskRunID      string    `json:"taskRunID"`
	ConversationID string    `json:"conversationID,omitempty"`
	Status         string    `json:"status"`
	Prompt         string    `json:"prompt"`
	CreatedAt      time.Time `json:"createdAt"`
	MatchedIn      []string  `json:"matchedIn,omitempty"`
}

type Result struct {
	Matches         []Match `json:"matches"`
	TotalMatched    int     `json:"totalMatched"`
	Scanned         int     `json:"scanned"`
	IsTruncated     bool    `json:"isTruncated"`
	DidStopScanning bool    `json:"didStopScanning"`
}

type Service struct {
	store Store
}

func New(store Store) Service {
	return Service{store: store}
}

// A person is required. The ledger holds message text, file contents and tool inputs
// from everyone on this device, so a search that names nobody is a search of all of it.
func (service Service) Search(request Request) (Result, error) {
	personID := strings.TrimSpace(request.RequesterPersonID)
	if personID == "" {
		return Result{}, errors.New("a ledger search names the person whose tasks it may read")
	}
	if service.store == nil {
		return Result{}, errors.New("no task store is configured")
	}
	limit := boundedLimit(request.Limit)
	result := Result{Matches: []Match{}}
	for _, taskRun := range service.store.ListTaskRunByPersonID(personID) {
		if !matchesConversation(taskRun, request.ConversationID) {
			continue
		}
		if result.Scanned >= maximumScannedTaskRuns {
			result.DidStopScanning = true
			break
		}
		result.Scanned++
		matchedIn := service.matchedFields(taskRun, request.Text)
		if matchedIn == nil {
			continue
		}
		result.TotalMatched++
		if len(result.Matches) < limit {
			result.Matches = append(result.Matches, matchFor(taskRun, matchedIn))
		}
	}
	result.IsTruncated = result.TotalMatched > len(result.Matches)
	return result, nil
}

func (service Service) matchedFields(taskRun task.TaskRun, text string) []string {
	trimmedText := strings.TrimSpace(text)
	if trimmedText == "" {
		return []string{}
	}
	matchedIn := []string{}
	for label, field := range map[string]string{"prompt": taskRun.Prompt, "result": taskRun.Result, "failureReason": taskRun.FailureReason} {
		if containsFold(field, trimmedText) {
			matchedIn = append(matchedIn, label)
		}
	}
	matchedIn = append(matchedIn, service.matchedEventNames(taskRun.TaskRunID, trimmedText)...)
	if len(matchedIn) == 0 {
		return nil
	}
	return sortedUnique(matchedIn)
}

func (service Service) matchedEventNames(taskRunID string, text string) []string {
	eventNames := []string{}
	for _, taskEvent := range service.store.ListTaskEvent(taskRunID) {
		if containsFold(taskEvent.Body, text) {
			eventNames = append(eventNames, strings.TrimSpace(taskEvent.Name))
		}
	}
	return eventNames
}

func matchFor(taskRun task.TaskRun, matchedIn []string) Match {
	return Match{
		TaskRunID:      taskRun.TaskRunID,
		ConversationID: taskRun.OriginConversationID,
		Status:         string(taskRun.Status),
		Prompt:         taskRun.Prompt,
		CreatedAt:      taskRun.CreatedAt,
		MatchedIn:      matchedIn,
	}
}

func matchesConversation(taskRun task.TaskRun, conversationID string) bool {
	trimmedConversationID := strings.TrimSpace(conversationID)
	if trimmedConversationID == "" {
		return true
	}
	return taskRun.OriginConversationID == trimmedConversationID
}

func boundedLimit(limit int) int {
	if limit <= 0 {
		return defaultLimit
	}
	if limit > maximumLimit {
		return maximumLimit
	}
	return limit
}

func containsFold(haystack string, needle string) bool {
	return strings.Contains(strings.ToLower(haystack), strings.ToLower(needle))
}

func sortedUnique(values []string) []string {
	seen := map[string]bool{}
	unique := []string{}
	for _, value := range values {
		if value == "" || seen[value] {
			continue
		}
		seen[value] = true
		unique = append(unique, value)
	}
	return unique
}