add configuration profiles with per-invocation --profile flag
- Add SystemPrompt field to HushConfig (empty = built-in default) - Refactor ConfigManager: extract ApplyTomlFields, add LoadWithProfile(), ListProfiles(), GetProfilePath(), EnsureProfilesDirExists(); remove HUSH_PROFILE env-var logic (profiles are now resolved by the CLI) - Extend socket protocol: action commands (START/STOP/TOGGLE/ABORT) now carry a [4-byte LE length][optional HushConfig JSON] payload so the CLI can pass a per-invocation config override without restarting the daemon - Add GENERATE_PROFILE (cmd 7) socket command: CLI sends a description, daemon calls the LLM and returns a generated system prompt - Orchestrator: StopAndProcessAsync accepts optional HushConfig override; ProcessWithLlmAsync uses proper system/user chat roles and respects config.SystemPrompt; add GenerateProfilePromptAsync - Split CompleteTextAsync signature to (systemPrompt, userMessage, model) across ITextStreamingProvider, GroqProvider, FireworksProvider - Add --profile/-p flag to hush toggle and hush stop - Add hush profiles subcommand: list, get, new (manual or AI-generated), edit
This commit is contained in:
@@ -15,5 +15,6 @@ public interface IAudioToTextProvider
|
||||
Task<string> TranscribeAsync(
|
||||
Stream audioStream,
|
||||
string modelName,
|
||||
string? language = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
namespace Hush.Providers.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for text generation with both synchronous and streaming capabilities.
|
||||
/// Interface for text generation.
|
||||
/// </summary>
|
||||
public interface ITextStreamingProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates text completion for a given prompt.
|
||||
/// Generates a text completion using a system prompt and a user message.
|
||||
/// </summary>
|
||||
/// <param name="prompt">The input prompt</param>
|
||||
/// <param name="systemPrompt">The system prompt that instructs the model how to behave</param>
|
||||
/// <param name="userMessage">The user message to process</param>
|
||||
/// <param name="modelName">The model name to use (e.g., llama-3.3-70b-versatile)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>The generated text</returns>
|
||||
Task<string> CompleteTextAsync(
|
||||
string prompt,
|
||||
string modelName,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Streams text generation for a given prompt.
|
||||
/// </summary>
|
||||
/// <param name="prompt">The input prompt</param>
|
||||
/// <param name="modelName">The model name to use (e.g., llama-3.3-70b-versatile)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Async enumerable of text chunks</returns>
|
||||
IAsyncEnumerable<string> StreamTextAsync(
|
||||
string prompt,
|
||||
string systemPrompt,
|
||||
string userMessage,
|
||||
string modelName,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public class FireworksProvider : IAudioToTextProvider, ITextStreamingProvider
|
||||
public async Task<string> TranscribeAsync(
|
||||
Stream audioStream,
|
||||
string modelName,
|
||||
string? language = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(audioStream);
|
||||
@@ -45,7 +46,7 @@ public class FireworksProvider : IAudioToTextProvider, ITextStreamingProvider
|
||||
? TRANSCRIPTION_ENDPOINT_TURBO
|
||||
: TRANSCRIPTION_ENDPOINT_PROD;
|
||||
|
||||
var request = new TranscriptionRequest { Model = modelName };
|
||||
var request = new TranscriptionRequest { Model = modelName, Language = language };
|
||||
|
||||
using var content = new MultipartFormDataContent();
|
||||
content.Add(new StreamContent(audioStream), "file", "audio.wav");
|
||||
@@ -84,12 +85,13 @@ public class FireworksProvider : IAudioToTextProvider, ITextStreamingProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> CompleteTextAsync(
|
||||
string prompt,
|
||||
string systemPrompt,
|
||||
string userMessage,
|
||||
string modelName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
throw new ArgumentException("Prompt is required", nameof(prompt));
|
||||
if (string.IsNullOrWhiteSpace(systemPrompt))
|
||||
throw new ArgumentException("System prompt is required", nameof(systemPrompt));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(modelName))
|
||||
throw new ArgumentException("Model name is required", nameof(modelName));
|
||||
@@ -97,7 +99,11 @@ public class FireworksProvider : IAudioToTextProvider, ITextStreamingProvider
|
||||
var request = new ChatCompletionRequest
|
||||
{
|
||||
Model = modelName,
|
||||
Messages = new List<Message> { new() { Role = "user", Content = prompt } }
|
||||
Messages = new List<Message>
|
||||
{
|
||||
new() { Role = "system", Content = systemPrompt },
|
||||
new() { Role = "user", Content = userMessage }
|
||||
}
|
||||
};
|
||||
|
||||
var jsonContent = new StringContent(
|
||||
@@ -126,86 +132,5 @@ public class FireworksProvider : IAudioToTextProvider, ITextStreamingProvider
|
||||
return result.Choices[0].Message.Content;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<string> StreamTextAsync(
|
||||
string prompt,
|
||||
string modelName,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
throw new ArgumentException("Prompt is required", nameof(prompt));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(modelName))
|
||||
throw new ArgumentException("Model name is required", nameof(modelName));
|
||||
|
||||
var request = new ChatCompletionRequest
|
||||
{
|
||||
Model = modelName,
|
||||
Stream = true,
|
||||
Messages = new List<Message> { new() { Role = "user", Content = prompt } }
|
||||
};
|
||||
|
||||
var jsonContent = new StringContent(
|
||||
JsonSerializer.Serialize(request, JsonSourceGeneration.Default.ChatCompletionRequest),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, CHAT_COMPLETION_ENDPOINT)
|
||||
{
|
||||
Content = jsonContent
|
||||
};
|
||||
|
||||
httpRequest.Headers.TryAddWithoutValidation("Authorization", _apiKey);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: "))
|
||||
continue;
|
||||
|
||||
var data = line.Substring(6).Trim(); // Remove "data: " prefix
|
||||
|
||||
if (data == "[DONE]")
|
||||
break;
|
||||
|
||||
var text = ParseTextFromStreamData(data);
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
yield return text;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ParseTextFromStreamData(string data)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var jsonDoc = JsonDocument.Parse(data);
|
||||
var choices = jsonDoc.RootElement.GetProperty("choices");
|
||||
var choice = choices[0];
|
||||
|
||||
if (choice.TryGetProperty("delta", out var delta))
|
||||
{
|
||||
if (delta.TryGetProperty("content", out var content))
|
||||
{
|
||||
return content.GetString();
|
||||
}
|
||||
}
|
||||
else if (choice.TryGetProperty("text", out var text))
|
||||
{
|
||||
return text.GetString();
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Skip malformed JSON chunks
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ public class GroqProvider : IAudioToTextProvider, ITextStreamingProvider
|
||||
public async Task<string> TranscribeAsync(
|
||||
Stream audioStream,
|
||||
string modelName,
|
||||
string? language = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(audioStream);
|
||||
@@ -40,7 +41,7 @@ public class GroqProvider : IAudioToTextProvider, ITextStreamingProvider
|
||||
if (string.IsNullOrWhiteSpace(modelName))
|
||||
throw new ArgumentException("Model name is required", nameof(modelName));
|
||||
|
||||
var request = new TranscriptionRequest { Model = modelName };
|
||||
var request = new TranscriptionRequest { Model = modelName, Language = language };
|
||||
|
||||
using var content = new MultipartFormDataContent();
|
||||
content.Add(new StreamContent(audioStream), "file", "audio.wav");
|
||||
@@ -79,20 +80,24 @@ public class GroqProvider : IAudioToTextProvider, ITextStreamingProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> CompleteTextAsync(
|
||||
string prompt,
|
||||
string systemPrompt,
|
||||
string userMessage,
|
||||
string modelName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
throw new ArgumentException("Prompt is required", nameof(prompt));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(systemPrompt))
|
||||
throw new ArgumentException("System prompt is required", nameof(systemPrompt));
|
||||
if (string.IsNullOrWhiteSpace(modelName))
|
||||
throw new ArgumentException("Model name is required", nameof(modelName));
|
||||
|
||||
var request = new ChatCompletionRequest
|
||||
{
|
||||
Model = modelName,
|
||||
Messages = new List<Models.Request.Message> { new() { Role = "user", Content = prompt } }
|
||||
Model = modelName,
|
||||
Messages = new List<Models.Request.Message>
|
||||
{
|
||||
new() { Role = "system", Content = systemPrompt },
|
||||
new() { Role = "user", Content = userMessage }
|
||||
}
|
||||
};
|
||||
|
||||
var jsonContent = new StringContent(
|
||||
@@ -121,86 +126,4 @@ public class GroqProvider : IAudioToTextProvider, ITextStreamingProvider
|
||||
return result.Choices[0].Message.Content;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<string> StreamTextAsync(
|
||||
string prompt,
|
||||
string modelName,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
throw new ArgumentException("Prompt is required", nameof(prompt));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(modelName))
|
||||
throw new ArgumentException("Model name is required", nameof(modelName));
|
||||
|
||||
var request = new ChatCompletionRequest
|
||||
{
|
||||
Model = modelName,
|
||||
Stream = true,
|
||||
Messages = new List<Models.Request.Message> { new() { Role = "user", Content = prompt } }
|
||||
};
|
||||
|
||||
var jsonContent = new StringContent(
|
||||
JsonSerializer.Serialize(request, JsonSourceGeneration.Default.ChatCompletionRequest),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, CHAT_COMPLETION_ENDPOINT)
|
||||
{
|
||||
Content = jsonContent
|
||||
};
|
||||
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data: "))
|
||||
continue;
|
||||
|
||||
var data = line.Substring(6).Trim(); // Remove "data: " prefix
|
||||
|
||||
if (data == "[DONE]")
|
||||
break;
|
||||
|
||||
var text = ParseTextFromStreamData(data);
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
yield return text;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ParseTextFromStreamData(string data)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var jsonDoc = JsonDocument.Parse(data);
|
||||
var choices = jsonDoc.RootElement.GetProperty("choices");
|
||||
var choice = choices[0];
|
||||
|
||||
if (choice.TryGetProperty("delta", out var delta))
|
||||
{
|
||||
if (delta.TryGetProperty("content", out var content))
|
||||
{
|
||||
return content.GetString();
|
||||
}
|
||||
}
|
||||
else if (choice.TryGetProperty("text", out var text))
|
||||
{
|
||||
return text.GetString();
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Skip malformed JSON chunks
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user