SendKmsByAppAsync Execution Flow: Tracing Knowledge Base Q&A in AntSK

SendKmsByAppAsync is the central async streaming method that orchestrates document retrieval, semantic search, optional reranking, and LLM invocation to power real-time knowledge base Q&A interactions in the AntSK framework.

The SendKmsByAppAsync method serves as the primary pipeline for knowledge-base question-answering within the AntSK project. Located in ChatService.cs, this IAsyncEnumerable<StreamingKernelContent> implementation bridges user queries from both web UI and API clients with semantic memory retrieval and large language model generation. Tracing its execution reveals a sophisticated flow that handles dynamic document ingestion, configurable reranking, and streaming response generation.

Service Entry Points

SendKmsByAppAsync accepts an application configuration, user question, chat history, and optional file path to initiate the Q&A pipeline. The method is invoked from two primary locations depending on the client interface.

Web UI Invocation via the Blazor component:

// ChatView.razor.cs – line 296
var chatResult = _chatService.SendKmsByAppAsync(app, questions, history, filePath, _relevantSources);

HTTP API Invocation for Server-Sent Events streaming:

// OpenApiService.cs – line 73
var chatResult = _chatService.SendKmsByAppAsync(app, questions, history, "");

Both callers receive an async enumerable stream of StreamingKernelContent chunks that yield partial responses until the LLM completes generation.

Kernel Selection and Initialization

Upon entry, the method immediately retrieves or constructs the appropriate Kernel instance for the specified application:

// ChatService.cs – line 95-96
var _kernel = _kernelService.GetKernelByApp(app);

The KernelService configures this instance based on the app's specific model settings, temperature parameters, and prompt templates. This kernel will later execute the semantic functions and manage the LLM communication.

Document Upload Path

When the caller provides a filePath parameter, SendKmsByAppAsync executes the file-based Q&A branch (ChatService.cs lines 98-133). This path handles dynamic document ingestion without requiring prior manual indexing.

