kenn-io--agentsview
f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
172 行
6.8 KiB
Go
172 行
6.8 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"go.kenn.io/agentsview/internal/db"
|
|
"go.kenn.io/agentsview/internal/service"
|
|
)
|
|
|
|
func (s *Server) registerSearchRoutes() {
|
|
group := newRouteGroup(s.api, "/api/v1/search", "Search")
|
|
|
|
get(s, group, "", "Search sessions", s.humaSearch)
|
|
getLong(s, group, "/content", "Search session content", s.humaSearchContent)
|
|
}
|
|
|
|
type searchSort string
|
|
|
|
type contentSearchMode string
|
|
|
|
type contentSearchScope string
|
|
|
|
type searchInput struct {
|
|
Query string `query:"q" required:"true" doc:"Search query"`
|
|
Project string `query:"project" doc:"Filter by project"`
|
|
Sort searchSort `query:"sort" enum:"relevance,recency" default:"relevance" doc:"Sort order"`
|
|
Limit int `query:"limit" minimum:"0" doc:"Maximum number of results"`
|
|
Cursor int `query:"cursor" minimum:"0" doc:"Pagination cursor"`
|
|
}
|
|
|
|
type contentSearchInput struct {
|
|
Pattern string `query:"pattern" required:"true" doc:"Pattern to search for"`
|
|
Mode contentSearchMode `query:"mode" enum:"substring,regex,fts,semantic,hybrid" doc:"Search mode"`
|
|
Scope contentSearchScope `query:"scope" enum:"top,all,subordinate" doc:"Semantic/hybrid result scope: top, all, or subordinate (default all)"`
|
|
SearchIntent string `header:"X-AgentsView-Search-Intent" doc:"Required for semantic/hybrid GET searches"`
|
|
In string `query:"in" doc:"Comma-separated content sources"`
|
|
ExcludeSystem bool `query:"exclude_system" doc:"Exclude system messages"`
|
|
Reveal bool `query:"reveal" doc:"Return unredacted secret matches for localhost callers"`
|
|
Project string `query:"project" doc:"Filter by project"`
|
|
ExcludeProject string `query:"exclude_project" doc:"Exclude a project"`
|
|
Machine string `query:"machine" doc:"Filter by machine"`
|
|
GitBranch string `query:"git_branch" doc:"Filter by git branch; opaque (project, branch) tokens from the /branches endpoint"`
|
|
Agent string `query:"agent" doc:"Filter by agent"`
|
|
Date string `query:"date" format:"date" doc:"Filter sessions active on this YYYY-MM-DD date"`
|
|
DateFrom string `query:"date_from" format:"date" doc:"Filter sessions active on or after this date"`
|
|
DateTo string `query:"date_to" format:"date" doc:"Filter sessions active on or before this date"`
|
|
ActiveSince string `query:"active_since" format:"date-time" doc:"Filter sessions active since this RFC3339 timestamp"`
|
|
IncludeChildren bool `query:"include_children" doc:"Include child sessions"`
|
|
IncludeAutomated bool `query:"include_automated" doc:"Include automated sessions"`
|
|
IncludeOneShot bool `query:"include_one_shot" doc:"Include one-shot sessions"`
|
|
Limit int `query:"limit" minimum:"0" doc:"Maximum number of results"`
|
|
Cursor int `query:"cursor" minimum:"0" doc:"Pagination cursor"`
|
|
Context int `query:"context" doc:"Include N messages of context before and after each match (max 10)"`
|
|
}
|
|
|
|
func (s *Server) humaSearch(
|
|
ctx context.Context,
|
|
in *searchInput,
|
|
) (*jsonOutput[searchResponse], error) {
|
|
query := strings.TrimSpace(in.Query)
|
|
if query == "" {
|
|
return nil, apiError(http.StatusBadRequest, "query required")
|
|
}
|
|
res, err := s.sessions.Search(ctx, service.SearchRequest{
|
|
Query: query,
|
|
Project: in.Project,
|
|
Sort: string(in.Sort),
|
|
Cursor: in.Cursor,
|
|
Limit: in.Limit,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrSearchUnavailable) {
|
|
return nil, apiError(http.StatusNotImplemented, "search not available")
|
|
}
|
|
var inputErr *db.SearchInputError
|
|
if errors.As(err, &inputErr) {
|
|
return nil, apiError(http.StatusBadRequest, err.Error())
|
|
}
|
|
return nil, serverError(err)
|
|
}
|
|
return &jsonOutput[searchResponse]{
|
|
Body: searchResponse{
|
|
Query: query,
|
|
Results: res.Results,
|
|
Count: len(res.Results),
|
|
Next: res.NextCursor,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *Server) humaSearchContent(
|
|
ctx context.Context,
|
|
in *contentSearchInput,
|
|
) (*jsonOutput[*service.ContentSearchResult], error) {
|
|
if in.Reveal && !isLocalhostContext(ctx) {
|
|
return nil, apiError(http.StatusForbidden, "reveal is only permitted from localhost")
|
|
}
|
|
if requiresSemanticSearchIntent(in.Mode) &&
|
|
in.SearchIntent != service.SemanticSearchIntentValue {
|
|
return nil, apiError(http.StatusForbidden,
|
|
"semantic and hybrid search require "+service.SemanticSearchIntentHeader)
|
|
}
|
|
if in.Scope != "" && !requiresSemanticSearchIntent(in.Mode) {
|
|
return nil, apiError(http.StatusBadRequest,
|
|
"scope is only supported for semantic and hybrid search modes")
|
|
}
|
|
var sources []string
|
|
if in.In != "" {
|
|
sources = strings.Split(in.In, ",")
|
|
}
|
|
if err := validateDateFilterValues(in.Date, in.DateFrom, in.DateTo, in.ActiveSince); err != nil {
|
|
return nil, err
|
|
}
|
|
res, err := s.sessions.SearchContent(ctx, service.ContentSearchRequest{
|
|
Pattern: in.Pattern,
|
|
Mode: string(in.Mode),
|
|
Sources: sources,
|
|
ExcludeSystem: in.ExcludeSystem,
|
|
Reveal: in.Reveal,
|
|
Project: in.Project,
|
|
ExcludeProject: in.ExcludeProject,
|
|
Machine: in.Machine,
|
|
GitBranch: in.GitBranch,
|
|
Agent: in.Agent,
|
|
Date: in.Date,
|
|
DateFrom: in.DateFrom,
|
|
DateTo: in.DateTo,
|
|
ActiveSince: in.ActiveSince,
|
|
IncludeChildren: in.IncludeChildren,
|
|
IncludeAutomated: in.IncludeAutomated,
|
|
IncludeOneShot: in.IncludeOneShot,
|
|
Scope: string(in.Scope),
|
|
Limit: in.Limit,
|
|
Cursor: in.Cursor,
|
|
Context: in.Context,
|
|
})
|
|
if err != nil {
|
|
if handled := handleHumaContextError(err); handled != nil {
|
|
return nil, handled
|
|
}
|
|
if handled := handleHumaReadOnly(err); handled != nil {
|
|
return nil, handled
|
|
}
|
|
if errors.Is(err, db.ErrSemanticTransient) {
|
|
// The embeddings endpoint itself is unreachable at query
|
|
// time; semantic search is configured and otherwise ready,
|
|
// so this must read as "temporarily unavailable, retry" (503)
|
|
// rather than 501's "not implemented / disabled".
|
|
return nil, apiError(http.StatusServiceUnavailable, err.Error())
|
|
}
|
|
if errors.Is(err, db.ErrSemanticUnavailable) {
|
|
return nil, apiError(http.StatusNotImplemented, err.Error())
|
|
}
|
|
var inputErr *db.SearchInputError
|
|
if errors.As(err, &inputErr) {
|
|
return nil, apiError(http.StatusBadRequest, err.Error())
|
|
}
|
|
return nil, apiError(http.StatusInternalServerError, err.Error())
|
|
}
|
|
if res.Matches == nil {
|
|
res.Matches = []db.ContentMatch{}
|
|
}
|
|
return &jsonOutput[*service.ContentSearchResult]{Body: res}, nil
|
|
}
|
|
|
|
func requiresSemanticSearchIntent(mode contentSearchMode) bool {
|
|
return mode == "semantic" || mode == "hybrid"
|
|
}
|