From ecf98037e30f36b8d5087b8a8a25150e173c783d Mon Sep 17 00:00:00 2001 From: Yannick Bazan Date: Fri, 16 Oct 2026 19:49:12 -0400 Subject: [PATCH 1/2] feat(chat): refusals show the best score; compact, clearly separated chat panels Co-Authored-By: Claude Sonnet 5.5 --- .../Answering/AnswerEvents.cs | 3 +- .../Answering/AnswerPipeline.cs | 3 +- .../Domain/RequestTrace.cs | 3 + .../Features/GetActivity/GetActivity.cs | 6 +- .../20261016233948_TraceBestScore.Designer.cs | 538 ++++++++++++++++++ .../20261016233948_TraceBestScore.cs | 30 + .../Migrations/ChatDbContextModelSnapshot.cs | 4 + .../Features/SearchPassages/SearchPassages.cs | 6 +- .../Retrieval/DocumentSearch.cs | 2 +- .../Contracts/Documents/SearchResult.cs | 3 +- .../Chat/ActivityPanel.razor | 12 +- .../Chat/AnswerFeedback.razor | 6 +- .../Chat/ConversationList.razor | 9 +- .../Chat/LiveAnswer.cs | 2 +- src/RagAssistant.Web.Client/Chat/Models.cs | 6 +- .../Chat/ScopeBar.razor | 5 +- .../Components/TrashIcon.razor | 5 + src/RagAssistant.Web.Client/Pages/Chat.razor | 2 +- src/RagAssistant.Web/wwwroot/app.css | 64 +++ .../Chat/ActivityTests.cs | 6 + .../Chat/ActivityPanelTests.cs | 14 + 21 files changed, 706 insertions(+), 23 deletions(-) create mode 100644 src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/20261016233948_TraceBestScore.Designer.cs create mode 100644 src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/20261016233948_TraceBestScore.cs create mode 100644 src/RagAssistant.Web.Client/Components/TrashIcon.razor diff --git a/src/Modules/Chat/RagAssistant.Modules.Chat/Answering/AnswerEvents.cs b/src/Modules/Chat/RagAssistant.Modules.Chat/Answering/AnswerEvents.cs index 683b992..ff3c04c 100644 --- a/src/Modules/Chat/RagAssistant.Modules.Chat/Answering/AnswerEvents.cs +++ b/src/Modules/Chat/RagAssistant.Modules.Chat/Answering/AnswerEvents.cs @@ -45,7 +45,8 @@ public sealed record RetrievalCompleted( IReadOnlyList LeftOut, double MinScore, double ConfidentScore, - int? RetrievalMs); + int? RetrievalMs, + double? BestScore = null); public sealed record AnswerDelta(string Text); diff --git a/src/Modules/Chat/RagAssistant.Modules.Chat/Answering/AnswerPipeline.cs b/src/Modules/Chat/RagAssistant.Modules.Chat/Answering/AnswerPipeline.cs index 79cfc9e..3870985 100644 --- a/src/Modules/Chat/RagAssistant.Modules.Chat/Answering/AnswerPipeline.cs +++ b/src/Modules/Chat/RagAssistant.Modules.Chat/Answering/AnswerPipeline.cs @@ -145,7 +145,7 @@ await events.WriteAsync( AnswerEvents.Item( AnswerEvents.RetrievalCompletedType, new AnswerEvents.RetrievalCompleted( - found.Outcome.ToApi(), sources, scope, attempt.UnavailableDocumentIds, found.Thresholds.MinScore, found.Thresholds.ConfidentScore, attempt.RetrievalMs)), + found.Outcome.ToApi(), sources, scope, attempt.UnavailableDocumentIds, found.Thresholds.MinScore, found.Thresholds.ConfidentScore, attempt.RetrievalMs, found.BestScore)), aborted); if (found.Outcome == EvidenceOutcome.NoEvidence) @@ -410,6 +410,7 @@ private RequestTrace Trace(Attempt attempt, AnswerStatus status, RefusalReason? MinScore = found?.Thresholds.MinScore, ConfidentScore = found?.Thresholds.ConfidentScore, DuplicateOverlap = found?.Thresholds.DuplicateOverlap, + BestScore = found?.BestScore, EmbeddingModel = found?.EmbeddingModel, RetrievedPassages = JsonSerializer.Serialize(passages, TraceJson), ChatModel = attempt.ChatModel, diff --git a/src/Modules/Chat/RagAssistant.Modules.Chat/Domain/RequestTrace.cs b/src/Modules/Chat/RagAssistant.Modules.Chat/Domain/RequestTrace.cs index 5e6276e..e1dc262 100644 --- a/src/Modules/Chat/RagAssistant.Modules.Chat/Domain/RequestTrace.cs +++ b/src/Modules/Chat/RagAssistant.Modules.Chat/Domain/RequestTrace.cs @@ -55,6 +55,9 @@ internal sealed class RequestTrace public double? DuplicateOverlap { get; set; } + /// The best score among the candidates before the evidence rules; shows how close a refused question came (null when nothing matched). + public double? BestScore { get; set; } + public string? EmbeddingModel { get; set; } /// JSON array of , stored as jsonb. diff --git a/src/Modules/Chat/RagAssistant.Modules.Chat/Features/GetActivity/GetActivity.cs b/src/Modules/Chat/RagAssistant.Modules.Chat/Features/GetActivity/GetActivity.cs index 881e8bf..1e13099 100644 --- a/src/Modules/Chat/RagAssistant.Modules.Chat/Features/GetActivity/GetActivity.cs +++ b/src/Modules/Chat/RagAssistant.Modules.Chat/Features/GetActivity/GetActivity.cs @@ -28,7 +28,9 @@ public sealed record ScopeStep(string? Language, IReadOnlyList Tags, IRe public sealed record ScopeDocument(Guid Id, string? Title, bool Available); - public sealed record RetrievalStep(string? Outcome, double? MinScore, double? ConfidentScore, int? Ms, IReadOnlyList Passages); + /// The best candidate before the evidence rules; explains a refusal (null when nothing matched, or on older traces). + public sealed record RetrievalStep( + string? Outcome, double? MinScore, double? ConfidentScore, int? Ms, IReadOnlyList Passages, double? BestScore = null); public sealed record PassageStep( int Rank, @@ -87,7 +89,7 @@ orderby t.Attempt descending trace.QuestionText, new RewriteStep(trace.RewriteUsed, trace.RetrievalQuery, trace.RewriteFallbackReason?.ToApi(), trace.RewriteMs), new ScopeStep(trace.FilterLanguage, trace.FilterTags, documents), - new RetrievalStep(trace.EvidenceOutcome is { } outcome ? JsonNamingPolicy.CamelCase.ConvertName(outcome) : null, trace.MinScore, trace.ConfidentScore, trace.RetrievalMs, passages), + new RetrievalStep(trace.EvidenceOutcome is { } outcome ? JsonNamingPolicy.CamelCase.ConvertName(outcome) : null, trace.MinScore, trace.ConfidentScore, trace.RetrievalMs, passages, trace.BestScore), new AnswerStep( trace.ChatModel is not null, trace.FinalStatus.ToApi(), diff --git a/src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/20261016233948_TraceBestScore.Designer.cs b/src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/20261016233948_TraceBestScore.Designer.cs new file mode 100644 index 0000000..d21862a --- /dev/null +++ b/src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/20261016233948_TraceBestScore.Designer.cs @@ -0,0 +1,538 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RagAssistant.Modules.Chat.Persistence; + +#nullable disable + +namespace RagAssistant.Modules.Chat.Persistence.Migrations +{ + [DbContext(typeof(ChatDbContext))] + [Migration("20261016233948_TraceBestScore")] + partial class TraceBestScore + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("chat") + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.CitationSnapshot", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("DocumentId") + .HasColumnType("uuid") + .HasColumnName("document_id"); + + b.Property("DocumentTitle") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("document_title"); + + b.Property("Marker") + .HasColumnType("integer") + .HasColumnName("marker"); + + b.Property("MessageId") + .HasColumnType("uuid") + .HasColumnName("message_id"); + + b.Property("PageEnd") + .HasColumnType("integer") + .HasColumnName("page_end"); + + b.Property("PageStart") + .HasColumnType("integer") + .HasColumnName("page_start"); + + b.Property("PassageId") + .HasColumnType("uuid") + .HasColumnName("passage_id"); + + b.Property("Score") + .HasColumnType("double precision") + .HasColumnName("score"); + + b.PrimitiveCollection("SectionPath") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("section_path"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text") + .HasColumnName("text"); + + b.Property("VersionId") + .HasColumnType("uuid") + .HasColumnName("version_id"); + + b.Property("VersionNumber") + .HasColumnType("integer") + .HasColumnName("version_number"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("citation_snapshots", "chat"); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.Conversation", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("LastActivityAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_activity_at"); + + b.Property("MessageCount") + .HasColumnType("integer") + .HasColumnName("message_count"); + + b.Property("OwnerSub") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_sub"); + + b.PrimitiveCollection("ScopeDocumentIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("scope_document_ids"); + + b.PrimitiveCollection("ScopeTags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scope_tags"); + + b.Property("Title") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.HasKey("Id"); + + b.HasIndex("OwnerSub", "LastActivityAt", "Id") + .IsDescending(false, true, false); + + b.ToTable("conversations", "chat"); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.IssueReport", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("category"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("comment"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("MessageId") + .HasColumnType("uuid") + .HasColumnName("message_id"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("issue_reports", "chat"); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.Message", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Attempts") + .HasColumnType("integer") + .HasColumnName("attempts"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed_at"); + + b.Property("ConversationId") + .HasColumnType("uuid") + .HasColumnName("conversation_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ErrorCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("error_code"); + + b.PrimitiveCollection("FilterDocumentIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("filter_document_ids"); + + b.Property("FilterLanguage") + .HasMaxLength(2) + .HasColumnType("character varying(2)") + .HasColumnName("filter_language"); + + b.PrimitiveCollection("FilterTags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("filter_tags"); + + b.Property("PendingSince") + .HasColumnType("timestamp with time zone") + .HasColumnName("pending_since"); + + b.Property("RefusalReason") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("refusal_reason"); + + b.Property("ReplyToMessageId") + .HasColumnType("uuid") + .HasColumnName("reply_to_message_id"); + + b.Property("RetrievalQuery") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("retrieval_query"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("role"); + + b.Property("Sequence") + .HasColumnType("integer") + .HasColumnName("sequence"); + + b.Property("Status") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("status"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text") + .HasColumnName("text"); + + b.HasKey("Id"); + + b.HasIndex("ConversationId") + .IsUnique() + .HasDatabaseName("ix_messages_one_pending_per_conversation") + .HasFilter("status = 'Pending'"); + + b.HasIndex("Status") + .HasDatabaseName("ix_messages_pending") + .HasFilter("status = 'Pending'"); + + b.HasIndex("ConversationId", "Sequence") + .IsUnique(); + + b.ToTable("messages", "chat"); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.MessageRating", b => + { + b.Property("MessageId") + .HasColumnType("uuid") + .HasColumnName("message_id"); + + b.Property("Rating") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("rating"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("MessageId"); + + b.ToTable("message_ratings", "chat"); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.RequestTrace", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("AssistantMessageId") + .HasColumnType("uuid") + .HasColumnName("assistant_message_id"); + + b.Property("Attempt") + .HasColumnType("integer") + .HasColumnName("attempt"); + + b.Property("BestScore") + .HasColumnType("double precision") + .HasColumnName("best_score"); + + b.Property("ChatModel") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("chat_model"); + + b.Property("ConfidentScore") + .HasColumnType("double precision") + .HasColumnName("confident_score"); + + b.Property("ConversationId") + .HasColumnType("uuid") + .HasColumnName("conversation_id"); + + b.Property("DuplicateOverlap") + .HasColumnType("double precision") + .HasColumnName("duplicate_overlap"); + + b.Property("EmbeddingModel") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("embedding_model"); + + b.Property("ErrorCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("error_code"); + + b.Property("EvidenceOutcome") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("evidence_outcome"); + + b.PrimitiveCollection("FilterDocumentIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("filter_document_ids"); + + b.Property("FilterLanguage") + .HasMaxLength(2) + .HasColumnType("character varying(2)") + .HasColumnName("filter_language"); + + b.PrimitiveCollection("FilterTags") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("filter_tags"); + + b.Property("FinalStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("final_status"); + + b.Property("FirstTokenMs") + .HasColumnType("integer") + .HasColumnName("first_token_ms"); + + b.Property("InputTokens") + .HasColumnType("bigint") + .HasColumnName("input_tokens"); + + b.Property("MinScore") + .HasColumnType("double precision") + .HasColumnName("min_score"); + + b.Property("OtelTraceId") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("otel_trace_id"); + + b.Property("OutputTokens") + .HasColumnType("bigint") + .HasColumnName("output_tokens"); + + b.Property("OwnerSub") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("owner_sub"); + + b.Property("PromptVersion") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("prompt_version"); + + b.Property("QuestionText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("question_text"); + + b.Property("RefusalReason") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("refusal_reason"); + + b.Property("RetrievalMs") + .HasColumnType("integer") + .HasColumnName("retrieval_ms"); + + b.Property("RetrievalQuery") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("retrieval_query"); + + b.Property("RetrievedPassages") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("retrieved_passages"); + + b.Property("RewriteFallbackReason") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("rewrite_fallback_reason"); + + b.Property("RewriteInputTokens") + .HasColumnType("bigint") + .HasColumnName("rewrite_input_tokens"); + + b.Property("RewriteMs") + .HasColumnType("integer") + .HasColumnName("rewrite_ms"); + + b.Property("RewriteOutputTokens") + .HasColumnType("bigint") + .HasColumnName("rewrite_output_tokens"); + + b.Property("RewritePromptVersion") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasColumnName("rewrite_prompt_version"); + + b.Property("RewriteUsed") + .HasColumnType("boolean") + .HasColumnName("rewrite_used"); + + b.Property("SentinelHit") + .HasColumnType("boolean") + .HasColumnName("sentinel_hit"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("started_at"); + + b.Property("StrippedMarkerCount") + .HasColumnType("integer") + .HasColumnName("stripped_marker_count"); + + b.Property("TotalMs") + .HasColumnType("integer") + .HasColumnName("total_ms"); + + b.PrimitiveCollection("UnavailableDocumentIds") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("unavailable_document_ids"); + + b.Property("UserMessageId") + .HasColumnType("uuid") + .HasColumnName("user_message_id"); + + b.HasKey("Id"); + + b.HasIndex("ConversationId"); + + b.HasIndex("StartedAt"); + + b.HasIndex("AssistantMessageId", "Attempt") + .IsUnique(); + + b.ToTable("request_traces", "chat"); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.CitationSnapshot", b => + { + b.HasOne("RagAssistant.Modules.Chat.Domain.Message", null) + .WithMany("Citations") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.IssueReport", b => + { + b.HasOne("RagAssistant.Modules.Chat.Domain.Message", null) + .WithMany() + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.Message", b => + { + b.HasOne("RagAssistant.Modules.Chat.Domain.Conversation", null) + .WithMany("Messages") + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.MessageRating", b => + { + b.HasOne("RagAssistant.Modules.Chat.Domain.Message", null) + .WithOne() + .HasForeignKey("RagAssistant.Modules.Chat.Domain.MessageRating", "MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.RequestTrace", b => + { + b.HasOne("RagAssistant.Modules.Chat.Domain.Conversation", null) + .WithMany() + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.Conversation", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("RagAssistant.Modules.Chat.Domain.Message", b => + { + b.Navigation("Citations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/20261016233948_TraceBestScore.cs b/src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/20261016233948_TraceBestScore.cs new file mode 100644 index 0000000..89ed32d --- /dev/null +++ b/src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/20261016233948_TraceBestScore.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RagAssistant.Modules.Chat.Persistence.Migrations +{ + /// + public partial class TraceBestScore : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "best_score", + schema: "chat", + table: "request_traces", + type: "double precision", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "best_score", + schema: "chat", + table: "request_traces"); + } + } +} diff --git a/src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/ChatDbContextModelSnapshot.cs b/src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/ChatDbContextModelSnapshot.cs index b018abd..a337a94 100644 --- a/src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/ChatDbContextModelSnapshot.cs +++ b/src/Modules/Chat/RagAssistant.Modules.Chat/Persistence/Migrations/ChatDbContextModelSnapshot.cs @@ -299,6 +299,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("attempt"); + b.Property("BestScore") + .HasColumnType("double precision") + .HasColumnName("best_score"); + b.Property("ChatModel") .HasMaxLength(200) .HasColumnType("character varying(200)") diff --git a/src/Modules/Documents/RagAssistant.Modules.Documents/Features/SearchPassages/SearchPassages.cs b/src/Modules/Documents/RagAssistant.Modules.Documents/Features/SearchPassages/SearchPassages.cs index 84b61e6..03ef068 100644 --- a/src/Modules/Documents/RagAssistant.Modules.Documents/Features/SearchPassages/SearchPassages.cs +++ b/src/Modules/Documents/RagAssistant.Modules.Documents/Features/SearchPassages/SearchPassages.cs @@ -32,7 +32,8 @@ public sealed record SearchRun( IReadOnlyList Discarded, EvidenceThresholds Thresholds, string EmbeddingModel, - int? SkippedForModelMismatch); + int? SkippedForModelMismatch, + double? BestScore = null); public sealed class Validator : AbstractValidator { @@ -115,7 +116,8 @@ public async ValueTask> Handle(Query query, CancellationToken metrics.SearchCompleted(outcome, clock.GetElapsedTime(started)); LogCompleted(logger, outcome, pool.Count, decision.Kept.Count); - return new SearchRun(text, topK, decision.Outcome, decision.Kept, decision.Discarded, thresholds, model, skipped); + double? best = pool.Count > 0 ? pool.Max(c => c.Score) : null; + return new SearchRun(text, topK, decision.Outcome, decision.Kept, decision.Discarded, thresholds, model, skipped, best); } [LoggerMessage(Level = LogLevel.Debug, Message = "Searching passages for \"{Query}\".")] diff --git a/src/Modules/Documents/RagAssistant.Modules.Documents/Retrieval/DocumentSearch.cs b/src/Modules/Documents/RagAssistant.Modules.Documents/Retrieval/DocumentSearch.cs index 04b14b7..aad604b 100644 --- a/src/Modules/Documents/RagAssistant.Modules.Documents/Retrieval/DocumentSearch.cs +++ b/src/Modules/Documents/RagAssistant.Modules.Documents/Retrieval/DocumentSearch.cs @@ -27,6 +27,6 @@ public async Task> SearchAsync( } var value = run.Value; - return new SearchResult(value.Outcome, value.Kept, value.Thresholds, value.EmbeddingModel); + return new SearchResult(value.Outcome, value.Kept, value.Thresholds, value.EmbeddingModel, value.BestScore); } } diff --git a/src/RagAssistant.SharedKernel/Contracts/Documents/SearchResult.cs b/src/RagAssistant.SharedKernel/Contracts/Documents/SearchResult.cs index c31a234..9712887 100644 --- a/src/RagAssistant.SharedKernel/Contracts/Documents/SearchResult.cs +++ b/src/RagAssistant.SharedKernel/Contracts/Documents/SearchResult.cs @@ -27,7 +27,8 @@ public sealed record SearchResult( EvidenceOutcome Outcome, IReadOnlyList Chunks, EvidenceThresholds Thresholds, - string EmbeddingModel); + string EmbeddingModel, + double? BestScore = null); /// The full passage text. /// Cosine similarity to the query (1 − cosine distance); higher is better. diff --git a/src/RagAssistant.Web.Client/Chat/ActivityPanel.razor b/src/RagAssistant.Web.Client/Chat/ActivityPanel.razor index 6aae39b..315f961 100644 --- a/src/RagAssistant.Web.Client/Chat/ActivityPanel.razor +++ b/src/RagAssistant.Web.Client/Chat/ActivityPanel.razor @@ -3,8 +3,8 @@ @* What the pipeline did for one answer (spec 015). Live while streaming, from the stored trace afterwards; recorded facts only. *@
- Activity + @(Open ? "▾" : "▸") Activity @if (Open) {
@@ -183,6 +183,14 @@ return "Searching…"; } + if (retrieval.Outcome == "noEvidence" && retrieval.MinScore is { } minimum) + { + var closest = retrieval.BestScore is { } best + ? $"the closest passage scored {Score(best)}" + : "no indexed passage matched"; + return $"No evidence: nothing reached the minimum score of {Score(minimum)}; {closest}{Ms(retrieval.Ms)}."; + } + var outcome = retrieval.Outcome switch { "sufficient" => "Enough evidence", diff --git a/src/RagAssistant.Web.Client/Chat/AnswerFeedback.razor b/src/RagAssistant.Web.Client/Chat/AnswerFeedback.razor index dca7a41..625da32 100644 --- a/src/RagAssistant.Web.Client/Chat/AnswerFeedback.razor +++ b/src/RagAssistant.Web.Client/Chat/AnswerFeedback.razor @@ -4,11 +4,11 @@ @if (Eligible) {
- Helpful - Not helpful - Report issue + Report issue @if (_reported) { diff --git a/src/RagAssistant.Web.Client/Chat/ConversationList.razor b/src/RagAssistant.Web.Client/Chat/ConversationList.razor index 12f13e8..b3742a5 100644 --- a/src/RagAssistant.Web.Client/Chat/ConversationList.razor +++ b/src/RagAssistant.Web.Client/Chat/ConversationList.razor @@ -1,7 +1,8 @@ @inject TimeProvider Clock