- 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.
509 lines
13 KiB
Markdown
509 lines
13 KiB
Markdown
# 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<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
|
|
|
|
```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<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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```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<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
|
|
|
|
```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<string> 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<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:
|
|
|
|
```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<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):
|
|
|
|
```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<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` - `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<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
|
|
|
|
```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<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
|
|
}
|
|
```
|