588 lines
24 KiB
Go
588 lines
24 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// ToolSearchWorkflowEvaluationCase is a reviewed diagnostic workflow. It is
|
|
// intentionally separate from release qrels: the small in-repository fixture
|
|
// measures mechanics and cannot approve a ranking configuration.
|
|
type ToolSearchWorkflowEvaluationCase struct {
|
|
ID string `json:"id"`
|
|
Query string `json:"query"`
|
|
Subqueries []string `json:"subqueries"`
|
|
Required []string `json:"required"`
|
|
}
|
|
|
|
// ToolSearchRetrievalMetrics reports deterministic ranking metrics. Values are
|
|
// fractions in [0,1], not percentages.
|
|
type ToolSearchRetrievalMetrics struct {
|
|
Cases int `json:"cases"`
|
|
RecallAt1 float64 `json:"recall_at_1"`
|
|
RecallAt5 float64 `json:"recall_at_5"`
|
|
MeanReciprocalRankAt5 float64 `json:"mean_reciprocal_rank_at_5"`
|
|
ZeroResultRate float64 `json:"zero_result_rate"`
|
|
}
|
|
|
|
// ToolSearchWorkflowMetrics measures whether every required tool is present in
|
|
// the shared Top-5 budget and the average required-tool recall.
|
|
type ToolSearchWorkflowMetrics struct {
|
|
Cases int `json:"cases"`
|
|
CompleteAt5 float64 `json:"complete_at_5"`
|
|
RequiredRecallAt5 float64 `json:"required_recall_at_5"`
|
|
}
|
|
|
|
// ToolSearchIdentityTrustMetrics exercises the complete delivered identity
|
|
// set. Exact filtered checks deliberately exclude each canonical path and
|
|
// verify that search abstains instead of falling through to a fuzzy sibling.
|
|
type ToolSearchIdentityTrustMetrics struct {
|
|
CanonicalCases int `json:"canonical_cases"`
|
|
CanonicalPassRate float64 `json:"canonical_pass_rate"`
|
|
PrimaryCLICases int `json:"primary_cli_cases"`
|
|
PrimaryCLIPassRate float64 `json:"primary_cli_pass_rate"`
|
|
AliasCases int `json:"alias_cases"`
|
|
AliasPassRate float64 `json:"alias_pass_rate"`
|
|
NFKCCases int `json:"nfkc_cases"`
|
|
NFKCPassRate float64 `json:"nfkc_pass_rate"`
|
|
ExactFilteredCases int `json:"exact_filtered_cases"`
|
|
ExactFilteredPassRate float64 `json:"exact_filtered_pass_rate"`
|
|
}
|
|
|
|
// ToolSearchNegativeTrustMetrics is a same-source diagnostic only. A
|
|
// forbidden exposure means the tool that owns an avoid_when sentence appears
|
|
// in the ranking; it does not prove that a model would call that tool.
|
|
type ToolSearchNegativeTrustMetrics struct {
|
|
Cases int `json:"cases"`
|
|
ExcludedOverBudget int `json:"excluded_over_budget"`
|
|
ForbiddenAt1 float64 `json:"forbidden_at_1"`
|
|
ForbiddenAt5 float64 `json:"forbidden_at_5"`
|
|
}
|
|
|
|
// ToolSearchIntegrityMetrics verifies invariants that do not depend on
|
|
// relevance labels.
|
|
type ToolSearchIntegrityMetrics struct {
|
|
ResponsesEvaluated int `json:"responses_evaluated"`
|
|
CatalogBindingFailures int `json:"catalog_binding_failures"`
|
|
UnknownCandidateCount int `json:"unknown_candidate_count"`
|
|
IneligibleCandidateCount int `json:"ineligible_candidate_count"`
|
|
ResponseBudgetViolations int `json:"response_budget_violations"`
|
|
}
|
|
|
|
// ToolSearchTrustMetrics keeps contract integrity separate from ranking
|
|
// relevance. Search rank is never presented as a probability of safe use.
|
|
type ToolSearchTrustMetrics struct {
|
|
Identity ToolSearchIdentityTrustMetrics `json:"identity"`
|
|
Negative ToolSearchNegativeTrustMetrics `json:"negative"`
|
|
Integrity ToolSearchIntegrityMetrics `json:"integrity"`
|
|
}
|
|
|
|
// ToolSearchContextComparison compares byte-level JSON envelopes, not model
|
|
// tokens. The ideal navigation path assumes an oracle already knows the right
|
|
// product; its selection success is therefore deliberately not estimated.
|
|
type ToolSearchContextComparison struct {
|
|
ToolCount int `json:"tool_count"`
|
|
FullSchemaAllBytes int `json:"full_schema_all_bytes"`
|
|
OverviewBytes int `json:"overview_bytes"`
|
|
AverageProductBytes float64 `json:"average_product_bytes"`
|
|
AverageInspectBytes float64 `json:"average_inspect_bytes"`
|
|
AverageIdealSchemaNavigationBytes float64 `json:"average_ideal_schema_navigation_bytes"`
|
|
AverageSearchInspectBytes float64 `json:"average_search_inspect_bytes"`
|
|
ReductionVsFullSchema float64 `json:"reduction_vs_full_schema"`
|
|
ReductionVsIdealNavigation float64 `json:"reduction_vs_ideal_navigation"`
|
|
}
|
|
|
|
// ToolSearchDiagnosticComparison is generated from the exact typed Catalog
|
|
// embedded in the binary. It is a diagnostic proxy, not an Agent completion
|
|
// benchmark: its intent queries come from reviewed use_when prose and therefore
|
|
// are not independently authored qrels.
|
|
type ToolSearchDiagnosticComparison struct {
|
|
Version string `json:"version"`
|
|
Catalog CatalogVersionRef `json:"catalog"`
|
|
Algorithm string `json:"algorithm"`
|
|
TopK int `json:"top_k"`
|
|
IntentProxy ToolSearchRetrievalMetrics `json:"intent_proxy"`
|
|
IntentExcludedOverBudget int `json:"intent_excluded_over_budget"`
|
|
IntentLanguageSlices map[string]ToolSearchRetrievalMetrics `json:"intent_language_slices"`
|
|
WorkflowRaw ToolSearchWorkflowMetrics `json:"workflow_raw"`
|
|
WorkflowDecomposed ToolSearchWorkflowMetrics `json:"workflow_decomposed"`
|
|
Trust ToolSearchTrustMetrics `json:"trust"`
|
|
Context ToolSearchContextComparison `json:"context"`
|
|
AgentABStatus string `json:"agent_ab_status"`
|
|
Limitations []string `json:"limitations"`
|
|
}
|
|
|
|
// BuildDeliveryToolSearchDiagnosticComparison evaluates the shipped Go
|
|
// retriever against the declaration-assembled Catalog without reading a
|
|
// generated repository JSON artifact. Results belong under policy-tmp.
|
|
func BuildDeliveryToolSearchDiagnosticComparison(ctx context.Context, workflows []ToolSearchWorkflowEvaluationCase) (ToolSearchDiagnosticComparison, error) {
|
|
return BuildDeliveryToolSearchDiagnosticComparisonForAlgorithm(ctx, workflows, ToolSearchLexicalBM25Action)
|
|
}
|
|
|
|
// BuildDeliveryToolSearchDiagnosticComparisonForAlgorithm runs one shipped Go
|
|
// lexical implementation over exactly the same typed Catalog and fixtures.
|
|
// This keeps shadow algorithm comparison out of the production default.
|
|
func BuildDeliveryToolSearchDiagnosticComparisonForAlgorithm(ctx context.Context, workflows []ToolSearchWorkflowEvaluationCase, algorithm string) (ToolSearchDiagnosticComparison, error) {
|
|
if err := deliverySchemaCatalogError(); err != nil {
|
|
return ToolSearchDiagnosticComparison{}, fmt.Errorf("assemble typed Schema registry for evaluation: %w", err)
|
|
}
|
|
loaded := deliverySchemaCatalog()
|
|
config := DefaultToolSearchConfig()
|
|
config.LexicalAlgorithm = algorithm
|
|
config.CatalogSourceHash = loaded.Snapshot.SourceHash
|
|
config.CatalogSurfaceHash = loaded.Snapshot.SurfaceHash
|
|
engine, err := NewToolSearchEngine(loaded.Registry, config)
|
|
if err != nil {
|
|
return ToolSearchDiagnosticComparison{}, err
|
|
}
|
|
allPayload, err := deliverySchemaAllPayload()
|
|
if err != nil {
|
|
return ToolSearchDiagnosticComparison{}, fmt.Errorf("render full Schema: %w", err)
|
|
}
|
|
overviewPayload, err := deliverySchemaOverviewPayload()
|
|
if err != nil {
|
|
return ToolSearchDiagnosticComparison{}, fmt.Errorf("render Schema overview: %w", err)
|
|
}
|
|
allBytes, err := compactJSONSize(allPayload)
|
|
if err != nil {
|
|
return ToolSearchDiagnosticComparison{}, err
|
|
}
|
|
overviewBytes, err := compactJSONSize(stripSchemaPayloadCompact(overviewPayload))
|
|
if err != nil {
|
|
return ToolSearchDiagnosticComparison{}, err
|
|
}
|
|
|
|
type proxyCase struct {
|
|
query string
|
|
gold string
|
|
product string
|
|
}
|
|
proxyCases := make([]proxyCase, 0)
|
|
intentExcludedOverBudget := 0
|
|
for _, product := range loaded.Registry.Products {
|
|
for _, tool := range product.Tools {
|
|
for _, query := range tool.Selection.UseWhen {
|
|
if query != "" && toolSearchQueryWithinBudget(query) {
|
|
proxyCases = append(proxyCases, proxyCase{query: query, gold: tool.Identity.CanonicalPath, product: product.ID})
|
|
} else if query != "" {
|
|
intentExcludedOverBudget++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(proxyCases, func(i, j int) bool {
|
|
if proxyCases[i].gold == proxyCases[j].gold {
|
|
return proxyCases[i].query < proxyCases[j].query
|
|
}
|
|
return proxyCases[i].gold < proxyCases[j].gold
|
|
})
|
|
|
|
intentAccumulator := &toolSearchRetrievalAccumulator{}
|
|
languageAccumulators := map[string]*toolSearchRetrievalAccumulator{}
|
|
var totalProductBytes, totalInspectBytes, totalSearchBytes float64
|
|
productSizes := make(map[string]int)
|
|
inspectSizes := make(map[string]int)
|
|
for _, item := range proxyCases {
|
|
if err := ctx.Err(); err != nil {
|
|
return ToolSearchDiagnosticComparison{}, err
|
|
}
|
|
response, searchErr := engine.Search(ctx, ToolSearchRequest{Query: item.query, Limit: 5, CandidateLimit: 20})
|
|
if searchErr != nil {
|
|
return ToolSearchDiagnosticComparison{}, fmt.Errorf("search proxy case %s: %w", item.gold, searchErr)
|
|
}
|
|
rank := 0
|
|
for index, candidate := range response.Candidates {
|
|
if candidate.CanonicalPath != item.gold {
|
|
continue
|
|
}
|
|
rank = index + 1
|
|
break
|
|
}
|
|
intentAccumulator.observe(rank, len(response.Candidates) == 0)
|
|
slice := toolSearchQueryLanguageSlice(item.query)
|
|
if languageAccumulators[slice] == nil {
|
|
languageAccumulators[slice] = &toolSearchRetrievalAccumulator{}
|
|
}
|
|
languageAccumulators[slice].observe(rank, len(response.Candidates) == 0)
|
|
encodedResponse, marshalErr := json.Marshal(response)
|
|
if marshalErr != nil {
|
|
return ToolSearchDiagnosticComparison{}, marshalErr
|
|
}
|
|
totalSearchBytes += float64(len(encodedResponse))
|
|
|
|
productBytes, ok := productSizes[item.product]
|
|
if !ok {
|
|
payload, payloadErr := queryDeliverySchemaPayload([]string{item.product})
|
|
if payloadErr != nil {
|
|
return ToolSearchDiagnosticComparison{}, payloadErr
|
|
}
|
|
productBytes, payloadErr = compactJSONSize(stripSchemaPayloadCompact(payload))
|
|
if payloadErr != nil {
|
|
return ToolSearchDiagnosticComparison{}, payloadErr
|
|
}
|
|
productSizes[item.product] = productBytes
|
|
}
|
|
totalProductBytes += float64(productBytes)
|
|
|
|
inspectBytes, ok := inspectSizes[item.gold]
|
|
if !ok {
|
|
payload, payloadErr := queryDeliverySchemaPayload([]string{item.gold})
|
|
if payloadErr != nil {
|
|
return ToolSearchDiagnosticComparison{}, payloadErr
|
|
}
|
|
payload = stripSchemaPayloadCompact(payload)
|
|
inspectBytes, payloadErr = compactJSONSize(SchemaInspectV1Response{
|
|
Version: "schema-inspect.v1",
|
|
Catalog: CatalogVersionRef{SourceHash: loaded.Snapshot.SourceHash, SurfaceHash: loaded.Snapshot.SurfaceHash},
|
|
ToolSpec: payload,
|
|
})
|
|
if payloadErr != nil {
|
|
return ToolSearchDiagnosticComparison{}, payloadErr
|
|
}
|
|
inspectSizes[item.gold] = inspectBytes
|
|
}
|
|
totalInspectBytes += float64(inspectBytes)
|
|
}
|
|
|
|
count := float64(len(proxyCases))
|
|
intent := intentAccumulator.metrics()
|
|
languageSlices := make(map[string]ToolSearchRetrievalMetrics, len(languageAccumulators))
|
|
for slice, accumulator := range languageAccumulators {
|
|
languageSlices[slice] = accumulator.metrics()
|
|
}
|
|
contextComparison := ToolSearchContextComparison{
|
|
ToolCount: len(loaded.Index.CanonicalPaths()),
|
|
FullSchemaAllBytes: allBytes,
|
|
OverviewBytes: overviewBytes,
|
|
}
|
|
if count > 0 {
|
|
contextComparison.AverageProductBytes = totalProductBytes / count
|
|
contextComparison.AverageInspectBytes = totalInspectBytes / count
|
|
contextComparison.AverageIdealSchemaNavigationBytes = float64(overviewBytes) + contextComparison.AverageProductBytes + contextComparison.AverageInspectBytes
|
|
contextComparison.AverageSearchInspectBytes = totalSearchBytes/count + contextComparison.AverageInspectBytes
|
|
contextComparison.ReductionVsFullSchema = reduction(float64(allBytes), contextComparison.AverageSearchInspectBytes)
|
|
contextComparison.ReductionVsIdealNavigation = reduction(contextComparison.AverageIdealSchemaNavigationBytes, contextComparison.AverageSearchInspectBytes)
|
|
}
|
|
|
|
rawWorkflow, decomposedWorkflow, err := evaluateToolSearchWorkflows(ctx, engine, workflows)
|
|
if err != nil {
|
|
return ToolSearchDiagnosticComparison{}, err
|
|
}
|
|
trust, err := evaluateToolSearchTrust(ctx, engine, loaded.Registry)
|
|
if err != nil {
|
|
return ToolSearchDiagnosticComparison{}, err
|
|
}
|
|
return ToolSearchDiagnosticComparison{
|
|
Version: "tool-search-diagnostic-comparison.v1",
|
|
Catalog: engine.catalogVersion(),
|
|
Algorithm: engine.lexical.Name(),
|
|
TopK: 5,
|
|
IntentProxy: intent,
|
|
IntentExcludedOverBudget: intentExcludedOverBudget,
|
|
IntentLanguageSlices: languageSlices,
|
|
WorkflowRaw: rawWorkflow,
|
|
WorkflowDecomposed: decomposedWorkflow,
|
|
Trust: trust,
|
|
Context: contextComparison,
|
|
AgentABStatus: "not_run_requires_independent_tasks_and_model_runs",
|
|
Limitations: []string{
|
|
"intent_proxy queries are reviewed use_when prose from the same Catalog and are not independent qrels",
|
|
"selection prose that exceeds the production query byte or rune budget is counted and excluded rather than weakening runtime limits",
|
|
"direct Schema navigation bytes assume an oracle selects the correct product and do not estimate model selection success",
|
|
"search plus inspect bytes also inspect the gold leaf even when retrieval misses and are an oracle-assisted capacity upper bound, not observed Agent cost",
|
|
"workflow decomposition uses reviewed fixture subqueries and is an upper-bound diagnostic",
|
|
"negative queries are same-source avoid_when prose without alternative gold and measure candidate exposure rather than Agent calls",
|
|
"byte counts are compact JSON bytes rather than provider-specific tokenizer counts",
|
|
"true Agent task success, unsafe action rate, recovery success, latency, and token cost require paired Agent A/B runs",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
type toolSearchRetrievalAccumulator struct {
|
|
cases int
|
|
hitAt1 int
|
|
hitAt5 int
|
|
reciprocal float64
|
|
zeroResults int
|
|
}
|
|
|
|
func (a *toolSearchRetrievalAccumulator) observe(rank int, zeroResults bool) {
|
|
a.cases++
|
|
if zeroResults {
|
|
a.zeroResults++
|
|
}
|
|
if rank == 1 {
|
|
a.hitAt1++
|
|
}
|
|
if rank > 0 && rank <= 5 {
|
|
a.hitAt5++
|
|
a.reciprocal += 1 / float64(rank)
|
|
}
|
|
}
|
|
|
|
func (a *toolSearchRetrievalAccumulator) metrics() ToolSearchRetrievalMetrics {
|
|
result := ToolSearchRetrievalMetrics{Cases: a.cases}
|
|
if a.cases == 0 {
|
|
return result
|
|
}
|
|
denominator := float64(a.cases)
|
|
result.RecallAt1 = float64(a.hitAt1) / denominator
|
|
result.RecallAt5 = float64(a.hitAt5) / denominator
|
|
result.MeanReciprocalRankAt5 = a.reciprocal / denominator
|
|
result.ZeroResultRate = float64(a.zeroResults) / denominator
|
|
return result
|
|
}
|
|
|
|
func toolSearchQueryLanguageSlice(query string) string {
|
|
hasChinese := false
|
|
hasASCIIWord := false
|
|
for _, character := range query {
|
|
if unicode.Is(unicode.Han, character) {
|
|
hasChinese = true
|
|
}
|
|
if character <= unicode.MaxASCII && (unicode.IsLetter(character) || unicode.IsDigit(character)) {
|
|
hasASCIIWord = true
|
|
}
|
|
}
|
|
if hasChinese && hasASCIIWord {
|
|
return "mixed_chinese_ascii"
|
|
}
|
|
if hasChinese {
|
|
return "chinese_only"
|
|
}
|
|
return "non_chinese"
|
|
}
|
|
|
|
func evaluateToolSearchTrust(ctx context.Context, engine *ToolSearchEngine, registry SchemaRegistry) (ToolSearchTrustMetrics, error) {
|
|
result := ToolSearchTrustMetrics{}
|
|
var canonicalPassed, cliPassed, aliasPassed, nfkcPassed, filteredPassed int
|
|
var forbiddenAt1, forbiddenAt5 int
|
|
for _, product := range registry.Products {
|
|
for _, tool := range product.Tools {
|
|
if err := ctx.Err(); err != nil {
|
|
return result, err
|
|
}
|
|
result.Identity.CanonicalCases++
|
|
response, err := engine.Search(ctx, ToolSearchRequest{Query: tool.Identity.CanonicalPath})
|
|
if err != nil {
|
|
return result, fmt.Errorf("audit exact canonical %s: %w", tool.Identity.CanonicalPath, err)
|
|
}
|
|
observeToolSearchIntegrity(&result.Integrity, engine, response)
|
|
if exactSearchMatches(response, tool.Identity.CanonicalPath) {
|
|
canonicalPassed++
|
|
}
|
|
|
|
if cliPath := tool.Identity.PrimaryCLIPath; cliPath != "" {
|
|
result.Identity.PrimaryCLICases++
|
|
response, err = engine.Search(ctx, ToolSearchRequest{Query: cliPath})
|
|
if err != nil {
|
|
return result, fmt.Errorf("audit exact CLI %s: %w", cliPath, err)
|
|
}
|
|
observeToolSearchIntegrity(&result.Integrity, engine, response)
|
|
if exactSearchMatches(response, tool.Identity.CanonicalPath) {
|
|
cliPassed++
|
|
}
|
|
}
|
|
for _, alias := range tool.Identity.Aliases {
|
|
result.Identity.AliasCases++
|
|
response, err = engine.Search(ctx, ToolSearchRequest{Query: alias})
|
|
if err != nil {
|
|
return result, fmt.Errorf("audit exact alias %s: %w", alias, err)
|
|
}
|
|
observeToolSearchIntegrity(&result.Integrity, engine, response)
|
|
if exactSearchMatches(response, tool.Identity.CanonicalPath) {
|
|
aliasPassed++
|
|
}
|
|
}
|
|
nfkcQuery := toolSearchFullwidthASCII(tool.Identity.CanonicalPath)
|
|
if nfkcQuery != tool.Identity.CanonicalPath {
|
|
result.Identity.NFKCCases++
|
|
response, err = engine.Search(ctx, ToolSearchRequest{Query: nfkcQuery})
|
|
if err != nil {
|
|
return result, fmt.Errorf("audit NFKC identity %s: %w", tool.Identity.CanonicalPath, err)
|
|
}
|
|
observeToolSearchIntegrity(&result.Integrity, engine, response)
|
|
if exactSearchMatches(response, tool.Identity.CanonicalPath) {
|
|
nfkcPassed++
|
|
}
|
|
}
|
|
|
|
result.Identity.ExactFilteredCases++
|
|
response, err = engine.Search(ctx, ToolSearchRequest{
|
|
Query: tool.Identity.CanonicalPath,
|
|
ExcludeCanonicalPaths: []string{tool.Identity.CanonicalPath},
|
|
})
|
|
if err != nil {
|
|
return result, fmt.Errorf("audit exact filtered %s: %w", tool.Identity.CanonicalPath, err)
|
|
}
|
|
observeToolSearchIntegrity(&result.Integrity, engine, response)
|
|
if response.Strategy == "exact_filtered" && response.Abstained && len(response.Candidates) == 0 && response.ExactFiltered != nil && response.ExactFiltered.CanonicalPath == tool.Identity.CanonicalPath && response.ExactFiltered.Reason == "excluded" {
|
|
filteredPassed++
|
|
}
|
|
|
|
for _, query := range tool.Selection.AvoidWhen {
|
|
if query == "" {
|
|
continue
|
|
}
|
|
if !toolSearchQueryWithinBudget(query) {
|
|
result.Negative.ExcludedOverBudget++
|
|
continue
|
|
}
|
|
result.Negative.Cases++
|
|
response, err = engine.Search(ctx, ToolSearchRequest{Query: query, Limit: 5, CandidateLimit: 20})
|
|
if err != nil {
|
|
return result, fmt.Errorf("audit negative %s: %w", tool.Identity.CanonicalPath, err)
|
|
}
|
|
observeToolSearchIntegrity(&result.Integrity, engine, response)
|
|
for rank, candidate := range response.Candidates {
|
|
if candidate.CanonicalPath != tool.Identity.CanonicalPath {
|
|
continue
|
|
}
|
|
if rank == 0 {
|
|
forbiddenAt1++
|
|
}
|
|
forbiddenAt5++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if result.Identity.CanonicalCases > 0 {
|
|
result.Identity.CanonicalPassRate = float64(canonicalPassed) / float64(result.Identity.CanonicalCases)
|
|
}
|
|
if result.Identity.PrimaryCLICases > 0 {
|
|
result.Identity.PrimaryCLIPassRate = float64(cliPassed) / float64(result.Identity.PrimaryCLICases)
|
|
}
|
|
if result.Identity.AliasCases > 0 {
|
|
result.Identity.AliasPassRate = float64(aliasPassed) / float64(result.Identity.AliasCases)
|
|
}
|
|
if result.Identity.NFKCCases > 0 {
|
|
result.Identity.NFKCPassRate = float64(nfkcPassed) / float64(result.Identity.NFKCCases)
|
|
}
|
|
if result.Identity.ExactFilteredCases > 0 {
|
|
result.Identity.ExactFilteredPassRate = float64(filteredPassed) / float64(result.Identity.ExactFilteredCases)
|
|
}
|
|
if result.Negative.Cases > 0 {
|
|
result.Negative.ForbiddenAt1 = float64(forbiddenAt1) / float64(result.Negative.Cases)
|
|
result.Negative.ForbiddenAt5 = float64(forbiddenAt5) / float64(result.Negative.Cases)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func toolSearchFullwidthASCII(value string) string {
|
|
runes := []rune(value)
|
|
for index, character := range runes {
|
|
if character >= '!' && character <= '~' {
|
|
runes[index] = character + 0xFEE0
|
|
}
|
|
}
|
|
return string(runes)
|
|
}
|
|
|
|
func toolSearchQueryWithinBudget(query string) bool {
|
|
return len(query) <= maxToolSearchQueryBytes && utf8.RuneCountInString(query) <= maxToolSearchQueryRunes
|
|
}
|
|
|
|
func exactSearchMatches(response ToolSearchResponse, canonical string) bool {
|
|
return response.Strategy == "exact_guard" && len(response.Candidates) == 1 && response.Candidates[0].CanonicalPath == canonical && response.Candidates[0].RequiresInspect
|
|
}
|
|
|
|
func observeToolSearchIntegrity(metrics *ToolSearchIntegrityMetrics, engine *ToolSearchEngine, response ToolSearchResponse) {
|
|
metrics.ResponsesEvaluated++
|
|
if response.Catalog != engine.catalogVersion() {
|
|
metrics.CatalogBindingFailures++
|
|
}
|
|
if encoded, err := json.Marshal(response); err != nil || len(encoded) > maxToolSearchResponseBytes {
|
|
metrics.ResponseBudgetViolations++
|
|
}
|
|
for _, candidate := range response.Candidates {
|
|
document, ok := engine.documents[candidate.CanonicalPath]
|
|
if !ok {
|
|
metrics.UnknownCandidateCount++
|
|
continue
|
|
}
|
|
if !toolSearchEligible(document.tool, nil, nil, nil) {
|
|
metrics.IneligibleCandidateCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
func evaluateToolSearchWorkflows(ctx context.Context, engine *ToolSearchEngine, workflows []ToolSearchWorkflowEvaluationCase) (ToolSearchWorkflowMetrics, ToolSearchWorkflowMetrics, error) {
|
|
raw := ToolSearchWorkflowMetrics{Cases: len(workflows)}
|
|
decomposed := ToolSearchWorkflowMetrics{Cases: len(workflows)}
|
|
if len(workflows) == 0 {
|
|
return raw, decomposed, nil
|
|
}
|
|
for _, workflow := range workflows {
|
|
if workflow.ID == "" || workflow.Query == "" || len(workflow.Required) == 0 {
|
|
return raw, decomposed, fmt.Errorf("workflow evaluation case must have id, query and required tools")
|
|
}
|
|
one, err := engine.Search(ctx, ToolSearchRequest{Query: workflow.Query, Limit: 5, CandidateLimit: 20})
|
|
if err != nil {
|
|
return raw, decomposed, fmt.Errorf("search workflow %s: %w", workflow.ID, err)
|
|
}
|
|
many, err := engine.SearchSubqueries(ctx, workflow.Subqueries, ToolSearchRequest{Query: workflow.Query, Limit: 5, CandidateLimit: 20})
|
|
if err != nil {
|
|
return raw, decomposed, fmt.Errorf("search decomposed workflow %s: %w", workflow.ID, err)
|
|
}
|
|
accumulateWorkflowMetrics(&raw, one.Candidates, workflow.Required)
|
|
accumulateWorkflowMetrics(&decomposed, many.Candidates, workflow.Required)
|
|
}
|
|
denominator := float64(len(workflows))
|
|
raw.CompleteAt5 /= denominator
|
|
raw.RequiredRecallAt5 /= denominator
|
|
decomposed.CompleteAt5 /= denominator
|
|
decomposed.RequiredRecallAt5 /= denominator
|
|
return raw, decomposed, nil
|
|
}
|
|
|
|
func accumulateWorkflowMetrics(metrics *ToolSearchWorkflowMetrics, candidates []ToolReference, required []string) {
|
|
found := make(map[string]bool, len(candidates))
|
|
for _, candidate := range candidates {
|
|
found[candidate.CanonicalPath] = true
|
|
}
|
|
hits := 0
|
|
for _, canonical := range required {
|
|
if found[canonical] {
|
|
hits++
|
|
}
|
|
}
|
|
metrics.RequiredRecallAt5 += float64(hits) / float64(len(required))
|
|
if hits == len(required) {
|
|
metrics.CompleteAt5++
|
|
}
|
|
}
|
|
|
|
func compactJSONSize(value any) (int, error) {
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("marshal compact JSON: %w", err)
|
|
}
|
|
return len(encoded), nil
|
|
}
|
|
|
|
func reduction(baseline, candidate float64) float64 {
|
|
if baseline <= 0 {
|
|
return 0
|
|
}
|
|
return math.Max(-1, math.Min(1, 1-candidate/baseline))
|
|
}
|