- 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.
16 KiB
Models Reference
Complete reference for all data models, DTOs, and records in OpenQuery.
📋 Table of Contents
Core Data Models
OpenQueryOptions
Location: Models/OpenQueryOptions.cs
Type: record
Purpose: Immutable options object for a single query execution
public record OpenQueryOptions(
int Chunks, // Number of top chunks to include in context
int Results, // Search results per generated query
int Queries, // Number of search queries to generate (if >1)
bool Short, // Request concise answer
bool Long, // Request detailed answer
bool Verbose, // Enable verbose logging
string Question // Original user question (required)
);
Lifecycle:
- Created in
Program.csby combining CLI options, config defaults, and environment variables - Passed to
OpenQueryApp.RunAsync(options)
Validation: None (assumes valid values from CLI parser/config)
Example:
var options = new OpenQueryOptions(
Chunks: 3,
Results: 5,
Queries: 3,
Short: false,
Long: false,
Verbose: true,
Question: "What is quantum entanglement?"
);
Chunk
Location: Models/Chunk.cs
Type: record
Purpose: Content chunk with metadata, embedding, and relevance score
public record Chunk(
string Content, // Text content (typically ~500 chars)
string SourceUrl, // Original article URL
string? Title = null // Article title (optional, may be null)
)
{
public float[]? Embedding { get; set; } // Vector embedding (1536-dim for text-embedding-3-small)
public float Score { get; set; } // Relevance score (0-1, higher = more relevant)
}
Lifecycle:
-
Created in
SearchTool.ExecuteParallelArticleFetchingAsync:chunks.Add(new Chunk(chunkText, result.Url, article.Title));At this point:
Embedding = null,Score = 0 -
Embedded in
SearchTool.ExecuteParallelEmbeddingsAsync:validChunks[i].Embedding = validEmbeddings[i]; -
Scored in
SearchTool.RankAndSelectTopChunks:chunk.Score = EmbeddingService.CosineSimilarity(queryEmbedding, chunk.Embedding!); -
Formatted into context string:
$"[Source {i+1}: {c.Title ?? "Unknown"}]({c.SourceUrl})\n{c.Content}"
Properties:
Content: Never null/empty (filters empty chunks inChunkingService)SourceUrl: Always provided (fromSearxngResult.Url)Title: May be null if article extraction failed to get titleEmbedding: Null until phase 3; may remain null if embedding failedScore: 0 until phase 4; irrelevant for non-embedded chunks
Equality: Records use value equality (all properties compared). Two chunks with same content/url/title are equal; embeddings and scores ignored for equality (as they're mutable).
ParallelProcessingOptions
Location: Models/ParallelOptions.cs
Type: class
Purpose: Configuration for parallel/concurrent operations
public class ParallelProcessingOptions
{
public int MaxConcurrentArticleFetches { get; set; } = 10;
public int MaxConcurrentEmbeddingRequests { get; set; } = 4;
public int EmbeddingBatchSize { get; set; } = 300;
}
Usage:
- Instantiated in
SearchToolconstructor (hardcoded new) - Passed to
EmbeddingServiceconstructor - Read by
SearchToolfor article fetching semaphore
Default Values:
| Property | Default | Effect |
|---|---|---|
MaxConcurrentArticleFetches |
10 | Up to 10 articles fetched simultaneously |
MaxConcurrentEmbeddingRequests |
4 | Up to 4 embedding batches in parallel |
EmbeddingBatchSize |
300 | Each embedding API call handles up to 300 texts |
Current Limitation: These are compile-time defaults (hardcoded in SearchTool.cs). To make them configurable:
- Add to
AppConfig - Read in
ConfigManager - Pass through
SearchToolconstructor
OpenRouter API Models
Location: Models/OpenRouter.cs
Purpose: DTOs for OpenRouter's REST API (JSON serialization)
Chat Completion
ChatCompletionRequest
public record ChatCompletionRequest(
[property: JsonPropertyName("model")] string Model,
[property: JsonPropertyName("messages")] List<Message> Messages,
[property: JsonPropertyName("tools")] List<ToolDefinition>? Tools = null,
[property: JsonPropertyName("stream")] bool Stream = false
);
Example:
{
"model": "qwen/qwen3.5-flash-02-23",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What is 2+2?" }
],
"stream": true
}
Message
public record Message(
[property: JsonPropertyName("role")] string Role,
[property: JsonPropertyName("content")] string? Content = null,
[property: JsonPropertyName("tool_calls")] List<ToolCall>? ToolCalls = null,
[property: JsonPropertyName("tool_call_id")] string? ToolCallId = null
)
{
// Factory method for tool responses
public static Message FromTool(string content, string toolCallId) =>
new Message("tool", content, null, toolCallId);
}
Roles: "system", "user", "assistant", "tool"
Usage:
Contentfor text messagesToolCallswhen assistant requests tool useToolCallIdwhen responding to tool call
ChatCompletionResponse
public record ChatCompletionResponse(
[property: JsonPropertyName("choices")] List<Choice> Choices,
[property: JsonPropertyName("usage")] Usage? Usage = null
);
public record Choice(
[property: JsonPropertyName("message")] Message Message,
[property: JsonPropertyName("finish_reason")] string? FinishReason = null
);
Response Example:
{
"choices": [
{
"message": {
"role": "assistant",
"content": "Answer text..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150
}
}
Usage
public record Usage(
[property: JsonPropertyName("prompt_tokens")] int PromptTokens,
[property: JsonPropertyName("completion_tokens")] int CompletionTokens,
[property: JsonPropertyName("total_tokens")] int TotalTokens
);
Tool Calling (Not Currently Used)
ToolDefinition / ToolFunction
public record ToolDefinition(
[property: JsonPropertyName("type")] string Type, // e.g., "function"
[property: JsonPropertyName("function")] ToolFunction Function
);
public record ToolFunction(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("description")] string Description,
[property: JsonPropertyName("parameters")] JsonElement Parameters // JSON Schema
);
ToolCall / FunctionCall
public record ToolCall(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("function")] FunctionCall Function
);
public record FunctionCall(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("arguments")] string Arguments // JSON string
);
Note: OpenQuery doesn't use tools currently, but models are defined for future tool-calling capability.
Streaming
StreamChunk
public record StreamChunk(
string? TextDelta = null,
ClientToolCall? Tool = null
);
Yielded by OpenRouterClient.StreamAsync() for each SSE event.
ChatCompletionChunk (Server Response)
public record ChatCompletionChunk(
[property: JsonPropertyName("choices")] List<ChunkChoice> Choices
);
public record ChunkChoice(
[property: JsonPropertyName("delta")] ChunkDelta Delta
);
public record ChunkDelta(
[property: JsonPropertyName("content")] string? Content = null,
[property: JsonPropertyName("tool_calls")] List<ToolCall>? ToolCalls = null
);
Streaming Response Example (SSE):
data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"choices":[{"delta":{"content":" world"}}]}
data: [DONE]
OpenRouterClient.StreamAsync parses and yields StreamChunk with non-null TextDelta for content.
Embeddings
EmbeddingRequest
public record EmbeddingRequest(
[property: JsonPropertyName("model")] string Model,
[property: JsonPropertyName("input")] List<string> Input
);
Example:
{
"model": "openai/text-embedding-3-small",
"input": ["text 1", "text 2", ...]
}
EmbeddingResponse
public record EmbeddingResponse(
[property: JsonPropertyName("data")] List<EmbeddingData> Data,
[property: JsonPropertyName("usage")] Usage Usage
);
public record EmbeddingData(
[property: JsonPropertyName("embedding")] float[] Embedding,
[property: JsonPropertyName("index")] int Index
);
Response Example:
{
"data": [
{ "embedding": [0.1, 0.2, ...], "index": 0 },
{ "embedding": [0.3, 0.4, ...], "index": 1 }
],
"usage": {
"prompt_tokens": 100,
"total_tokens": 100
}
}
Note: _client.EmbedAsync orders by index to match input order.
SearxNG API Models
Location: Models/Searxng.cs
Purpose: DTOs for SearxNG's JSON response format
SearxngRoot
public record SearxngRoot(
[property: JsonPropertyName("results")] List<SearxngResult> Results
);
Top-level response object.
SearxngResult
public record SearxngResult(
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("content")] string Content // Snippet/description
);
Fields:
Title: Result title (from page<title>or OpenGraph)Url: Absolute URL to articleContent: Short snippet (~200 chars) from search engine
Usage:
Urlpassed toArticleService.FetchArticleAsyncTitleused as fallback if article extraction failsContentcurrently unused (could be for quick answer without fetching)
Example Response:
{
"results": [
{
"title": "Quantum Entanglement - Wikipedia",
"url": "https://en.wikipedia.org/wiki/Quantum_entanglement",
"content": "Quantum entanglement is a physical phenomenon..."
}
]
}
JSON Serialization
JsonContext (Source Generation)
Location: Models/JsonContexts.cs
Purpose: Provide source-generated JSON serializer context for AOT compatibility
Declaration
[JsonSerializable(typeof(ChatCompletionRequest))]
[JsonSerializable(typeof(ChatCompletionResponse))]
[JsonSerializable(typeof(ChatCompletionChunk))]
[JsonSerializable(typeof(EmbeddingRequest))]
[JsonSerializable(typeof(EmbeddingResponse))]
[JsonSerializable(typeof(SearxngRoot))]
[JsonJsonSerializer(typeof(List<string>))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
Usage:
var json = JsonSerializer.Serialize(request, AppJsonContext.Default.ChatCompletionRequest);
var response = JsonSerializer.Deserialize(json, AppJsonContext.Default.ChatCompletionResponse);
Benefits:
- AOT-compatible: No reflection, works with PublishAot=true
- Performance: Pre-compiled serializers are faster
- Trimming safe: Unused serializers trimmed automatically
Generated: Partial class compiled by source generator (no manual implementation)
Important: Must include ALL types that will be serialized/deserialized in [JsonSerializable] attributes, otherwise runtime exception in AOT.
Model Relationships
Object Graph (Typical Execution)
OpenQueryOptions
↓
OpenQueryApp.RunAsync()
│
├─ queryGenerationMessages (List<Message>)
│ ├─ system: "You are an expert researcher..."
│ └─ user: "Generate N queries for: {question}"
│ ↓
│ ChatCompletionRequest → OpenRouter → ChatCompletionResponse
│ ↓
│ List<string> generatedQueries
│
├─ SearchTool.ExecuteAsync()
│ ↓
│ ┌─────────────────────────────────────┐
│ │ Phase 1: Parallel Searches │
│ │ SearxngClient.SearchAsync(query) × N
│ │ → List<SearxngResult> │
│ │ (Title, Url, Content) │
│ └─────────────────────────────────────┘
│ ↓
│ ┌─────────────────────────────────────┐
│ │ Phase 2: Article Fetch & Chunking │
│ │ ArticleService.FetchAsync(Url) × M
│ │ → Article (TextContent, Title)
│ │ → ChunkingService.ChunkText → List<string> chunks
│ │ → Chunk(content, url, title) × K │
│ └─────────────────────────────────────┘
│ ↓
│ ┌─────────────────────────────────────┐
│ │ Phase 3: Embeddings │
│ │ EmbeddingService.GetEmbeddingsAsync(chunkContents)
│ │ → float[][] chunkEmbeddings │
│ │ → Set chunk.Embedding for each │
│ │ Also: GetEmbeddingAsync(question) → float[] queryEmbedding
│ └─────────────────────────────────────┘
│ ↓
│ ┌─────────────────────────────────────┐
│ │ Phase 4: Ranking │
│ │ For each chunk: Score = CosineSimilarity(queryEmbedding, chunk.Embedding)
│ │ → Set chunk.Score │
│ │ → OrderByDescending(Score) │
│ │ → Take(topChunksLimit) → topChunks (List<Chunk>)
│ └─────────────────────────────────────┘
│ ↓
│ Context string: formatted topChunks
│ ↓
└─ OpenQueryApp → final ChatCompletionRequest
System: "Answer based on context..."
User: "Context:\n{context}\n\nQuestion: {question}"
↓
StreamAsync() → StreamChunk.TextDelta → Console
Record Immutability
Most DTOs are record types:
- Immutable: Properties are init-only (
{ get; init; }) - Value semantics: Equality based on content
- Thread-safe: Can be shared across threads
Exception:
Chunk: Has mutable propertiesEmbeddingandScore(set during pipeline)ParallelProcessingOptions: Class with mutable settersAppConfig: Class with mutable setters
Next Steps
- API Reference - How these models are used in CLI commands
- OpenRouterClient - Uses OpenRouter models
- SearxngClient - Uses Searxng models
- SearchTool - Orchestrates all models
Quick Reference Table
| Model | Category | Purpose | Mutable? |
|---|---|---|---|
OpenQueryOptions |
Core | CLI options | No (record) |
Chunk |
Core | Content + metadata + ranking | Partially (Embedding, Score) |
ParallelProcessingOptions |
Config | Concurrency settings | Yes (class) |
ChatCompletionRequest/Response |
OpenRouter | LLM API | No |
EmbeddingRequest/Response |
OpenRouter | Embeddings API | No |
SearxngRoot/Result |
SearxNG | Search results | No |
AppJsonContext |
Internal | JSON serialization | No (generated partial) |