internal/sessionquery/sessionquery.go
Rama main · réplica de lectura del repositorio original.
// 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
}