The system performs these operations:

  • Extracts a GUID from the filename using regex pattern \b[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\b
  • Checks indexing status via memory.IsDocumentReadyAsync(fileId, index: KmsConstantcs.KmsIndex)
  • Imports new documents using memory.ImportDocumentAsync() with the file-specific FileIndex and metadata tags (AppIdTag, FileIdTag)
  • Searches the file index with filtered memory queries restricted to the specific fileId

Relevant chunks are converted to RelevantSource objects containing SourceName, Text (converted from Markdown to HTML), and Relevance scores.

Knowledge Base Retrieval Path

When no file path is provided, the method falls back to the standard knowledge base search (ChatService.cs lines 136-138):

else
{
    // 从知识库问答
    relevantSourceList = await _kMService.GetRelevantSourceList(app, questions);
}

The KMService queries the KMS memory index using the application's configured tags, returning candidate chunks that match the semantic meaning of the user's question.

Optional Reranking and Relevance Filtering

If the application configuration specifies a rerank model ID, the method executes a secondary scoring pass to improve result quality (ChatService.cs lines 144-158):

if (!string.IsNullOrEmpty(app.RerankModelID))
{
    var rerankModel = _aIModels_Repositories.GetById(app.RerankModelID);
    BegRerankConfig.LoadModel(rerankModel.EndPoint, rerankModel.ModelName);
    foreach (var item in relevantSourceList)
    {
        List<string> rerank = new List<string> { questions, item.Text };
        item.RerankScore = BegRerankConfig.Rerank(rerank);
    }
    relevantSourceList = relevantSourceList
        .OrderByDescending(p => p.RerankScore)
        .Take(app.MaxMatchesCount)
        .ToList();
}

Following reranking (or immediately after retrieval if no model is configured), the system filters sources against the application's relevance threshold (ChatService.cs lines 160-176). The threshold comparison uses app.Relevance / 100 to normalize the percentage value:

  • With reranking: checks item.RerankScore >= threshold
  • Without reranking: checks item.Relevance >= threshold

Passing sources are concatenated into a dataMsg markdown string that serves as the grounded context for the LLM prompt.

Prompt Construction and Streaming Invocation

When at least one source passes the relevance filter, SendKmsByAppAsync constructs the final prompt and invokes the kernel streaming interface (ChatService.cs lines 186-215):

var temperature = app.Temperature / 100;
OpenAIPromptExecutionSettings settings = new() { Temperature = temperature };
var func = _kernel.CreateFunctionFromPrompt(app.Prompt, settings);

var chatResult = _kernel.InvokeStreamingAsync(
    function: func,
    arguments: new KernelArguments()
    {
        ["doc"] = dataMsg.ToString(),
        ["history"] = string.Join("\n", history.Select(x => x.Role + ": " + x.Content)),
        ["input"] = questions
    });

await foreach (var content in chatResult)
{
    yield return content;
}

The temperature normalization converts the UI's 0-100 scale to the LLM's expected 0.0-1.0 range. The KernelArguments inject the concatenated document context (doc), formatted chat history (history), and original user input (input) into the prompt template.

If no sources meet the relevance threshold, the method yields a fallback message:

// ChatService.cs – line 221-224
else
{
    yield return new StreamingTextContent(KmsConstantcs.KmsSearchNull);
}

Consuming the Stream

Server-Sent Events (SSE)

The OpenApiService consumes the async enumerable and transmits chunks as SSE payloads:

// OpenApiService.cs – line 73-81
var chatResult = _chatService.SendKmsByAppAsync(app, questions, history, "");
int i = 0;
await foreach (var content in chatResult)
{
    result.choices[0].delta.content = content.ConvertToString();
    string message = $"data: {JsonConvert.SerializeObject(result)}\n\n";
    await HttpContext.Response.WriteAsync(message, Encoding.UTF8);
    await HttpContext.Response.Body.FlushAsync();
    await Task.Delay(TimeSpan.FromMilliseconds(50));
}
await HttpContext.Response.WriteAsync("data: [DONE]");

UI Rendering

The Blazor ChatView component follows a similar pattern, iterating the IAsyncEnumerable and appending each chunk to the rendered answer buffer while triggering StateHasChanged() for real-time UI updates.

Summary

  • SendKmsByAppAsync in ChatService.cs is the central async streaming pipeline for knowledge base Q&A in AntSK
  • The method supports dual entry points: web UI components and HTTP API endpoints
  • Execution branches between file-based ingestion (dynamic import) and static knowledge base retrieval based on the filePath parameter
  • Optional reranking via BegRerankConfig improves result relevance when configured
  • Strict filtering by relevance scores ensures only quality context reaches the LLM
  • Streaming invocation through InvokeStreamingAsync enables real-time token-by-token response delivery
  • Graceful degradation returns a "no answer" constant when no relevant sources are found

Frequently Asked Questions

What triggers the file upload branch in SendKmsByAppAsync?

The file upload branch activates when the filePath parameter contains a non-empty string. The method extracts a GUID from the filename to check if the document is already indexed via IsDocumentReadyAsync. If not indexed, it imports the document into the FileIndex with appropriate metadata tags before searching that specific index.

How does the reranking mechanism improve search results?

When app.RerankModelID is configured, the method loads a dedicated rerank model via BegRerankConfig.LoadModel() and scores each candidate source against the original question. Sources are then reordered by their RerankScore and truncated to app.MaxMatchesCount. This secondary scoring pass typically improves precision over initial vector similarity by using cross-encoder models trained specifically for relevance ranking.

Why does the method use IAsyncEnumerable instead of returning a complete string?

The IAsyncEnumerable<StreamingKernelContent> return type enables Server-Sent Events (SSE) and real-time UI updates. As the LLM generates tokens, each chunk yields immediately to the caller, allowing users to see partial responses before generation completes. This approach minimizes perceived latency and improves user experience for long-form answers.

Where is the fallback "no answer" message defined?

When no sources pass the relevance threshold check (isSearch == false), the method yields KmsConstantcs.KmsSearchNull as a StreamingTextContent. This constant provides a standardized response indicating that the knowledge base contains no relevant information for the given query, preventing hallucinated answers from the LLM.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →