- Add user-friendly README.md with quick start guide - Create docs/ folder with structured technical documentation: - installation.md: Build and setup instructions - configuration.md: Complete config reference - usage.md: CLI usage guide with examples - architecture.md: System design and patterns - components/: Deep dive into each component (OpenQueryApp, SearchTool, Services, Models) - api/: CLI reference, environment variables, programmatic API - troubleshooting.md: Common issues and solutions - performance.md: Latency, throughput, and optimization - All documentation fully cross-referenced with internal links - Covers project overview, architecture, components, APIs, and support See individual files for complete documentation.
13 KiB
Programmatic API Reference
How to use OpenQuery components programmatically in your own C# code.
📋 Table of Contents
- Overview
- Using OpenQueryApp Programmatically
- Using Individual Services
- Custom Implementations
- Thread Safety
- Error Handling
Overview
OpenQuery is designed as a library of composable services, not just a CLI tool. You can reference the project (or extract the core classes) and use them in your own applications.
Core Interfaces
Currently, OpenQuery uses concrete classes rather than interfaces. To use programmatically:
- Reference the
OpenQueryproject/dll - Add
using OpenQuery.Services;andusing OpenQuery.Tools; - Instantiate dependencies
- Call methods
Dependency Chain
Your Code
├── OpenRouterClient (LLM API)
├── SearxngClient (Search API)
├── EmbeddingService (requires OpenRouterClient)
└── SearchTool (requires SearxngClient + EmbeddingService)
└── (internally uses ArticleService, ChunkingService, RateLimiter)
Using OpenQueryApp Programmatically
Minimal Example
using OpenQuery;
using OpenQuery.Services;
using OpenQuery.Tools;
using OpenQuery.Models;
// 1. Configure
string apiKey = Environment.GetEnvironmentVariable("OPENROUTER_API_KEY")
?? throw new InvalidOperationException("API key required");
string searxngUrl = Environment.GetEnvironmentVariable("SEARXNG_URL")
?? "http://localhost:8002";
string model = Environment.GetEnvironmentVariable("OPENROUTER_MODEL")
?? "qwen/qwen3.5-flash-02-23";
// 2. Instantiate services
var openRouterClient = new OpenRouterClient(apiKey);
var searxngClient = new SearxngClient(searxngUrl);
var embeddingService = new EmbeddingService(openRouterClient);
var searchTool = new SearchTool(searxngClient, embeddingService);
var openQuery = new OpenQueryApp(openRouterClient, searchTool, model);
// 3. Execute
var options = new OpenQueryOptions(
Chunks: 3,
Results: 5,
Queries: 3,
Short: false,
Long: false,
Verbose: false,
Question: "What is quantum entanglement?"
);
await openQuery.RunAsync(options);
Output: Streams answer to Console.Out (hardcoded in OpenQueryApp). To capture output, modify OpenQueryApp or redirect console.
Capturing Output
OpenQueryApp.RunAsync writes directly to Console. To capture:
Option 1: Redirect Console (hacky)
var sw = new StringWriter();
Console.SetOut(sw);
await openQuery.RunAsync(options);
string answer = sw.ToString();
Option 2: Modify OpenQueryApp to accept TextWriter (not currently supported)
Option 3: Reimplement using OpenQuery components without OpenQueryApp
public async Task<string> GetAnswerAsync(string question, OpenQueryOptions options)
{
var sb = new StringBuilder();
var reporter = new StatusReporter(options.Verbose);
// Replicate OpenQueryApp.RunAsync but collect output
// ... (copy logic from OpenQuery.cs)
return sb.ToString();
}
Using Individual Services
OpenRouterClient
var client = new OpenRouterClient("your-api-key");
// Non-streaming chat completion
var request = new ChatCompletionRequest(
model: "qwen/qwen3.5-flash-02-23",
messages: new List<Message>
{
new Message("system", "You are a helpful assistant."),
new Message("user", "What is 2+2?")
}
);
var response = await client.CompleteAsync(request);
Console.WriteLine(response.Choices[0].Message.Content);
// Streaming chat completion
var streamRequest = request with { Stream = true };
await foreach (var chunk in client.StreamAsync(streamRequest))
{
if (chunk.TextDelta != null)
Console.Write(chunk.TextDelta);
}
// Embeddings
var embeddingRequest = new EmbeddingRequest(
model: "openai/text-embedding-3-small",
input: new List<string> { "text 1", "text 2" }
);
float[][] embeddings = await client.EmbedAsync(embeddingRequest.Model, embeddingRequest.Input);
// embeddings[0] is vector for "text 1"
SearxngClient
var searxng = new SearxngClient("http://localhost:8002");
List<SearxngResult> results = await searxng.SearchAsync("quantum physics", limit: 5);
foreach (var result in results)
{
Console.WriteLine($"{result.Title}");
Console.WriteLine($"{result.Url}");
Console.WriteLine($"{result.Content}");
Console.WriteLine();
}
EmbeddingService
var client = new OpenRouterClient("your-api-key");
var embeddingService = new EmbeddingService(client); // default model: openai/text-embedding-3-small
// Single embedding
float[] embedding = await embeddingService.GetEmbeddingAsync("Hello world");
// Batch embeddings (with progress)
List<string> texts = new() { "text 1", "text 2", "text 3" };
float[][] embeddings = await embeddingService.GetEmbeddingsAsync(
texts,
onProgress: msg => Console.WriteLine(msg)
);
// Cosine similarity
float similarity = EmbeddingService.CosineSimilarity(embedding1, embedding2);
ArticleService
var article = await ArticleService.FetchArticleAsync("https://example.com/article");
Console.WriteLine(article.Title);
Console.WriteLine(article.TextContent);
Console.WriteLine($"Readable: {article.IsReadable}");
Note: Article type comes from SmartReader library (not OpenQuery-specific).
ChunkingService
List<string> chunks = ChunkingService.ChunkText("Long article text...");
foreach (var chunk in chunks)
{
Console.WriteLine($"Chunk ({chunk.Length} chars): {chunk.Substring(0, 50)}...");
}
SearchTool (Orchestration)
var searxngClient = new SearxngClient("http://localhost:8002");
var embeddingService = new EmbeddingService(openRouterClient);
var searchTool = new SearchTool(searxngClient, embeddingService);
string context = await searchTool.ExecuteAsync(
originalQuery: "What is quantum entanglement?",
generatedQueries: new List<string>
{
"quantum entanglement definition",
"how quantum entanglement works"
},
maxResults: 5,
topChunksLimit: 3,
onProgress: msg => Console.WriteLine(msg),
verbose: true
);
Console.WriteLine("Context:");
Console.WriteLine(context);
Output is a formatted string:
[Source 1: Title](https://example.com/1)
Content chunk...
[Source 2: Title](https://example.com/2)
Content chunk...
Custom Implementations
Custom Progress Reporter
SearchTool.ExecuteAsync accepts Action<string>? onProgress. Provide your own:
public class MyProgressReporter
{
public void Report(string message)
{
// Log to file
File.AppendAllText("log.txt", $"{DateTime.UtcNow}: {message}\n");
// Update UI
myLabel.Text = message;
// Send to telemetry
Telemetry.TrackEvent("OpenQueryProgress", new { message });
}
}
// Usage
var reporter = new MyProgressReporter();
await searchTool.ExecuteAsync(..., reporter.Report, verbose: false);
Custom Chunking Strategy
Extend ChunkingService or implement your own:
public static class MyChunkingService
{
public static List<string> ChunkText(string text, int maxSize = 500, int overlap = 50)
{
// Overlapping chunks for better context retrieval
var chunks = new List<string>();
int start = 0;
while (start < text.Length)
{
int end = Math.Min(start + maxSize, text.Length);
var chunk = text.Substring(start, end - start);
chunks.Add(chunk);
start += maxSize - overlap; // Slide window
}
return chunks;
}
}
Custom Rate Limiter
Implement IAsyncDisposable with your own strategy (token bucket, leaky bucket):
public class TokenBucketRateLimiter : IAsyncDisposable
{
private readonly SemaphoreSlim _semaphore;
private readonly TimeSpan _refillPeriod;
private int _tokens;
private readonly int _maxTokens;
// Implementation details...
public async Task<T> ExecuteAsync<T>(Func<Task<T>> action, CancellationToken ct)
{
await WaitForTokenAsync(ct);
try
{
return await action();
}
finally
{
// Return tokens or replenish bucket
}
}
}
Thread Safety
Thread-Safe Components:
RateLimiter-SemaphoreSlimis thread-safeStatusReporter- Channel is thread-safe- Static utility classes (
ChunkingService) - no state
Not Thread-Safe (instances should not be shared across threads):
OpenRouterClient- wrapsHttpClient(which is thread-safe but instance may have state)SearxngClient-HttpClient(thread-safe but reuse recommendations apply)EmbeddingService- has mutable fields (_rateLimiter,_retryPipeline)SearchTool- has mutable_options
Recommendation: Create new instances per operation or use locks if sharing.
Example: Parallel Queries
var tasks = questions.Select(async question =>
{
var options = new OpenQueryOptions(..., question: question);
var query = new OpenQueryApp(client, searchTool, model);
await query.RunAsync(options);
// Separate instances per task
});
await Task.WhenAll(tasks);
Better: Create factory that spawns fresh instances.
Error Handling
All public async methods may throw:
HttpRequestException- network errors, non-2xx responsesTaskCanceledException- timeout or cancellationJsonException- malformed JSONArgument*Exception- invalid argumentsException- any other error
Pattern: Try-Catch
try
{
var response = await client.CompleteAsync(request);
Console.WriteLine(response.Choices[0].Message.Content);
}
catch (HttpRequestException ex)
{
Console.Error.WriteLine($"Network error: {ex.Message}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Unexpected error: {ex.Message}");
}
Pattern: Resilience with Polly
EmbeddingService already wraps client.EmbedAsync with Polly retry. For other calls, you can add your own:
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));
await retryPolicy.ExecuteAsync(async () =>
{
var response = await client.CompleteAsync(request);
// ...
});
Advanced Usage
Streaming Responses to Network
var request = new ChatCompletionRequest(model, messages) { Stream = true };
var response = await client.StreamAsync(request);
await foreach (var chunk in response)
{
if (chunk.TextDelta != null)
{
await networkStream.WriteAsync(Encoding.UTF8.GetBytes(chunk.TextDelta));
}
}
Parallel Embedding Batches with Progress
var texts = Enumerable.Range(0, 1000).Select(i => $"Text {i}").ToList();
await embeddingService.GetEmbeddingsAsync(texts,
onProgress: progress =>
{
Console.WriteLine(progress); // "[Generating embeddings: batch 5/4]"
});
Custom Embedding Service with Different Model
var client = new OpenRouterClient(apiKey);
var customService = new EmbeddingService(client, "your-embedding-model");
float[] embedding = await customService.GetEmbeddingAsync("text");
Limitations
No Interface-based Design
OpenQuery uses concrete classes. For mocking in tests, you'd need to create wrappers or use tools like JustMock/Moq that can mock non-virtual methods (not recommended). Better: define interfaces like IOpenRouterClient and have implementations.
Hardcoded Concurrency Settings
ParallelProcessingOptions is instantiated in SearchTool with hardcoded defaults. To customize, you'd need to:
- Subclass
SearchTooland override access to_options - Or modify source to accept
ParallelProcessingOptionsin constructor - Or use reflection (hacky)
Suggested improvement: Add constructor parameter.
Single Responsibility Blur
OpenQueryApp does query generation + pipeline + streaming. Could split:
IQueryGenerator(for expanding queries)IPipelineExecutor(for search tool)IAnswerStreamer(for final LLM streaming)
Currently, OpenQueryApp is the facade.
Next Steps
- Components - Understand architecture
- CLI Reference - CLI that uses these APIs
- Source Code - Read implementation details
Code Snippet: Full Programmatic Flow
using OpenQuery.Services;
using OpenQuery.Tools;
using OpenQuery.Models;
async Task<string> Research(string question)
{
var apiKey = GetApiKey(); // your method
var client = new OpenRouterClient(apiKey);
var searxng = new SearxngClient("http://localhost:8002");
var embeddings = new EmbeddingService(client);
var search = new SearchTool(searxng, embeddings);
var app = new OpenQueryApp(client, search, "qwen/qwen3.5-flash-02-23");
var options = new OpenQueryOptions(
Chunks: 3,
Results: 5,
Queries: 3,
Short: false,
Long: false,
Verbose: false,
Question: question
);
// Capture output by redirecting Console or modifying OpenQueryApp
await app.RunAsync(options);
return "streamed to console"; // would need custom capture
}