initial release
This commit is contained in:
12
Services/ArticleService.cs
Normal file
12
Services/ArticleService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using SmartReader;
|
||||
|
||||
namespace OpenQuery.Services;
|
||||
|
||||
public class ArticleService
|
||||
{
|
||||
public static async Task<Article> FetchArticleAsync(string url)
|
||||
{
|
||||
var article = await Reader.ParseArticleAsync(url);
|
||||
return article;
|
||||
}
|
||||
}
|
||||
32
Services/ChunkingService.cs
Normal file
32
Services/ChunkingService.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
namespace OpenQuery.Services;
|
||||
|
||||
public static class ChunkingService
|
||||
{
|
||||
private const int MAX_CHUNK_SIZE = 500;
|
||||
|
||||
public static List<string> ChunkText(string text)
|
||||
{
|
||||
var chunks = new List<string>();
|
||||
var start = 0;
|
||||
|
||||
while (start < text.Length)
|
||||
{
|
||||
var length = Math.Min(MAX_CHUNK_SIZE, text.Length - start);
|
||||
|
||||
if (start + length < text.Length)
|
||||
{
|
||||
var lastSpace = text.LastIndexOfAny([' ', '\n', '\r', '.', '!'], start + length, length);
|
||||
if (lastSpace > start)
|
||||
length = lastSpace - start + 1;
|
||||
}
|
||||
|
||||
var chunk = text.Substring(start, length).Trim();
|
||||
if (!string.IsNullOrEmpty(chunk))
|
||||
chunks.Add(chunk);
|
||||
|
||||
start += length;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
38
Services/EmbeddingService.cs
Normal file
38
Services/EmbeddingService.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Numerics.Tensors;
|
||||
|
||||
namespace OpenQuery.Services;
|
||||
|
||||
public class EmbeddingService
|
||||
{
|
||||
private readonly OpenRouterClient _client;
|
||||
private readonly string _embeddingModel;
|
||||
|
||||
public EmbeddingService(OpenRouterClient client, string embeddingModel = "openai/text-embedding-3-small")
|
||||
{
|
||||
_client = client;
|
||||
_embeddingModel = embeddingModel;
|
||||
}
|
||||
|
||||
public async Task<float[][]> GetEmbeddingsAsync(List<string> texts)
|
||||
{
|
||||
var results = new List<float[]>();
|
||||
const int batchSize = 300;
|
||||
|
||||
for (var i = 0; i < texts.Count; i += batchSize)
|
||||
{
|
||||
if (texts.Count > batchSize)
|
||||
Console.WriteLine(
|
||||
$"[Generating {Math.Ceiling(i / (double)batchSize)}/{Math.Ceiling(texts.Count / (double)batchSize)} batch of embeddings]");
|
||||
var batch = texts.Skip(i).Take(batchSize).ToList();
|
||||
var batchResults = await _client.EmbedAsync(_embeddingModel, batch);
|
||||
results.AddRange(batchResults);
|
||||
}
|
||||
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
public static float CosineSimilarity(float[] vector1, float[] vector2)
|
||||
{
|
||||
return TensorPrimitives.CosineSimilarity(vector1, vector2);
|
||||
}
|
||||
}
|
||||
123
Services/OpenRouterClient.cs
Normal file
123
Services/OpenRouterClient.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using OpenQuery.Models;
|
||||
|
||||
namespace OpenQuery.Services;
|
||||
|
||||
public class OpenRouterClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _apiKey;
|
||||
private readonly string _baseUrl = "https://openrouter.ai/api/v1";
|
||||
|
||||
public OpenRouterClient(string apiKey)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
_httpClient = new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<StreamChunk> StreamAsync(ChatCompletionRequest request, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
request = request with { Stream = true };
|
||||
|
||||
var json = JsonSerializer.Serialize(request, AppJsonContext.Default.ChatCompletionRequest);
|
||||
var content = new StringContent(json, Encoding.UTF8, new MediaTypeHeaderValue("application/json"));
|
||||
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/chat/completions")
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
while (await reader.ReadLineAsync(cancellationToken) is { } line)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line) || line.StartsWith($":"))
|
||||
continue;
|
||||
|
||||
if (!line.StartsWith("data: ")) continue;
|
||||
var data = line[6..];
|
||||
if (data == "[DONE]")
|
||||
yield break;
|
||||
|
||||
var chunk = JsonSerializer.Deserialize<ChatCompletionChunk>(data, AppJsonContext.Default.ChatCompletionChunk);
|
||||
if (!(chunk?.Choices?.Count > 0)) continue;
|
||||
var delta = chunk.Choices[0].Delta;
|
||||
if (!string.IsNullOrEmpty(delta?.Content))
|
||||
yield return new StreamChunk(delta.Content);
|
||||
|
||||
if (delta?.ToolCalls is not { Count: > 0 }) continue;
|
||||
var toolCall = delta.ToolCalls[0];
|
||||
yield return new StreamChunk(null, new ClientToolCall(
|
||||
toolCall.Id,
|
||||
toolCall.Function.Name,
|
||||
toolCall.Function.Arguments
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ChatCompletionResponse> CompleteAsync(ChatCompletionRequest request)
|
||||
{
|
||||
request = request with { Stream = false };
|
||||
|
||||
var json = JsonSerializer.Serialize(request, AppJsonContext.Default.ChatCompletionRequest);
|
||||
var content = new StringContent(json, Encoding.UTF8, new MediaTypeHeaderValue("application/json"));
|
||||
|
||||
var response = await _httpClient.PostAsync($"{_baseUrl}/chat/completions", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var responseJson = await response.Content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<ChatCompletionResponse>(responseJson, AppJsonContext.Default.ChatCompletionResponse)!;
|
||||
}
|
||||
|
||||
public async Task<float[][]> EmbedAsync(string model, List<string> inputs)
|
||||
{
|
||||
var request = new EmbeddingRequest(model, inputs);
|
||||
var json = JsonSerializer.Serialize(request, AppJsonContext.Default.EmbeddingRequest);
|
||||
var content = new StringContent(json, Encoding.UTF8, new MediaTypeHeaderValue("application/json"));
|
||||
|
||||
var response = await _httpClient.PostAsync($"{_baseUrl}/embeddings", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var responseJson = await response.Content.ReadAsStringAsync();
|
||||
var embeddingResponse = JsonSerializer.Deserialize<EmbeddingResponse>(responseJson, AppJsonContext.Default.EmbeddingResponse)!;
|
||||
|
||||
return embeddingResponse.Data
|
||||
.OrderBy(d => d.Index)
|
||||
.Select(d => d.Embedding)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public record StreamChunk(
|
||||
string? TextDelta = null,
|
||||
ClientToolCall? Tool = null
|
||||
);
|
||||
|
||||
public record ClientToolCall(
|
||||
string ToolId,
|
||||
string ToolName,
|
||||
string Arguments
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
30
Services/SearxngClient.cs
Normal file
30
Services/SearxngClient.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Text.Json;
|
||||
using OpenQuery.Models;
|
||||
|
||||
namespace OpenQuery.Services;
|
||||
|
||||
public class SearxngClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public SearxngClient(string baseUrl)
|
||||
{
|
||||
_baseUrl = baseUrl.TrimEnd('/');
|
||||
_httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
public async Task<List<SearxngResult>> SearchAsync(string query, int limit = 10)
|
||||
{
|
||||
var encodedQuery = Uri.EscapeDataString(query);
|
||||
var url = $"{_baseUrl}/search?q={encodedQuery}&format=json";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var results = JsonSerializer.Deserialize<SearxngRoot>(json, AppJsonContext.Default.SearxngRoot);
|
||||
|
||||
return results?.Results?.Take(limit).ToList() ?? [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user