Compare commits

..

No commits in common. "c074036607733cce78705db2be6fd4bb1e015835" and "20a62b1bd48e1863f9a831e664fce36315209eda" have entirely different histories.

8 changed files with 34 additions and 80 deletions

View File

@ -77,6 +77,7 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
var executor = new StatelessExecutor(_weights!, context.Params);
// Build multi-turn Phi-3 prompt with full conversation history
var sb = new StringBuilder();
sb.Append($"<|system|>\n{BuildChatSystemPrompt()}<|end|>\n");
@ -114,10 +115,8 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
return await RunSessionAsync(prompt,
$"You are a coaching assistant inside a private journaling app. " +
$"Today's date is {dateStr}. " +
$"You MUST respond with ONLY a single valid JSON object. " +
$"Do NOT write any text, explanation, or commentary before or after the JSON. " +
$"Output MUST start with {{ and end with }}.",
maxTokens: 2048, temperature: 0.2f, cancellationToken: cancellationToken);
$"You MUST respond with a single valid JSON object. No text before or after the JSON.",
maxTokens: 1024, cancellationToken: cancellationToken);
}
public async Task<string> SummarizeEntryAsync(string content, string? fileStem = null,
@ -195,13 +194,16 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
private async Task<string> EnsureModelAsync(CancellationToken cancellationToken = default)
{
// 1. Configured path takes priority if the file already exists
if (File.Exists(_configuredModelPath))
return _configuredModelPath;
// 2. Check the standard app-data location
var defaultPath = GetDefaultModelPath();
if (File.Exists(defaultPath))
return defaultPath;
// 3. Download from HuggingFace
var modelDirectory = Path.GetDirectoryName(defaultPath)!;
Directory.CreateDirectory(modelDirectory);
@ -228,22 +230,21 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
// ── Session / weights lifecycle ────────────────────────────────────────
private Task<string> RunSessionAsync(string prompt, string systemPrompt,
int maxTokens, CancellationToken cancellationToken)
=> RunSessionAsync(prompt, systemPrompt, maxTokens, temperature: 0.7f, cancellationToken);
private async Task<string> RunSessionAsync(string prompt, string systemPrompt,
int maxTokens, float temperature, CancellationToken cancellationToken)
int maxTokens, CancellationToken cancellationToken)
{
var modelPath = await EnsureModelAsync(cancellationToken);
EnsureWeights(modelPath);
// Fresh context per call — prevents KV cache accumulation across requests
using var context = _weights!.CreateContext(new ModelParams(modelPath)
{
ContextSize = _contextSize,
GpuLayerCount = _gpuLayers
});
// Use StatelessExecutor with explicit Phi-3 chat template so the model
// never sees raw system text it can echo back to the user.
var executor = new StatelessExecutor(_weights!, context.Params);
var fullPrompt = $"<|system|>\n{systemPrompt}<|end|>\n" +
@ -261,7 +262,7 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
],
SamplingPipeline = new DefaultSamplingPipeline
{
Temperature = temperature
Temperature = 0.7f
}
};
@ -275,6 +276,7 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
return StripSpecialTokens(sb.ToString());
}
/// <summary>Strips only Phi-3 special tokens — safe for JSON output.</summary>
private static string StripSpecialTokens(string raw)
{
var text = raw;
@ -283,15 +285,22 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
return text.Trim();
}
// Matches role labels like "System:", "**System:**", "**Assistant:**", "User:" etc.
private static readonly Regex RoleMarkerRegex = MyRegex();
/// <summary>Aggressive cleanup for conversational (non-JSON) responses.</summary>
private static string CleanChatResponse(string raw)
{
var text = StripSpecialTokens(raw);
// Strip role markers in any formatting variant (plain, bold-markdown, etc.)
text = RoleMarkerRegex.Replace(text, "");
// Remove orphaned bold markers left behind after stripping
text = text.Replace("**", "");
text = MyRegex2().Replace(text, "\n\n");
// Collapse runs of 3+ newlines into 2
text = Regex.Replace(text, @"\n{3,}", "\n\n");
return text.Trim();
}
@ -324,6 +333,4 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
private static partial Regex MyRegex();
[GeneratedRegex(@"\n{3,}")]
private static partial Regex MyRegex1();
[GeneratedRegex(@"\n{3,}")]
private static partial Regex MyRegex2();
}

