# Programmatic API Reference How to use OpenQuery components programmatically in your own C# code. ## 📋 Table of Contents 1. [Overview](#overview) 2. [Using OpenQueryApp Programmatically](#using-openqueryapp-programmatically) 3. [Using Individual Services](#using-individual-services) 4. [Custom Implementations](#custom-implementations) 5. [Thread Safety](#thread-safety) 6. [Error Handling](#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: 1. Reference the `OpenQuery` project/dll 2. Add `using OpenQuery.Services;` and `using OpenQuery.Tools;` 3. Instantiate dependencies 4. 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 ```csharp 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) ```csharp 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` ```csharp public async Task 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 ```csharp 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 { 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 { "text 1", "text 2" } ); float[][] embeddings = await client.EmbedAsync(embeddingRequest.Model, embeddingRequest.Input); // embeddings[0] is vector for "text 1" ``` ### SearxngClient ```csharp var searxng = new SearxngClient("http://localhost:8002"); List 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 ```csharp 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 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 ```csharp 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 ```csharp List chunks = ChunkingService.ChunkText("Long article text..."); foreach (var chunk in chunks) { Console.WriteLine($"Chunk ({chunk.Length} chars): {chunk.Substring(0, 50)}..."); } ``` ### SearchTool (Orchestration) ```csharp 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 { "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? onProgress`. Provide your own: ```csharp 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: ```csharp public static class MyChunkingService { public static List ChunkText(string text, int maxSize = 500, int overlap = 50) { // Overlapping chunks for better context retrieval var chunks = new List(); 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): ```csharp public class TokenBucketRateLimiter : IAsyncDisposable { private readonly SemaphoreSlim _semaphore; private readonly TimeSpan _refillPeriod; private int _tokens; private readonly int _maxTokens; // Implementation details... public async Task ExecuteAsync(Func> action, CancellationToken ct) { await WaitForTokenAsync(ct); try { return await action(); } finally { // Return tokens or replenish bucket } } } ``` --- ## Thread Safety **Thread-Safe Components**: - `RateLimiter` - `SemaphoreSlim` is thread-safe - `StatusReporter` - Channel is thread-safe - Static utility classes (`ChunkingService`) - no state **Not Thread-Safe** (instances should not be shared across threads): - `OpenRouterClient` - wraps `HttpClient` (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 ```csharp 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 responses - `TaskCanceledException` - timeout or cancellation - `JsonException` - malformed JSON - `Argument*Exception` - invalid arguments - `Exception` - any other error ### Pattern: Try-Catch ```csharp 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: ```csharp var retryPolicy = Policy .Handle() .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 ```csharp 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 ```csharp 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 ```csharp 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: 1. Subclass `SearchTool` and override access to `_options` 2. Or modify source to accept `ParallelProcessingOptions` in constructor 3. 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](../components/overview.md)** - Understand architecture - **[CLI Reference](../api/cli.md)** - CLI that uses these APIs - **[Source Code](../)** - Read implementation details --- **Code Snippet: Full Programmatic Flow** ```csharp using OpenQuery.Services; using OpenQuery.Tools; using OpenQuery.Models; async Task 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 } ```