feat: parallel async processing and compact output mode
Major performance improvements: - Parallel search execution across all queries - Parallel article fetching with 10 concurrent limit - Parallel embeddings with rate limiting (4 concurrent) - Polly integration for retry resilience New features: - Add -v/--verbose flag for detailed output - Compact single-line status mode with braille spinner - StatusReporter service for unified output handling - Query generation and errors hidden in compact mode - ANSI escape codes for clean line updates New files: - Services/RateLimiter.cs - Semaphore-based concurrency control - Services/StatusReporter.cs - Verbose/compact output handler - Models/ParallelOptions.cs - Parallel processing configuration All changes maintain Native AOT compatibility.
This commit is contained in:
119
OpenQuery.cs
119
OpenQuery.cs
@@ -12,7 +12,6 @@ public class OpenQueryApp
|
||||
private readonly OpenRouterClient _client;
|
||||
private readonly SearchTool _searchTool;
|
||||
private readonly string _model;
|
||||
private static readonly char[] Function = ['|', '/', '-', '\\'];
|
||||
|
||||
public OpenQueryApp(
|
||||
OpenRouterClient client,
|
||||
@@ -26,12 +25,22 @@ public class OpenQueryApp
|
||||
|
||||
public async Task RunAsync(OpenQueryOptions options)
|
||||
{
|
||||
using var reporter = new StatusReporter(options.Verbose);
|
||||
reporter.StartSpinner();
|
||||
|
||||
var queries = new List<string> { options.Question };
|
||||
|
||||
if (options.Queries > 1)
|
||||
{
|
||||
Console.WriteLine($"[Generating {options.Queries} search queries based on your question...]");
|
||||
|
||||
if (options.Verbose)
|
||||
{
|
||||
reporter.WriteLine($"[Generating {options.Queries} search queries based on your question...]");
|
||||
}
|
||||
else
|
||||
{
|
||||
reporter.UpdateStatus("Generating search queries...");
|
||||
}
|
||||
|
||||
var queryGenMessages = new List<Message>
|
||||
{
|
||||
new Message("system", """
|
||||
@@ -63,23 +72,67 @@ public class OpenQueryApp
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
content = Regex.Replace(content, @"```json\s*|\s*```", "").Trim();
|
||||
|
||||
|
||||
var generatedQueries = JsonSerializer.Deserialize(content, AppJsonContext.Default.ListString);
|
||||
if (generatedQueries != null && generatedQueries.Count > 0)
|
||||
{
|
||||
queries = generatedQueries;
|
||||
Console.WriteLine($"[Generated queries: {string.Join(", ", queries)}]");
|
||||
if (options.Verbose)
|
||||
{
|
||||
reporter.WriteLine($"[Generated queries: {string.Join(", ", queries)}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Failed to generate queries, falling back to original question. Error: {ex.Message}]");
|
||||
if (options.Verbose)
|
||||
{
|
||||
reporter.WriteLine($"[Failed to generate queries, falling back to original question. Error: {ex.Message}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var searchResult = await _searchTool.ExecuteAsync(options.Question, queries, options.Results, options.Chunks, msg => Console.WriteLine(msg));
|
||||
Console.WriteLine();
|
||||
reporter.UpdateStatus("Searching web...");
|
||||
var searchResult = await _searchTool.ExecuteAsync(
|
||||
options.Question,
|
||||
queries,
|
||||
options.Results,
|
||||
options.Chunks,
|
||||
(progress) => {
|
||||
if (options.Verbose)
|
||||
{
|
||||
reporter.WriteLine(progress);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse progress messages for compact mode
|
||||
if (progress.StartsWith("[Fetching article") && progress.Contains("/"))
|
||||
{
|
||||
// Extract "X/Y" from "[Fetching article X/Y: domain]"
|
||||
var match = Regex.Match(progress, @"\[(\d+)/(\d+)");
|
||||
if (match.Success)
|
||||
{
|
||||
reporter.UpdateStatus($"Fetching articles {match.Groups[1].Value}/{match.Groups[2].Value}...");
|
||||
}
|
||||
}
|
||||
else if (progress.Contains("embeddings"))
|
||||
{
|
||||
reporter.UpdateStatus("Processing embeddings...");
|
||||
}
|
||||
}
|
||||
},
|
||||
options.Verbose);
|
||||
|
||||
if (!options.Verbose)
|
||||
{
|
||||
reporter.UpdateStatus("Asking AI...");
|
||||
}
|
||||
else
|
||||
{
|
||||
reporter.ClearStatus();
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
var systemPrompt = "You are a helpful AI assistant. Answer the user's question in depth, based on the provided context. Be precise and accurate. You can mention sources or citations.";
|
||||
if (options.Short)
|
||||
@@ -94,46 +147,27 @@ public class OpenQueryApp
|
||||
};
|
||||
|
||||
var requestStream = new ChatCompletionRequest(_model, messages);
|
||||
|
||||
|
||||
var assistantResponse = new StringBuilder();
|
||||
var isFirstChunk = true;
|
||||
|
||||
Console.Write("[Sending request to AI model...] ");
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
var spinnerTask = Task.Run(async () =>
|
||||
{
|
||||
var spinner = Function;
|
||||
var index = 0;
|
||||
while (cts is { Token.IsCancellationRequested: false })
|
||||
{
|
||||
if (Console.CursorLeft > 0)
|
||||
{
|
||||
Console.Write(spinner[index++ % spinner.Length]);
|
||||
Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
|
||||
}
|
||||
try
|
||||
{
|
||||
await Task.Delay(100, cts.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, cts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var chunk in _client.StreamAsync(requestStream, cts.Token))
|
||||
using var streamCts = new CancellationTokenSource();
|
||||
await foreach (var chunk in _client.StreamAsync(requestStream, streamCts.Token))
|
||||
{
|
||||
if (chunk.TextDelta == null) continue;
|
||||
if (isFirstChunk)
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
await spinnerTask;
|
||||
Console.WriteLine();
|
||||
Console.Write("Assistant: ");
|
||||
reporter.StopSpinner();
|
||||
if (!options.Verbose)
|
||||
{
|
||||
reporter.ClearStatus();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Write("Assistant: ");
|
||||
}
|
||||
isFirstChunk = false;
|
||||
}
|
||||
Console.Write(chunk.TextDelta);
|
||||
@@ -142,12 +176,9 @@ public class OpenQueryApp
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!cts.IsCancellationRequested)
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
}
|
||||
reporter.StopSpinner();
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user