View File

@ -47,6 +47,8 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
CancellationToken cancellationToken)
{
var raw = await _ai.ChatJsonAsync(prompt, cancellationToken);
// Try to extract JSON from the response
var json = ExtractJson(raw);
if (json is not null)
{
@ -58,10 +60,11 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
}
catch (JsonException)
{
// Fall through to fallback
}
}
// Fallback: wrap raw text into a CoachPlanDto
return new CoachPlanDto(
Kind: fallbackKind,
Title: "Coach Response",
@ -101,6 +104,7 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
private static string? ExtractJson(string text)
{
// 1. Try ```json ... ``` code block
var codeBlockStart = text.IndexOf("```json", StringComparison.Ordinal);
if (codeBlockStart >= 0)
{
@ -114,6 +118,7 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
}
}
// 2. Look specifically for {"kind" — the expected first field of CoachPlanDto
var kindMarker = text.IndexOf("{\"kind\"", StringComparison.Ordinal);
if (kindMarker < 0)
kindMarker = text.IndexOf("{ \"kind\"", StringComparison.Ordinal);
@ -128,6 +133,7 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
}
}
// 3. Fallback: try each { position until one parses as valid JSON
var searchFrom = 0;
var globalLastBrace = text.LastIndexOf('}');
while (searchFrom < text.Length && globalLastBrace > searchFrom)
@ -143,67 +149,9 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
searchFrom = bracePos + 1;
}
var firstBrace = text.IndexOf('{');
if (firstBrace >= 0)
{
var repaired = TryRepairJson(text[firstBrace..]);
if (repaired is not null)
return repaired;
}
return null;
}
private static string? TryRepairJson(string text)
{
var trimmed = text.TrimEnd();
var lastUseful = trimmed.Length - 1;
while (lastUseful >= 0)
{
var ch = trimmed[lastUseful];
if (ch is '}' or ']' or '"' or ',' or ':' or '{' or '[' || char.IsDigit(ch)
|| ch is 't' or 'r' or 'u' or 'e' or 'f' or 'a' or 'l' or 's' or 'n')
break;
lastUseful--;
}
if (lastUseful < 0) return null;
trimmed = trimmed[..(lastUseful + 1)];
if (trimmed.EndsWith(','))
trimmed = trimmed[..^1];
var openBraces = 0;
var openBrackets = 0;
var inString = false;
var escape = false;
foreach (var ch in trimmed)
{
if (escape) { escape = false; continue; }
if (ch == '\\' && inString) { escape = true; continue; }
if (ch == '"') { inString = !inString; continue; }
if (inString) continue;
switch (ch)
{
case '{': openBraces++; break;
case '}': openBraces--; break;
case '[': openBrackets++; break;
case ']': openBrackets--; break;
}
}
if (openBraces <= 0 && openBrackets <= 0) return null;
var sb = new StringBuilder(trimmed);
for (var i = 0; i < openBrackets; i++) sb.Append(']');
for (var i = 0; i < openBraces; i++) sb.Append('}');
var repaired = sb.ToString();
return TryValidateJson(repaired) ? repaired : null;
}
private static bool TryValidateJson(string json)
{
try
@ -221,7 +169,11 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = assembly.GetManifestResourceNames()
.FirstOrDefault(n => n.EndsWith(fileName, StringComparison.OrdinalIgnoreCase)) ?? throw new FileNotFoundException($"Embedded resource not found: {fileName}");
.FirstOrDefault(n => n.EndsWith(fileName, StringComparison.OrdinalIgnoreCase));
if (resourceName is null)
throw new FileNotFoundException($"Embedded resource not found: {fileName}");
using var stream = assembly.GetManifestResourceStream(resourceName)!;
using var reader = new StreamReader(stream, Encoding.UTF8);
return reader.ReadToEnd().Trim();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/icon.ico" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"

View File

@ -25,7 +25,7 @@
<aside class="navbar" aria-label="Primary navigation">
<div class="navbar-header">
<img src="icon.png" alt="Journal logo" style="height: 48px; width: 48px;" />
<img src="svelte.svg" alt="Journal logo" />
</div>
<nav class="nav-groups" aria-label="Journal sections">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.0 MiB