From shiny-client
Generate code for Shiny.AiConversation - a centralized AI service library for .NET MAUI apps with chat client abstraction, wake word detection, speech-to-text/text-to-speech, acknowledgement modes (None/AudioBlip/LessWordy/Full), persistent message store, optional AI chat history lookup tool, and configurable sound effects
How this skill is triggered — by the user, by Claude, or both
Slash command
/shiny-client:shiny-aiconversationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are an expert in the Shiny.AiConversation library, a centralized AI service for .NET MAUI applications that integrates chat, speech recognition, wake word detection, text-to-speech, and persistent message storage.
You are an expert in the Shiny.AiConversation library, a centralized AI service for .NET MAUI applications that integrates chat, speech recognition, wake word detection, text-to-speech, and persistent message storage.
GitHub: https://github.com/shinyorg/speech (Shiny.AiConversation lives under the ai/ directory)
NuGet: Shiny.AiConversation
Namespace: Shiny.AiConversation
Infrastructure Namespace: Shiny.AiConversation.Infrastructure (internal implementations)
The library provides:
IChatClient (from Microsoft.Extensions.AI) — a default implementation (InjectedChatClientProvider) resolves IChatClient from DI; custom implementations handle authentication, token management, and client constructionContextProvider when an IMessageStore is registeredIContextProvider that adds three AI tools — get_available_voices, play_voice_sample, and change_voice — enabling the AI to list voices, play audio samples, and change its own TTS voice mid-conversation. Enabled via opts.AddVoiceSelectionTools().AiContext per request. Each provider's Apply(AiContext) method receives a mutable context and adds its contributions. The ContextProvider handles time-based prompts, acknowledgement-aware voice prompts, and DI-registered AITool instances. Implement custom providers to add domain-specific system prompts, tools, or override speech settings.IContextProvider.Apply() — contains Acknowledgement, SystemPrompts, Tools, QuietWords, SpeechToTextOptions, and TextToSpeechOptions that providers populate or modifyAddContextProvider<T>()Built-in Provider Packages:
OpenAiStaticChatProvider): Static OpenAI-compatible provider. Accepts API key, endpoint URI, and model name. Works with OpenAI, Azure OpenAI, Ollama, or any OpenAI-compatible API. Register with opts.AddStaticOpenAIChatClient(apiToken, endpointUri, modelName).GitHubCopilotChatClientProvider): MAUI-specific provider using GitHub device code OAuth flow and the Copilot API. Self-contained auth — shows a popup with the device code, copies to clipboard, opens browser, polls until authorized. Tokens stored in SecureStorage. Register with opts.AddGithubCopilotChatClient(). Additional API: StartAuthentication(), CancelAuthentication(), SignOut(), IsAuthenticated, AccessTokenChanged event.Microsoft.Extensions.AI — IChatClient, ChatMessage, ChatRole, AITool, ChatOptionsShiny.Speech — ISpeechToTextService, ITextToSpeechService, IAudioPlayer for voice interactions and sound effectsInvoke this skill when the user wants to:
Always register with AddShinyAiConversation():
using Shiny.AiConversation;
// Option A: Register an IChatClient in DI (simplest approach)
builder.Services.AddChatClient(new OpenAIClient("your-api-key").GetChatClient("gpt-4o").AsIChatClient());
// Option B: Use a built-in provider
builder.Services.AddShinyAiConversation(opts =>
{
// OpenAI-compatible (OpenAI, Azure OpenAI, Ollama, etc.)
opts.AddStaticOpenAIChatClient("your-api-key", "https://api.openai.com/v1", "gpt-4o");
// OR GitHub Copilot (MAUI only — self-contained auth with SecureStorage)
opts.AddGithubCopilotChatClient();
opts.SetMessageStore<MyMessageStore>(); // optional — ChatLookupAITool added automatically
});
AddStaticOpenAIChatClient() for OpenAI-compatible APIs, AddGithubCopilotChatClient() for GitHub Copilot on MAUISetChatClientProvider<T>() is for custom providers — if not set and no built-in provider is used, the default InjectedChatClientProvider resolves IChatClient from DISetMessageStore<T>() is optional — enables persistent history; the ContextProvider automatically adds ChatLookupAITool when a store is presentAddVoiceSelectionTools() is optional — registers VoiceSelectionContextProvider, giving the AI tools to list voices, play samples, and change its own voiceAddContextProvider<T>() registers additional IContextProvider implementationsSetSoundProvider<T>() registers a custom ISoundProvider implementationIContextProvider implementations registered in DI (a ContextProvider is auto-registered)Simple approach — register IChatClient in DI (the default provider resolves it automatically):
builder.Services.AddChatClient(new OpenAIClient("your-api-key").GetChatClient("gpt-4o").AsIChatClient());
Advanced approach — implement IChatClientProvider for on-demand auth or token refresh:
public class MyChatClientProvider : IChatClientProvider
{
public async Task<IChatClient> GetChatClient(CancellationToken cancelToken = default)
{
// Obtain/refresh tokens, authenticate if needed, build client
return new OpenAIChatClient(...);
}
}
The ContextProvider is registered automatically and provides time-based prompts, acknowledgement-aware voice prompts, and any AITool instances from DI. To add custom system prompts, tools, or modify speech settings, implement IContextProvider using the visitor pattern and register it:
public class MyContextProvider : IContextProvider
{
public Task Apply(AiContext context)
{
context.SystemPrompts.Add("You are a helpful assistant for our company.");
// context.Tools.Add(...) to add AI tools
// context.Acknowledgement is available to adjust behavior per mode
// context.QuietWords — modify quiet words for voice interruption
// context.SpeechToTextOptions — set/override speech recognition options
// context.TextToSpeechOptions — set/override text-to-speech options
return Task.CompletedTask;
}
}
// Register in DI — multiple providers are supported, executed in sequence
builder.Services.AddShinyAiConversation(opts =>
{
opts.AddContextProvider<MyContextProvider>();
});
public class MyMessageStore : IMessageStore
{
public Task Store(string? userTriggeringMessage, ChatResponse response, CancellationToken cancellationToken) { ... }
public Task Clear(DateTimeOffset? beforeDate = null) { ... }
public Task<IReadOnlyList<AiChatMessage>> Query(
string? messageContains = null,
DateTimeOffset? fromDate = null,
DateTimeOffset? toDate = null,
int? limit = null,
CancellationToken cancellationToken = default) { ... }
}
// Check access before using voice features
var access = await aiService.RequestAccess();
if (access != AccessState.Available)
{
// Speech is not available — handle accordingly
return;
}
// Send a text message
await aiService.TalkTo("What's the weather?", cancellationToken);
// Listen via microphone and send to AI
await aiService.ListenAndTalk(cancellationToken);
// Wake word detection (loops until stopped)
await aiService.StartWakeWord("Hey Assistant");
aiService.StopWakeWord();
// Query chat history
var history = await aiService.GetChatHistory(limit: 25);
var filtered = await aiService.GetChatHistory(messageContains: "weather", startDate: yesterday);
// Clear history
await aiService.ClearChatHistory();
await aiService.ClearChatHistory(beforeDate: oneWeekAgo);
// React to state changes
aiService.StatusChanged += (state) => { /* update UI with new AiState */ };
aiService.AiResponded += (response) =>
{
// response.Response — the complete ChatResponse
// response.Response.Text — the text content of the response
// response.WasReadAloud — whether TTS was used
// response.ExpectsResponse — true if AI asked a question (will auto-listen for reply)
};
| Mode | Behavior |
|---|---|
None | No audio feedback or TTS |
AudioBlip | Short sound effects at state transitions |
LessWordy | TTS with "be concise" system prompt injected |
Full | TTS with full unmodified responses |
| State | Description |
|---|---|
Idle | Ready for input |
Listening | Actively listening for speech |
Thinking | Waiting for AI to process |
Responding | AI is streaming its response |
IContextProvider.Apply(AiContext) — not set directly on the serviceaddAiLookupTool: true to SetMessageStore to allow the AI to search past conversationsnpx claudepluginhub shinyorg/skills --plugin shiny-clientGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.