Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
7497ec1
start mongodb implementation
JWMB Dec 12, 2025
f689714
semi-working
JWMB Dec 13, 2025
dce8b04
split into files
JWMB Dec 13, 2025
4104153
experiments
JWMB Dec 14, 2025
3124b19
semi-working
JWMB Dec 14, 2025
a2caec6
cleanup
JWMB Dec 14, 2025
30fef0d
cleanup
JWMB Dec 14, 2025
8ece083
migration tooling
JWMB Jan 24, 2026
8ec4d67
revert to actual migration
JWMB Jan 24, 2026
46ae24f
migrate users, config
JWMB Jan 25, 2026
37c9308
cleanup
JWMB Jan 25, 2026
6e3407e
MongoTrainingRepository: TId -> string
JWMB Jan 26, 2026
963ee99
mongodb working
JWMB Jan 31, 2026
2bb1c98
local MLPredictNumberlineLevelService, QueueListener: QueueClient -> …
JWMB Feb 1, 2026
ac79eae
dotnet 10, a few nugets
JWMB Feb 21, 2026
23356fa
nugets
JWMB Feb 21, 2026
08efd10
forgot LightGbm
JWMB Feb 21, 2026
e571abe
revert breaking nuget upgrades
JWMB Feb 21, 2026
5b73706
fix build errors
JWMB Feb 21, 2026
2b4bb45
Merge branch 'main' into mongodb-dotnet10
JWMB Feb 21, 2026
a091945
Startup: log instead of throw
JWMB Feb 21, 2026
536bb6e
remove unnecessary System.Text.Encodings.Web
JWMB Feb 21, 2026
09e6655
ASPNETCORE_ENVIRONMENT: Development, allow fake login when Development
JWMB Feb 21, 2026
a222db8
working compose
JWMB Feb 23, 2026
958b64a
svelte app attempts
JWMB Feb 24, 2026
2606c9f
http://api et.al
JWMB Feb 25, 2026
35e93bc
.dockerignore - add build (otherwise vite uses old files?!)
JWMB Feb 26, 2026
1f5d7f1
adminapp logging
JWMB Feb 26, 2026
bc75c75
compose: adminapp port
JWMB Feb 26, 2026
198f9c4
http-server --proxy
JWMB Feb 26, 2026
8307779
svelte: attempts to read runtime env
JWMB Feb 26, 2026
2bab64a
trainingapi environment=Docker
JWMB Feb 26, 2026
f809f10
better Dockerfile.web
JWMB Feb 26, 2026
6538109
initial users from env variables
JWMB Feb 26, 2026
b0aa11d
some svelte+firefox bug
JWMB Feb 27, 2026
3a27189
mongodb-related fixes
JWMB Feb 27, 2026
b0dfd48
ITrainingSummaryRepository.GetByIds
JWMB Feb 28, 2026
507dd15
IPredictNumberlineLevelService: LocalMLPredictor
JWMB Feb 28, 2026
329739e
allow "test" / "auto_"
JWMB Apr 24, 2026
fa45347
Migrate not running by default
JWMB Apr 24, 2026
4079114
Merge branch 'main' into mongodb-dotnet10
JWMB May 28, 2026
17ef73f
merge fix
JWMB May 28, 2026
4260d41
Merge branch 'main' into mongodb-dotnet10
JWMB May 28, 2026
67cf665
nugets 1
JWMB May 28, 2026
f56eb14
nuget 2
JWMB May 28, 2026
a706d16
nuget 4
JWMB May 28, 2026
3a11fb1
nuget 5
JWMB May 28, 2026
8fc0528
nuget 5, revert Swashbuckle.AspNetCore
JWMB May 28, 2026
ca92208
nuget Azure
JWMB May 28, 2026
c5daea3
nuget Microsoft.ApplicationInsights
JWMB May 28, 2026
af50185
Revert "nuget Microsoft.ApplicationInsights"
JWMB May 28, 2026
32ff96f
nugets ms
JWMB May 28, 2026
9b29c5f
nuget ms
JWMB May 28, 2026
7e44d0d
nuget ms
JWMB May 28, 2026
7d8cb99
nuget ms
JWMB May 28, 2026
3a8e1fd
nuget Microsoft.Bcl.Memory
JWMB May 28, 2026
bb966e1
FakeItEasy, nswag, start on GetAiAnalysis action
JWMB May 28, 2026
8239ebb
nswag client, AnalysisDto, start configuring LlmService
JWMB May 28, 2026
0d60cca
llm / ai analysis temp
JWMB May 29, 2026
1bffd3a
deployment adjustments
JWMB Jun 15, 2026
43e207d
infrastructure
JWMB Jun 27, 2026
71eae7e
use nginx instead
JWMB Jul 2, 2026
dfda4cf
nginx path
JWMB Jul 6, 2026
cc9c7ce
nginx
JWMB Jul 9, 2026
94b53a5
attempts to handle env
JWMB Jul 9, 2026
8b03f7c
compose adjustments
JWMB Jul 11, 2026
beb11ac
null Llm fallback service
JWMB Jul 16, 2026
840f726
persistance + rootless experiments
JWMB Jul 19, 2026
f272eda
persistence
JWMB Jul 19, 2026
b584eb1
log-server info
JWMB Aug 11, 2026
b719002
log-server fix
JWMB Aug 12, 2026
4958533
Merge branch 'main' into mongodb-dotnet10
JWMB Aug 13, 2026
33ba2fb
move ExportTrainings
JWMB Aug 13, 2026
2b78b71
TrainingsController: import
JWMB Aug 13, 2026
03ac8b6
training importing
JWMB Aug 13, 2026
4d838a7
import, analysis fixes
JWMB Aug 13, 2026
d00497d
AiCoachAnalyzer: handle empty training
JWMB Aug 13, 2026
8d8fdde
GetAiAnalysis: bool onlyPrompt
JWMB Aug 13, 2026
154e19a
remove http directive from app nginx, ai analyzer fixes
JWMB Aug 13, 2026
ac16383
more exercise descriptions
JWMB Aug 13, 2026
a66af4b
import user state
JWMB Aug 13, 2026
b707c26
import fix
JWMB Aug 13, 2026
e092d95
gender/consent GUI
JWMB Aug 13, 2026
84225a1
prepare for "random" template
JWMB Aug 13, 2026
b5c6475
update vulnerable nugets
JWMB Aug 14, 2026
e33822e
enable nswag build step, add patch training
JWMB Aug 14, 2026
1acf855
experiment svelte gender/consent GUI
JWMB Aug 14, 2026
1cffebc
upsert fix, 2026 test training plan
JWMB Aug 17, 2026
d206b6e
server-setup
JWMB Aug 17, 2026
296e818
remove unused types, register MongoTrainingAssociatedDocumentWrapper<…
JWMB Aug 19, 2026
1d17251
disconnect AzureTableLogSink
JWMB Aug 19, 2026
ab5d69e
temp handle incorrect multiple userstates
JWMB Aug 19, 2026
121cbeb
oops, forgot to register training plans. also find case insensitive
JWMB Aug 19, 2026
19a79de
handle erroneous TrainingSummary multiples
JWMB Aug 19, 2026
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
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,6 @@

