docs: add comprehensive documentation with README and detailed guides
- 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.
This commit is contained in:
309
docs/api/cli.md
Normal file
309
docs/api/cli.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# CLI Reference
|
||||
|
||||
Complete command-line interface reference for OpenQuery.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Command Structure](#command-structure)
|
||||
2. [Main Command: `openquery`](#main-command-openquery)
|
||||
3. [Configure Command: `openquery configure`](#configure-command-openquery-configure)
|
||||
4. [Exit Codes](#exit-codes)
|
||||
5. [Examples by Use Case](#examples-by-use-case)
|
||||
6. [Shell Integration](#shell-integration)
|
||||
|
||||
## Command Structure
|
||||
|
||||
OpenQuery uses [System.CommandLine](https://learn.microsoft.com/dotnet/standard/commandline/) for CLI parsing.
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
openquery [GLOBAL-OPTIONS] <COMMAND> [COMMAND-OPTIONS] [ARGUMENTS]
|
||||
```
|
||||
|
||||
If no command specified, `openquery` (main command) is assumed.
|
||||
|
||||
### Help
|
||||
```bash
|
||||
openquery --help
|
||||
openquery configure --help
|
||||
```
|
||||
|
||||
Shows usage, options, examples.
|
||||
|
||||
### Version
|
||||
```bash
|
||||
openquery --version # if implemented
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Main Command: `openquery`
|
||||
|
||||
Ask a question and get an AI-powered answer.
|
||||
|
||||
### Synopsis
|
||||
```bash
|
||||
openquery [OPTIONS] <question>
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
| Name | Arity | Type | Description |
|
||||
|------|-------|------|-------------|
|
||||
| `question` | ZeroOrMore | `string[]` | The question to ask (positional, concatenated with spaces) |
|
||||
|
||||
**Notes**:
|
||||
- `ZeroOrMore` means you can omit the question (shows help)
|
||||
- Multiple words are combined: `openquery what is quantum` → `"what is quantum"`
|
||||
- Use quotes for questions with special characters: `openquery "what's the weather?"`
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Aliases | Type | Default | Description |
|
||||
|--------|---------|------|---------|-------------|
|
||||
| `--chunks` | `-c` | `int` | `DefaultChunks` (config) | Number of top context chunks to pass to LLM |
|
||||
| `--results` | `-r` | `int` | `DefaultResults` (config) | Number of search results per query |
|
||||
| `--queries` | `-q` | `int` | `DefaultQueries` (config) | Number of search queries to generate |
|
||||
| `--short` | `-s` | `bool` | `false` | Request a concise answer |
|
||||
| `--long` | `-l` | `bool` | `false` | Request a detailed answer |
|
||||
| `--verbose` | `-v` | `bool` | `false` | Show detailed progress information |
|
||||
|
||||
**Option Notes**:
|
||||
- `--short` and `--long` are flags; if both specified, `--long` takes precedence
|
||||
- Integer options validate as positive numbers (parsed by System.CommandLine)
|
||||
- Defaults come from config file or hardcoded (3, 5, 3 respectively)
|
||||
|
||||
### Behavior
|
||||
|
||||
1. Loads API key (env `OPENROUTER_API_KEY` or config file)
|
||||
2. Loads model (env `OPENROUTER_MODEL` or config)
|
||||
3. Executes workflow:
|
||||
- Generate queries (if `--queries > 1`)
|
||||
- Run search pipeline
|
||||
- Stream final answer
|
||||
4. Exits with code 0 on success, 1 on error
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Basic
|
||||
openquery "What is the capital of France?"
|
||||
|
||||
# With options
|
||||
openquery -q 5 -r 10 -c 4 "Explain quantum computing"
|
||||
|
||||
# Short answer
|
||||
openquery -s "Who won the 2024 election?"
|
||||
|
||||
# Verbose mode
|
||||
openquery -v "How does photosynthesis work?"
|
||||
|
||||
# Combined
|
||||
openquery -l -v -q 8 "History of the internet"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configure Command: `openquery configure`
|
||||
|
||||
Configure OpenQuery settings (API key, model, defaults).
|
||||
|
||||
### Synopsis
|
||||
```bash
|
||||
openquery configure [OPTIONS]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `--interactive` / `-i` | `bool` | Launch interactive configuration wizard |
|
||||
| `--key` | `string` | Set OpenRouter API key |
|
||||
| `--model` | `string` | Set default LLM model |
|
||||
| `--queries` | `int?` | Set default number of queries |
|
||||
| `--chunks` | `int?` | Set default number of chunks |
|
||||
| `--results` | `int?` | Set default number of results |
|
||||
|
||||
**Note**: Nullable options (`int?`) only update if provided.
|
||||
|
||||
### Behavior
|
||||
|
||||
- **Interactive mode** (`-i`): Prompts for each setting with current defaults shown in brackets
|
||||
- **Non-interactive**: Only updates provided options, leaves others untouched
|
||||
- Writes to `~/.config/openquery/config` (creates directory if missing)
|
||||
- Overwrites entire file (not incremental)
|
||||
|
||||
### Interactive Mode Details
|
||||
|
||||
Models presented with numbered menu:
|
||||
|
||||
```
|
||||
Available models:
|
||||
1. qwen/qwen3.5-flash-02-23
|
||||
2. qwen/qwen3.5-122b-a10b
|
||||
3. minimax/minimax-m2.5
|
||||
4. google/gemini-3-flash-preview
|
||||
5. deepseek/deepseek-v3.2
|
||||
6. moonshotai/kuki-k2.5
|
||||
Model [qwen/qwen3.5-flash-02-23]:
|
||||
```
|
||||
|
||||
- Enter number (1-6) to select preset
|
||||
- Or enter custom model string (any OpenRouter model)
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Interactive wizard
|
||||
openquery configure -i
|
||||
|
||||
# Set just API key
|
||||
openquery configure --key "sk-or-xxxxxxxxxxxx"
|
||||
|
||||
# Set multiple defaults
|
||||
openquery configure --model "google/gemini-3-flash-preview" --queries 5 --chunks 4
|
||||
|
||||
# Update model only
|
||||
openquery configure --model "deepseek/deepseek-v3.2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `0` | Success - answer generated and streamed |
|
||||
| `1` | Error - API key missing, network failure, or exception |
|
||||
|
||||
**Usage in scripts**:
|
||||
```bash
|
||||
openquery "question"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Success"
|
||||
else
|
||||
echo "Failed" >&2
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples by Use Case
|
||||
|
||||
### Quick Facts
|
||||
```bash
|
||||
openquery -s "capital of France"
|
||||
```
|
||||
Fast, concise, minimal tokens.
|
||||
|
||||
### Research Paper
|
||||
```bash
|
||||
openquery -l -q 5 -r 10 -c 4 "quantum entanglement experiments"
|
||||
```
|
||||
Multiple angles, deep sources, detailed synthesis.
|
||||
|
||||
### News & Current Events
|
||||
```bash
|
||||
openquery -v "latest news about OpenAI"
|
||||
```
|
||||
See everything: queries, results, which sources fetched.
|
||||
|
||||
### Troubleshooting
|
||||
```bash
|
||||
# Reduce scope if errors
|
||||
openquery -q 1 -r 2 "test question"
|
||||
```
|
||||
|
||||
### Save Answer to File
|
||||
```bash
|
||||
openquery "question" 2>/dev/null | sed 's/.\x08//g' > answer.md
|
||||
```
|
||||
|
||||
(Removes spinner characters)
|
||||
|
||||
### Batch Processing
|
||||
```bash
|
||||
for q in $(cat questions.txt); do
|
||||
echo "## $q" >> all-answers.md
|
||||
openquery -s "$q" 2>/dev/null | sed 's/.\x08//g' >> all-answers.md
|
||||
echo "" >> all-answers.md
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shell Integration
|
||||
|
||||
### Aliases (add to ~/.bashrc or ~/.zshrc)
|
||||
|
||||
```bash
|
||||
# Short alias
|
||||
alias oq='openquery'
|
||||
|
||||
# Presets
|
||||
alias oqs='openquery -s' # short
|
||||
alias oql='openquery -l' # long
|
||||
alias oqv='openquery -v' # verbose
|
||||
alias oqr='openquery -q 5 -r 10 -c 4' # research mode
|
||||
|
||||
# Config shortcuts
|
||||
alias oqcfg='openquery configure -i'
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
```bash
|
||||
# Save answer cleanly (removes spinner chars)
|
||||
oqsave() {
|
||||
local query="$*"
|
||||
local filename="answer-$(date +%Y%m%d-%H%M%S).md"
|
||||
openquery "$query" 2>/dev/null | sed 's/.\x08//g' > "$filename"
|
||||
echo "Saved to $filename"
|
||||
}
|
||||
|
||||
# Search and grep results
|
||||
oqgrep() {
|
||||
openquery "$1" 2>/dev/null | sed 's/.\x08//g' | grep -i "$2"
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Setup Script
|
||||
|
||||
```bash
|
||||
# ~/.local/bin/openquery-env.sh
|
||||
export OPENROUTER_API_KEY="sk-or-..."
|
||||
export OPENROUTER_MODEL="qwen/qwen3.5-flash-02-23"
|
||||
export SEARXNG_URL="http://localhost:8002"
|
||||
```
|
||||
|
||||
Source it: `source ~/.local/bin/openquery-env.sh`
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Configuration](configuration.md)** - Set up your environment
|
||||
- **[Usage](usage.md)** - Learn usage patterns and tips
|
||||
- **[Troubleshooting](troubleshooting.md)** - Fix common problems
|
||||
|
||||
---
|
||||
|
||||
**Quick Reference Card**
|
||||
|
||||
```
|
||||
# Ask
|
||||
openquery "question"
|
||||
openquery -s "quick fact"
|
||||
openquery -l -q 5 "deep research"
|
||||
|
||||
# Configure
|
||||
openquery configure -i
|
||||
openquery configure --key "..."
|
||||
openquery configure --model "..."
|
||||
|
||||
# Debug
|
||||
openquery -v "question"
|
||||
|
||||
# Help
|
||||
openquery --help
|
||||
```
|
||||
235
docs/api/environment-variables.md
Normal file
235
docs/api/environment-variables.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# Environment Variables
|
||||
|
||||
Reference for all environment variables used by OpenQuery.
|
||||
|
||||
## 📋 Summary
|
||||
|
||||
| Variable | Purpose | Required | Default | Example |
|
||||
|----------|---------|----------|---------|---------|
|
||||
| `OPENROUTER_API_KEY` | OpenRouter authentication | **Yes** | (none) | `sk-or-...` |
|
||||
| `OPENROUTER_MODEL` | Override default LLM model | No | `qwen/qwen3.5-flash-02-23` | `google/gemini-3-flash-preview` |
|
||||
| `SEARXNG_URL` | SearxNG instance URL | No | `http://localhost:8002` | `https://searx.example.com` |
|
||||
|
||||
## Detailed Reference
|
||||
|
||||
### `OPENROUTER_API_KEY`
|
||||
|
||||
**Purpose**: Your OpenRouter API authentication token.
|
||||
|
||||
**Required**: Yes, unless you have `ApiKey` set in config file.
|
||||
|
||||
**How to Obtain**:
|
||||
1. Sign up at https://openrouter.ai
|
||||
2. Go to Dashboard → API Keys
|
||||
3. Copy your key (starts with `sk-or-`)
|
||||
|
||||
**Priority**: Overrides config file `ApiKey`.
|
||||
|
||||
**Setting**:
|
||||
|
||||
```bash
|
||||
# Bash/Zsh
|
||||
export OPENROUTER_API_KEY="sk-or-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# Fish
|
||||
set -x OPENROUTER_API_KEY "sk-or-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# PowerShell
|
||||
$env:OPENROUTER_API_KEY="sk-or-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# Windows CMD
|
||||
set OPENROUTER_API_KEY=sk-or-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
**Security**:
|
||||
- Never commit API key to version control
|
||||
- Don't share key publicly
|
||||
- Use environment variables or config file with restrictive permissions (600)
|
||||
- Rotate key if accidentally exposed
|
||||
|
||||
**Validation**: OpenQuery checks if key is empty string and exits with error if missing:
|
||||
|
||||
```
|
||||
[Error] API Key is missing. Set OPENROUTER_API_KEY environment variable or run 'configure -i' to set it up.
|
||||
```
|
||||
|
||||
### `OPENROUTER_MODEL`
|
||||
|
||||
**Purpose**: Override the default LLM model used for both query generation and final answer.
|
||||
|
||||
**Required**: No.
|
||||
|
||||
**Default**: `qwen/qwen3.5-flash-02-23`
|
||||
|
||||
**Available Models** (from OpenRouter):
|
||||
|
||||
| Model | Provider | Context | Cost (Input/Output per 1M tokens) |
|
||||
|-------|----------|---------|-----------------------------------|
|
||||
| `qwen/qwen3.5-flash-02-23` | Alibaba | 200K | \$0.10 / \$0.20 |
|
||||
| `qwen/qwen3.5-122b-a10b` | Alibaba | 200K | ~\$0.20 / ~\$0.40 |
|
||||
| `minimax/minimax-m2.5` | MiniMax | 200K | ~\$0.20 / ~\$0.40 |
|
||||
| `google/gemini-3-flash-preview` | Google | 1M | ~\$0.10 / ~\$0.40 |
|
||||
| `deepseek/deepseek-v3.2` | DeepSeek | 200K | ~\$0.10 / ~\$0.30 |
|
||||
| `moonshotai/kimi-k2.5` | Moonshot AI | 200K | ~\$0.10 / ~\$0.30 |
|
||||
|
||||
(See OpenRouter for current pricing.)
|
||||
|
||||
**Setting**:
|
||||
|
||||
```bash
|
||||
export OPENROUTER_MODEL="google/gemini-3-flash-preview"
|
||||
```
|
||||
|
||||
**Interactive Config Models**: The `configure -i` wizard shows only these 6 models for convenience, but you can set any OpenRouter model via environment variable or non-interactive configure.
|
||||
|
||||
**Note**: Different models have different:
|
||||
- Speed (Flash models faster)
|
||||
- Cost (check pricing)
|
||||
- Quality (may vary by task)
|
||||
- Context window size (Gemini 3 Flash has 1M tokens, others ~200K)
|
||||
|
||||
### `SEARXNG_URL`
|
||||
|
||||
**Purpose**: URL of the SearxNG metasearch instance.
|
||||
|
||||
**Required**: No.
|
||||
|
||||
**Default**: `http://localhost:8002`
|
||||
|
||||
**Format**: Must include protocol (`http://` or `https://`) and host:port.
|
||||
|
||||
**Setting**:
|
||||
|
||||
```bash
|
||||
# Local Docker instance
|
||||
export SEARXNG_URL="http://localhost:8002"
|
||||
|
||||
# Remote instance with HTTPS
|
||||
export SEARXNG_URL="https://searx.example.com"
|
||||
|
||||
# Custom port
|
||||
export SEARXNG_URL="http://localhost:8080"
|
||||
```
|
||||
|
||||
**Finding a Public Instance**:
|
||||
- Visit https://searx.space for list of public instances
|
||||
- Choose one with HTTPS and low latency
|
||||
- Note: Public instances may have rate limits or require attribution
|
||||
|
||||
**Priority**: Overrides any default, but not config file (no config setting for SearxNG URL - only env var). Could be added to config in future.
|
||||
|
||||
**Test Your Instance**:
|
||||
```bash
|
||||
curl "$SEARXNG_URL/search?q=test&format=json" | head
|
||||
```
|
||||
|
||||
Expected: JSON with `"results": [...]`.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Priority Recap
|
||||
|
||||
When OpenQuery needs a value:
|
||||
|
||||
1. **Command-line option** (`--model`, `--key` from configure) - highest
|
||||
2. **Environment variable** (`OPENROUTER_MODEL`, `OPENROUTER_API_KEY`, `SEARXNG_URL`)
|
||||
3. **Configuration file** (`~/.config/openquery/config`: `Model`, `ApiKey`)
|
||||
4. **Hard-coded default** (only for model)
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
# Config file: Model=qwen/qwen3.5-flash-02-23
|
||||
export OPENROUTER_MODEL="deepseek/deepseek-v3.2"
|
||||
openquery --model "google/gemini-3-flash-preview" "question"
|
||||
# Uses: model=google (CLI override), overrides env and config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Environment Variables
|
||||
|
||||
### Variable Not Taking Effect
|
||||
|
||||
**Symptom**: `openquery` still uses old value after export.
|
||||
|
||||
**Causes**:
|
||||
- Exported in different shell session
|
||||
- Exported after running `openquery`
|
||||
- Shell profile not reloaded
|
||||
|
||||
**Check**:
|
||||
```bash
|
||||
echo $OPENROUTER_API_KEY
|
||||
# Should print the key (or blank if unset)
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
```bash
|
||||
# Export in current session
|
||||
export OPENROUTER_API_KEY="sk-or-..."
|
||||
|
||||
# Or add to ~/.bashrc / ~/.zshrc and restart terminal
|
||||
```
|
||||
|
||||
### Special Characters in Values
|
||||
|
||||
If your API key contains special characters (`$`, `!`, etc.), quote properly:
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY='sk-or-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||
# Single quotes prevent shell expansion
|
||||
```
|
||||
|
||||
### Variable Name Typos
|
||||
|
||||
`OPENROUTER_API_KEY` is all caps with underscores. `openrouter_api_key` (lowercase) won't work.
|
||||
|
||||
**Check spelling**:
|
||||
```bash
|
||||
env | grep -i openrouter
|
||||
```
|
||||
|
||||
### Windows Environment Variables
|
||||
|
||||
On Windows, environment variables are set per-session or user-level:
|
||||
|
||||
**PowerShell** (current session):
|
||||
```powershell
|
||||
$env:OPENROUTER_API_KEY="sk-or-..."
|
||||
```
|
||||
|
||||
**Persistent** (PowerShell):
|
||||
```powershell
|
||||
[Environment]::SetEnvironmentVariable("OPENROUTER_API_KEY", "sk-or-...", "User")
|
||||
```
|
||||
|
||||
**CMD**:
|
||||
```cmd
|
||||
set OPENROUTER_API_KEY=sk-or-...
|
||||
```
|
||||
|
||||
**System Properties** → Advanced → Environment Variables (GUI)
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Configuration File](../configuration.md)** - Persistent configuration
|
||||
- **[Usage Guide](../usage.md)** - How to use these variables
|
||||
- **[Troubleshooting](../troubleshooting.md)** - Diagnose environment issues
|
||||
|
||||
---
|
||||
|
||||
**Quick Reference**
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export OPENROUTER_API_KEY="sk-or-..."
|
||||
|
||||
# Optional (override defaults)
|
||||
export OPENROUTER_MODEL="google/gemini-3-flash-preview"
|
||||
export SEARXNG_URL="https://searx.example.com"
|
||||
|
||||
# Run
|
||||
openquery "your question"
|
||||
```
|
||||
508
docs/api/programmatic.md
Normal file
508
docs/api/programmatic.md
Normal file
@@ -0,0 +1,508 @@
|
||||
# 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
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user