This commit is contained in:
Jacob Dubin
2026-05-19 06:03:34 -05:00
parent 6bae858da9
commit 54b32bc9cf
41 changed files with 135 additions and 117 deletions

View File

@@ -12,6 +12,7 @@ public sealed record MediaContentSnapshot
{
public string ContentType { get; init; } = "application/octet-stream";
public byte[] Content { get; init; } = [];
public IReadOnlyDictionary<string, object?> Meta { get; init; } =
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
}

View File

@@ -1,3 +1,4 @@
using System.Text;
using System.Text.Json;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models;
@@ -6,7 +7,6 @@ namespace Jibo.Cloud.Application.Services;
public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMediaContentStore? mediaContentStore = null)
{
private readonly IMediaContentStore _mediaContentStore = mediaContentStore ?? new NullMediaContentStore();
private static readonly string[] AcceptedHosts =
[
"api.jibo.com",
@@ -15,6 +15,8 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
"localhost"
];
private readonly IMediaContentStore _mediaContentStore = mediaContentStore ?? new NullMediaContentStore();
public Task<ProtocolDispatchResult> DispatchAsync(ProtocolEnvelope envelope,
CancellationToken cancellationToken = default)
{
@@ -344,7 +346,7 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
if (!string.IsNullOrWhiteSpace(envelope.BodyText)) meta["bodyText"] = envelope.BodyText;
_mediaContentStore.StoreAsync(path, contentType,
string.IsNullOrWhiteSpace(envelope.BodyText) ? [] : System.Text.Encoding.UTF8.GetBytes(envelope.BodyText),
string.IsNullOrWhiteSpace(envelope.BodyText) ? [] : Encoding.UTF8.GetBytes(envelope.BodyText),
meta as IReadOnlyDictionary<string, object?>, CancellationToken.None).GetAwaiter().GetResult();
return ProtocolDispatchResult.Ok(
@@ -367,7 +369,8 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
{
var body = envelope.TryParseBody();
var requestedName = ReadString(body, "name") ?? ReadString(body, "backupName");
return ProtocolDispatchResult.Ok(stateStore.CreateBackup(requestedName ?? $"backup-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}"));
return ProtocolDispatchResult.Ok(
stateStore.CreateBackup(requestedName ?? $"backup-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}"));
}
return ProtocolDispatchResult.Ok(Array.Empty<object>());
@@ -514,25 +517,11 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
var contentType = storedContent?.ContentType ?? TryReadMetaString(media.Meta, "contentType") ??
"application/octet-stream";
var bodyText = storedContent is not null
? System.Text.Encoding.UTF8.GetString(storedContent.Content)
? Encoding.UTF8.GetString(storedContent.Content)
: TryReadMetaString(media.Meta, "bodyText") ?? string.Empty;
return ProtocolDispatchResult.Raw(200, bodyText, contentType);
}
private sealed class NullMediaContentStore : IMediaContentStore
{
public Task StoreAsync(string path, string contentType, byte[] content,
IReadOnlyDictionary<string, object?>? meta, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task<MediaContentSnapshot?> LoadAsync(string path, CancellationToken cancellationToken = default)
{
return Task.FromResult<MediaContentSnapshot?>(null);
}
}
private ProtocolDispatchResult HandleGetUpdateFrom(string? subsystem, string? fromVersion, string? filter)
{
var update = stateStore.GetUpdateFrom(subsystem, fromVersion, filter);
@@ -662,4 +651,18 @@ public sealed class JiboCloudProtocolService(ICloudStateStore stateStore, IMedia
bool.TryParse(value, out var parsed) &&
parsed;
}
private sealed class NullMediaContentStore : IMediaContentStore
{
public Task StoreAsync(string path, string contentType, byte[] content,
IReadOnlyDictionary<string, object?>? meta, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task<MediaContentSnapshot?> LoadAsync(string path, CancellationToken cancellationToken = default)
{
return Task.FromResult<MediaContentSnapshot?>(null);
}
}
}

View File

@@ -676,7 +676,8 @@ internal static class PersonalReportOrchestrator
var selected = ChooseShortestTemplate(briefings);
if (string.IsNullOrWhiteSpace(selected)) return string.Empty;
var firstSentence = selected.Split(['.', '!', '?'], 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
var firstSentence = selected.Split(['.', '!', '?'], 2,
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.FirstOrDefault();
return string.IsNullOrWhiteSpace(firstSentence) ? selected : firstSentence;
}

View File

@@ -1,3 +1,4 @@
using System.Text.RegularExpressions;
using Jibo.Runtime.Abstractions;
namespace Jibo.Cloud.Application.Services;
@@ -57,10 +58,10 @@ public sealed class SyntheticBufferedAudioSttStrategy : ISttStrategy
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
var lowered = value.Trim().ToLowerInvariant();
lowered = System.Text.RegularExpressions.Regex.Replace(lowered, @"[^\p{L}\p{N}\s']+", " ",
System.Text.RegularExpressions.RegexOptions.CultureInvariant | System.Text.RegularExpressions.RegexOptions.Compiled);
lowered = System.Text.RegularExpressions.Regex.Replace(lowered, @"\s+"," ",
System.Text.RegularExpressions.RegexOptions.CultureInvariant | System.Text.RegularExpressions.RegexOptions.Compiled);
lowered = Regex.Replace(lowered, @"[^\p{L}\p{N}\s']+", " ",
RegexOptions.CultureInvariant | RegexOptions.Compiled);
lowered = Regex.Replace(lowered, @"\s+", " ",
RegexOptions.CultureInvariant | RegexOptions.Compiled);
return lowered.Trim();
}
}

View File

@@ -2,7 +2,7 @@ using System.Text.RegularExpressions;
namespace Jibo.Cloud.Application.Services;
internal static partial class TranscriptTextNormalizer
internal static class TranscriptTextNormalizer
{
private static readonly Regex PunctuationToSpaceRegex = new(
@"[^\p{L}\p{N}\s']+",

View File

@@ -1,12 +1,11 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models;
using Jibo.Runtime.Abstractions;
namespace Jibo.Cloud.Application.Services;
public sealed partial class WebSocketTurnFinalizationService(
public sealed class WebSocketTurnFinalizationService(
IConversationBroker conversationBroker,
ISttStrategySelector sttStrategySelector,
ITurnTelemetrySink sink

View File

@@ -371,18 +371,17 @@ public static class LegacyMimCatalogImporter
{
private readonly List<string> _calendarNothingReplies = [];
private readonly List<string> _calendarNothingTodayReplies = [];
private readonly List<string> _calendarServiceDownReplies = [];
private readonly List<string> _calendarOutroReplies = [];
private readonly List<string> _calendarServiceDownReplies = [];
private readonly List<string> _commuteNowReplies = [];
private readonly List<string> _commuteServiceDownReplies = [];
private readonly List<JiboConditionedReply> _emotionReplies = [];
private readonly List<string> _fallbacks = [];
private readonly List<string> _greetings = [];
private readonly List<string> _jokes = [];
private readonly List<string> _robotFacts = [];
private readonly List<string> _humanFacts = [];
private readonly List<string> _howAreYous = [];
private readonly List<string> _funFacts = [];
private readonly List<string> _greetings = [];
private readonly List<string> _howAreYous = [];
private readonly List<string> _humanFacts = [];
private readonly List<string> _jokes = [];
private readonly List<string> _newsCategoryIntroReplies = [];
private readonly List<string> _newsIntroReplies = [];
private readonly List<string> _newsOutroReplies = [];
@@ -390,6 +389,7 @@ public static class LegacyMimCatalogImporter
private readonly List<string> _personalReportKickOffReplies = [];
private readonly List<string> _personalReportOutroReplies = [];
private readonly List<string> _reportSkillTemplates = [];
private readonly List<string> _robotFacts = [];
private readonly List<string> _weatherIntroReplies = [];
private readonly List<string> _weatherServiceDownReplies = [];
private readonly List<string> _weatherTodayHighLowReplies = [];
@@ -564,7 +564,8 @@ public static class LegacyMimCatalogImporter
private LegacyMimBucket ResolveFunFactTarget(string prompt)
{
var lowered = NormalizePrompt(prompt, false).ToLowerInvariant();
if (ContainsAny(lowered, "robot", "humanoid", "machine", "about me", "my cameras", "turing", "deep blue", "rossum"))
if (ContainsAny(lowered, "robot", "humanoid", "machine", "about me", "my cameras", "turing", "deep blue",
"rossum"))
return LegacyMimBucket.RobotFacts;
if (ContainsAny(lowered, "human", "people", "grown ups", "human being", "humans"))

View File

@@ -67,7 +67,8 @@ public static class ServiceCollectionExtensions
configuration.GetSection("OpenJibo:Media").Bind(mediaOptions);
if (string.IsNullOrWhiteSpace(mediaOptions.ConnectionString))
mediaOptions.ConnectionString = Environment.GetEnvironmentVariable("OPENJIBO_MEDIA_STORAGE_CONNECTION_STRING");
mediaOptions.ConnectionString =
Environment.GetEnvironmentVariable("OPENJIBO_MEDIA_STORAGE_CONNECTION_STRING");
services.AddSingleton<IPersistenceSnapshotStoreFactory, PersistenceSnapshotStoreFactory>();
services.AddSingleton<IMediaContentStoreFactory, MediaContentStoreFactory>();

View File

@@ -30,14 +30,14 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore
var contentBlob = _containerClient.GetBlobClient($"{relative}.bin");
var metaBlob = _containerClient.GetBlobClient($"{relative}.json");
await _containerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
await contentBlob.UploadAsync(new MemoryStream(content), overwrite: true, cancellationToken);
await contentBlob.UploadAsync(new MemoryStream(content), true, cancellationToken);
var payload = JsonSerializer.Serialize(new
{
path,
contentType,
meta
}, JsonOptions);
await metaBlob.UploadAsync(BinaryData.FromString(payload), overwrite: true, cancellationToken);
await metaBlob.UploadAsync(BinaryData.FromString(payload), true, cancellationToken);
}
public async Task<MediaContentSnapshot?> LoadAsync(string path, CancellationToken cancellationToken = default)
@@ -52,7 +52,6 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore
var metaBlob = _containerClient.GetBlobClient($"{relative}.json");
if (await metaBlob.ExistsAsync(cancellationToken))
{
try
{
var json = (await metaBlob.DownloadContentAsync(cancellationToken)).Value.Content.ToString();
@@ -70,7 +69,6 @@ internal sealed class AzureBlobMediaContentStore : IMediaContentStore
{
// Keep the raw binary available even if metadata parsing fails.
}
}
return new MediaContentSnapshot
{

View File

@@ -53,7 +53,6 @@ internal sealed class FileMediaContentStore : IMediaContentStore
IDictionary<string, object?> meta = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
if (File.Exists(metaPath))
{
try
{
using var document = JsonDocument.Parse(await File.ReadAllTextAsync(metaPath, cancellationToken));
@@ -72,7 +71,6 @@ internal sealed class FileMediaContentStore : IMediaContentStore
{
// Keep binary content available even if the manifest is malformed.
}
}
return new MediaContentSnapshot
{

View File

@@ -5,7 +5,8 @@ internal static class MediaPathHelper
public static string GetRelativeStoragePath(string path)
{
var trimmed = path.Trim().TrimStart('/', '\\');
var segments = trimmed.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
var segments = trimmed
.Split(['/', '\\'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(SanitizeSegment)
.Where(segment => !string.IsNullOrWhiteSpace(segment))
.ToArray();

View File

@@ -10,7 +10,8 @@ internal static class CaptureIndexWriter
WriteIndented = false
};
private static readonly ConcurrentDictionary<string, SemaphoreSlim> DirectoryLocks = new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, SemaphoreSlim> DirectoryLocks =
new(StringComparer.OrdinalIgnoreCase);
public static async Task AppendAsync(
string directoryPath,

View File

@@ -218,7 +218,8 @@ public sealed class OpenWeatherReportProvider(
var summary = TryReadWeatherSummary(root);
var condition = TryReadWeatherCondition(root);
var temperature = TryReadInt(main, "temp");
var currentDayHighLow = await TryResolveCurrentDayHighLowFromForecastAsync(location, useCelsius, cancellationToken);
var currentDayHighLow =
await TryResolveCurrentDayHighLowFromForecastAsync(location, useCelsius, cancellationToken);
var high = currentDayHighLow?.High ?? TryReadInt(main, "temp_max");
var low = currentDayHighLow?.Low ?? TryReadInt(main, "temp_min");

View File

@@ -92,7 +92,8 @@ public sealed class LegacyMimCatalogImporterTests
Assert.Contains("I like all the colors of the rainbow. But blue is my favorite.",
catalog.PersonalityReplies);
Assert.Contains("I never eat, so I don't have a favorite food by taste. But my favorite food by shape, is macaroni.",
Assert.Contains(
"I never eat, so I don't have a favorite food by taste. But my favorite food by shape, is macaroni.",
catalog.PersonalityReplies);
Assert.Contains("I mostly like fun music I can dance to.", catalog.PersonalityReplies);
Assert.Contains("The only thing I consume is electricity.", catalog.PersonalityReplies);
@@ -199,7 +200,8 @@ public sealed class LegacyMimCatalogImporterTests
var catalog = LegacyMimCatalogImporter.ImportCatalog(rootDirectory);
Assert.Contains("I love jokes. Did you hear about the theater actor who fell through the floorboards? He was just going through a stage.",
Assert.Contains(
"I love jokes. Did you hear about the theater actor who fell through the floorboards? He was just going through a stage.",
catalog.Jokes);
Assert.Contains("Sure I got one. What did the zero say to the eight. Nice belt.", catalog.Jokes);
Assert.Contains(catalog.RobotFacts, reply =>
@@ -211,7 +213,8 @@ public sealed class LegacyMimCatalogImporterTests
Assert.Contains(catalog.RobotFacts, reply =>
reply.Contains("two cameras but they're different focal lengths", StringComparison.OrdinalIgnoreCase));
Assert.Contains("A random fact for you. A shrimp's heart is in its head.", catalog.FunFacts);
Assert.Contains("An amazing but true fact for you. Dogs and elephants are the only animals that understand pointing.",
Assert.Contains(
"An amazing but true fact for you. Dogs and elephants are the only animals that understand pointing.",
catalog.FunFacts);
}

View File

@@ -426,7 +426,8 @@ public sealed class ProviderCachingTests
entries.Select(entry =>
$$"""{"dt":{{entry.Timestamp.ToUnixTimeSeconds()}},"main":{"temp":{{entry.Temp}},"temp_min":{{entry.Low}},"temp_max":{{entry.High}}},"weather":[{"main":"{{entry.Main}}","description":"{{entry.Description}}"}]}"""));
return $$"""{"city":{"name":"{{cityName}}","country":"{{country}}","timezone":{{timezoneSeconds}}},"list":[{{list}}]}""";
return
$$"""{"city":{"name":"{{cityName}}","country":"{{country}}","timezone":{{timezoneSeconds}}},"list":[{{list}}]}""";
}
private sealed class CountingHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responseFactory)

View File

@@ -60,7 +60,8 @@ public sealed class FileProtocolTelemetrySinkTests : IDisposable
Assert.Contains("Notification_20150505", contents);
Assert.DoesNotContain(Path.Combine("bin", "Debug"), captureFile, StringComparison.OrdinalIgnoreCase);
Assert.Contains(indexLines, line => line.Contains("\"eventType\":\"protocol_record\"", StringComparison.Ordinal));
Assert.Contains(indexLines,
line => line.Contains("\"eventType\":\"protocol_record\"", StringComparison.Ordinal));
Assert.Contains(indexLines, line => line.Contains("\"servicePrefix\":\"Notification_20150505\"",
StringComparison.Ordinal));
}

View File

@@ -163,7 +163,8 @@ public sealed class JiboCloudProtocolServiceTests
Method = "POST",
ServicePrefix = "Update_20160715",
Operation = "CreateUpdate",
BodyText = """{"fromVersion":"1.0.0","toVersion":"1.0.1","changes":"Restore proof","subsystem":"robot"}"""
BodyText =
"""{"fromVersion":"1.0.0","toVersion":"1.0.1","changes":"Restore proof","subsystem":"robot"}"""
});
await firstService.DispatchAsync(new ProtocolEnvelope
@@ -209,16 +210,15 @@ public sealed class JiboCloudProtocolServiceTests
Assert.NotNull(secondInfo.LastLoadedUtc);
Assert.NotNull(secondInfo.LastSavedUtc);
Assert.NotEmpty(updatesPayload.RootElement.EnumerateArray());
Assert.Contains(updatesPayload.RootElement.EnumerateArray(), item => item.GetProperty("changes").GetString() == "Restore proof");
Assert.Contains(updatesPayload.RootElement.EnumerateArray(),
item => item.GetProperty("changes").GetString() == "Restore proof");
Assert.NotEmpty(backupsPayload.RootElement.EnumerateArray());
Assert.Contains(backupsPayload.RootElement.EnumerateArray(), item => item.TryGetProperty("Name", out var name) && name.GetString() == "manual-backup");
Assert.Contains(backupsPayload.RootElement.EnumerateArray(),
item => item.TryGetProperty("Name", out var name) && name.GetString() == "manual-backup");
}
finally
{
if (File.Exists(persistencePath))
{
File.Delete(persistencePath);
}
if (File.Exists(persistencePath)) File.Delete(persistencePath);
}
}

View File

@@ -62,9 +62,11 @@ public sealed class FileTurnTelemetrySinkTests
var indexPath = Path.Combine(directoryPath, "capture-index.ndjson");
var lines = await File.ReadAllLinesAsync(indexPath);
Assert.Contains(lines, line => line.Contains("\"eventType\":\"yes_no_turn_received\"", StringComparison.Ordinal));
Assert.Contains(lines,
line => line.Contains("\"eventType\":\"yes_no_turn_received\"", StringComparison.Ordinal));
Assert.Contains(lines, line => line.Contains("\"eventType\":\"transcript_error\"", StringComparison.Ordinal));
Assert.Contains(lines, line => line.Contains("\"message\":\"Turn telemetry diagnostic\"", StringComparison.Ordinal));
Assert.Contains(lines,
line => line.Contains("\"message\":\"Turn telemetry diagnostic\"", StringComparison.Ordinal));
Assert.Contains(lines, line => line.Contains("\"message\":\"Turn telemetry error\"", StringComparison.Ordinal));
}

View File

@@ -1,5 +1,5 @@
using System.Text.Json;
using System.Text;
using System.Text.Json;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Application.Services;
using Jibo.Cloud.Infrastructure.Content;
@@ -3108,7 +3108,8 @@ public sealed class JiboInteractionServiceTests
});
Assert.Equal("robot_can_sleep", decision.IntentName);
Assert.Contains("I do. I usually fall asleep at night.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("I do. I usually fall asleep at night.", decision.ReplyText,
StringComparison.OrdinalIgnoreCase);
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
}

View File

@@ -1,5 +1,5 @@
using System.Text.Json;
using System.Text;
using System.Text.Json;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Application.Services;
using Jibo.Cloud.Domain.Models;
@@ -21,7 +21,7 @@ public sealed class JiboWebSocketServiceTests
var contentRepository = new InMemoryJiboExperienceContentRepository();
var contentCache = new JiboExperienceContentCache(contentRepository);
var conversationBroker = new DemoConversationBroker(new JiboInteractionService(contentCache,
new LastItemRandomizer(), new InMemoryPersonalMemoryStore(), null, null, null));
new LastItemRandomizer(), new InMemoryPersonalMemoryStore()));
var sttSelector = new DefaultSttStrategySelector(
[
new SyntheticBufferedAudioSttStrategy()
@@ -2727,7 +2727,8 @@ public sealed class JiboWebSocketServiceTests
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-radio-noise-token",
Text = """{"type":"CLIENT_ASR","transID":"trans-radio-noise","data":{"text":"Hey Jibo - so open the radio"}}"""
Text =
"""{"type":"CLIENT_ASR","transID":"trans-radio-noise","data":{"text":"Hey Jibo - so open the radio"}}"""
});
Assert.Equal(4, replies.Count);
@@ -4749,7 +4750,8 @@ public sealed class JiboWebSocketServiceTests
Path = "/listen",
Kind = "neo-hub-listen",
Token = token,
Text = """{"type":"LISTEN","transID":"trans-smoke-greeting","data":{"text":"hello jibo","rules":["wake-word"]}}"""
Text =
"""{"type":"LISTEN","transID":"trans-smoke-greeting","data":{"text":"hello jibo","rules":["wake-word"]}}"""
});
Assert.Equal(3, greetingReplies.Count);
@@ -4794,7 +4796,8 @@ public sealed class JiboWebSocketServiceTests
Path = "/listen",
Kind = "neo-hub-listen",
Token = token,
Text = """{"type":"CLIENT_ASR","transID":"trans-smoke-color","data":{"text":"what is your favorite color"}}"""
Text =
"""{"type":"CLIENT_ASR","transID":"trans-smoke-color","data":{"text":"what is your favorite color"}}"""
});
Assert.Equal(3, personalityReplies.Count);

View File

@@ -190,7 +190,8 @@ public sealed class LocalWhisperCppBufferedAudioSttStrategyTests
return page;
}
private sealed class FakeExternalProcessRunner(string whisperStdOut = "[00:00:00.000 --> 00:00:01.000] tell me a joke")
private sealed class FakeExternalProcessRunner(
string whisperStdOut = "[00:00:00.000 --> 00:00:01.000] tell me a joke")
: IExternalProcessRunner
{
public List<(string FileName, IReadOnlyList<string> Arguments)> Calls { get; } = [];