Add commit message generation prompt

This commit is contained in:
Jacob Dubin
2026-05-10 00:30:31 -05:00
parent 8ae6d86a8c
commit 80c4ae38fb
13 changed files with 884 additions and 25 deletions

View File

@@ -0,0 +1,115 @@
using System.Net;
using System.Text;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Infrastructure.News;
using Jibo.Cloud.Infrastructure.Weather;
using Microsoft.Extensions.Logging.Abstractions;
namespace Jibo.Cloud.Tests.Infrastructure;
public sealed class ProviderCachingTests
{
[Fact]
public async Task OpenWeatherReportProvider_ReusesCachedWeatherAndGeocodeResponses()
{
var handler = new CountingHttpMessageHandler(message =>
{
var path = message.RequestUri?.AbsolutePath ?? string.Empty;
return path switch
{
"/geo/1.0/direct" => JsonResponse(
"""[{"name":"Boston","state":"Massachusetts","country":"US","lat":42.3601,"lon":-71.0589}]"""),
"/data/2.5/weather" => JsonResponse(
"""{"name":"Boston","weather":[{"main":"Clouds","description":"overcast clouds"}],"main":{"temp":70.2,"temp_max":72.9,"temp_min":66.1}}"""),
_ => new HttpResponseMessage(HttpStatusCode.NotFound)
};
});
var provider = new OpenWeatherReportProvider(
new HttpClient(handler),
new OpenWeatherOptions
{
ApiKey = "test-key",
CurrentCacheTtlSeconds = 300,
GeocodeCacheTtlSeconds = 300,
FailureCacheTtlSeconds = 30
},
NullLogger<OpenWeatherReportProvider>.Instance);
var request = new WeatherReportRequest("Boston,US", null, null, false, false, 0);
var first = await provider.GetReportAsync(request);
var second = await provider.GetReportAsync(request);
Assert.NotNull(first);
Assert.NotNull(second);
Assert.Equal(1, handler.GetCallCount("/geo/1.0/direct"));
Assert.Equal(1, handler.GetCallCount("/data/2.5/weather"));
}
[Fact]
public async Task NewsApiBriefingProvider_ReusesCachedHeadlinesForIdenticalRequests()
{
var handler = new CountingHttpMessageHandler(message =>
{
var path = message.RequestUri?.AbsolutePath ?? string.Empty;
return path switch
{
"/v2/top-headlines" => JsonResponse(
"""{"status":"ok","articles":[{"title":"Robotics team wins regional title","description":"A big local victory.","source":{"name":"AP News"},"url":"https://example.com/a"}]}"""),
_ => new HttpResponseMessage(HttpStatusCode.NotFound)
};
});
var provider = new NewsApiBriefingProvider(
new HttpClient(handler),
new NewsApiOptions
{
ApiKey = "test-key",
CacheTtlSeconds = 300,
FailureCacheTtlSeconds = 30
},
NullLogger<NewsApiBriefingProvider>.Instance);
var request = new NewsBriefingRequest(["sports"], 3);
var first = await provider.GetBriefingAsync(request);
var second = await provider.GetBriefingAsync(request);
Assert.NotNull(first);
Assert.NotNull(second);
Assert.Equal(1, handler.GetCallCount("/v2/top-headlines"));
}
private static HttpResponseMessage JsonResponse(string body)
{
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(body, Encoding.UTF8, "application/json")
};
}
private sealed class CountingHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responseFactory)
: HttpMessageHandler
{
private readonly Dictionary<string, int> callsByPath = new(StringComparer.OrdinalIgnoreCase);
private readonly object gate = new();
public int GetCallCount(string path)
{
lock (gate)
{
return callsByPath.TryGetValue(path, out var count) ? count : 0;
}
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var path = request.RequestUri?.AbsolutePath ?? string.Empty;
lock (gate)
{
callsByPath[path] = callsByPath.TryGetValue(path, out var count) ? count + 1 : 1;
}
return Task.FromResult(responseFactory(request));
}
}
}

View File

