mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 11:07:41 +00:00
Silent merge fail
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using WinStudentGoalTracker.Models.ResponseTypes;
|
||||
|
||||
namespace WinStudentGoalTracker.Services;
|
||||
|
||||
public class OllamaService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _model;
|
||||
private readonly int _maxRetries = 3;
|
||||
|
||||
public OllamaService(HttpClient httpClient, IConfiguration config)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_model = config["Ollama:Model"] ?? "gpt-oss:20b";
|
||||
}
|
||||
|
||||
public async Task<GoalBreakdownResponse> BreakdownGoalAsync(string goal, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(goal))
|
||||
throw new ArgumentException("Goal cannot be empty.", nameof(goal));
|
||||
|
||||
var prompt = "You are a task planning assistant. Given a goal, break it down into smaller, actionable subgoals.\n\n" +
|
||||
$"Goal: {goal}\n\n" +
|
||||
"Respond with ONLY a JSON object in this exact format, no other text:\n" +
|
||||
"{\"subgoals\": [\"subgoal 1\", \"subgoal 2\", \"subgoal 3\"]}\n\n" +
|
||||
"Be specific and practical. Generate 3-7 subgoals depending on complexity.";
|
||||
|
||||
var requestBody = new OllamaChatRequest
|
||||
{
|
||||
Model = _model,
|
||||
Messages = [new OllamaChatMessage { Role = "user", Content = prompt }],
|
||||
Format = "json",
|
||||
Stream = false
|
||||
};
|
||||
|
||||
for (var attempt = 0; attempt < _maxRetries; attempt++)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/chat", requestBody, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new HttpRequestException(
|
||||
$"Ollama service returned {(int)response.StatusCode}: {errorBody}",
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var chatResponse = await response.Content.ReadFromJsonAsync<OllamaChatResponse>(cancellationToken)
|
||||
?? throw new JsonException("Ollama returned an empty response.");
|
||||
|
||||
var parsed = JsonSerializer.Deserialize<OllamaSubgoalsResult>(chatResponse.Message.Content)
|
||||
?? throw new JsonException("LLM response deserialized to null.");
|
||||
|
||||
var subgoals = NormalizeSubgoals(parsed.Subgoals);
|
||||
|
||||
if (subgoals.Count == 0)
|
||||
throw new JsonException("LLM response contained no valid subgoals.");
|
||||
|
||||
return new GoalBreakdownResponse
|
||||
{
|
||||
Goal = goal,
|
||||
Subgoals = subgoals
|
||||
};
|
||||
}
|
||||
catch (JsonException) when (attempt < _maxRetries - 1)
|
||||
{
|
||||
// Malformed response from the model — retry
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to get a valid response from the LLM after {_maxRetries} attempts.");
|
||||
}
|
||||
|
||||
private static List<string> NormalizeSubgoals(List<JsonElement> rawSubgoals)
|
||||
{
|
||||
var normalized = new List<string>();
|
||||
|
||||
foreach (var item in rawSubgoals)
|
||||
{
|
||||
switch (item.ValueKind)
|
||||
{
|
||||
case JsonValueKind.String:
|
||||
normalized.Add(item.GetString()!);
|
||||
break;
|
||||
|
||||
case JsonValueKind.Object:
|
||||
string[] preferredKeys = ["description", "task", "subtask", "subgoal", "name", "action"];
|
||||
var added = false;
|
||||
|
||||
foreach (var key in preferredKeys)
|
||||
{
|
||||
if (item.TryGetProperty(key, out var value) && value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
normalized.Add(value.GetString()!);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!added)
|
||||
{
|
||||
foreach (var prop in item.EnumerateObject())
|
||||
{
|
||||
if (prop.Value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
normalized.Add(prop.Value.GetString()!);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
#region Ollama API DTOs
|
||||
|
||||
private class OllamaChatRequest
|
||||
{
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("messages")]
|
||||
public List<OllamaChatMessage> Messages { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("format")]
|
||||
public string Format { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("stream")]
|
||||
public bool Stream { get; set; }
|
||||
}
|
||||
|
||||
private class OllamaChatMessage
|
||||
{
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private class OllamaChatResponse
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public OllamaChatMessage Message { get; set; } = new();
|
||||
}
|
||||
|
||||
private class OllamaSubgoalsResult
|
||||
{
|
||||
[JsonPropertyName("subgoals")]
|
||||
public List<JsonElement> Subgoals { get; set; } = [];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace WinStudentGoalTracker.Services;
|
||||
|
||||
public class PasswordHasher
|
||||
{
|
||||
private const int SaltSize = 16; // 128 bit
|
||||
private const int HashSize = 32; // 256 bit
|
||||
private const int Iterations = 100_000;
|
||||
|
||||
public static (string Hash, string Salt) HashPassword(string password)
|
||||
{
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
byte[] saltBytes = new byte[SaltSize];
|
||||
rng.GetBytes(saltBytes);
|
||||
|
||||
using var pbkdf2 = new Rfc2898DeriveBytes(password, saltBytes, Iterations, HashAlgorithmName.SHA256);
|
||||
byte[] hashBytes = pbkdf2.GetBytes(HashSize);
|
||||
|
||||
return (Convert.ToBase64String(hashBytes), Convert.ToBase64String(saltBytes));
|
||||
}
|
||||
|
||||
public static bool VerifyPassword(string password, string storedHash, string storedSalt)
|
||||
{
|
||||
byte[] saltBytes = Convert.FromBase64String(storedSalt);
|
||||
|
||||
using var pbkdf2 = new Rfc2898DeriveBytes(password, saltBytes, Iterations, HashAlgorithmName.SHA256);
|
||||
byte[] hashBytes = pbkdf2.GetBytes(HashSize);
|
||||
|
||||
string incomingHash = Convert.ToBase64String(hashBytes);
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.UTF8.GetBytes(incomingHash),
|
||||
Encoding.UTF8.GetBytes(storedHash));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace WinStudentGoalTracker.Services;
|
||||
|
||||
public class TokenService
|
||||
{
|
||||
private readonly IConfiguration _config;
|
||||
private readonly int _tokenExpiryInSeconds = 60 * 15; // 15 minutes
|
||||
|
||||
public TokenService(IConfiguration config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string GenerateToken(Guid userId, string email, string? roleName)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Email, email),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new Claim("user_id", userId.ToString())
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(roleName))
|
||||
{
|
||||
claims.Add(new Claim(ClaimTypes.Role, roleName));
|
||||
}
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]!));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _config["Jwt:Issuer"],
|
||||
audience: null,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddSeconds(_tokenExpiryInSeconds),
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public int GetTokenExpiryInSeconds(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var jwtToken = tokenHandler.ReadJwtToken(token);
|
||||
|
||||
var expiryTime = jwtToken.ValidTo;
|
||||
var currentTime = DateTime.UtcNow;
|
||||
|
||||
var timeUntilExpiry = expiryTime - currentTime;
|
||||
|
||||
return timeUntilExpiry.TotalSeconds > 0 ? (int)timeUntilExpiry.TotalSeconds : 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace WinStudentGoalTracker.Services;
|
||||
|
||||
public class TranscriptionService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public TranscriptionService(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<string> TranscribeAsync(Stream audioStream, string fileName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var content = new MultipartFormDataContent();
|
||||
|
||||
var fileContent = new StreamContent(audioStream);
|
||||
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
content.Add(fileContent, "file", fileName);
|
||||
content.Add(new StringContent("whisper-1"), "model");
|
||||
content.Add(new StringContent("json"), "response_format");
|
||||
|
||||
var response = await _httpClient.PostAsync("/v1/audio/transcriptions", content, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new HttpRequestException(
|
||||
$"Transcription service returned {(int)response.StatusCode}: {errorBody}",
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<TranscriptionResult>(cancellationToken)
|
||||
?? throw new InvalidOperationException("Transcription service returned an empty response.");
|
||||
|
||||
return result.Text;
|
||||
}
|
||||
|
||||
private record TranscriptionResult(string Text);
|
||||
}
|
||||
Reference in New Issue
Block a user