# performance testing sandbox
**/sandbox

# Vite
# **/build/ huh, also ignored when building inside dockerfile, thought it was only during COPY...
7 changes: 3 additions & 4 deletions Common.Web/Common.Web.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand All @@ -9,8 +9,7 @@
<PackageReference Include="Microsoft.ApplicationInsights" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" VersionOverride="8.0.1" />
<PackageReference Include="System.Text.Encodings.Web" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" VersionOverride="8.18.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PluginModuleBase\PluginModuleBase.csproj" />
Expand Down
16 changes: 16 additions & 0 deletions Common.Web/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.Extensions.Hosting;

namespace Common.Web
{
public static class Extensions
{
public static bool HasEnvironmentPart(this IHostEnvironment hostEnvironment, string environmentPart)
{
ArgumentNullException.ThrowIfNull(hostEnvironment);
return hostEnvironment.EnvironmentName.Split('.')
.Any(part => string.Equals(environmentPart, part, StringComparison.OrdinalIgnoreCase));
}
public static bool HasDevelopmentEnvironment(this IHostEnvironment hostEnvironment)
=> hostEnvironment.HasEnvironmentPart(Environments.Development);
}
}
9 changes: 5 additions & 4 deletions Common.Web/ServiceConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ namespace Common.Web
{
public static class ServiceConfiguration
{
public static void ConfigureProcessingPipelineServices(IServiceCollection services, IEnumerable<IPluginModule> pluginModules)
public static void ConfigureProcessingPipelineServices(IServiceCollection services, IConfiguration config, IEnumerable<IPluginModule> pluginModules)
{
services.AddSingleton<ITableClientFactory, TableClientFactory>(); //(sp => new TableClientFactory("vektor")
services.AddSingleton<IDataSink, AzureTableLogSink>();
services.AddSingleton<IDataSink, NullDataSink>();
services.AddSingleton<IProcessingMiddlewarePipelineRepository, ProcessingPipelineRepository>();
services.AddSingleton<SinkProcessingMiddleware>();

foreach (var plugin in pluginModules)
plugin.ConfigureServices(services);
plugin.ConfigureServices(services, config);
}

public static void ConfigurePlugins(IApplicationBuilder app, IEnumerable<IPluginModule> pluginModules)
Expand All @@ -36,7 +36,8 @@ public static void ConfigureApplicationInsights(IApplicationBuilder app, IConfig
if (aiConn == "SECRET" || aiConn == string.Empty)
{
if (isDevelopment == false)
throw new ArgumentException("InstrumentationKey not set");
Console.WriteLine($"Warning: InstrumentationKey not set ({aiConn})");
//throw new ArgumentException("InstrumentationKey not set");
}
else
{
Expand Down
128 changes: 87 additions & 41 deletions Common.Web/TypedConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,55 +21,101 @@ public class TypedConfiguration
// https://referbruv.com/blog/posts/working-with-options-pattern-in-aspnet-core-the-complete-guide
var appSettings = new T();

config.GetSection(sectionKey).Bind(appSettings);
services.AddSingleton(appSettings.GetType(), appSettings!);
//config.GetSection(sectionKey).Bind(appSettings);
//services.AddSingleton(appSettings.GetType(), appSettings!);

RecurseBind(appSettings, services, config);
//RecurseBind(appSettings, services, config);
XBind(appSettings, services, config.GetSection(typeof(T).Name));
// https://kaylumah.nl/2021/11/29/validated-strongly-typed-ioptions.html
// If we want to inject IOptions<Type> instead of just Type, this is needed: https://stackoverflow.com/a/61157181 services.ConfigureOptions(instance)
//services.Configure<AceKnowledgeOptions>(config.GetSection("AceKnowledge"));

// https://kaylumah.nl/2021/11/29/validated-strongly-typed-ioptions.html
// If we want to inject IOptions<Type> instead of just Type, this is needed: https://stackoverflow.com/a/61157181 services.ConfigureOptions(instance)
//services.Configure<AceKnowledgeOptions>(config.GetSection("AceKnowledge"));

return appSettings;
return appSettings;
}

private static void RecurseBind(object appSettings, IServiceCollection services, IConfiguration config)
private static void XBind(object setting, IServiceCollection services, IConfiguration config)
{
config.Bind(setting);
Rec(setting);
void Rec(object s)
{
services.AddSingleton(s.GetType(), s);
var props = s.GetType()
.GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
foreach (var item in props)
{
if (item.PropertyType == typeof(string) || item.PropertyType.IsPrimitive)
{ }
else if (item.PropertyType.IsAssignableTo(typeof(System.Collections.IEnumerable))
&& item.PropertyType.IsGenericType)
{ }
else
{
var v = item.GetValue(s);
if (v != null)
Rec(v);
}
}
}
}

private static void RecurseBind(object appSettings, IServiceCollection services, IConfiguration config)
{
var props = appSettings.GetType()
.GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)
.Where(o => !o.PropertyType.IsSealed); // TODO: (low) better check than IsSealed (also unit test)
var props = appSettings.GetType()
.GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)
; //.Where(o => !o.PropertyType.IsSealed); // TODO: (low) better check than IsSealed (also unit test)

foreach (var prop in props)
{
var instance = prop.GetValue(appSettings);
//config.GetSection(prop.Name).Bind(instance);
services.AddSingleton(instance!.GetType(), instance!);
foreach (var prop in props)
{
var instance = prop.GetValue(appSettings);
if (instance == null)
{
var isNullable = Nullable.GetUnderlyingType(prop.PropertyType) != null;
if (isNullable)
continue;
if (prop.GetCustomAttributes(typeof(System.Runtime.CompilerServices.NullableAttribute), true).Any())
continue;
throw new Exception($"Null value for non-nullable property '{prop.Name}'");
}
//config.GetSection(prop.Name).Bind(instance);

RecurseBind(instance, services, config);
if (instance is System.Collections.IList lst)
{
foreach (var item in lst)
{
services.AddSingleton(item.GetType(), item);
//RecurseBind(item, services, config);
}
}
else
{
services.AddSingleton(instance!.GetType(), instance!);
RecurseBind(instance, services, config);
}

//var asOptions = Microsoft.Extensions.Options.Options.Create(instance);
//services.ConfigureOptions(instance);
//var asOptions = Microsoft.Extensions.Options.Options.Create(instance);
//services.ConfigureOptions(instance);

// Execute validation (if available)
var validatorType = instance.GetType().Assembly.GetTypes()
.Where(t =>
{
var validatorInterface = t.GetInterfaces().SingleOrDefault(o =>
o.IsGenericType && o.GetGenericTypeDefinition() == typeof(Microsoft.Extensions.Options.IValidateOptions<>));
return validatorInterface != null && validatorInterface.GenericTypeArguments.Single() == instance.GetType();
}
).FirstOrDefault();
if (validatorType != null)
{
var validator = Activator.CreateInstance(validatorType);
var m = validatorType.GetMethod("Validate");
var result = (Microsoft.Extensions.Options.ValidateOptionsResult?)m?.Invoke(validator, new object[] { "", instance });
if (result!.Failed)
{
throw new Exception($"{validatorType.Name}: {result.FailureMessage}");
}
}
}
}
// Execute validation (if available)
var validatorType = instance.GetType().Assembly.GetTypes()
.Where(t =>
{
var validatorInterface = t.GetInterfaces().SingleOrDefault(o =>
o.IsGenericType && o.GetGenericTypeDefinition() == typeof(Microsoft.Extensions.Options.IValidateOptions<>));
return validatorInterface != null && validatorInterface.GenericTypeArguments.Single() == instance.GetType();
}
).FirstOrDefault();
if (validatorType != null)
{
var validator = Activator.CreateInstance(validatorType);
var m = validatorType.GetMethod("Validate");
var result = (Microsoft.Extensions.Options.ValidateOptionsResult?)m?.Invoke(validator, new object[] { "", instance });
if (result!.Failed)
{
throw new Exception($"{validatorType.Name}: {result.FailureMessage}");
}
}
}
}
}
}
75 changes: 74 additions & 1 deletion Common/Class1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,69 @@

