Add implicit scope weighting and suggestion viability
This commit is contained in:
@@ -63,6 +63,17 @@ public static class UnifiedSearchEndpoints
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.Produces(StatusCodes.Status403Forbidden);
|
||||
|
||||
group.MapPost("/suggestions/evaluate", EvaluateSuggestionsAsync)
|
||||
.WithName("UnifiedSearchEvaluateSuggestions")
|
||||
.WithSummary("Preflights contextual search suggestions against the active corpus.")
|
||||
.WithDescription(
|
||||
"Evaluates a bounded list of suggested queries without recording user-search analytics so the UI can suppress dead suggestion chips. " +
|
||||
"Returns per-query viability plus aggregate domain coverage for the active context.")
|
||||
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
|
||||
.Produces<UnifiedSearchSuggestionViabilityApiResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.Produces(StatusCodes.Status403Forbidden);
|
||||
|
||||
group.MapPost("/synthesize", SynthesizeAsync)
|
||||
.WithName("UnifiedSearchSynthesize")
|
||||
.WithSummary("Streams deterministic-first search synthesis as SSE.")
|
||||
@@ -131,6 +142,44 @@ public static class UnifiedSearchEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> EvaluateSuggestionsAsync(
|
||||
HttpContext httpContext,
|
||||
UnifiedSearchSuggestionViabilityApiRequest request,
|
||||
IUnifiedSearchService searchService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request is null || request.Queries is not { Count: > 0 })
|
||||
{
|
||||
return Results.BadRequest(new { error = _t("advisoryai.validation.suggestions_required") });
|
||||
}
|
||||
|
||||
var tenant = ResolveTenant(httpContext);
|
||||
if (tenant is null)
|
||||
{
|
||||
return Results.BadRequest(new { error = _t("advisoryai.validation.tenant_required") });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var userScopes = ResolveUserScopes(httpContext);
|
||||
var userId = ResolveUserId(httpContext);
|
||||
var domainRequest = new SearchSuggestionViabilityRequest(
|
||||
request.Queries
|
||||
.Where(static query => !string.IsNullOrWhiteSpace(query))
|
||||
.Select(static query => query.Trim())
|
||||
.ToArray(),
|
||||
NormalizeFilter(request.Filters, tenant, userScopes, userId),
|
||||
NormalizeAmbient(request.Ambient));
|
||||
|
||||
var response = await searchService.EvaluateSuggestionsAsync(domainRequest, cancellationToken).ConfigureAwait(false);
|
||||
return Results.Ok(MapSuggestionViabilityResponse(response));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SynthesizeAsync(
|
||||
HttpContext httpContext,
|
||||
UnifiedSearchSynthesizeApiRequest request,
|
||||
@@ -460,7 +509,9 @@ public static class UnifiedSearchEndpoints
|
||||
suggestions = response.Suggestions.Select(static s => new UnifiedSearchApiSuggestion
|
||||
{
|
||||
Text = s.Text,
|
||||
Reason = s.Reason
|
||||
Reason = s.Reason,
|
||||
Domain = s.Domain,
|
||||
CandidateCount = s.CandidateCount
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
@@ -470,7 +521,9 @@ public static class UnifiedSearchEndpoints
|
||||
refinements = response.Refinements.Select(static r => new UnifiedSearchApiRefinement
|
||||
{
|
||||
Text = r.Text,
|
||||
Source = r.Source
|
||||
Source = r.Source,
|
||||
Domain = r.Domain,
|
||||
CandidateCount = r.CandidateCount
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
@@ -504,6 +557,59 @@ public static class UnifiedSearchEndpoints
|
||||
Query = response.Query,
|
||||
TopK = response.TopK,
|
||||
Cards = cards,
|
||||
Overflow = response.Overflow is null
|
||||
? null
|
||||
: new UnifiedSearchApiOverflow
|
||||
{
|
||||
CurrentScopeDomain = response.Overflow.CurrentScopeDomain,
|
||||
Reason = response.Overflow.Reason,
|
||||
Cards = response.Overflow.Cards.Select(static card => new UnifiedSearchApiCard
|
||||
{
|
||||
EntityKey = card.EntityKey,
|
||||
EntityType = card.EntityType,
|
||||
Domain = card.Domain,
|
||||
Title = card.Title,
|
||||
Snippet = card.Snippet,
|
||||
Score = card.Score,
|
||||
Severity = card.Severity,
|
||||
Actions = card.Actions.Select(static action => new UnifiedSearchApiAction
|
||||
{
|
||||
Label = action.Label,
|
||||
ActionType = action.ActionType,
|
||||
Route = action.Route,
|
||||
Command = action.Command,
|
||||
IsPrimary = action.IsPrimary
|
||||
}).ToArray(),
|
||||
Metadata = card.Metadata,
|
||||
Sources = card.Sources.ToArray(),
|
||||
Facets = card.Facets.Select(static facet => new UnifiedSearchApiFacet
|
||||
{
|
||||
Domain = facet.Domain,
|
||||
Title = facet.Title,
|
||||
Snippet = facet.Snippet,
|
||||
Score = facet.Score,
|
||||
Metadata = facet.Metadata
|
||||
}).ToArray(),
|
||||
Connections = card.Connections.ToArray(),
|
||||
SynthesisHints = card.SynthesisHints
|
||||
}).ToArray()
|
||||
},
|
||||
Coverage = response.Coverage is null
|
||||
? null
|
||||
: new UnifiedSearchApiCoverage
|
||||
{
|
||||
CurrentScopeDomain = response.Coverage.CurrentScopeDomain,
|
||||
CurrentScopeWeighted = response.Coverage.CurrentScopeWeighted,
|
||||
Domains = response.Coverage.Domains.Select(static domain => new UnifiedSearchApiDomainCoverage
|
||||
{
|
||||
Domain = domain.Domain,
|
||||
CandidateCount = domain.CandidateCount,
|
||||
VisibleCardCount = domain.VisibleCardCount,
|
||||
TopScore = domain.TopScore,
|
||||
IsCurrentScope = domain.IsCurrentScope,
|
||||
HasVisibleResults = domain.HasVisibleResults
|
||||
}).ToArray()
|
||||
},
|
||||
Synthesis = synthesis,
|
||||
Suggestions = suggestions,
|
||||
Refinements = refinements,
|
||||
@@ -528,6 +634,40 @@ public static class UnifiedSearchEndpoints
|
||||
};
|
||||
}
|
||||
|
||||
private static UnifiedSearchSuggestionViabilityApiResponse MapSuggestionViabilityResponse(
|
||||
SearchSuggestionViabilityResponse response)
|
||||
{
|
||||
return new UnifiedSearchSuggestionViabilityApiResponse
|
||||
{
|
||||
Suggestions = response.Suggestions.Select(static suggestion => new UnifiedSearchSuggestionViabilityApiResult
|
||||
{
|
||||
Query = suggestion.Query,
|
||||
Viable = suggestion.Viable,
|
||||
Status = suggestion.Status,
|
||||
Code = suggestion.Code,
|
||||
CardCount = suggestion.CardCount,
|
||||
LeadingDomain = suggestion.LeadingDomain,
|
||||
Reason = suggestion.Reason
|
||||
}).ToArray(),
|
||||
Coverage = response.Coverage is null
|
||||
? null
|
||||
: new UnifiedSearchApiCoverage
|
||||
{
|
||||
CurrentScopeDomain = response.Coverage.CurrentScopeDomain,
|
||||
CurrentScopeWeighted = response.Coverage.CurrentScopeWeighted,
|
||||
Domains = response.Coverage.Domains.Select(static domain => new UnifiedSearchApiDomainCoverage
|
||||
{
|
||||
Domain = domain.Domain,
|
||||
CandidateCount = domain.CandidateCount,
|
||||
VisibleCardCount = domain.VisibleCardCount,
|
||||
TopScore = domain.TopScore,
|
||||
IsCurrentScope = domain.IsCurrentScope,
|
||||
HasVisibleResults = domain.HasVisibleResults
|
||||
}).ToArray()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasSynthesisScope(HttpContext context)
|
||||
{
|
||||
var scopes = ResolveUserScopes(context);
|
||||
@@ -723,6 +863,15 @@ public sealed record UnifiedSearchApiRequest
|
||||
public UnifiedSearchApiAmbientContext? Ambient { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchSuggestionViabilityApiRequest
|
||||
{
|
||||
public IReadOnlyList<string> Queries { get; init; } = [];
|
||||
|
||||
public UnifiedSearchApiFilter? Filters { get; init; }
|
||||
|
||||
public UnifiedSearchApiAmbientContext? Ambient { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchApiAmbientContext
|
||||
{
|
||||
public string? CurrentRoute { get; init; }
|
||||
@@ -802,6 +951,10 @@ public sealed record UnifiedSearchApiResponse
|
||||
|
||||
public IReadOnlyList<UnifiedSearchApiCard> Cards { get; init; } = [];
|
||||
|
||||
public UnifiedSearchApiOverflow? Overflow { get; init; }
|
||||
|
||||
public UnifiedSearchApiCoverage? Coverage { get; init; }
|
||||
|
||||
public UnifiedSearchApiSynthesis? Synthesis { get; init; }
|
||||
|
||||
public IReadOnlyList<UnifiedSearchApiSuggestion>? Suggestions { get; init; }
|
||||
@@ -815,6 +968,30 @@ public sealed record UnifiedSearchApiResponse
|
||||
public IReadOnlyList<UnifiedSearchApiFederationDiagnostic>? Federation { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchSuggestionViabilityApiResponse
|
||||
{
|
||||
public IReadOnlyList<UnifiedSearchSuggestionViabilityApiResult> Suggestions { get; init; } = [];
|
||||
|
||||
public UnifiedSearchApiCoverage? Coverage { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchSuggestionViabilityApiResult
|
||||
{
|
||||
public string Query { get; init; } = string.Empty;
|
||||
|
||||
public bool Viable { get; init; }
|
||||
|
||||
public string Status { get; init; } = string.Empty;
|
||||
|
||||
public string Code { get; init; } = string.Empty;
|
||||
|
||||
public int CardCount { get; init; }
|
||||
|
||||
public string? LeadingDomain { get; init; }
|
||||
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchApiCard
|
||||
{
|
||||
public string EntityKey { get; init; } = string.Empty;
|
||||
@@ -845,6 +1022,39 @@ public sealed record UnifiedSearchApiCard
|
||||
new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchApiOverflow
|
||||
{
|
||||
public string CurrentScopeDomain { get; init; } = string.Empty;
|
||||
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
|
||||
public IReadOnlyList<UnifiedSearchApiCard> Cards { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchApiCoverage
|
||||
{
|
||||
public string? CurrentScopeDomain { get; init; }
|
||||
|
||||
public bool CurrentScopeWeighted { get; init; }
|
||||
|
||||
public IReadOnlyList<UnifiedSearchApiDomainCoverage> Domains { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchApiDomainCoverage
|
||||
{
|
||||
public string Domain { get; init; } = string.Empty;
|
||||
|
||||
public int CandidateCount { get; init; }
|
||||
|
||||
public int VisibleCardCount { get; init; }
|
||||
|
||||
public double TopScore { get; init; }
|
||||
|
||||
public bool IsCurrentScope { get; init; }
|
||||
|
||||
public bool HasVisibleResults { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchApiFacet
|
||||
{
|
||||
public string Domain { get; init; } = "knowledge";
|
||||
@@ -889,6 +1099,10 @@ public sealed record UnifiedSearchApiSuggestion
|
||||
public string Text { get; init; } = string.Empty;
|
||||
|
||||
public string Reason { get; init; } = string.Empty;
|
||||
|
||||
public string? Domain { get; init; }
|
||||
|
||||
public int CandidateCount { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchApiRefinement
|
||||
@@ -896,6 +1110,10 @@ public sealed record UnifiedSearchApiRefinement
|
||||
public string Text { get; init; } = string.Empty;
|
||||
|
||||
public string Source { get; init; } = string.Empty;
|
||||
|
||||
public string? Domain { get; init; }
|
||||
|
||||
public int CandidateCount { get; init; }
|
||||
}
|
||||
|
||||
public sealed record UnifiedSearchApiContextAnswer
|
||||
|
||||
Reference in New Issue
Block a user