@@ -2665,6 +2665,81 @@ public sealed class JiboInteractionServiceTests
Assert.DoesNotContain("future cloud integration", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_TellMeTheNews_WithProvider_UsesProviderHeadlines()
{
var provider = new CapturingNewsBriefingProvider
{
Snapshot = new NewsBriefingSnapshot(
[
new NewsHeadline("Local robotics team unveils weather-ready helper"),
new NewsHeadline("Community makerspace hosts weekend AI expo")
],
"NewsAPI")
};
var service = CreateService(newsBriefingProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "tell me the news",
NormalizedTranscript = "tell me the news"
});
Assert.Equal("news", decision.IntentName);
Assert.Equal("news", decision.SkillName);
Assert.Equal("news", decision.SkillPayload!["skillId"]);
Assert.Equal("news", decision.SkillPayload["cloudSkill"]);
Assert.Equal("runtime-news", decision.SkillPayload["mim_id"]);
Assert.Equal("NewsAPI", decision.SkillPayload["news_source"]);
Assert.Equal(2, decision.SkillPayload["news_headline_count"]);
Assert.Contains("Local robotics team unveils weather-ready helper", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.NotNull(provider.LastRequest);
Assert.Equal(3, provider.LastRequest!.MaxHeadlines);
}
[Fact]
public async Task BuildDecisionAsync_TellMeTheNews_WithMemoryPreference_UsesCategoryHints()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var provider = new CapturingNewsBriefingProvider
{
Snapshot = new NewsBriefingSnapshot(
[
new NewsHeadline("City soccer clubs prepare for summer playoffs")
],
"NewsAPI")
};
var service = CreateService(memoryStore, newsBriefingProvider: provider);
await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "i like sports",
NormalizedTranscript = "i like sports",
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "tell me the news",
NormalizedTranscript = "tell me the news",
Attributes = new Dictionary<string, object?>
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("news", decision.IntentName);
Assert.NotNull(provider.LastRequest);
Assert.Contains("sports", provider.LastRequest!.PreferredCategories, StringComparer.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_CloudVersion_UsesSharedBuildInfo()
{
@@ -2786,13 +2861,15 @@ public sealed class JiboInteractionServiceTests
private static JiboInteractionService CreateService(
IPersonalMemoryStore? personalMemoryStore = null,
IWeatherReportProvider? weatherReportProvider = null)
IWeatherReportProvider? weatherReportProvider = null,
INewsBriefingProvider? newsBriefingProvider = null)
{
return new JiboInteractionService(
new JiboExperienceContentCache(new InMemoryJiboExperienceContentRepository()),
new FirstItemRandomizer(),
personalMemoryStore ?? new InMemoryPersonalMemoryStore(),
weatherReportProvider);
weatherReportProvider,
newsBriefingProvider);
}
private sealed class FirstItemRandomizer : IJiboRandomizer
@@ -2817,4 +2894,19 @@ public sealed class JiboInteractionServiceTests
return Task.FromResult(Snapshot);
}
}
private sealed class CapturingNewsBriefingProvider : INewsBriefingProvider
{
public NewsBriefingRequest? LastRequest { get; private set; }
public NewsBriefingSnapshot? Snapshot { get; init; }
public Task<NewsBriefingSnapshot?> GetBriefingAsync(
NewsBriefingRequest request,
CancellationToken cancellationToken = default)
{
LastRequest = request;
return Task.FromResult(Snapshot);
}
}
}

View File

@@ -1970,6 +1970,57 @@ public sealed class JiboWebSocketServiceTests
Assert.Equal("announcement", meta.GetProperty("mim_type").GetString());
}
[Fact]
public async Task ClientAsr_TellMeTheNews_WithProvider_UsesProviderHeadlinesInSpeech()
{
var service = CreateService(
new InMemoryCloudStateStore(),
newsBriefingProvider: new StubNewsBriefingProvider(
new NewsBriefingSnapshot(
[
new NewsHeadline("Robotics club opens a new community lab"),
new NewsHeadline("Local students win a regional coding challenge")
],
"NewsAPI")));
await service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-news-provider-token",
Text = """{"type":"LISTEN","transID":"trans-news-provider","data":{"hotphrase":true,"rules":["launch","globals/global_commands_launch"]}}"""
});
var replies = await service.HandleMessageAsync(new WebSocketMessageEnvelope
{
HostName = "neo-hub.jibo.com",
Path = "/listen",
Kind = "neo-hub-listen",
Token = "hub-news-provider-token",
Text = """{"type":"CLIENT_ASR","transID":"trans-news-provider","data":{"text":"tell me the news"}}"""
});
Assert.Equal(3, replies.Count);
Assert.Equal("LISTEN", ReadReplyType(replies[0]));
Assert.Equal("EOS", ReadReplyType(replies[1]));
Assert.Equal("SKILL_ACTION", ReadReplyType(replies[2]));
using var speakPayload = JsonDocument.Parse(replies[2].Text!);
var esml = speakPayload.RootElement
.GetProperty("data")
.GetProperty("action")
.GetProperty("config")
.GetProperty("jcp")
.GetProperty("config")
.GetProperty("play")
.GetProperty("esml")
.GetString();
Assert.Contains("Robotics club opens a new community lab", esml, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Source: NewsAPI.", esml, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task ClientAsr_HowIsTheWeather_EmitsSpokenWeatherFallbackWithoutRedirect()
{
@@ -3998,7 +4049,8 @@ public sealed class JiboWebSocketServiceTests
private static JiboWebSocketService CreateService(
InMemoryCloudStateStore stateStore,
IWeatherReportProvider? weatherReportProvider = null)
IWeatherReportProvider? weatherReportProvider = null,
INewsBriefingProvider? newsBriefingProvider = null)
{
var contentRepository = new InMemoryJiboExperienceContentRepository();
var contentCache = new JiboExperienceContentCache(contentRepository);
@@ -4006,7 +4058,8 @@ public sealed class JiboWebSocketServiceTests
contentCache,
new DefaultJiboRandomizer(),
new InMemoryPersonalMemoryStore(),
weatherReportProvider);
weatherReportProvider,
newsBriefingProvider);
var conversationBroker = new DemoConversationBroker(interactionService);
var sttSelector = new DefaultSttStrategySelector(
[
@@ -4035,4 +4088,14 @@ public sealed class JiboWebSocketServiceTests
return Task.FromResult<WeatherReportSnapshot?>(snapshot);
}
}
private sealed class StubNewsBriefingProvider(NewsBriefingSnapshot snapshot) : INewsBriefingProvider
{
public Task<NewsBriefingSnapshot?> GetBriefingAsync(
NewsBriefingRequest request,
CancellationToken cancellationToken = default)
{
return Task.FromResult<NewsBriefingSnapshot?>(snapshot);
}
}
}