namespace Common.LLM
{
public interface ILlmServiceFactory
{
ILlmService? Get(string? preferredModelName = null, string? preferredService = null);

public ILlmService GetOrDefault(ILlmService defaultValue, string? preferredModelName = null, string? preferredService = null)
=> Get(preferredModelName, preferredService) ?? defaultValue;

public ILlmService GetOrThrow(string? preferredModelName = null, string? preferredService = null)
{
var result = Get(preferredModelName, preferredService);
if (result == null)
throw new Exception($"Model='{preferredModelName}' service='{preferredService}' not registered");
return result;
}
}

public class LlmServiceFactory : ILlmServiceFactory
{
private readonly List<LlmModelSpecification> models;
private readonly List<LlmServiceSpecification> serviceConfigs;
private readonly Func<HttpClient> httpClientFactory;

public LlmServiceFactory(IEnumerable<LlmModelSpecification> models, IEnumerable<LlmServiceSpecification> serviceConfigs, Func<HttpClient> httpClientFactory)
{
this.models = models.ToList();
this.serviceConfigs = serviceConfigs.ToList();
this.httpClientFactory = httpClientFactory;
}

public ILlmService? Get(string? preferredModelName = null, string? preferredService = null)
{
LlmModelSpecification? model = null;
LlmServiceSpecification? service = null;

if (preferredModelName != null)
model = models.FirstOrDefault(o => o.Name.Equals(preferredModelName ?? "", StringComparison.OrdinalIgnoreCase));
else if (preferredService != null)
service = serviceConfigs.First(o => o.Name == preferredService);

if (model == null && service == null)
{
model = models.FirstOrDefault();
if (model == null)
return null;
}

if (model == null)
model = models.FirstOrDefault(o => o.AvailableOnServices.Contains(service.Name));
else if (service == null)
service = serviceConfigs.FirstOrDefault(o => model.AvailableOnServices.Contains(o.Name));

if (model == null || service == null || model.AvailableOnServices.Contains(service.Name) == false)
return null;

return service.Name switch
{
"Azure" => new AzureOpenAIRESTService(model, service),
//"AzureREST" => new AzureRESTLlmService(model, service, httpClientFactory),
//"Berget" => new BergetLlmService(model, service, httpClientFactory),
_ => throw new NotImplementedException($"Service {service.Name}")
};
}
}
// Temp until common nuget
public interface ILlmService
{
Expand All @@ -12,7 +75,6 @@ public interface ILlmService
}
public record LlmContentReceivedData(string ContentSinceLast, int TotalLength);


public class LlmModelSpecification
{
public List<string> AvailableOnServices { get; set; } = new();
Expand Down Expand Up @@ -45,6 +107,17 @@ public class LlmResult
public string Model { get; set; } = "";
}

public class NullLlmService : ILlmService
{
public LlmModelSpecification ModelSpecification => new LlmModelSpecification();

public int CountTokens(string text) => 0;

public Task<LlmResult?> Invoke(string prompt, LlmCallOptions? options = null)
=> Task.FromResult((LlmResult?)new LlmResult { Completion = "" });
}


public class AzureOpenAIRESTService : ILlmService
{
private readonly Config config;
Expand Down
2 changes: 1 addition & 1 deletion Common/Common.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Loading