Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions Models/OllamaModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SymptomCheckerApp.Models
{
/// <summary>Request body for the Ollama /api/chat endpoint.</summary>
public class OllamaChatRequest
{
[JsonPropertyName("model")]
public string Model { get; set; } = "llama3";

[JsonPropertyName("messages")]
public List<OllamaChatMessage> Messages { get; set; } = new();

[JsonPropertyName("stream")]
public bool Stream { get; set; } = false;

[JsonPropertyName("options")]
public OllamaOptions? Options { get; set; }
}

public class OllamaChatMessage
{
[JsonPropertyName("role")]
public string Role { get; set; } = "user";

[JsonPropertyName("content")]
public string Content { get; set; } = string.Empty;

/// <summary>Base64-encoded images for multimodal models (e.g. LLaVA).</summary>
[JsonPropertyName("images")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<string>? Images { get; set; }
}

public class OllamaOptions
{
[JsonPropertyName("temperature")]
public double Temperature { get; set; } = 0.3;

[JsonPropertyName("num_predict")]
public int NumPredict { get; set; } = 1024;
}

/// <summary>Response from the Ollama /api/chat endpoint.</summary>
public class OllamaChatResponse
{
[JsonPropertyName("model")]
public string? Model { get; set; }

[JsonPropertyName("message")]
public OllamaChatMessage? Message { get; set; }

[JsonPropertyName("done")]
public bool Done { get; set; }

[JsonPropertyName("total_duration")]
public long? TotalDuration { get; set; }
}

/// <summary>Response from the Ollama /api/tags endpoint (list models).</summary>
public class OllamaTagsResponse
{
[JsonPropertyName("models")]
public List<OllamaModelInfo>? Models { get; set; }
}

public class OllamaModelInfo
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;

[JsonPropertyName("size")]
public long Size { get; set; }

[JsonPropertyName("modified_at")]
public string? ModifiedAt { get; set; }
}

/// <summary>Structured AI diagnosis result parsed from Ollama response.</summary>
public class AiDiagnosisResult
{
public string RawResponse { get; set; } = string.Empty;
public string DiagnosticAssessment { get; set; } = string.Empty;
public List<MedicationProposal> Medications { get; set; } = new();
public List<string> RedFlags { get; set; } = new();
public string SelfCareAdvice { get; set; } = string.Empty;
public string Disclaimer { get; set; } = string.Empty;
public double? ConfidenceReinforcement { get; set; }
public long ElapsedMs { get; set; }
}

public class MedicationProposal
{
public string Name { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty; // OTC, Prescription-only, etc.
public string Purpose { get; set; } = string.Empty;
public string Dosage { get; set; } = string.Empty;
public string Warning { get; set; } = string.Empty;
}

/// <summary>Result of AI-powered image analysis for symptom detection.</summary>
public class ImageAnalysisResult
{
public string RawResponse { get; set; } = string.Empty;
/// <summary>Body region detected (skin, throat, eye, etc.).</summary>
public string BodyRegion { get; set; } = string.Empty;
/// <summary>Visual observations from the image.</summary>
public List<string> Observations { get; set; } = new();
/// <summary>Symptoms deduced from visual analysis.</summary>
public List<string> DeducedSymptoms { get; set; } = new();
/// <summary>Possible conditions suggested by the image.</summary>
public List<string> PossibleConditions { get; set; } = new();
/// <summary>Severity assessment (mild, moderate, severe).</summary>
public string Severity { get; set; } = string.Empty;
/// <summary>Recommendation to see a doctor or self-care.</summary>
public string Recommendation { get; set; } = string.Empty;
public string Disclaimer { get; set; } = string.Empty;
public long ElapsedMs { get; set; }
}

/// <summary>Result of AI-powered blood microscope image analysis.</summary>
public class BloodAnalysisResult
{
public string RawResponse { get; set; } = string.Empty;
/// <summary>Stain or preparation type detected (e.g. Giemsa, Wright, unstained).</summary>
public string StainType { get; set; } = string.Empty;
/// <summary>Magnification estimated from image context.</summary>
public string Magnification { get; set; } = string.Empty;

/// <summary>Red blood cell (erythrocyte) findings.</summary>
public List<string> RbcFindings { get; set; } = new();
/// <summary>White blood cell (leukocyte) findings.</summary>
public List<string> WbcFindings { get; set; } = new();
/// <summary>Platelet findings.</summary>
public List<string> PlateletFindings { get; set; } = new();
/// <summary>Other observations (parasites, inclusions, artefacts).</summary>
public List<string> OtherFindings { get; set; } = new();

/// <summary>Estimated differential count if identifiable.</summary>
public List<string> DifferentialCount { get; set; } = new();
/// <summary>Morphological abnormalities detected.</summary>
public List<string> Abnormalities { get; set; } = new();

/// <summary>Possible haematological conditions.</summary>
public List<string> PossibleConditions { get; set; } = new();
/// <summary>Deduced symptoms that may relate to the findings.</summary>
public List<string> DeducedSymptoms { get; set; } = new();

/// <summary>Severity (normal / mild / moderate / severe).</summary>
public string Severity { get; set; } = string.Empty;
/// <summary>Clinical recommendation.</summary>
public string Recommendation { get; set; } = string.Empty;
public string Disclaimer { get; set; } = string.Empty;
public long ElapsedMs { get; set; }
}
}
1 change: 1 addition & 0 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
var logDir = Path.Combine(AppContext.BaseDirectory, "logs");
var logger = new LoggerService(logDir);
Expand Down
49 changes: 49 additions & 0 deletions Services/CosineModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SymptomCheckerApp.Models;

namespace SymptomCheckerApp.Services
{
/// <summary>
/// Binary-vector cosine similarity: dot(A,B) / (|A| * |B|)
/// </summary>
public class CosineModel : IMatchingModel
{
public string Name => "Cosine";

public List<ConditionMatch> ComputeMatches(
HashSet<string> selectedSymptoms,
IReadOnlyList<Condition> conditions,
IReadOnlyDictionary<string, HashSet<string>> conditionSets,
IReadOnlyList<string> vocabulary,
double threshold,
MatchingOptions? options = null)
{
var results = new List<ConditionMatch>();

foreach (var c in conditions)
{
if (!conditionSets.TryGetValue(c.Name, out var condSet)) continue;

var interSymptoms = condSet.Intersect(selectedSymptoms, StringComparer.OrdinalIgnoreCase).ToList();
int dot = interSymptoms.Count;
double denom = Math.Sqrt(condSet.Count) * Math.Sqrt(selectedSymptoms.Count);
double score = denom == 0 ? 0 : dot / denom;

if (score >= threshold)
{
results.Add(new ConditionMatch
{
Name = c.Name,
Score = score,
MatchCount = dot,
MatchedSymptoms = interSymptoms
});
}
}

return results;
}
}
}
16 changes: 16 additions & 0 deletions Services/IDataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using SymptomCheckerApp.Models;

namespace SymptomCheckerApp.Services
{
/// <summary>
/// Abstraction for loading / saving application data.
/// Enables swapping JSON files for database, API, or in-memory test sources.
/// </summary>
public interface IDataProvider
{
ConditionDatabase LoadConditions();
void SaveConditions(ConditionDatabase db);
SymptomCategoryDatabase LoadCategories();
SynonymDatabase LoadSynonyms();
}
}
43 changes: 43 additions & 0 deletions Services/IMatchingModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Collections.Generic;
using SymptomCheckerApp.Models;

namespace SymptomCheckerApp.Services
{
/// <summary>
/// Abstraction for symptom-condition matching algorithms.
/// Each implementation computes scored matches between a set of selected symptoms
/// and a collection of conditions.
/// </summary>
public interface IMatchingModel
{
/// <summary>Human-readable name used for display and serialization.</summary>
string Name { get; }

/// <summary>
/// Compute scored matches for the selected symptoms against all conditions.
/// </summary>
/// <param name="selectedSymptoms">Case-insensitive set of selected symptom names.</param>
/// <param name="conditions">All known conditions.</param>
/// <param name="conditionSets">Pre-built per-condition symptom sets (cache).</param>
/// <param name="vocabulary">Full vocabulary of unique symptoms.</param>
/// <param name="threshold">Minimum score to include (0..1).</param>
/// <param name="options">Optional model-specific parameters.</param>
/// <returns>Unordered list of matches above threshold.</returns>
List<ConditionMatch> ComputeMatches(
HashSet<string> selectedSymptoms,
IReadOnlyList<Condition> conditions,
IReadOnlyDictionary<string, HashSet<string>> conditionSets,
IReadOnlyList<string> vocabulary,
double threshold,
MatchingOptions? options = null);
}

/// <summary>
/// Optional parameters that individual models may use.
/// </summary>
public class MatchingOptions
{
/// <summary>Temperature scaling for Naive Bayes softmax (default 1.0).</summary>
public double? NaiveBayesTemperature { get; set; }
}
}
49 changes: 49 additions & 0 deletions Services/JaccardModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SymptomCheckerApp.Models;

namespace SymptomCheckerApp.Services
{
/// <summary>
/// Jaccard similarity: |A ∩ B| / |A ∪ B|
/// </summary>
public class JaccardModel : IMatchingModel
{
public string Name => "Jaccard";

public List<ConditionMatch> ComputeMatches(
HashSet<string> selectedSymptoms,
IReadOnlyList<Condition> conditions,
IReadOnlyDictionary<string, HashSet<string>> conditionSets,
IReadOnlyList<string> vocabulary,
double threshold,
MatchingOptions? options = null)
{
var results = new List<ConditionMatch>();

foreach (var c in conditions)
{
if (!conditionSets.TryGetValue(c.Name, out var condSet)) continue;

var interSymptoms = condSet.Intersect(selectedSymptoms, StringComparer.OrdinalIgnoreCase).ToList();
int inter = interSymptoms.Count;
int union = condSet.Union(selectedSymptoms, StringComparer.OrdinalIgnoreCase).Count();
double score = union == 0 ? 0 : (double)inter / union;

if (score >= threshold)
{
results.Add(new ConditionMatch
{
Name = c.Name,
Score = score,
MatchCount = inter,
MatchedSymptoms = interSymptoms
});
}
}

return results;
}
}
}
59 changes: 59 additions & 0 deletions Services/JsonFileDataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.IO;
using System.Text.Json;
using SymptomCheckerApp.Models;

namespace SymptomCheckerApp.Services
{
/// <summary>
/// Default IDataProvider that reads/writes JSON files from disk.
/// </summary>
public class JsonFileDataProvider : IDataProvider
{
private readonly string _dataDir;
private static readonly JsonSerializerOptions _readOptions = new() { PropertyNameCaseInsensitive = true };
private static readonly JsonSerializerOptions _writeOptions = new() { WriteIndented = true };

public JsonFileDataProvider(string dataDirectory)
{
_dataDir = dataDirectory ?? throw new ArgumentNullException(nameof(dataDirectory));
}

public ConditionDatabase LoadConditions()
{
var path = Path.Combine(_dataDir, "conditions.json");
if (!File.Exists(path))
throw new FileNotFoundException($"Conditions JSON not found at '{path}'");

var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<ConditionDatabase>(json, _readOptions) ?? new ConditionDatabase();
}

public void SaveConditions(ConditionDatabase db)
{
var path = Path.Combine(_dataDir, "conditions.json");
var json = JsonSerializer.Serialize(db, _writeOptions);
File.WriteAllText(path, json);
}

public SymptomCategoryDatabase LoadCategories()
{
var path = Path.Combine(_dataDir, "categories.json");
if (!File.Exists(path))
throw new FileNotFoundException($"Categories JSON not found at '{path}'");

var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<SymptomCategoryDatabase>(json, _readOptions) ?? new SymptomCategoryDatabase();
}

public SynonymDatabase LoadSynonyms()
{
var path = Path.Combine(_dataDir, "synonyms.json");
if (!File.Exists(path))
throw new FileNotFoundException($"Synonyms JSON not found at '{path}'");

var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<SynonymDatabase>(json, _readOptions) ?? new SynonymDatabase();
}
}
}
Loading