diff --git a/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj b/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj index 20ffba5d..1272ea00 100644 --- a/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj +++ b/EssentialCSharp.Chat.Shared/EssentialCSharp.Chat.Common.csproj @@ -9,6 +9,7 @@ + diff --git a/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs b/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs index ccc3c5b5..9692e002 100644 --- a/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs +++ b/EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience; using Microsoft.Extensions.Options; using Microsoft.SemanticKernel; using Npgsql; @@ -15,6 +16,7 @@ namespace EssentialCSharp.Chat.Common.Extensions; public static class ServiceCollectionExtensions { private static readonly string[] _PostgresScopes = ["https://ossrdbms-aad.database.windows.net/.default"]; + private const string LocalChatHttpClientName = "LocalAIChat"; /// /// Adds Azure OpenAI and related AI services to the service collection using Managed Identity @@ -85,6 +87,82 @@ public static IServiceCollection AddAzureOpenAIServices( return services; } + /// + /// Registers chat services using configuration-driven backend selection. + /// This method never throws for missing or partial AI configuration; it falls back to + /// so the app can continue running. + /// + public static IServiceCollection AddConfiguredChatServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("AIOptions")); + + var aiOptions = configuration.GetSection("AIOptions").Get() ?? new AIOptions(); + string? postgresConnectionString = configuration.GetConnectionString("PostgresVectorStore"); + + bool hasAzureEndpoint = !string.IsNullOrWhiteSpace(aiOptions.Endpoint); + bool hasAzureChatDeployment = !string.IsNullOrWhiteSpace(aiOptions.ChatDeploymentName); + bool hasAzureVectorDeployment = !string.IsNullOrWhiteSpace(aiOptions.VectorGenerationDeploymentName); + bool hasAzureConfig = hasAzureEndpoint && hasAzureChatDeployment && hasAzureVectorDeployment + && IsValidNpgsqlConnectionString(postgresConnectionString); + + string localEndpoint = ResolveLocalEndpoint(aiOptions, configuration); + bool hasLocalConfig = !string.IsNullOrWhiteSpace(localEndpoint) + && !string.IsNullOrWhiteSpace(aiOptions.LocalChatModel); + + if (hasAzureConfig) + { + // Pre-validate endpoint URI to avoid exceptions in AddAzureOpenAIServices for + // non-empty but invalid endpoint values. + if (!Uri.TryCreate(aiOptions.Endpoint, UriKind.Absolute, out var azureUri) + || azureUri.Scheme != Uri.UriSchemeHttps) + { + Console.Error.WriteLine("[AI] Azure endpoint must be a valid https URI. Falling back to local/unavailable."); + } + else + { + services.AddAzureOpenAIServices(aiOptions, postgresConnectionString!); + // Bind EmbeddingRetry from config so operator appsettings/env overrides are honored. + // The AIOptions overload of AddAzureOpenAIServices only registers validation, not config binding. + services.AddOptions() + .Bind(configuration.GetSection(EmbeddingRetryOptions.SectionPath)); + services.AddSingleton(provider => provider.GetRequiredService()); + Console.WriteLine("[AI] Selected backend: Azure/Foundry."); + return services; + } + } + + if (hasLocalConfig) + { + if (!Uri.TryCreate(localEndpoint, UriKind.Absolute, out var localEndpointUri) + || (localEndpointUri.Scheme != Uri.UriSchemeHttp && localEndpointUri.Scheme != Uri.UriSchemeHttps)) + { + services.AddSingleton(); + Console.Error.WriteLine("[AI] Local backend selected but LocalEndpoint is invalid. Falling back to unavailable backend."); + return services; + } + +#pragma warning disable EXTEXP0001 + services.AddHttpClient(LocalChatHttpClientName, client => + { + client.BaseAddress = localEndpointUri; + client.Timeout = TimeSpan.FromSeconds(120); + }) + // Disable the global standard resilience handler (set by ConfigureHttpClientDefaults + // in Program.cs). Its default attempt timeout (30s) and total timeout (90s) would + // cut off long local-LLM completions. We set HttpClient.Timeout directly instead. + // Retries are also wrong for LLM calls (non-idempotent, partial responses). + .RemoveAllResilienceHandlers(); +#pragma warning restore EXTEXP0001 + services.AddSingleton(); + Console.WriteLine("[AI] Selected backend: Local (Ollama/OpenAI-compatible)."); + return services; + } + + services.AddSingleton(); + Console.WriteLine("[AI] Selected backend: Unavailable (missing or invalid AI configuration)."); + return services; + } + /// /// Adds Azure OpenAI and related AI services to the service collection using configuration /// @@ -198,4 +276,34 @@ private static IServiceCollection AddPostgresVectorStoreWithManagedIdentity( return services; } + private static string ResolveLocalEndpoint(AIOptions options, IConfiguration configuration) + { + if (!string.IsNullOrWhiteSpace(options.LocalEndpoint)) + { + return options.LocalEndpoint!; + } + + return configuration.GetConnectionString("ollama-chat") ?? string.Empty; + } + + /// + /// Returns true if can be parsed by + /// and resolves to a non-empty Host. + /// Rejects null, empty, and placeholder strings like "your-postgres-connection-string-here". + /// + private static bool IsValidNpgsqlConnectionString(string? connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) + return false; + try + { + var builder = new NpgsqlConnectionStringBuilder(connectionString); + return !string.IsNullOrWhiteSpace(builder.Host); + } + catch (ArgumentException) + { + return false; + } + } + } diff --git a/EssentialCSharp.Chat.Shared/Models/AIOptions.cs b/EssentialCSharp.Chat.Shared/Models/AIOptions.cs index 87a01a87..d0574fba 100644 --- a/EssentialCSharp.Chat.Shared/Models/AIOptions.cs +++ b/EssentialCSharp.Chat.Shared/Models/AIOptions.cs @@ -2,6 +2,21 @@ public class AIOptions { + /// + /// Enables local AI backend usage when Azure endpoint is not configured. + /// + public bool UseLocalAI { get; set; } + + /// + /// Local OpenAI-compatible endpoint (for example, Ollama). + /// + public string? LocalEndpoint { get; set; } + + /// + /// Local chat model identifier (for example, qwen2.5-coder:7b). + /// + public string? LocalChatModel { get; set; } + /// /// The Azure OpenAI deployment name for text embedding generation. /// diff --git a/EssentialCSharp.Chat.Shared/Services/AIChatService.cs b/EssentialCSharp.Chat.Shared/Services/AIChatService.cs index a66bbc65..6c384a76 100644 --- a/EssentialCSharp.Chat.Shared/Services/AIChatService.cs +++ b/EssentialCSharp.Chat.Shared/Services/AIChatService.cs @@ -12,7 +12,7 @@ namespace EssentialCSharp.Chat.Common.Services; /// /// Service for handling AI chat completions using the OpenAI Responses API /// -public partial class AIChatService +public partial class AIChatService : IChatCompletionService { private readonly AIOptions _Options; private readonly AzureOpenAIClient _AzureClient; @@ -22,6 +22,8 @@ public partial class AIChatService private readonly AISearchService _SearchService; private readonly ILogger _Logger; private readonly FrozenSet _AllowedMcpTools; + public bool IsAvailable => true; + public bool SupportsContextualSearch => true; public AIChatService(IOptions options, AISearchService searchService, AzureOpenAIClient azureClient, ILogger logger) { diff --git a/EssentialCSharp.Chat.Shared/Services/ChatBackendUnavailableException.cs b/EssentialCSharp.Chat.Shared/Services/ChatBackendUnavailableException.cs new file mode 100644 index 00000000..61346060 --- /dev/null +++ b/EssentialCSharp.Chat.Shared/Services/ChatBackendUnavailableException.cs @@ -0,0 +1,4 @@ +namespace EssentialCSharp.Chat.Common.Services; + +public class ChatBackendUnavailableException(string message, Exception? innerException = null) + : Exception(message, innerException); diff --git a/EssentialCSharp.Chat.Shared/Services/IChatCompletionService.cs b/EssentialCSharp.Chat.Shared/Services/IChatCompletionService.cs new file mode 100644 index 00000000..487486d5 --- /dev/null +++ b/EssentialCSharp.Chat.Shared/Services/IChatCompletionService.cs @@ -0,0 +1,37 @@ +using ModelContextProtocol.Client; +using OpenAI.Responses; + +namespace EssentialCSharp.Chat.Common.Services; + +public interface IChatCompletionService +{ + bool IsAvailable { get; } + + bool SupportsContextualSearch { get; } + + Task<(string response, string responseId)> GetChatCompletion( + string prompt, + string? systemPrompt = null, + string? previousResponseId = null, + McpClient? mcpClient = null, +#pragma warning disable OPENAI001 + IEnumerable? tools = null, + ResponseReasoningEffortLevel? reasoningEffortLevel = null, +#pragma warning restore OPENAI001 + bool enableContextualSearch = false, + string? endUserId = null, + CancellationToken cancellationToken = default); + + IAsyncEnumerable<(string text, string? responseId)> GetChatCompletionStream( + string prompt, + string? systemPrompt = null, + string? previousResponseId = null, + McpClient? mcpClient = null, +#pragma warning disable OPENAI001 + IEnumerable? tools = null, + ResponseReasoningEffortLevel? reasoningEffortLevel = null, +#pragma warning restore OPENAI001 + bool enableContextualSearch = false, + string? endUserId = null, + CancellationToken cancellationToken = default); +} diff --git a/EssentialCSharp.Chat.Shared/Services/LocalChatService.cs b/EssentialCSharp.Chat.Shared/Services/LocalChatService.cs new file mode 100644 index 00000000..f607082d --- /dev/null +++ b/EssentialCSharp.Chat.Shared/Services/LocalChatService.cs @@ -0,0 +1,231 @@ +using System.Linq; +using System.IO; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ModelContextProtocol.Client; +using OpenAI.Responses; + +namespace EssentialCSharp.Chat.Common.Services; + +public partial class LocalChatService : IChatCompletionService, IDisposable +{ + private const int MaxConversationMessages = 20; + private const int MaxConversationEntries = 500; + private static readonly TimeSpan _HistoryTtl = TimeSpan.FromMinutes(30); + + private readonly AIOptions _Options; + private readonly IHttpClientFactory _HttpClientFactory; + private readonly ILogger _Logger; + private readonly MemoryCache _ConversationHistory = new(new MemoryCacheOptions { SizeLimit = MaxConversationEntries }); + + public bool IsAvailable => true; + public bool SupportsContextualSearch => false; + + public LocalChatService(IOptions options, IHttpClientFactory httpClientFactory, ILogger logger) + { + _Options = options.Value; + _HttpClientFactory = httpClientFactory; + _Logger = logger; + } + + public void Dispose() + { + _ConversationHistory.Dispose(); + GC.SuppressFinalize(this); + } + + public async Task<(string response, string responseId)> GetChatCompletion( + string prompt, + string? systemPrompt = null, + string? previousResponseId = null, + McpClient? mcpClient = null, +#pragma warning disable OPENAI001 + IEnumerable? tools = null, + ResponseReasoningEffortLevel? reasoningEffortLevel = null, +#pragma warning restore OPENAI001 + bool enableContextualSearch = false, + string? endUserId = null, + CancellationToken cancellationToken = default) + { + if (enableContextualSearch) + { + throw new NotSupportedException("Local AI backend does not support contextual search."); + } + + var (client, history, jsonPayload) = PrepareRequest(prompt, systemPrompt, previousResponseId); + + HttpResponseMessage response; + using var request = new HttpRequestMessage(HttpMethod.Post, "/v1/chat/completions") + { + Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json") + }; + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "local-dev-key"); + + try + { + response = await client.SendAsync(request, cancellationToken); + } + catch (HttpRequestException ex) + { + throw new ChatBackendUnavailableException("Local AI backend is unavailable.", ex); + } + catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested) + { + throw new ChatBackendUnavailableException("Local AI backend timed out.", ex); + } + + using (response) + { + string body; + try + { + body = await response.Content.ReadAsStringAsync(cancellationToken); + } + catch (HttpRequestException ex) + { + throw new ChatBackendUnavailableException("Local AI backend is unavailable while reading response.", ex); + } + catch (IOException ex) + { + throw new ChatBackendUnavailableException("Local AI backend connection closed while reading response.", ex); + } + catch (TaskCanceledException ex) when (!cancellationToken.IsCancellationRequested) + { + throw new ChatBackendUnavailableException("Local AI backend timed out while reading response.", ex); + } + + if (!response.IsSuccessStatusCode) + { + LogLocalRequestFailed(_Logger, (int)response.StatusCode, body); + throw new ChatBackendUnavailableException("Local AI backend returned a non-success status."); + } + + try + { + var (text, responseId) = ParseResponse(body); + history.Add(new LocalChatMessage("user", prompt)); + history.Add(new LocalChatMessage("assistant", text)); + var entryOptions = new MemoryCacheEntryOptions() + .SetSlidingExpiration(_HistoryTtl) + .SetSize(1); + _ConversationHistory.Set(responseId, history.TakeLast(MaxConversationMessages).ToList(), entryOptions); + return (text, responseId); + } + catch (Exception ex) when (ex is JsonException || ex is InvalidOperationException || ex is NotSupportedException) + { + throw new ChatBackendUnavailableException("Local AI backend returned an invalid response.", ex); + } + } + } + + public async IAsyncEnumerable<(string text, string? responseId)> GetChatCompletionStream( + string prompt, + string? systemPrompt = null, + string? previousResponseId = null, + McpClient? mcpClient = null, +#pragma warning disable OPENAI001 + IEnumerable? tools = null, + ResponseReasoningEffortLevel? reasoningEffortLevel = null, +#pragma warning restore OPENAI001 + bool enableContextualSearch = false, + string? endUserId = null, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (enableContextualSearch) + { + throw new NotSupportedException("Local AI backend does not support contextual search."); + } + + var (response, responseId) = await GetChatCompletion( + prompt, + systemPrompt, + previousResponseId, + mcpClient, + tools, + reasoningEffortLevel, + enableContextualSearch, + endUserId, + cancellationToken); + + if (!string.IsNullOrEmpty(response)) + { + yield return (response, responseId: null); + } + + yield return (string.Empty, responseId); + } + + private (HttpClient Client, List History, string JsonPayload) PrepareRequest( + string prompt, + string? systemPrompt, + string? previousResponseId = null) + { + var client = _HttpClientFactory.CreateClient("LocalAIChat"); + var effectiveSystemPrompt = string.IsNullOrWhiteSpace(systemPrompt) ? _Options.SystemPrompt : systemPrompt; + var history = ResolveHistory(previousResponseId); + + var messages = new List { new { role = "system", content = effectiveSystemPrompt } }; + messages.AddRange(history.Select(m => new { role = m.Role, content = m.Content })); + messages.Add(new { role = "user", content = prompt }); + + var payload = new + { + model = _Options.LocalChatModel, + messages, + stream = false + }; + + return (client, history, JsonSerializer.Serialize(payload)); + } + + private List ResolveHistory(string? previousResponseId) + { + if (string.IsNullOrWhiteSpace(previousResponseId)) + return []; + + return _ConversationHistory.TryGetValue(previousResponseId, out List? history) && history is not null + ? [.. history] + : []; + } + + private static (string Text, string ResponseId) ParseResponse(string body) + { + using var doc = JsonDocument.Parse(body); + var root = doc.RootElement; + + string responseId = root.TryGetProperty("id", out var idProp) && idProp.ValueKind == JsonValueKind.String + ? idProp.GetString() ?? Guid.NewGuid().ToString("N") + : Guid.NewGuid().ToString("N"); + + if (root.TryGetProperty("choices", out var choices) + && choices.ValueKind == JsonValueKind.Array + && choices.GetArrayLength() > 0) + { + var choice = choices[0]; + if (choice.TryGetProperty("message", out var message) + && message.TryGetProperty("content", out var content) + && content.ValueKind == JsonValueKind.String) + { + return (content.GetString() ?? string.Empty, responseId); + } + } + + if (root.TryGetProperty("message", out var ollamaMessage) + && ollamaMessage.TryGetProperty("content", out var ollamaContent) + && ollamaContent.ValueKind == JsonValueKind.String) + { + return (ollamaContent.GetString() ?? string.Empty, responseId); + } + + throw new InvalidOperationException("Local AI response did not contain any content."); + } + + [LoggerMessage(Level = LogLevel.Warning, Message = "Local chat request failed with status {StatusCode}. Body: {Body}")] + private static partial void LogLocalRequestFailed(ILogger logger, int statusCode, string body); + + private sealed record LocalChatMessage(string Role, string Content); +} diff --git a/EssentialCSharp.Chat.Shared/Services/UnavailableChatService.cs b/EssentialCSharp.Chat.Shared/Services/UnavailableChatService.cs new file mode 100644 index 00000000..c0212d07 --- /dev/null +++ b/EssentialCSharp.Chat.Shared/Services/UnavailableChatService.cs @@ -0,0 +1,42 @@ +using ModelContextProtocol.Client; +using OpenAI.Responses; + +namespace EssentialCSharp.Chat.Common.Services; + +public class UnavailableChatService : IChatCompletionService +{ + public bool IsAvailable => false; + public bool SupportsContextualSearch => false; + + public Task<(string response, string responseId)> GetChatCompletion( + string prompt, + string? systemPrompt = null, + string? previousResponseId = null, + McpClient? mcpClient = null, +#pragma warning disable OPENAI001 + IEnumerable? tools = null, + ResponseReasoningEffortLevel? reasoningEffortLevel = null, +#pragma warning restore OPENAI001 + bool enableContextualSearch = false, + string? endUserId = null, + CancellationToken cancellationToken = default) + { + throw new ChatBackendUnavailableException("Chat service is unavailable in this environment."); + } + + public IAsyncEnumerable<(string text, string? responseId)> GetChatCompletionStream( + string prompt, + string? systemPrompt = null, + string? previousResponseId = null, + McpClient? mcpClient = null, +#pragma warning disable OPENAI001 + IEnumerable? tools = null, + ResponseReasoningEffortLevel? reasoningEffortLevel = null, +#pragma warning restore OPENAI001 + bool enableContextualSearch = false, + string? endUserId = null, + CancellationToken cancellationToken = default) + { + throw new ChatBackendUnavailableException("Chat service is unavailable in this environment."); + } +} diff --git a/EssentialCSharp.Chat.Tests/LocalChatServiceTests.cs b/EssentialCSharp.Chat.Tests/LocalChatServiceTests.cs new file mode 100644 index 00000000..b5010487 --- /dev/null +++ b/EssentialCSharp.Chat.Tests/LocalChatServiceTests.cs @@ -0,0 +1,201 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using EssentialCSharp.Chat; +using EssentialCSharp.Chat.Common.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; + +namespace EssentialCSharp.Chat.Tests; + +public class LocalChatServiceTests +{ + [Test] + public async Task GetChatCompletion_BuildsExpectedRequest() + { + var requests = new List(); + var responses = new Queue(); + responses.Enqueue(CreateJsonResponse(""" + {"id":"resp-1","choices":[{"message":{"content":"hello back"}}]} + """)); + + var (service, client) = CreateService(new RecordingHttpMessageHandler(requests, responses)); + using var clientScope = client; + using var serviceScope = service; + + var (response, responseId) = await service.GetChatCompletion("hello"); + + await Assert.That(response).IsEqualTo("hello back"); + await Assert.That(responseId).IsEqualTo("resp-1"); + await Assert.That(requests.Count).IsEqualTo(1); + + var request = requests[0]; + await Assert.That(request.RequestUri!.AbsolutePath).IsEqualTo("/v1/chat/completions"); + await Assert.That(request.Headers.Authorization?.Scheme).IsEqualTo("Bearer"); + await Assert.That(request.Headers.Authorization?.Parameter).IsEqualTo("local-dev-key"); + + string requestBody = await request.Content!.ReadAsStringAsync(); + using var payload = JsonDocument.Parse(requestBody); + await Assert.That(payload.RootElement.GetProperty("model").GetString()).IsEqualTo("qwen2.5-coder:7b"); + await Assert.That(payload.RootElement.GetProperty("stream").GetBoolean()).IsFalse(); + + var messages = payload.RootElement.GetProperty("messages"); + await Assert.That(messages.GetArrayLength()).IsEqualTo(2); + await Assert.That(messages[0].GetProperty("role").GetString()).IsEqualTo("system"); + await Assert.That(messages[0].GetProperty("content").GetString()).IsEqualTo("system prompt"); + await Assert.That(messages[1].GetProperty("role").GetString()).IsEqualTo("user"); + await Assert.That(messages[1].GetProperty("content").GetString()).IsEqualTo("hello"); + } + + [Test] + public async Task LocalChatService_DoesNotSupportContextualSearch() + { + var (service, client) = CreateService(new RecordingHttpMessageHandler([], [])); + using var clientScope = client; + using var serviceScope = service; + + await Assert.That(service.SupportsContextualSearch).IsFalse(); + await Assert.ThrowsAsync(() => service.GetChatCompletion("hello", enableContextualSearch: true)); + } + + [Test] + [Arguments("non-success")] + [Arguments("invalid-payload")] + public async Task GetChatCompletion_WhenResponseIsInvalid_ThrowsChatBackendUnavailableException(string caseName) + { + var requests = new List(); + var responses = new Queue(); + responses.Enqueue(CreateFailureResponse(caseName)); + + var (service, client) = CreateService(new RecordingHttpMessageHandler(requests, responses)); + using var clientScope = client; + using var serviceScope = service; + + await Assert.ThrowsAsync(() => service.GetChatCompletion("hello")); + } + + [Test] + public async Task GetChatCompletion_ReusesConversationHistory_WhenPreviousResponseIdProvided() + { + var requests = new List(); + var responses = new Queue(); + responses.Enqueue(CreateJsonResponse(""" + {"id":"resp-1","choices":[{"message":{"content":"assistant one"}}]} + """)); + responses.Enqueue(CreateJsonResponse(""" + {"id":"resp-2","choices":[{"message":{"content":"assistant two"}}]} + """)); + + var (service, client) = CreateService(new RecordingHttpMessageHandler(requests, responses)); + using var clientScope = client; + using var serviceScope = service; + + var first = await service.GetChatCompletion("first"); + _ = await service.GetChatCompletion("second", previousResponseId: first.responseId); + + await Assert.That(requests.Count).IsEqualTo(2); + + string firstBody = await requests[0].Content!.ReadAsStringAsync(); + using var firstPayload = JsonDocument.Parse(firstBody); + var firstMessages = firstPayload.RootElement.GetProperty("messages"); + await Assert.That(firstMessages.GetArrayLength()).IsEqualTo(2); + + string secondBody = await requests[1].Content!.ReadAsStringAsync(); + using var secondPayload = JsonDocument.Parse(secondBody); + var secondMessages = secondPayload.RootElement.GetProperty("messages"); + await Assert.That(secondMessages.GetArrayLength()).IsEqualTo(4); + await Assert.That(secondMessages[1].GetProperty("role").GetString()).IsEqualTo("user"); + await Assert.That(secondMessages[1].GetProperty("content").GetString()).IsEqualTo("first"); + await Assert.That(secondMessages[2].GetProperty("role").GetString()).IsEqualTo("assistant"); + await Assert.That(secondMessages[2].GetProperty("content").GetString()).IsEqualTo("assistant one"); + await Assert.That(secondMessages[3].GetProperty("role").GetString()).IsEqualTo("user"); + await Assert.That(secondMessages[3].GetProperty("content").GetString()).IsEqualTo("second"); + } + + [Test] + public async Task GetChatCompletionStream_EmitsResponseTextAndFinalResponseId() + { + var requests = new List(); + var responses = new Queue(); + responses.Enqueue(CreateJsonResponse(""" + {"id":"stream-resp-1","choices":[{"message":{"content":"streamed response text"}}]} + """)); + + var (service, client) = CreateService(new RecordingHttpMessageHandler(requests, responses)); + using var clientScope = client; + using var serviceScope = service; + + var streamItems = new List<(string text, string? responseId)>(); + await foreach (var item in service.GetChatCompletionStream("stream prompt")) + { + streamItems.Add(item); + } + + await Assert.That(streamItems.Count).IsEqualTo(2); + await Assert.That(streamItems[0].text).IsEqualTo("streamed response text"); + await Assert.That(streamItems[0].responseId).IsNull(); + await Assert.That(streamItems[1].text).IsEmpty(); + await Assert.That(streamItems[1].responseId).IsEqualTo("stream-resp-1"); + } + + private static (LocalChatService Service, HttpClient Client) CreateService(HttpMessageHandler handler) + { + var options = Options.Create(new AIOptions + { + LocalChatModel = "qwen2.5-coder:7b", + SystemPrompt = "system prompt" + }); + + var client = new HttpClient(handler) + { + BaseAddress = new Uri("http://localhost:11434") + }; + + var httpClientFactory = new Mock(); + httpClientFactory + .Setup(f => f.CreateClient("LocalAIChat")) + .Returns(client); + + var logger = Mock.Of>(); + return (new LocalChatService(options, httpClientFactory.Object, logger), client); + } + + private static HttpResponseMessage CreateJsonResponse(string json) => + new(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + + private static HttpResponseMessage CreateFailureResponse(string caseName) => caseName switch + { + "non-success" => new HttpResponseMessage(HttpStatusCode.BadGateway) + { + Content = new StringContent("upstream error", Encoding.UTF8, "text/plain") + }, + "invalid-payload" => CreateJsonResponse("""{"id":"resp-1","choices":[]}"""), + _ => throw new ArgumentOutOfRangeException(nameof(caseName), caseName, "Unknown failure case.") + }; + + private sealed class RecordingHttpMessageHandler( + List requests, + Queue responses) : HttpMessageHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var snapshot = new HttpRequestMessage(request.Method, request.RequestUri) + { + Content = request.Content is null + ? null + : new StringContent(await request.Content.ReadAsStringAsync(cancellationToken), Encoding.UTF8, request.Content.Headers.ContentType?.MediaType) + }; + if (request.Headers.Authorization is not null) + { + snapshot.Headers.Authorization = request.Headers.Authorization; + } + + requests.Add(snapshot); + return responses.Dequeue(); + } + } +} diff --git a/EssentialCSharp.Chat.Tests/ServiceCollectionExtensionsTests.cs b/EssentialCSharp.Chat.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..53272ffa --- /dev/null +++ b/EssentialCSharp.Chat.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,105 @@ +using EssentialCSharp.Chat.Common.Extensions; +using EssentialCSharp.Chat.Common.Models; +using EssentialCSharp.Chat.Common.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace EssentialCSharp.Chat.Tests; + +public class ServiceCollectionExtensionsTests +{ + [Test] + public async Task AddConfiguredChatServices_WithFullAzureConfig_SelectsAzureChatService_AndBindsEmbeddingRetry() + { + var services = CreateServices(new Dictionary + { + ["AIOptions:Endpoint"] = "https://example.openai.azure.com", + ["AIOptions:ChatDeploymentName"] = "chat-deployment", + ["AIOptions:VectorGenerationDeploymentName"] = "embedding-deployment", + ["ConnectionStrings:PostgresVectorStore"] = "Host=localhost;Database=test;Username=test;Password=test", + ["AIOptions:EmbeddingRetry:MaxRetries"] = "7" + }); + + var descriptor = GetChatServiceDescriptor(services); + await Assert.That(descriptor.ImplementationFactory).IsNotNull(); + await Assert.That(services.Any(d => d.ServiceType == typeof(AIChatService))).IsTrue(); + + using var provider = services.BuildServiceProvider(); + var retry = provider.GetRequiredService>().Value; + await Assert.That(retry.MaxRetries).IsEqualTo(7); + } + + [Test] + [Arguments("invalid-azure-falls-back-to-local")] + [Arguments("valid-local")] + [Arguments("connection-string-fallback")] + [Arguments("invalid-local")] + [Arguments("missing-config")] + public async Task AddConfiguredChatServices_SelectsExpectedBackend(string scenario) + { + var (configValues, expectedChatServiceType) = CreateBackendSelectionScenario(scenario); + var services = CreateServices(configValues); + + var descriptor = GetChatServiceDescriptor(services); + await Assert.That(descriptor.ImplementationType).IsEqualTo(expectedChatServiceType); + } + + private static ServiceCollection CreateServices(Dictionary values) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + var services = new ServiceCollection(); + services.AddLogging(); + services.AddConfiguredChatServices(configuration); + return services; + } + + private static ServiceDescriptor GetChatServiceDescriptor(IServiceCollection services) => + services.Single(d => d.ServiceType == typeof(IChatCompletionService)); + + private static (Dictionary ConfigValues, Type ExpectedChatServiceType) CreateBackendSelectionScenario(string scenario) => scenario switch + { + "invalid-azure-falls-back-to-local" => ( + new Dictionary + { + ["AIOptions:Endpoint"] = "not-a-valid-uri", + ["AIOptions:ChatDeploymentName"] = "chat-deployment", + ["AIOptions:VectorGenerationDeploymentName"] = "embedding-deployment", + ["ConnectionStrings:PostgresVectorStore"] = "Host=localhost;Database=test;Username=test;Password=test", + ["AIOptions:UseLocalAI"] = "true", + ["AIOptions:LocalEndpoint"] = "http://localhost:11434", + ["AIOptions:LocalChatModel"] = "qwen2.5-coder:7b" + }, + typeof(LocalChatService)), + "valid-local" => ( + new Dictionary + { + ["AIOptions:UseLocalAI"] = "true", + ["AIOptions:LocalEndpoint"] = "http://localhost:11434", + ["AIOptions:LocalChatModel"] = "qwen2.5-coder:7b" + }, + typeof(LocalChatService)), + "connection-string-fallback" => ( + new Dictionary + { + ["ConnectionStrings:ollama-chat"] = "http://localhost:11434", + ["AIOptions:LocalChatModel"] = "qwen2.5-coder:7b" + }, + typeof(LocalChatService)), + "invalid-local" => ( + new Dictionary + { + ["AIOptions:UseLocalAI"] = "true", + ["AIOptions:LocalEndpoint"] = "invalid-uri", + ["AIOptions:LocalChatModel"] = "qwen2.5-coder:7b" + }, + typeof(UnavailableChatService)), + "missing-config" => ( + new Dictionary(), + typeof(UnavailableChatService)), + _ => throw new ArgumentOutOfRangeException(nameof(scenario), scenario, "Unknown backend selection scenario.") + }; +} diff --git a/EssentialCSharp.Web.Tests/ChatAvailabilityTests.cs b/EssentialCSharp.Web.Tests/ChatAvailabilityTests.cs new file mode 100644 index 00000000..74c6d919 --- /dev/null +++ b/EssentialCSharp.Web.Tests/ChatAvailabilityTests.cs @@ -0,0 +1,33 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; + +namespace EssentialCSharp.Web.Tests; + +public class ChatAvailabilityTests : IntegrationTestBase +{ + private const string HCaptchaTestToken = "10000000-aaaa-bbbb-cccc-000000000001"; + + [Test] + [Arguments("/api/chat/message", "chat-unavailable-message")] + [Arguments("/api/chat/stream", "chat-unavailable-stream")] + public async Task ChatEndpoint_WhenBackendUnavailable_Returns503WithContract(string endpoint, string userNameSeed) + { + HttpClient client = McpTestHelper.CreateClient(Factory); + + string userId = await McpTestHelper.CreateUserAsync(Factory, userNameSeed); + (string cookieName, string cookieValue) = await McpTestHelper.CreateIdentityApplicationCookieAsync(Factory, userId); + + using var request = new HttpRequestMessage(HttpMethod.Post, endpoint) + { + Content = JsonContent.Create(new { message = "Hello", enableContextualSearch = false, captchaResponse = HCaptchaTestToken }) + }; + McpTestHelper.AddCookie(request, cookieName, cookieValue); + + using HttpResponseMessage response = await client.SendAsync(request); + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.ServiceUnavailable); + + using JsonDocument payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + await Assert.That(payload.RootElement.GetProperty("errorCode").GetString()).IsEqualTo("chat_unavailable"); + } +} diff --git a/EssentialCSharp.Web.Tests/WebApplicationFactory.cs b/EssentialCSharp.Web.Tests/WebApplicationFactory.cs index cf7a978e..f5f80972 100644 --- a/EssentialCSharp.Web.Tests/WebApplicationFactory.cs +++ b/EssentialCSharp.Web.Tests/WebApplicationFactory.cs @@ -1,5 +1,7 @@ using System.Data.Common; +using EssentialCSharp.Chat.Common.Services; using EssentialCSharp.Web.Data; +using EssentialCSharp.Web.Models; using EssentialCSharp.Web.Services; using TUnit.AspNetCore; using Microsoft.AspNetCore.Hosting; @@ -98,6 +100,16 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.RemoveAll(); services.AddSingleton( _ => TestListingSourceCodeServiceHelper.CreateService()); + + // Override IChatCompletionService with UnavailableChatService so tests are not + // affected by developer AI configuration or environment variables. + services.RemoveAll(); + services.AddSingleton(); + + // Keep chat contract tests off the real hCaptcha network path so failures reflect + // chat availability behavior rather than captcha service reachability. + services.RemoveAll(); + services.AddSingleton(); }); } @@ -159,4 +171,19 @@ public async Task StartAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } + + private sealed class PassingCaptchaService : ICaptchaService + { + private static readonly HCaptchaResult _Success = new() + { + Success = true, + Hostname = "localhost" + }; + + public Task VerifyAsync(string? response, CancellationToken cancellationToken = default) => + Task.FromResult(_Success); + + public Task VerifyAsync(string? response, string? remoteIp, CancellationToken cancellationToken = default) => + Task.FromResult(_Success); + } } diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs index 998ee7cb..16c16154 100644 --- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using EssentialCSharp.Web.Areas.Identity.Data; using EssentialCSharp.Web.Models; using EssentialCSharp.Web.Services; @@ -68,7 +68,9 @@ public async Task OnPostAsync(string? returnUrl = null) returnUrl ??= Url.Content("~/"); string? captchaToken = Request.Form[CaptchaOptions.HttpPostResponseKeyName]; + HCaptchaResult? captchaResult = await captchaService.VerifyAsync(captchaToken, HttpContext.Connection.RemoteIpAddress?.ToString()); + if (captchaResult?.Success != true) { ModelState.AddModelError(string.Empty, "Human verification failed. Please try again."); diff --git a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Register.cshtml.cs b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Register.cshtml.cs index a29d2254..5527cd0c 100644 --- a/EssentialCSharp.Web/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/EssentialCSharp.Web/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -96,6 +96,7 @@ public async Task OnPostAsync(string? returnUrl = null) } HCaptchaResult? response = await captchaService.VerifyAsync(hCaptcha_response, HttpContext.Connection.RemoteIpAddress?.ToString()); + if (response is null) { ModelState.AddModelError(string.Empty, "Captcha verification is temporarily unavailable. Please try again later."); diff --git a/EssentialCSharp.Web/Controllers/ChatController.cs b/EssentialCSharp.Web/Controllers/ChatController.cs index 08f0d29a..7cfcb636 100644 --- a/EssentialCSharp.Web/Controllers/ChatController.cs +++ b/EssentialCSharp.Web/Controllers/ChatController.cs @@ -18,17 +18,17 @@ namespace EssentialCSharp.Web.Controllers; [IgnoreAntiforgeryToken] public partial class ChatController : ControllerBase { - private readonly AIChatService _AIChatService; + private readonly IChatCompletionService _ChatService; private readonly ResponseIdValidationService _ResponseIdValidationService; private readonly ICaptchaService _CaptchaService; private readonly CaptchaOptions _CaptchaOptions; private readonly ILogger _Logger; - public ChatController(ILogger logger, AIChatService aiChatService, + public ChatController(ILogger logger, IChatCompletionService chatService, ResponseIdValidationService responseIdValidationService, ICaptchaService captchaService, IOptions captchaOptions) { - _AIChatService = aiChatService; + _ChatService = chatService; _ResponseIdValidationService = responseIdValidationService; _CaptchaService = captchaService; _CaptchaOptions = captchaOptions.Value; @@ -87,9 +87,15 @@ public async Task SendMessage([FromBody] ChatMessageRequest reque if (!_ResponseIdValidationService.ValidateResponseId(userId, previousResponseId)) return BadRequest(new { error = "Invalid conversation context." }); + if (!_ChatService.IsAvailable) + return StatusCode(StatusCodes.Status503ServiceUnavailable, new { error = "Chat service unavailable", errorCode = "chat_unavailable" }); + + if (request.EnableContextualSearch && !_ChatService.SupportsContextualSearch) + return BadRequest(new { error = "Contextual search is not supported by the selected chat backend.", errorCode = "contextual_search_unsupported" }); + try { - var (response, responseId) = await _AIChatService.GetChatCompletion( + var (response, responseId) = await _ChatService.GetChatCompletion( prompt: request.Message, previousResponseId: previousResponseId, enableContextualSearch: request.EnableContextualSearch, @@ -109,6 +115,10 @@ public async Task SendMessage([FromBody] ChatMessageRequest reque { return BadRequest(new { error = "This conversation has grown too long. Please start a new one.", errorCode = "context_limit_exceeded" }); } + catch (ChatBackendUnavailableException) + { + return StatusCode(StatusCodes.Status503ServiceUnavailable, new { error = "Chat service unavailable", errorCode = "chat_unavailable" }); + } } [HttpPost("stream")] @@ -155,13 +165,29 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat return; } + if (!_ChatService.IsAvailable) + { + Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + Response.ContentType = "application/json"; + await Response.WriteAsJsonAsync(new { error = "Chat service unavailable", errorCode = "chat_unavailable" }, cancellationToken); + return; + } + + if (request.EnableContextualSearch && !_ChatService.SupportsContextualSearch) + { + Response.StatusCode = StatusCodes.Status400BadRequest; + Response.ContentType = "application/json"; + await Response.WriteAsJsonAsync(new { error = "Contextual search is not supported by the selected chat backend.", errorCode = "contextual_search_unsupported" }, cancellationToken); + return; + } + Response.ContentType = "text/event-stream"; Response.Headers.CacheControl = "no-cache"; Response.Headers.Connection = "keep-alive"; try { - await foreach (var (text, responseId) in _AIChatService.GetChatCompletionStream( + await foreach (var (text, responseId) in _ChatService.GetChatCompletionStream( prompt: request.Message, previousResponseId: previousResponseId, enableContextualSearch: request.EnableContextualSearch, @@ -247,6 +273,26 @@ public async Task StreamMessage([FromBody] ChatMessageRequest request, Cancellat } } } + catch (ChatBackendUnavailableException ex) when (!Response.HasStarted) + { + LogChatStreamErrorBeforeResponseStarted(_Logger, ex, User.Identity?.Name); + Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + Response.ContentType = "application/json"; + await Response.WriteAsJsonAsync(new { error = "Chat service unavailable", errorCode = "chat_unavailable" }, cancellationToken); + } + catch (ChatBackendUnavailableException ex) + { + LogChatStreamErrorMidStream(_Logger, ex, User.Identity?.Name); + try + { + await Response.WriteAsync("data: {\"type\":\"error\",\"message\":\"Chat service unavailable\",\"errorCode\":\"chat_unavailable\"}\n\n", CancellationToken.None); + await Response.Body.FlushAsync(CancellationToken.None); + } + catch (Exception writeException) when (writeException is IOException or OperationCanceledException or ObjectDisposedException) + { + // Best-effort write to an already-streaming response. + } + } catch (Exception ex) when (!Response.HasStarted) { LogChatStreamErrorBeforeResponseStarted(_Logger, ex, User.Identity?.Name); diff --git a/EssentialCSharp.Web/Program.cs b/EssentialCSharp.Web/Program.cs index f5c980ad..dc7f0185 100644 --- a/EssentialCSharp.Web/Program.cs +++ b/EssentialCSharp.Web/Program.cs @@ -274,11 +274,8 @@ private static void Main(string[] args) builder.Services.AddSingleton(); builder.Services.AddScoped(); - // Add AI Chat services - if (!builder.Environment.IsDevelopment()) - { - builder.Services.AddAzureOpenAIServices(configuration); - } + // Add AI Chat services using configuration-driven backend selection. + builder.Services.AddConfiguredChatServices(configuration); // MCP server — always enabled, authenticated via opaque DB-backed tokens. builder.Services.AddScoped(); diff --git a/EssentialCSharp.Web/wwwroot/js/chapter-listings.json b/EssentialCSharp.Web/wwwroot/js/chapter-listings.json new file mode 100644 index 00000000..902633b3 --- /dev/null +++ b/EssentialCSharp.Web/wwwroot/js/chapter-listings.json @@ -0,0 +1,3135 @@ +{ + "chapters": { + "Chapter01": [ + { + "filename": "01.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.04.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.05.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.07.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "01.08.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.09.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.10.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.11.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.12.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.13.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.14.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.15.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.16.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.17.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.18.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.19.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.20.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.21.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "01.22.cs", + "can_compile": false, + "can_run": true + } + ], + "Chapter02": [ + { + "filename": "02.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.04.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.05.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.08.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.09.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.10.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.11.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.12.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.13.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.14.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.15.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.16.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.17.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.18.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.19.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.20.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.21.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.22.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.23.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.24.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.25.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.26.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.27.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.28.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.29.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.30.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.31.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.32.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.33.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.34.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.35.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "02.36.cs", + "can_compile": true, + "can_run": true + } + ], + "Chapter03": [ + { + "filename": "03.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.04.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "03.05.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.08.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.09.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.10.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.11.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.12.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.13.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.14.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.15.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.16.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.17.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.18.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.19.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.20.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.21.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.22.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.23.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.24.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.25.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.26.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.27.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.28.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.29.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.30.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "03.31.cs", + "can_compile": true, + "can_run": true + } + ], + "Chapter04": [ + { + "filename": "04.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.04.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.05.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.08.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.09.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.10.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.11.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.12.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.13.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.14.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.15.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.16.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.17.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.18.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.19.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.20.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.21.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.22.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.23.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.24.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.25.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.26.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.27.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.28.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.29.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.30.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.31.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.32.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.33.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.34.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.35.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.36.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.37.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.38.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.39.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "04.40.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.41.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.42.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.43.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.44.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.45.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.46.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.47.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.48.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.49.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.50.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.51.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.52.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.53.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.54.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.55.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.56.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.57.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.58.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.59.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.60.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.61.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.62.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "04.63.cs", + "can_compile": true, + "can_run": true + } + ], + "Chapter05": [ + { + "filename": "05.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.04.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.05.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.08.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.09.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "05.10.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.12.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.13.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.14.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.15.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.16.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.17.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.18.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.19.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.20.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.21.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.22.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.23.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.24.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.25.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.26.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.27.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.28.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.29.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.30.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.31.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.32.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.33.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "05.34.cs", + "can_compile": true, + "can_run": true + } + ], + "Chapter06": [ + { + "filename": "06.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.02.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "06.03.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "06.04.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.05.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.06.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "06.07.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.08.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.09.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.10.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.11.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.12.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.13.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "06.14.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.15.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.16.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.17.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.18.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.19.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.20.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.21.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.22.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.23.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.24.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "06.25.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "06.26.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.27.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "06.28.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.29.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "06.30.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.31.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "06.32.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.33.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.34.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.35.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.36.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.37.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.38.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.39.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "06.40.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.41.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.42.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.43.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.44.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "06.45.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.46.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.47.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.48.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.49.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.50.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.51.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.52.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.53.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.54.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.55.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.56.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.57.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "06.58.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "06.59.cs", + "can_compile": true, + "can_run": false + } + ], + "Chapter07": [ + { + "filename": "07.01.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.02.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "07.03.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.04.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "07.05.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "07.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "07.08.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.09.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.10.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "07.11.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.12.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "07.13.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.14.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.15.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.16.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "07.17.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.18.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "07.19.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.20.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "07.21.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.22.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.23.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.24.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.25.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.26.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.27.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "07.28.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "07.29.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.30.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.31.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "07.32.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "07.33.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "07.34.cs", + "can_compile": true, + "can_run": false + } + ], + "Chapter08": [ + { + "filename": "08.01.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "08.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "08.03.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "08.04.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "08.05.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "08.06.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "08.07.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "08.08.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "08.09.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "08.10.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "08.11.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "08.12.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "08.13.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "08.14.cs", + "can_compile": true, + "can_run": true + } + ], + "Chapter09": [ + { + "filename": "09.01.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.02.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.03.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "09.04.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.05.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "09.06.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "09.07.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "09.08.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.09.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.10.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "09.11.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "09.12.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.13.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "09.14.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.15.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "09.16.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.17.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.18.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.19.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.20.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.21.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.22.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.23.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.24.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.25.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "09.26.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "09.27.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "09.28.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "09.29.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "09.30.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.31.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.32.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "09.33.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "09.34.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "09.35.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.36.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "09.37.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "09.38.cs", + "can_compile": true, + "can_run": true + } + ], + "Chapter10": [ + { + "filename": "10.01.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.02.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.03.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "10.04.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.05.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.06.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.07.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "10.08.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.09.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.10.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.11.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.12.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "10.14.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.15.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.16.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "10.17.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "10.18.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "10.19.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "10.20.cs", + "can_compile": false, + "can_run": false + } + ], + "Chapter11": [ + { + "filename": "11.01.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "11.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "11.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "11.04.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "11.05.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "11.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "11.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "11.08.cs", + "can_compile": true, + "can_run": true + } + ], + "Chapter12": [ + { + "filename": "12.01.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "12.03.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.04.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.05.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "12.07.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.08.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.09.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.10.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.11.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.12.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.13.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.14.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.15.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "12.16.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.17.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "12.18.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.19.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.20.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.21.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.22.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.23.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.24.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.25.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.26.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.27.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.28.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.29.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.30.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.31.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.32.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.33.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.34.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.35.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.36.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "12.37.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "12.38.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "12.39.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.40.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "12.41.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "12.42.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "12.43.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "12.44.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "12.45.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "12.46.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.47.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.48.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "12.49.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "12.50.cs", + "can_compile": true, + "can_run": false + } + ], + "Chapter13": [ + { + "filename": "13.01.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "13.02.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "13.03.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "13.04.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "13.05.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "13.06.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "13.07.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "13.08.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "13.09.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "13.10.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "13.11.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "13.12.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "13.13.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "13.14.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "13.15.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "13.16.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "13.17.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "13.18.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "13.19.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "13.20.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "13.21.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "13.22.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "13.23.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "13.24.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "13.25.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "13.26.cs", + "can_compile": true, + "can_run": true + } + ], + "Chapter14": [ + { + "filename": "14.01.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "14.02.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "14.03.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "14.04.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "14.05.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "14.06.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "14.07.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "14.08.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "14.09.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "14.10.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "14.11.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "14.12.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "14.13.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "14.14.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "14.15.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "14.16.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "14.17.cs", + "can_compile": true, + "can_run": false + } + ], + "Chapter15": [ + { + "filename": "15.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.04.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.05.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.08.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.09.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.10.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.11.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.12.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.13.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.14.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.15.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.16.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.17.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.18.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.19.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.20.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.21.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.22.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.23.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "15.24.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.25.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.26.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.27.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.28.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "15.29.cs", + "can_compile": true, + "can_run": true + } + ], + "Chapter16": [ + { + "filename": "16.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "16.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "16.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "16.04.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "16.05.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "16.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "16.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "16.08.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "16.09.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "16.10.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "16.11.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "16.12.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "16.13.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "16.14.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "16.15.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "16.16.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "16.17.cs", + "can_compile": false, + "can_run": true + } + ], + "Chapter17": [ + { + "filename": "17.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "17.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "17.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "17.04.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "17.05.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "17.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "17.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "17.08.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "17.09.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "17.10.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "17.11.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "17.12.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "17.13.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "17.14.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "17.15.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "17.16.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "17.17.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "17.18.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "17.19.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "17.20.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "17.21.cs", + "can_compile": false, + "can_run": true + } + ], + "Chapter18": [ + { + "filename": "18.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "18.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "18.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "18.04.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.05.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "18.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "18.07.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.08.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.09.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.10.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "18.11.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.12.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.13.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.14.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.15.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.16.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "18.17.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.18.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "18.19.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.20.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.21.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.22.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.23.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "18.24.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "18.25.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "18.26.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.27.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.28.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "18.29.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "18.30.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "18.31.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "18.32.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "18.33.cs", + "can_compile": false, + "can_run": false + } + ], + "Chapter19": [ + { + "filename": "19.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "19.02.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "19.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "19.04.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "19.05.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "19.06.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "19.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "19.08.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "19.09.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "19.10.cs", + "can_compile": false, + "can_run": true + } + ], + "Chapter20": [ + { + "filename": "20.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "20.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "20.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "20.04.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "20.05.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "20.06.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "20.07.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "20.08.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "20.09.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "20.10.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "20.11.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "20.12.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "20.13.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "20.14.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "20.15.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "20.16.cs", + "can_compile": false, + "can_run": false + } + ], + "Chapter21": [ + { + "filename": "21.01.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "21.02.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "21.03.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "21.04.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "21.05.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "21.06.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "21.07.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "21.08.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "21.09.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "21.10.cs", + "can_compile": false, + "can_run": true + } + ], + "Chapter22": [ + { + "filename": "22.01.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "22.02.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "22.03.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "22.04.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "22.05.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "22.06.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "22.07.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "22.08.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "22.09.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "22.10.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "22.11.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "22.12.cs", + "can_compile": true, + "can_run": true + }, + { + "filename": "22.13.cs", + "can_compile": true, + "can_run": false + } + ], + "Chapter23": [ + { + "filename": "23.01.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "23.02.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "23.03.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "23.04.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "23.05.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "23.06.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "23.07.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "23.08.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "23.09.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "23.10.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "23.11.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "23.13.cs", + "can_compile": true, + "can_run": false + }, + { + "filename": "23.14.cs", + "can_compile": false, + "can_run": false + }, + { + "filename": "23.15.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "23.16.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "23.17.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "23.18.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "23.19.cs", + "can_compile": false, + "can_run": true + }, + { + "filename": "23.20.cs", + "can_compile": false, + "can_run": true + } + ] + } +}