search and ai stabilization work, localization stablized.

This commit is contained in:
master
2026-02-24 23:29:36 +02:00
parent 4f947a8b61
commit b07d27772e
766 changed files with 55299 additions and 3221 deletions

View File

@@ -33,7 +33,10 @@ internal static class KnowledgeSearchCommandGroup
{
"docs",
"api",
"doctor"
"doctor",
"findings",
"vex",
"policy"
};
internal static Command BuildSearchCommand(
@@ -329,6 +332,32 @@ internal static class KnowledgeSearchCommandGroup
};
var backend = services.GetRequiredService<IBackendOperationsClient>();
// Try unified search endpoint first (covers all domains)
var unifiedResult = await TryUnifiedSearchAsync(
backend, normalizedQuery, normalizedTypes, normalizedTags,
product, version, service, boundedTopK, verbose,
cancellationToken).ConfigureAwait(false);
if (unifiedResult is not null)
{
if (emitJson)
{
WriteJson(ToUnifiedJsonPayload(unifiedResult));
return;
}
if (suggestMode)
{
RenderUnifiedSuggestionOutput(unifiedResult, verbose);
return;
}
RenderUnifiedSearchOutput(unifiedResult, verbose);
return;
}
// Fallback to legacy knowledge search
AdvisoryKnowledgeSearchResponseModel response;
try
{
@@ -1281,4 +1310,194 @@ internal static class KnowledgeSearchCommandGroup
{
Console.WriteLine(JsonSerializer.Serialize(payload, JsonOutputOptions));
}
private static async Task<UnifiedSearchResponseModel?> TryUnifiedSearchAsync(
IBackendOperationsClient backend,
string query,
IReadOnlyList<string> types,
IReadOnlyList<string> tags,
string? product,
string? version,
string? service,
int? topK,
bool verbose,
CancellationToken cancellationToken)
{
var domains = MapTypesToDomains(types);
var request = new UnifiedSearchRequestModel
{
Q = query,
K = topK,
Filters = new UnifiedSearchFilterModel
{
Domains = domains.Count > 0 ? domains : null,
Product = product,
Version = version,
Service = service,
Tags = tags.Count > 0 ? tags : null
},
IncludeSynthesis = true,
IncludeDebug = verbose
};
return await backend.SearchUnifiedAsync(request, cancellationToken).ConfigureAwait(false);
}
private static IReadOnlyList<string> MapTypesToDomains(IReadOnlyList<string> types)
{
if (types.Count == 0) return [];
var domains = new HashSet<string>(StringComparer.Ordinal);
foreach (var type in types)
{
switch (type)
{
case "docs":
case "api":
case "doctor":
domains.Add("knowledge");
break;
case "findings":
domains.Add("findings");
break;
case "vex":
domains.Add("vex");
break;
case "policy":
domains.Add("policy");
break;
}
}
return domains.ToArray();
}
private static void RenderUnifiedSearchOutput(UnifiedSearchResponseModel response, bool verbose)
{
Console.WriteLine($"Query: {response.Query}");
Console.WriteLine($"Results: {response.Cards.Count.ToString(CultureInfo.InvariantCulture)} cards / topK {response.TopK.ToString(CultureInfo.InvariantCulture)}");
Console.WriteLine($"Mode: {response.Diagnostics.Mode} (fts={response.Diagnostics.FtsMatches.ToString(CultureInfo.InvariantCulture)}, vector={response.Diagnostics.VectorMatches.ToString(CultureInfo.InvariantCulture)}, duration={response.Diagnostics.DurationMs.ToString(CultureInfo.InvariantCulture)}ms)");
Console.WriteLine();
if (response.Synthesis is not null)
{
Console.WriteLine($"Summary ({response.Synthesis.Confidence} confidence):");
Console.WriteLine($" {response.Synthesis.Summary}");
Console.WriteLine();
}
if (response.Cards.Count == 0)
{
Console.WriteLine("No results found.");
return;
}
for (var index = 0; index < response.Cards.Count; index++)
{
var card = response.Cards[index];
var severity = string.IsNullOrWhiteSpace(card.Severity)
? string.Empty
: $" severity={card.Severity}";
Console.WriteLine($"[{(index + 1).ToString(CultureInfo.InvariantCulture)}] {card.Domain.ToUpperInvariant()}/{card.EntityType.ToUpperInvariant()} score={card.Score.ToString("F6", CultureInfo.InvariantCulture)}{severity}");
Console.WriteLine($" {card.Title}");
var snippet = CollapseWhitespace(card.Snippet);
if (!string.IsNullOrWhiteSpace(snippet))
{
Console.WriteLine($" {snippet}");
}
foreach (var action in card.Actions)
{
var actionDetail = action.IsPrimary ? " [primary]" : "";
if (!string.IsNullOrWhiteSpace(action.Route))
{
Console.WriteLine($" -> {action.Label}: {action.Route}{actionDetail}");
}
else if (!string.IsNullOrWhiteSpace(action.Command))
{
Console.WriteLine($" -> {action.Label}: {action.Command}{actionDetail}");
}
}
Console.WriteLine();
}
}
private static void RenderUnifiedSuggestionOutput(UnifiedSearchResponseModel response, bool verbose)
{
Console.WriteLine($"Symptom: {response.Query}");
Console.WriteLine($"Mode: {response.Diagnostics.Mode} (duration={response.Diagnostics.DurationMs.ToString(CultureInfo.InvariantCulture)}ms)");
Console.WriteLine();
if (response.Synthesis is not null)
{
Console.WriteLine($"Analysis ({response.Synthesis.Confidence}):");
Console.WriteLine($" {response.Synthesis.Summary}");
Console.WriteLine();
}
var byDomain = response.Cards
.GroupBy(static c => c.Domain, StringComparer.Ordinal)
.OrderBy(static g => g.Key, StringComparer.Ordinal);
foreach (var group in byDomain)
{
Console.WriteLine($"{group.Key.ToUpperInvariant()} results:");
var items = group.ToArray();
for (var i = 0; i < items.Length; i++)
{
var card = items[i];
Console.WriteLine($" {(i + 1).ToString(CultureInfo.InvariantCulture)}. {card.Title} (score={card.Score.ToString("F6", CultureInfo.InvariantCulture)})");
var snippet = CollapseWhitespace(card.Snippet);
if (!string.IsNullOrWhiteSpace(snippet))
{
Console.WriteLine($" {snippet}");
}
}
Console.WriteLine();
}
}
private static object ToUnifiedJsonPayload(UnifiedSearchResponseModel response)
{
return new
{
query = response.Query,
topK = response.TopK,
diagnostics = new
{
ftsMatches = response.Diagnostics.FtsMatches,
vectorMatches = response.Diagnostics.VectorMatches,
entityCardCount = response.Diagnostics.EntityCardCount,
durationMs = response.Diagnostics.DurationMs,
usedVector = response.Diagnostics.UsedVector,
mode = response.Diagnostics.Mode
},
synthesis = response.Synthesis is null ? null : new
{
summary = response.Synthesis.Summary,
template = response.Synthesis.Template,
confidence = response.Synthesis.Confidence,
sourceCount = response.Synthesis.SourceCount,
domainsCovered = response.Synthesis.DomainsCovered
},
cards = response.Cards.Select(static card => new
{
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
{
label = action.Label,
actionType = action.ActionType,
route = action.Route,
command = action.Command,
isPrimary = action.IsPrimary
}).ToArray(),
sources = card.Sources
}).ToArray()
};
}
}