diff --git a/Directory.Build.props b/Directory.Build.props index 57f06a40f636..55f8a7d31e96 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,6 +26,7 @@ false false false + false @@ -256,6 +257,13 @@ $(MauiPreviousPlatforms) $(MauiGraphicsPreviousPlatforms);net$(_MauiPreviousDotNetVersion)-macos$(MacosPreviousTargetFrameworkVersion) + + + $(MauiPlatforms) + $(MauiEssentialsAIPlatforms);net$(_MauiDotNetVersion)-macos$(MacosTargetFrameworkVersion) + + $(MauiPreviousPlatforms) + $(MauiEssentialsAIPreviousPlatforms);net$(_MauiPreviousDotNetVersion)-macos$(MacosPreviousTargetFrameworkVersion) diff --git a/Microsoft.Maui-dev.sln b/Microsoft.Maui-dev.sln index 794ff6270d15..9af223fe1b84 100644 --- a/Microsoft.Maui-dev.sln +++ b/Microsoft.Maui-dev.sln @@ -246,6 +246,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Maui.Controls.Sample.Embedd EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.ManualTests", "src\Controls\tests\ManualTests\Controls.ManualTests.csproj", "{E2BFD1F1-07A8-8DBE-3661-894D0FE37D9C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AI", "AI", "{BA58FF10-E1F2-1F22-EED5-4647CFF8BF60}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9B194A14-0963-842E-BCDD-4A2CBB559451}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{81F26DD8-70CC-7F24-050C-594BCCA2AEBB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BE0B3DF2-F37C-449D-F624-39E4FE56170C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI", "src\AI\src\Essentials.AI\Essentials.AI.csproj", "{376DAEDF-D41D-4AF4-9548-9DDC9538D533}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.Sample", "src\AI\samples\Essentials.AI.Sample\Essentials.AI.Sample.csproj", "{7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.Benchmarks", "src\AI\tests\Essentials.AI.Benchmarks\Essentials.AI.Benchmarks.csproj", "{B578E852-193C-4F37-ACB1-2B9B1B32F6B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.DeviceTests", "src\AI\tests\Essentials.AI.DeviceTests\Essentials.AI.DeviceTests.csproj", "{90B0A751-82EA-4575-B22C-256EF3A4FEF1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.UnitTests", "src\AI\tests\Essentials.AI.UnitTests\Essentials.AI.UnitTests.csproj", "{BD5D52FA-CA8E-4236-979A-95B4CE4158D8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -635,6 +653,26 @@ Global {E2BFD1F1-07A8-8DBE-3661-894D0FE37D9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {E2BFD1F1-07A8-8DBE-3661-894D0FE37D9C}.Release|Any CPU.ActiveCfg = Release|Any CPU {E2BFD1F1-07A8-8DBE-3661-894D0FE37D9C}.Release|Any CPU.Build.0 = Release|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Debug|Any CPU.Build.0 = Debug|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Release|Any CPU.ActiveCfg = Release|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Release|Any CPU.Build.0 = Release|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Release|Any CPU.Build.0 = Release|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Release|Any CPU.Build.0 = Release|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Release|Any CPU.Build.0 = Release|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -748,6 +786,14 @@ Global {55905937-1399-46DB-BA38-E426801CB759} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C} {4ADCBA87-30DB-44F5-85E9-94A4F4132FD9} = {E1082E26-D700-4127-9329-66D673FD2D55} {E2BFD1F1-07A8-8DBE-3661-894D0FE37D9C} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} + {9B194A14-0963-842E-BCDD-4A2CBB559451} = {BA58FF10-E1F2-1F22-EED5-4647CFF8BF60} + {81F26DD8-70CC-7F24-050C-594BCCA2AEBB} = {BA58FF10-E1F2-1F22-EED5-4647CFF8BF60} + {BE0B3DF2-F37C-449D-F624-39E4FE56170C} = {BA58FF10-E1F2-1F22-EED5-4647CFF8BF60} + {376DAEDF-D41D-4AF4-9548-9DDC9538D533} = {9B194A14-0963-842E-BCDD-4A2CBB559451} + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0} = {81F26DD8-70CC-7F24-050C-594BCCA2AEBB} + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6} = {BE0B3DF2-F37C-449D-F624-39E4FE56170C} + {90B0A751-82EA-4575-B22C-256EF3A4FEF1} = {BE0B3DF2-F37C-449D-F624-39E4FE56170C} + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8} = {BE0B3DF2-F37C-449D-F624-39E4FE56170C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50} diff --git a/Microsoft.Maui-mac.slnf b/Microsoft.Maui-mac.slnf index d459143e2a5a..623e1571661a 100644 --- a/Microsoft.Maui-mac.slnf +++ b/Microsoft.Maui-mac.slnf @@ -2,6 +2,11 @@ "solution": { "path": "Microsoft.Maui-dev.sln", "projects": [ + "src\\AI\\samples\\Essentials.AI.Sample\\Essentials.AI.Sample.csproj", + "src\\AI\\src\\Essentials.AI\\Essentials.AI.csproj", + "src\\AI\\tests\\Essentials.AI.Benchmarks\\Essentials.AI.Benchmarks.csproj", + "src\\AI\\tests\\Essentials.AI.DeviceTests\\Essentials.AI.DeviceTests.csproj", + "src\\AI\\tests\\Essentials.AI.UnitTests\\Essentials.AI.UnitTests.csproj", "src\\BlazorWebView\\samples\\MauiRazorClassLibrarySample\\MauiRazorClassLibrarySample.csproj", "src\\BlazorWebView\\samples\\WebViewAppShared\\WebViewAppShared.csproj", "src\\BlazorWebView\\src\\Maui\\Microsoft.AspNetCore.Components.WebView.Maui.csproj", diff --git a/Microsoft.Maui-vscode.sln b/Microsoft.Maui-vscode.sln index 7217d8c92cfd..3e88b3db593e 100644 --- a/Microsoft.Maui-vscode.sln +++ b/Microsoft.Maui-vscode.sln @@ -220,6 +220,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGen.UnitTests", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Maui.Controls.Xaml.Benchmarks", "src\Controls\tests\Xaml.Benchmarks\Microsoft.Maui.Controls.Xaml.Benchmarks.csproj", "{9A0A5037-DB03-4C80-876C-61FCDAE4CCAD}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AI", "AI", "{BA58FF10-E1F2-1F22-EED5-4647CFF8BF60}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9B194A14-0963-842E-BCDD-4A2CBB559451}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{81F26DD8-70CC-7F24-050C-594BCCA2AEBB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BE0B3DF2-F37C-449D-F624-39E4FE56170C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI", "src\AI\src\Essentials.AI\Essentials.AI.csproj", "{376DAEDF-D41D-4AF4-9548-9DDC9538D533}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.Sample", "src\AI\samples\Essentials.AI.Sample\Essentials.AI.Sample.csproj", "{7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.Benchmarks", "src\AI\tests\Essentials.AI.Benchmarks\Essentials.AI.Benchmarks.csproj", "{B578E852-193C-4F37-ACB1-2B9B1B32F6B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.DeviceTests", "src\AI\tests\Essentials.AI.DeviceTests\Essentials.AI.DeviceTests.csproj", "{90B0A751-82EA-4575-B22C-256EF3A4FEF1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.UnitTests", "src\AI\tests\Essentials.AI.UnitTests\Essentials.AI.UnitTests.csproj", "{BD5D52FA-CA8E-4236-979A-95B4CE4158D8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -563,6 +581,26 @@ Global {9A0A5037-DB03-4C80-876C-61FCDAE4CCAD}.Debug|Any CPU.Build.0 = Debug|Any CPU {9A0A5037-DB03-4C80-876C-61FCDAE4CCAD}.Release|Any CPU.ActiveCfg = Release|Any CPU {9A0A5037-DB03-4C80-876C-61FCDAE4CCAD}.Release|Any CPU.Build.0 = Release|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Debug|Any CPU.Build.0 = Debug|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Release|Any CPU.ActiveCfg = Release|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Release|Any CPU.Build.0 = Release|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Release|Any CPU.Build.0 = Release|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Release|Any CPU.Build.0 = Release|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Release|Any CPU.Build.0 = Release|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -664,6 +702,14 @@ Global {4ADCBA87-30DB-44F5-85E9-94A4F4132FD9} = {E1082E26-D700-4127-9329-66D673FD2D55} {A426B2FC-F012-436B-BDD9-BEC0025DB96B} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} {9A0A5037-DB03-4C80-876C-61FCDAE4CCAD} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} + {9B194A14-0963-842E-BCDD-4A2CBB559451} = {BA58FF10-E1F2-1F22-EED5-4647CFF8BF60} + {81F26DD8-70CC-7F24-050C-594BCCA2AEBB} = {BA58FF10-E1F2-1F22-EED5-4647CFF8BF60} + {BE0B3DF2-F37C-449D-F624-39E4FE56170C} = {BA58FF10-E1F2-1F22-EED5-4647CFF8BF60} + {376DAEDF-D41D-4AF4-9548-9DDC9538D533} = {9B194A14-0963-842E-BCDD-4A2CBB559451} + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0} = {81F26DD8-70CC-7F24-050C-594BCCA2AEBB} + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6} = {BE0B3DF2-F37C-449D-F624-39E4FE56170C} + {90B0A751-82EA-4575-B22C-256EF3A4FEF1} = {BE0B3DF2-F37C-449D-F624-39E4FE56170C} + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8} = {BE0B3DF2-F37C-449D-F624-39E4FE56170C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50} diff --git a/Microsoft.Maui-windows.slnf b/Microsoft.Maui-windows.slnf index 708d9629a8d9..4ed4c36e2225 100644 --- a/Microsoft.Maui-windows.slnf +++ b/Microsoft.Maui-windows.slnf @@ -2,6 +2,11 @@ "solution": { "path": "Microsoft.Maui-dev.sln", "projects": [ + "src\\AI\\samples\\Essentials.AI.Sample\\Essentials.AI.Sample.csproj", + "src\\AI\\src\\Essentials.AI\\Essentials.AI.csproj", + "src\\AI\\tests\\Essentials.AI.Benchmarks\\Essentials.AI.Benchmarks.csproj", + "src\\AI\\tests\\Essentials.AI.DeviceTests\\Essentials.AI.DeviceTests.csproj", + "src\\AI\\tests\\Essentials.AI.UnitTests\\Essentials.AI.UnitTests.csproj", "src\\BlazorWebView\\samples\\BlazorWinFormsApp\\BlazorWinFormsApp.csproj", "src\\BlazorWebView\\samples\\BlazorWpfApp\\BlazorWpfApp.csproj", "src\\BlazorWebView\\samples\\MauiRazorClassLibrarySample\\MauiRazorClassLibrarySample.csproj", diff --git a/Microsoft.Maui.sln b/Microsoft.Maui.sln index fbbeaadc0200..37c133515923 100644 --- a/Microsoft.Maui.sln +++ b/Microsoft.Maui.sln @@ -251,6 +251,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Maui.Controls.Sample.Embedd EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.ManualTests", "src\Controls\tests\ManualTests\Controls.ManualTests.csproj", "{E2BFD1F1-07A8-8DBE-3661-894D0FE37D9C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AI", "AI", "{BA58FF10-E1F2-1F22-EED5-4647CFF8BF60}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9B194A14-0963-842E-BCDD-4A2CBB559451}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{81F26DD8-70CC-7F24-050C-594BCCA2AEBB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BE0B3DF2-F37C-449D-F624-39E4FE56170C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI", "src\AI\src\Essentials.AI\Essentials.AI.csproj", "{376DAEDF-D41D-4AF4-9548-9DDC9538D533}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.Sample", "src\AI\samples\Essentials.AI.Sample\Essentials.AI.Sample.csproj", "{7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.Benchmarks", "src\AI\tests\Essentials.AI.Benchmarks\Essentials.AI.Benchmarks.csproj", "{B578E852-193C-4F37-ACB1-2B9B1B32F6B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.DeviceTests", "src\AI\tests\Essentials.AI.DeviceTests\Essentials.AI.DeviceTests.csproj", "{90B0A751-82EA-4575-B22C-256EF3A4FEF1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Essentials.AI.UnitTests", "src\AI\tests\Essentials.AI.UnitTests\Essentials.AI.UnitTests.csproj", "{BD5D52FA-CA8E-4236-979A-95B4CE4158D8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -648,6 +666,26 @@ Global {E2BFD1F1-07A8-8DBE-3661-894D0FE37D9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {E2BFD1F1-07A8-8DBE-3661-894D0FE37D9C}.Release|Any CPU.ActiveCfg = Release|Any CPU {E2BFD1F1-07A8-8DBE-3661-894D0FE37D9C}.Release|Any CPU.Build.0 = Release|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Debug|Any CPU.Build.0 = Debug|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Release|Any CPU.ActiveCfg = Release|Any CPU + {376DAEDF-D41D-4AF4-9548-9DDC9538D533}.Release|Any CPU.Build.0 = Release|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0}.Release|Any CPU.Build.0 = Release|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6}.Release|Any CPU.Build.0 = Release|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90B0A751-82EA-4575-B22C-256EF3A4FEF1}.Release|Any CPU.Build.0 = Release|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -764,6 +802,14 @@ Global {F1BC506B-3A9E-4779-994E-339AFB21C9B9} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C} {4ADCBA87-30DB-44F5-85E9-94A4F4132FD9} = {E1082E26-D700-4127-9329-66D673FD2D55} {E2BFD1F1-07A8-8DBE-3661-894D0FE37D9C} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} + {9B194A14-0963-842E-BCDD-4A2CBB559451} = {BA58FF10-E1F2-1F22-EED5-4647CFF8BF60} + {81F26DD8-70CC-7F24-050C-594BCCA2AEBB} = {BA58FF10-E1F2-1F22-EED5-4647CFF8BF60} + {BE0B3DF2-F37C-449D-F624-39E4FE56170C} = {BA58FF10-E1F2-1F22-EED5-4647CFF8BF60} + {376DAEDF-D41D-4AF4-9548-9DDC9538D533} = {9B194A14-0963-842E-BCDD-4A2CBB559451} + {7A6CB0C2-B5A9-44BA-B774-D1E1C1E371D0} = {81F26DD8-70CC-7F24-050C-594BCCA2AEBB} + {B578E852-193C-4F37-ACB1-2B9B1B32F6B6} = {BE0B3DF2-F37C-449D-F624-39E4FE56170C} + {90B0A751-82EA-4575-B22C-256EF3A4FEF1} = {BE0B3DF2-F37C-449D-F624-39E4FE56170C} + {BD5D52FA-CA8E-4236-979A-95B4CE4158D8} = {BE0B3DF2-F37C-449D-F624-39E4FE56170C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50} diff --git a/eng/Microsoft.Maui.Packages-mac.slnf b/eng/Microsoft.Maui.Packages-mac.slnf index 5034642aaa1c..5529edb22fdb 100644 --- a/eng/Microsoft.Maui.Packages-mac.slnf +++ b/eng/Microsoft.Maui.Packages-mac.slnf @@ -2,6 +2,7 @@ "solution": { "path": "..\\Microsoft.Maui.sln", "projects": [ + "src\\AI\\src\\Essentials.AI\\Essentials.AI.csproj", "src\\BlazorWebView\\src\\Maui\\Microsoft.AspNetCore.Components.WebView.Maui.csproj", "src\\Compatibility\\Core\\src\\Compatibility.csproj", "src\\Controls\\Foldable\\src\\Controls.Foldable.csproj", diff --git a/eng/Microsoft.Maui.Packages.slnf b/eng/Microsoft.Maui.Packages.slnf index 45bbff7c3a3a..7c21491d97e3 100644 --- a/eng/Microsoft.Maui.Packages.slnf +++ b/eng/Microsoft.Maui.Packages.slnf @@ -2,6 +2,7 @@ "solution": { "path": "..\\Microsoft.Maui.sln", "projects": [ + "src\\AI\\src\\Essentials.AI\\Essentials.AI.csproj", "src\\BlazorWebView\\src\\Maui\\Microsoft.AspNetCore.Components.WebView.Maui.csproj", "src\\BlazorWebView\\src\\WindowsForms\\Microsoft.AspNetCore.Components.WebView.WindowsForms.csproj", "src\\BlazorWebView\\src\\Wpf\\Microsoft.AspNetCore.Components.WebView.Wpf.csproj", diff --git a/eng/NuGetVersions.targets b/eng/NuGetVersions.targets index 123b99d6c4ff..6063ac89b505 100644 --- a/eng/NuGetVersions.targets +++ b/eng/NuGetVersions.targets @@ -84,6 +84,14 @@ Update="System.Runtime.CompilerServices.Unsafe" Version="$(SystemRuntimeCompilerServicesUnsafePackageVersion)" /> + + + https://github.com/dotnet/templating 3f4da9ced34942d83054e647f3b1d9d7dde281e8 + + https://github.com/dotnet/dotnet + 7b29526f2107416f68578bcb9deaca74fcfcf7f0 + + + https://github.com/dotnet/dotnet + 7b29526f2107416f68578bcb9deaca74fcfcf7f0 + https://github.com/dotnet/dotnet 7b29526f2107416f68578bcb9deaca74fcfcf7f0 diff --git a/eng/Versions.props b/eng/Versions.props index 5295426aefa5..a3c239e7d6b4 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -40,6 +40,8 @@ $(MicrosoftNETCoreAppRefPackageVersion) $(MicrosoftNETCoreAppRefPackageVersion) + 10.0.0 + 10.0.0 10.0.0 10.0.0 10.0.0 @@ -52,6 +54,7 @@ 10.0.0 10.0.0 10.0.0 + 10.0.0 36.1.2 35.0.105 diff --git a/src/AI/docs/json-stream-chunker-design.md b/src/AI/docs/json-stream-chunker-design.md new file mode 100644 index 000000000000..3f553f9f3912 --- /dev/null +++ b/src/AI/docs/json-stream-chunker-design.md @@ -0,0 +1,1227 @@ +# JSON Stream Chunker - Complete Design Document + +## Problem Statement + +We have an AI model that receives progressive JSON internally but outputs complete, valid JSON objects each time. The property order may vary between chunks. We need to convert these complete JSON objects back into streaming chunks that, when concatenated, produce valid JSON matching the final output. + +**Input**: JSONL file where each line is a complete JSON object representing progressive construction +**Output**: Chunks that when concatenated produce valid JSON structurally equivalent to the final line + +### Real-World Example (from serengeti-itinerary-1.jsonl) + +``` +Line 1: {"days": [{"subtitle": "Day"}]} +Line 2: {"days": [{"subtitle": "Day 1: Arrival and Wildlife Safari", "activities": []}]} +Line 3: {"days": [{"subtitle": "Day 1: Arrival and Wildlife Safari", "activities": [{"title": "", "type": "Sightseeing"}]}]} +Line 4: {"days": [{"activities": [{"type": "Sightseeing", "description": "Embark", "title": "Morning Game Drive"}], "subtitle": "Day 1: Arrival and Wildlife Safari"}]} +... +``` + +**Observations from real data:** +1. Property order changes between chunks (subtitle moves around) +2. Strings grow progressively ("Day" → "Day 1: Arrival and Wildlife Safari") +3. Empty strings `""` appear and then grow ("title": "" → "title": "Morning Game Drive") +4. Arrays grow (activities: [] → activities: [{...}]) +5. New properties appear (description appears in line 4) +6. Only ONE string changes per chunk (confirmed by data analysis) + +## Key Constraints + +1. **Must stream output** - Cannot wait until the end to emit +2. **1-2 chunk delay OK** - For disambiguation when multiple growable items appear +3. **Property order varies** - Must track by path, not position +4. **Objects only grow** - Properties never removed, values only get longer/deeper +5. **Only one value changes per chunk** - Confirmed by analysis of all test data + +## API Design + +### Class Signature + +```csharp +namespace Microsoft.Maui.Essentials.AI; + +/// +/// Converts complete JSON objects (from progressive AI output) back into streaming chunks. +/// +public class JsonStreamChunker +{ + /// + /// Process one complete JSON object and return a streaming chunk. + /// May return empty string (data pending) or a chunk to emit. + /// + /// A complete, valid JSON object representing the current state. + /// A chunk to emit, or empty string if data is pending. + public string Process(string completeJson); + + /// + /// Finalize processing and return any remaining output. + /// Call this after all input has been processed. + /// + /// Final chunk including closing brackets and any pending strings. + public string Flush(); +} +``` + +### Usage Pattern + +```csharp +// Consuming an async stream from an AI model +public async IAsyncEnumerable ConvertToChunks(IAsyncEnumerable completeJsonStream) +{ + var chunker = new JsonStreamChunker(); + + await foreach (var completeJson in completeJsonStream) + { + var chunk = chunker.Process(completeJson); + if (!string.IsNullOrEmpty(chunk)) + { + yield return chunk; + } + } + + var finalChunk = chunker.Flush(); + if (!string.IsNullOrEmpty(finalChunk)) + { + yield return finalChunk; + } +} + +// Result: Concatenating all yielded chunks produces valid JSON +// equivalent to the final input object +``` + +### Behavior Contract + +1. **Process()** returns a chunk that should be emitted immediately +2. **Process()** may return empty string if data is ambiguous (pending resolution) +3. **Flush()** must be called after all input to close any open structures +4. Concatenating all non-empty chunks from all Process() + Flush() calls produces valid JSON +5. The output JSON is structurally equivalent to the final input JSON (property order may differ) + +--- + +## Data Analysis Findings + +Analysis of all 4 test JSONL files revealed: + +| Pattern | Frequency | Notes | +|---------|-----------|-------| +| String grows | ~40-50 per file | Most common change type | +| New string appears | ~39 per file | Often 1 at a time | +| 2 new growable items at once | 0-4 per file | Requires pending list | +| Multiple values change | 0 | Never happens - confirms assumption | +| Empty array `[]` | Occasional | Gets populated in later chunk | +| Empty object `{}` | Occasional | Gets populated in later chunk | +| Non-string primitives | 0 in test data | All values are strings in these examples | + +**Key insight**: The "only one value changes per chunk" assumption holds in all test data (where values include strings, arrays, and objects). + +--- + +## Core Rules + +### Rule 1: Sibling Rule +If a new property appears at the same level as the open string, the open string is complete. +*The AI moved on horizontally.* + +**Example:** +``` +Previous: {"name": "Mat"} +Current: {"name": "Matthew", "age": 30} + +"age" is a NEW sibling at same level as "name" +→ "name" is COMPLETE (emit extension "thew", then close) +→ Then emit the new sibling +``` + +### Rule 2: Parent-Level Rule +If new content appears at a higher level (e.g., new array item in parent), the current container and its open string are complete. +*The AI moved on vertically.* + +**Example:** +``` +Previous: {"days": [{"title": "Day 1"}]} +Current: {"days": [{"title": "Day 1"}, {"title": "Day 2"}]} + +days[1] appeared at parent level (days array) +→ days[0] and everything inside it is COMPLETE +→ Close days[0], then emit new array item +``` + +### Rule 3: Unchanged Rule +If the open string's value is unchanged from the previous chunk, it is complete. + +**Example:** +``` +Previous: {"name": "Matthew"} +Current: {"name": "Matthew", "age": 30} + +"name" value unchanged +→ "name" is COMPLETE +(Note: Sibling rule would also apply here) +``` + +### Rule 4: Pending Rule +If 2+ new **growable** items appear at the same parent level in the same chunk, add them to pending and wait for the next chunk to see which one changes. + +**Growable types:** +- Strings: Can grow (characters appended) +- Arrays: Can grow (items added) +- Objects: Can grow (properties added) + +Numbers, bools, and null are NOT growable - they are always complete. + +**Example 1 - Multiple strings:** +``` +Previous: {"count": 5} +Current: {"count": 5, "a": "Hello", "b": "World"} + +2 new strings appeared at root level (siblings) - which one will grow? +→ Add BOTH to pending +→ Emit nothing for strings yet +→ Wait for next chunk to see which changes +``` + +**Example 2 - String and array at same level:** +``` +Previous: {"days": [{}]} +Current: {"days": [{"subtitle": "", "activities": []}]} + +1 new string (subtitle) + 1 new array (activities) at days[0] level +Total: 2 growable items at same level - which one will grow? +→ Add subtitle (string) to _pendingStrings +→ Add activities (array) to _pendingContainers +→ Emit NOTHING for either yet +→ Wait for next chunk to see which changes: + - If subtitle value changes → subtitle is active, activities was complete + - If activities gets children → activities is active, subtitle was complete + - If both unchanged → both were complete +``` + +--- + +## State Variables + +``` +_prevState: Dictionary? + - Flattened path→value dictionary from last chunk (null on first call) + - Used to detect what changed + - JsonValue is a record struct: (JsonValueKind Kind, string? StringValue, string? RawValue) + - Stores the Kind, StringValue (for strings), and RawValue (raw JSON for non-strings) + - IMPORTANT: Empty containers are stored as entries with JsonValueKind.Array or JsonValueKind.Object + (so we know they exist even with no children) + +_openStringPath: string? + - Path of the currently open string (no closing quote emitted) + - At most ONE string can be open at a time + - null if no string is currently open + +_pendingStrings: Dictionary + - Map of path → value for strings we haven't emitted yet + - Populated when: + - 2+ new growable items (strings, arrays, objects) appear at the SAME parent level + - OR we already have an open value and encounter a new growable item + - Resolved at start of next chunk by comparing values + - Note: pending items may or may not be siblings (different nesting levels also go to pending) + +_pendingContainers: Dictionary + - Map of path → isArray for containers we haven't emitted yet + - Populated when 2+ new growable items appear at the same parent level + - Resolved at start of next chunk by checking if container grew (got children) + - Detection: count paths starting with container path in prev vs curr + +_emittedStrings: Dictionary + - Map of path → emitted value for strings we HAVE emitted + - Used to calculate extension: extension = current[emitted.Length..] + +_openStructures: Stack<(string path, bool isArray)> + - Stack of currently open containers + - Used to properly close structures when moving to different parts of tree + - IMPORTANT: When emitting at a different level, close structures down to target path + +_emittedPaths: HashSet + - Tracks which paths have been emitted + - Used to know when to emit commas (if sibling already emitted, prepend comma) + - IMPORTANT: Do NOT skip processing of existing array items just because path is in _emittedPaths +``` + +### Comma Rules + +- **First property in an object**: No leading comma → `"prop":"value` +- **Subsequent properties**: Leading comma → `,"prop":"value` +- **First item in an array**: No leading comma → `{` or `"value` +- **Subsequent items**: Leading comma → `,{` or `,"value` + +Check `_emittedPaths` to see if any sibling was already emitted at the same parent level. +If yes, prepend comma. If no, don't. + +## Path Notation + +We flatten JSON into paths: +- Object properties: `parent.child` +- Array items: `parent[0]`, `parent[1]` +- Nested: `days[0].activities[1].title` + +**Example:** +```json +{"days": [{"subtitle": "Day", "activities": [{"title": "Game"}]}]} +``` +Flattens to: +``` +days[0].subtitle = "Day" +days[0].activities[0].title = "Game" +``` + +**Parent path calculation:** +- `days[0].subtitle` → parent is `days[0]` +- `days[0].activities[0].title` → parent is `days[0].activities[0]` +- `days[0]` → parent is `days` + +--- + +## Algorithm Overview + +``` +For each chunk: + +1. FIRST CHUNK (special path - no previous state): + - Parse and flatten JSON + - Emit root structure opening "{" + - Process all containers depth-first via EmitStructure() + - For each container, count its direct GROWABLE children (strings, arrays, objects): + - If 0 growable values at this level: emit all non-growables (numbers, bools, null), continue to nested containers + - If 1 growable value at this level: + - If string: emit property open (no closing quote), set _openStringPath + - If container (array/object): emit opening bracket, push to _openStructures, recurse into children + - If 2+ growable values at this level: + - Add strings to _pendingStrings + - Add containers to _pendingContainers + - Emit NOTHING for these until next chunk resolves which is active + - Result: at most 1 open string, rest in pending or fully emitted + +2. SUBSEQUENT CHUNKS (compare with previous state): + a. Step A - Handle open string (if _openStringPath is set): + - Check for new siblings at same level + - Check for new content at parent level + - If new sibling OR parent-level change: + → Emit extension (if value changed) + → Emit closing quote + → Set _openStringPath = null + - Else if value changed: + → Emit extension + → Keep open (might still grow) + - Else (value same): + → Emit closing quote + → Set _openStringPath = null + + b. Step B - Resolve pending (if _pendingStrings or _pendingContainers not empty): + - For strings: Categorize as COMPLETE (unchanged) vs CHANGED + - For containers: Categorize as COMPLETE (no new children) vs CHANGED (got children) + - Detection: count paths with prefix in prev vs curr state + - Emit all COMPLETE items first (sorted by path for consistency): + - Strings: emit with closing quote + - Containers: emit opening AND closing brackets AND all content (they're complete) + - Emit the CHANGED one as open (if any): + - String: becomes _openStringPath + - Container: emit opening bracket, push to _openStructures, recurse into children + - After setting active string, check for new siblings → if found, close immediately + + c. Step C - Process new content: + - For objects: iterate properties, categorize as existing vs new + - Existing non-strings: recurse into them + - New properties: categorize by growable type + - For arrays: iterate items, compare index to previous count + - Existing items: recurse into non-strings + - New items: emit via EmitNewArrayItem + - For new growable items: + - Count growables at same parent level + - If 1 growable AND no open value: emit open, set as active + - If 1 growable AND have open value: add to pending + - If 2+ growable: add ALL to pending, emit nothing + +3. FINALIZE (after last chunk): + - Emit any remaining pending items (all complete - sorted by path) + - Close _openStringPath if set (emit closing ") + - Close all open structures (emit } or ] for each, in reverse order from stack) +``` + +--- + +## Detailed Step A: Handle Open String + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ STEP A: HANDLE OPEN STRING │ +└─────────────────────────────────────────────────────────────────────────────┘ + +IF _openStringPath is set: + │ + ├─► Get current value at _openStringPath + │ Get emitted value from _emittedStrings[_openStringPath] + │ + ├─► Check for NEW siblings at same level + │ (properties in current that weren't in previous, at same parent path) + │ + ├─► Check for NEW content at PARENT level + │ (e.g., new array item in parent array) + │ + ├─► IF NEW SIBLING or PARENT-LEVEL CHANGE: + │ │ + │ │ String is COMPLETE (AI moved on) + │ │ + │ ├─► IF value changed: + │ │ extension = current[emitted.Length..] + │ │ Emit: extension + closing quote " + │ │ + │ └─► ELSE: + │ Emit: closing quote " + │ + │ Close any containers as needed: + │ Pop from _openStructures until we reach the level where new content will be emitted + │ Emit } or ] for each popped container + │ Set _openStringPath = null + │ + └─► ELSE (no new siblings, no parent changes): + │ + ├─► IF value CHANGED: + │ extension = current[emitted.Length..] + │ Emit: extension (no closing quote) + │ Update _emittedStrings[path] = current + │ Keep _openStringPath set (might still grow) + │ + └─► ELSE (value SAME): + Emit: closing quote " + Set _openStringPath = null +``` + +**Real example - Sibling Rule (Line 2):** +``` +Previous: {"days": [{"subtitle": "Day"}]} +Current: {"days": [{"subtitle": "Day 1: Arrival and Wildlife Safari", "activities": []}]} + +_openStringPath = "days[0].subtitle" +emitted = "Day" +current = "Day 1: Arrival and Wildlife Safari" + +Check siblings at days[0]: + Previous had: subtitle + Current has: subtitle, activities + → "activities" is NEW SIBLING! + +Action: + extension = "Day 1: Arrival and Wildlife Safari"["Day".Length..] = " 1: Arrival and Wildlife Safari" + Emit: 1: Arrival and Wildlife Safari" + Set _openStringPath = null + Then emit new sibling: ,"activities":[ + +Output: 1: Arrival and Wildlife Safari","activities":[ +``` + +**Real example - Parent-Level Rule (Line 6):** +``` +Previous: {"days": [{"activities": [{"description": "Embark on a thrilling..."}]}]} +Current: {"days": [{"activities": [{"description": "...full text..."}, {"type": ""}]}]} + +_openStringPath = "days[0].activities[0].description" +Parent of description = days[0].activities[0] +Parent's parent = days[0].activities (array) + +Check: days[0].activities[1] is NEW +→ New content at parent level! + +Action: + Emit extension for description + Close description: " + Close activities[0] object: } + Emit new array item: ,{ + Handle strings in new item... +``` + +--- + +## Detailed Step B: Resolve Pending + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ STEP B: RESOLVE PENDING │ +└─────────────────────────────────────────────────────────────────────────────┘ + +IF _pendingStrings OR _pendingContainers is not empty: + │ + ├─► RESOLVE PENDING STRINGS: + │ │ + │ ├─► Categorize all pending strings: + │ │ For each (path, storedValue) in _pendingStrings: + │ │ currentValue = current[path] + │ │ IF currentValue == storedValue: + │ │ Add to COMPLETE_STRINGS list + │ │ ELSE: + │ │ Add to CHANGED_STRINGS list (should be exactly 0 or 1) + │ │ + │ ├─► FIRST: Emit all COMPLETE strings (with closing quotes): + │ │ For each in COMPLETE_STRINGS: + │ │ needsComma = any sibling already in _emittedPaths + │ │ Emit: [,]"path":"value" + │ │ Add path to _emittedPaths + │ │ + │ └─► Clear _pendingStrings + │ + ├─► RESOLVE PENDING CONTAINERS: + │ │ + │ ├─► Categorize all pending containers: + │ │ For each (path, isArray) in _pendingContainers: + │ │ previousChildCount = count of previous paths starting with this container + │ │ currentChildCount = count of current paths starting with this container + │ │ IF currentChildCount == previousChildCount: + │ │ Add to COMPLETE_CONTAINERS list (container didn't grow) + │ │ ELSE: + │ │ Add to CHANGED_CONTAINERS list (container got children) + │ │ + │ ├─► Emit all COMPLETE containers (with opening AND closing brackets): + │ │ For each in COMPLETE_CONTAINERS: + │ │ needsComma = any sibling already in _emittedPaths + │ │ IF isArray: Emit: [,]"path":[] + │ │ ELSE: Emit: [,]"path":{} + │ │ Add path to _emittedPaths + │ │ + │ └─► Clear _pendingContainers + │ + ├─► EMIT THE CHANGED ITEM (at most 1 across strings and containers): + │ IF CHANGED_STRINGS has exactly 1: + │ needsComma = any sibling already in _emittedPaths + │ Emit: [,]"path":"value (no closing quote) + │ Set _openStringPath = path + │ Add path to _emittedPaths + │ Update _emittedStrings[path] = currentValue + │ + │ IF CHANGED_CONTAINERS has exactly 1: + │ needsComma = any sibling already in _emittedPaths + │ IF isArray: Emit: [,]"path":[ + │ ELSE: Emit: [,]"path":{ + │ Push to _openStructures + │ Add path to _emittedPaths + │ → Recursively process children of this container + │ + │ IF total CHANGED has 2+: + │ This should never happen per "only one value changes per chunk" invariant + │ Log warning and treat all as complete + │ + └─► IF _openStringPath was just set: + Check for new siblings at same level (in current, not in previous) + IF new sibling exists: + → Close immediately (Sibling Rule) + Emit: " + Set _openStringPath = null +``` + +**Real example (Line 4):** +``` +_pendingStrings = { + "days[0].activities[0].title": "", + "days[0].activities[0].type": "Sightseeing" +} + +Current values: + title = "Morning Game Drive" (was "") + type = "Sightseeing" (unchanged) + +Categorize: + COMPLETE = [type] + CHANGED = [title] + +Emit COMPLETE first: + type: no siblings emitted yet → no comma + Emit: "type":"Sightseeing" + Add to _emittedPaths + +Emit CHANGED: + title: type already emitted (sibling) → needs comma + Emit: ,"title":"Morning Game Drive + Set _openStringPath = "days[0].activities[0].title" + Add to _emittedPaths + +Check for new siblings of title: + "description" is NEW at same level! + → Sibling Rule: close title immediately + Emit: " + → Then handle description in Step C + +Output: "type":"Sightseeing","title":"Morning Game Drive" +``` + +--- + +## Detailed Step C: Process New Content + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ STEP C: PROCESS NEW CONTENT │ +└─────────────────────────────────────────────────────────────────────────────┘ + +For each new path in current that wasn't in previous: + │ + ├─► IF value is NON-GROWABLE (number, bool, null): + │ needsComma = any sibling already in _emittedPaths + │ Emit complete: [,]"path":value + │ Add path to _emittedPaths + │ + └─► IF value is GROWABLE (string, object, array): + Group all new GROWABLE items BY PARENT (siblings only): + │ + ├─► For each parent with new growable items: + │ Count new growable items at this parent + │ │ + │ ├─► IF 1 new growable item at this parent: + │ │ IF no open value (_openStringPath is null AND no pending): + │ │ │ + │ │ ├─► IF string: + │ │ │ needsComma = any sibling in _emittedPaths + │ │ │ Emit open: [,]"path":"value (no closing quote) + │ │ │ Set _openStringPath = path + │ │ │ Add to _emittedPaths + │ │ │ Update _emittedStrings[path] = value + │ │ │ + │ │ └─► IF object or array: + │ │ needsComma = any sibling in _emittedPaths + │ │ Emit opening: [,]"path":{ or [,]"path":[ + │ │ Push to _openStructures + │ │ Add path to _emittedPaths + │ │ Recursively process children + │ │ + │ │ ELSE (already have open value): + │ │ IF string: Add to _pendingStrings (wait for next chunk) + │ │ IF container: Add to _pendingContainers (wait for next chunk) + │ │ + │ └─► IF 2+ new growable items at this parent: + │ For each growable item: + │ IF string: Add to _pendingStrings + │ IF container: Add to _pendingContainers + │ Do NOT emit values for these + │ (Will resolve in next chunk to see which one changes) +``` + +**Note**: We count new growable items per parent level, not globally. If we get: +```json +{"a": {"x": "hello"}, "b": "world"} +``` +- `a.x` has 1 new growable item at parent `a` +- `b` has 1 new growable item at parent root +- These are NOT siblings, so each is handled separately +- First one encountered becomes the open value +- Second goes to pending (can only have one open value at a time) + +**Example - 1 new string (Line 7):** +``` +Previous: days[0].activities[1] = {type: ""} +Current: days[0].activities[1] = {type: "FoodAndDining", title: "Lunch"} + +After Step A closes type (sibling rule): + _openStringPath = null + _emittedPaths contains "days[0].activities[1].type" + +Step C - New content: + "days[0].activities[1].title" is new + Parent = "days[0].activities[1]" + 1 new string at this parent + "type" already in _emittedPaths (sibling) → needs comma + +Action: + Emit: ,"title":"Lunch + Set _openStringPath = "days[0].activities[1].title" + Add to _emittedPaths + +Output: ,"title":"Lunch +``` + +**Example - 2+ new strings (Line 3):** +``` +Previous: days[0].activities = [] +Current: days[0].activities = [{title: "", type: "Sightseeing"}] + +Step C - New content: + days[0].activities[0] is new object + Parent of this object = days[0].activities + No siblings emitted → no comma for object + + Emit: { + Push to _openStructures + Add "days[0].activities[0]" to _emittedPaths + + Inside it: title and type (2 strings!) + Parent = days[0].activities[0] + 2 new strings at this parent → pending + + Add both to pending: + _pendingStrings = { + "days[0].activities[0].title": "", + "days[0].activities[0].type": "Sightseeing" + } + Do NOT emit string values + +Output: { +``` + +--- + +## Complete Real-World Walkthrough + +### Line 1 +```json +{"days": [{"subtitle": "Day"}]} +``` + +**First chunk processing:** +- Parse and flatten: + - Root object + - `days` = array + - `days[0]` = object + - `days[0].subtitle` = "Day" +- Process containers: + - Root: emit `{`, push to _openStructures + - `days`: emit `"days":[`, push to _openStructures + - `days[0]`: emit `{`, push to _openStructures +- Process strings at `days[0]`: + - 1 string (`subtitle`), no open string yet + - Emit open: `"subtitle":"Day` (no closing quote) + - Set _openStringPath = "days[0].subtitle" + +``` +Output: {"days":[{"subtitle":"Day + ↑ no closing quote +State: + _openStringPath = "days[0].subtitle" + _emittedStrings = {"days[0].subtitle": "Day"} + _emittedPaths = {"days", "days[0]", "days[0].subtitle"} + _openStructures = [root, days, days[0]] +``` + +### Line 2 +```json +{"days": [{"subtitle": "Day 1: Arrival and Wildlife Safari", "activities": []}]} +``` + +**Step A - Handle open string:** +- _openStringPath = "days[0].subtitle" +- emitted = "Day" +- current = "Day 1: Arrival and Wildlife Safari" +- Check siblings at days[0]: `activities` is NEW! +- Sibling Rule → subtitle is COMPLETE + +``` +extension = " 1: Arrival and Wildlife Safari" +Emit: 1: Arrival and Wildlife Safari" +_openStringPath = null +``` + +**Step C - New content:** +- `days[0].activities` is new (empty array) +- Emit: ,"activities":[ +- Push to _openStructures + +``` +Output for Line 2: 1: Arrival and Wildlife Safari","activities":[ + +State: + _openStringPath = null + _openStructures = [root, days, days[0], days[0].activities] +``` + +### Line 3 +```json +{"days": [{"subtitle": "Day 1: Arrival and Wildlife Safari", "activities": [{"title": "", "type": "Sightseeing"}]}]} +``` + +**Step A - No open string** (was closed in Line 2) + +**Step C - New content:** +- `days[0].activities[0]` is new (object) +- No siblings in activities array yet → no comma +- Emit: `{` +- Push to _openStructures +- Add to _emittedPaths +- Inside: title="" and type="Sightseeing" (2 strings at same parent!) +- Pending Rule → add both to pending, don't emit string values + +``` +Emit: { +_pendingStrings = { + "days[0].activities[0].title": "", + "days[0].activities[0].type": "Sightseeing" +} + +Output for Line 3: { + +State: + _openStructures = [..., days[0].activities[0]] + _emittedPaths += {"days[0].activities[0]"} + _pendingStrings = 2 entries +``` + +### Line 4 +```json +{"days": [{"activities": [{"type": "Sightseeing", "description": "Embark", "title": "Morning Game Drive"}], "subtitle": "Day 1: Arrival and Wildlife Safari"}]} +``` + +**Step A - No open string** + +**Step B - Resolve pending:** +- type: "Sightseeing" → "Sightseeing" = UNCHANGED → complete +- title: "" → "Morning Game Drive" = CHANGED → active + +``` +Emit for type: "type":"Sightseeing" +Emit for title: ,"title":"Morning Game Drive +_openStringPath = "days[0].activities[0].title" +``` + +Check for siblings of title: +- `description` is NEW at same level! +- Sibling Rule → title is COMPLETE + +``` +Emit: " +_openStringPath = null +``` + +**Step C - New content:** +- `description` = "Embark" is new +- 1 new string + +``` +Emit: ,"description":"Embark +_openStringPath = "days[0].activities[0].description" + +Output for Line 4: "type":"Sightseeing","title":"Morning Game Drive","description":"Embark + +State: + _openStringPath = "days[0].activities[0].description" + _emittedStrings[...description] = "Embark" +``` + +### Line 5 +```json +{"days": [{"activities": [{"description": "Embark on a thrilling morning game drive to witness the Great Migration in all its glory.", "title": "Morning Game Drive", "type": "Sightseeing"}], "subtitle": "Day 1: Arrival and Wildlife Safari"}]} +``` + +**Step A - Handle open string:** +- _openStringPath = "days[0].activities[0].description" +- emitted = "Embark" +- current = "Embark on a thrilling..." +- Check siblings: none new +- Check parent level: nothing new +- Value CHANGED → emit extension, keep open + +``` +extension = " on a thrilling..." +Emit: on a thrilling... +_openStringPath still set + +Output for Line 5: on a thrilling... +``` + +### Line 6 +```json +{"days": [{"subtitle": "Day 1: Arrival and Wildlife Safari", "activities": [{"description": "Embark on a thrilling morning game drive to witness the Great Migration in all its glory.", "type": "Sightseeing", "title": "Morning Game Drive"}, {"type": ""}]}]} +``` + +**Step A - Handle open string:** +- _openStringPath = "days[0].activities[0].description" +- emitted = "Embark on a thrilling..." +- current = "...full text..." +- Check siblings at activities[0]: none new +- Check parent level: `days[0].activities[1]` is NEW! +- Parent-Level Rule → activities[0] is COMPLETE + +``` +extension = " morning game drive to witness the Great Migration in all its glory." +Emit extension: ... +Close description: " +Close activities[0] object: } +_openStringPath = null +``` + +**Step C - New content:** +- `days[0].activities[1]` is new object +- Inside: type="" (1 string) + +``` +Emit: ,{ +Emit: "type":" +_openStringPath = "days[0].activities[1].type" +_emittedStrings[...type] = "" + +Output for Line 6: morning game drive to witness the Great Migration in all its glory."},{"type":" +``` + +### Line 7 +```json +{"days": [{"subtitle": "Day 1: Arrival and Wildlife Safari", "activities": [{"title": "Morning Game Drive", "description": "...", "type": "Sightseeing"}, {"type": "FoodAndDining", "title": "Lunch"}]}]} +``` + +**Step A - Handle open string:** +- _openStringPath = "days[0].activities[1].type" +- emitted = "" +- current = "FoodAndDining" +- Check siblings: `title` is NEW! +- Sibling Rule → type is COMPLETE + +``` +extension = "FoodAndDining" +Emit: FoodAndDining" +_openStringPath = null +``` + +**Step C - New content:** +- `title` = "Lunch" is new +- 1 new string + +``` +Emit: ,"title":"Lunch +_openStringPath = "days[0].activities[1].title" + +Output for Line 7: FoodAndDining","title":"Lunch +``` + +### Line 8 +```json +{"days": [{"activities": [{"type": "Sightseeing", "description": "...", "title": "Morning Game Drive"}, {"description": "Enjoy", "title": "Lunch at Restaurant 1", "type": "FoodAndDining"}], "subtitle": "Day 1: Arrival and Wildlife Safari"}]} +``` + +**Step A - Handle open string:** +- _openStringPath = "days[0].activities[1].title" +- emitted = "Lunch" +- current = "Lunch at Restaurant 1" +- Check siblings: `description` is NEW! +- Sibling Rule → title is COMPLETE + +``` +extension = " at Restaurant 1" +Emit: at Restaurant 1" +_openStringPath = null +``` + +**Step C - New content:** +- `description` = "Enjoy" is new +- 1 new string + +``` +Emit: ,"description":"Enjoy +_openStringPath = "days[0].activities[1].description" + +Output for Line 8: at Restaurant 1","description":"Enjoy +``` + +--- + +## Edge Cases + +### Empty string grows +``` +Line 3: "title": "" +Line 4: "title": "Morning Game Drive" +``` +- Empty string is just a string with length 0 +- Extension = "Morning Game Drive"[0..] = "Morning Game Drive" +- No special handling needed - treat `""` like any other string + +### Empty containers get populated +``` +Line 2: "activities": [] +Line 3: "activities": [{"title": ""}] +``` +- Empty array `[]` → emit `[` only, push to _openStructures +- Do NOT emit closing `]` +- When items appear → emit them normally +- Closing brackets emitted when moving to sibling/parent or at finalize + +**Note on flattening**: Empty containers ARE stored in the flattened state dictionary +with their JsonValueKind (Array or Object). This allows us to: +1. Know the container exists even with no children +2. Detect when children are added (container grew) + +### Nested container changes propagate up + +Containers (arrays and objects) can have nested children that grow. When determining if a container is "changing", we must check ALL descendants, not just direct children. + +**The Rule**: A container is "still active/growing" if ANY descendant path changes. + +**Example - Nested array with growing string:** +``` +Previous: {"items": [{"name": "Jo"}]} +Current: {"items": [{"name": "John"}]} +``` +- `items` is an array containing an object +- The object contains a string `name` that grew ("Jo" → "John") +- Even though `items[0]` exists in both, the string inside changed +- Therefore `items` (the array) is still "active" - don't close it + +**Example - Deep nesting:** +``` +Previous: {"root": {"level1": {"level2": {"value": "He"}}}} +Current: {"root": {"level1": {"level2": {"value": "Hello"}}}} +``` +- `value` is the actual string that changed +- But `level2`, `level1`, and `root` are ALL still active because a descendant changed +- None of these containers should be closed + +**Detection Algorithm:** +When checking if a container at path P is "complete" vs "still active": +1. Find all current paths that START WITH P (all descendants) +2. Find all previous paths that START WITH P +3. If ANY descendant value changed → container is still active +4. Only if ALL descendants are unchanged → container is complete + +**Why this matters for pending:** +When we have pending containers (e.g., a string and an array both appeared): +``` +Previous: {"days": [{}]} +Current: {"days": [{"subtitle": "", "activities": []}]} +``` +- `subtitle` (string) and `activities` (array) are both pending +- Next chunk: + ``` + Current: {"days": [{"subtitle": "Day 1", "activities": []}]} + ``` +- `subtitle` changed ("" → "Day 1") → subtitle is the active one +- `activities` has no new children → activities is complete +- BUT if instead: + ``` + Current: {"days": [{"subtitle": "", "activities": [{"type": ""}]}]} + ``` +- `subtitle` unchanged ("" → "") +- `activities` now has children → activities is the active one +- The array "grew" even though `activities` itself didn't change - its DESCENDANTS did + +### Array grows by new item +``` +Line 5: activities has 1 item +Line 6: activities has 2 items +``` +- Detect new array index (activities[1]) +- This triggers Parent-Level Rule for activities[0] +- Close activities[0], emit `,{`, process new item + +### Deep nesting +- Paths handle arbitrary depth: `days[0].activities[2].details.notes[0]` +- Stack of _openStructures handles closing in correct order +- Parent detection walks up the path segments + +### New strings at different nesting levels +``` +Previous: {"count": 5} +Current: {"count": 5, "a": {"x": "hello"}, "b": "world"} +``` +- `a.x` is at parent `a` (1 string at this level) +- `b` is at parent root (1 string at this level) +- These are NOT siblings - different parents +- We can only have ONE open string at a time +- First encountered (e.g., `a.x`) becomes _openStringPath +- Second (`b`) goes to pending even though it's alone at its level +- Next chunk resolves: whichever changed is active (per invariant, at most one changes) + +### All pending strings complete (none changed) +``` +Line N: Two strings added to pending +Line N+1: Both strings have same values (unchanged) +``` +- Both are COMPLETE +- Emit both with closing quotes +- No _openStringPath is set +- Continue to Step C for new content + +--- + +## Key Decision Points + +### When to close the open string? + +1. **Sibling Rule**: New property at same level +2. **Parent Rule**: New content at higher level +3. **Unchanged Rule**: Value same as previous chunk +4. **Finalize**: End of stream + +### When to use pending? + +- First chunk has 2+ strings +- 2+ new strings appear in same chunk +- NEVER emit pending strings immediately - always wait for next chunk + +### How to calculate "same level"? + +Two paths are at the same level (siblings) if they have the same parent: +- `days[0].title` and `days[0].subtitle` → same parent `days[0]` ✓ +- `days[0].title` and `days[1].title` → different parents ✗ +- `days[0].activities[0].title` and `days[0].activities[0].type` → same parent ✓ + +### How to detect parent-level changes? + +For open string at path P, check if any new content appeared at: +- P's parent path +- P's grandparent path +- etc. + +Example: If open string is at `days[0].activities[0].description`: +- Parent = `days[0].activities[0]` +- Grandparent = `days[0].activities` +- If `days[0].activities[1]` appears → parent-level change! + +### How to handle empty containers? + +- Emit `[` or `{` only (no closing bracket) +- Add to _openStructures +- Close when moving to sibling/parent or at finalize + +--- + +## Key Invariants + +1. **At most ONE growable value is open** at any time (never two) +2. **Pending items (strings and containers) are NEVER emitted** until resolved in next chunk +3. **Extension = current[emitted.Length..]** - strings only grow, never shrink +4. **Order: Step A (open) → Step B (pending) → Step C (new)** - always this order +5. **2+ new growable items at same parent = pending** - never guess which is active +6. **New sibling = complete** - AI moved on horizontally +7. **Parent-level change = complete** - AI moved on vertically +8. **Track by path, not position** - property order doesn't matter +9. **Containers stay open** until sibling/parent change or finalize +10. **Empty containers are valid** - `{}` and `[]` can get populated later +11. **Comma before sibling** - check _emittedPaths to know if comma needed +12. **At most ONE value changes per chunk** - if Step B finds 2+ changed (strings or containers), that's an error +13. **Growable types: strings, arrays, objects** - all can grow and need pending logic +14. **Non-growable types: numbers, bools, null** - always complete immediately +15. **Nested changes propagate up** - if ANY descendant of a container changes, the container is still active +16. **Complete containers emit all content** - when a pending container is resolved as complete, emit its full content (opening bracket, all children recursively, closing bracket) +17. **Structure closing is level-aware** - use `CloseStructuresDownTo` to close structures when emitting at a different tree level +18. **Pending resolution preserves new pending** - when resolving pending items, NEW pending items may be added during container emission (e.g., emitting a pending array creates new pending strings for its children). Only remove the original pending items, not the newly added ones. +19. **Array iteration closes previous items** - when iterating array items in first chunk, close any pending items from the previous array item before moving to the next one +20. **Container growth detection compares key sets** - when checking if a pending container grew, compare the SET of descendant keys, not just the count. An empty object `{}` becoming a nested object changes the keys even if the count stays the same. + +--- + +## Structure Management + +### CloseStructuresDownTo Algorithm + +When we need to emit content at a different level in the JSON tree, we must first close any open structures that are not ancestors of the target path. + +``` +CloseStructuresDownTo(targetPath): + while _openStructures is not empty: + (topPath, isArray) = peek top of stack + + // Check if targetPath is at or inside topPath + isPrefix = targetPath.startsWith(topPath) AND + (lengths equal OR next char is '.' or '[') + + if topPath is root ("") OR isPrefix: + break // Don't close - we're inside this structure + + pop from stack + emit ']' if isArray else '}' +``` + +**Example:** +``` +_openStructures = [("", false), ("days", true), ("days[0]", false), ("days[0].activities", true)] + +We want to emit at "days[1]" (new array item in days) + +1. Check "days[0].activities" - "days[1]" doesn't start with this → close with ']' +2. Check "days[0]" - "days[1]" doesn't start with this → close with '}' +3. Check "days" - "days[1]" DOES start with "days" → stop + +Result: emitted "]}" and stack is now [("", false), ("days", true)] +``` + +### When Structure Closing Happens + +1. **Before emitting a new property** - `EmitNewProperty` calls `CloseStructuresDownTo(parentPath)` +2. **Before emitting a pending string** - `EmitPendingString` calls `CloseStructuresDownTo(parentPath)` +3. **Before emitting a new array item** - `EmitNewArrayItem` calls `CloseStructuresDownTo(arrayPath)` + +This ensures we're always at the correct nesting level before emitting new content. + +--- + +## First Chunk vs Subsequent Chunk + +The implementation handles the first chunk differently from subsequent chunks: + +### First Chunk (ProcessFirstChunk) +- No previous state to compare against +- Uses `EmitStructure` to recursively process the JSON tree +- Counts growables at each level to decide open vs pending +- Emits structure (brackets) and handles string ambiguity + +### Subsequent Chunks (ProcessSubsequentChunk) +- Has previous state for comparison +- Follows the Step A → Step B → Step C order +- Uses `ProcessNewContent` to find and emit new properties/items +- Can detect siblings and parent-level changes + +## Critical Rules Checklist + +### NEVER Do These Things + +1. **NEVER defer all output to the end** - This defeats the entire purpose of streaming +2. **NEVER accumulate all chunks and process at the end** - Each chunk must produce output +3. **NEVER rely on property order** - Use path-based comparison, not positional +4. **NEVER close a string prematurely** - Only close when CERTAIN it won't grow +5. **NEVER re-serialize to find diff** - Compare objects structurally, emit directly + +### ALWAYS Do These Things + +1. **ALWAYS use intermediate deserialization** - Parse each chunk to dictionary for comparison +2. **ALWAYS compare by path** - Flatten objects to `path → value` dictionaries +3. **ALWAYS track what's "open"** - Use stack to know what needs closing tokens +4. **ALWAYS emit extension BEFORE closing** - When closing a string that grew, emit extension first +5. **ALWAYS validate concatenation** - Concatenate all chunks, parse as JSON, compare to final + +### Order of Operations + +When processing a chunk where a new property appears (signaling completion): + +1. **FIRST**: Emit any remaining string extension (the part that grew since last chunk) +2. **THEN**: Emit the closing quote +3. **THEN**: Emit new content (comma, new property, etc.) + +This prevents truncation of the final string extension. + +--- + +## Why Naive Approaches Fail + +### The String Diff Problem + +A naive approach of "serialize with open strings, diff, emit diff" fails because: + +1. **Line 2**: We serialize with `subtitle` open (no closing quote): + `...Safari,"activities":[` (note: no quote after Safari) + +2. **Line 3**: We serialize with `type` open (different string): + `...Safari","activities":[{"title":"","type":"Sightseeing` (quote after Safari) + +3. When we diff these, they diverge at position 55 where one has `,` and the other has `"`. + The diff produces content that DUPLICATES properties! + +### The Root Cause + +The **closing quote for a string is NOT at the end of our emitted output**. It's embedded in the middle, before subsequent content. When we change which string is "open", the quote position moves, causing serializations to diverge unexpectedly. + +### The Solution + +Track what we've **ACTUALLY EMITTED** separately from any serialization: + +1. Emit content immediately, but track that a string is "open" (withheld quote) +2. When a new property appears (signaling previous string is complete): + - Emit the closing quote FIRST + - THEN emit the new content +3. The emitted stream is what we actually output, not a re-serialization + +--- + +## Files + +- **Implementation**: `src/AI/src/Essentials.AI/JsonStreamChunker.cs` +- **Tests**: `src/AI/tests/Essentials.AI.UnitTests/JsonStreamChunkerTests.cs` +- **Test data**: `src/AI/tests/Essentials.AI.UnitTests/TestData/ObjectStreams/*.jsonl` diff --git a/src/AI/samples/AppleNative/.gitignore b/src/AI/samples/AppleNative/.gitignore new file mode 100644 index 000000000000..fda4de3a857d --- /dev/null +++ b/src/AI/samples/AppleNative/.gitignore @@ -0,0 +1,62 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output \ No newline at end of file diff --git a/src/AI/samples/AppleNative/EssentialsAI.xcworkspace/contents.xcworkspacedata b/src/AI/samples/AppleNative/EssentialsAI.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..a4007c161b82 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAI.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/AI/samples/AppleNative/EssentialsAI.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/src/AI/samples/AppleNative/EssentialsAI.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..0c67376ebacb --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAI.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample.xcodeproj/project.pbxproj b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..94b8524beed1 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample.xcodeproj/project.pbxproj @@ -0,0 +1,375 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 344600CD2ECBA12600425C44 /* EssentialsAI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 344600CC2ECBA12600425C44 /* EssentialsAI.framework */; }; + 344600CE2ECBA12600425C44 /* EssentialsAI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 344600CC2ECBA12600425C44 /* EssentialsAI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 344600CF2ECBA12600425C44 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 344600CE2ECBA12600425C44 /* EssentialsAI.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 344600AE2ECBA0C300425C44 /* EssentialsAISample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EssentialsAISample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 344600CC2ECBA12600425C44 /* EssentialsAI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = EssentialsAI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 344600C52ECBA0C400425C44 /* Exceptions for "EssentialsAISample" folder in "EssentialsAISample" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 344600AD2ECBA0C300425C44 /* EssentialsAISample */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 344600B02ECBA0C300425C44 /* EssentialsAISample */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 344600C52ECBA0C400425C44 /* Exceptions for "EssentialsAISample" folder in "EssentialsAISample" target */, + ); + path = EssentialsAISample; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 344600AB2ECBA0C300425C44 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 344600CD2ECBA12600425C44 /* EssentialsAI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 344600A52ECBA0C300425C44 = { + isa = PBXGroup; + children = ( + 344600B02ECBA0C300425C44 /* EssentialsAISample */, + 344600CB2ECBA12600425C44 /* Frameworks */, + 344600AF2ECBA0C300425C44 /* Products */, + ); + sourceTree = ""; + }; + 344600AF2ECBA0C300425C44 /* Products */ = { + isa = PBXGroup; + children = ( + 344600AE2ECBA0C300425C44 /* EssentialsAISample.app */, + ); + name = Products; + sourceTree = ""; + }; + 344600CB2ECBA12600425C44 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 344600CC2ECBA12600425C44 /* EssentialsAI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 344600AD2ECBA0C300425C44 /* EssentialsAISample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 344600C62ECBA0C400425C44 /* Build configuration list for PBXNativeTarget "EssentialsAISample" */; + buildPhases = ( + 344600AA2ECBA0C300425C44 /* Sources */, + 344600AB2ECBA0C300425C44 /* Frameworks */, + 344600AC2ECBA0C300425C44 /* Resources */, + 344600CF2ECBA12600425C44 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 344600B02ECBA0C300425C44 /* EssentialsAISample */, + ); + name = EssentialsAISample; + packageProductDependencies = ( + ); + productName = EssentialsAISample; + productReference = 344600AE2ECBA0C300425C44 /* EssentialsAISample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 344600A62ECBA0C300425C44 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastUpgradeCheck = 2610; + TargetAttributes = { + 344600AD2ECBA0C300425C44 = { + CreatedOnToolsVersion = 26.1; + }; + }; + }; + buildConfigurationList = 344600A92ECBA0C300425C44 /* Build configuration list for PBXProject "EssentialsAISample" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 344600A52ECBA0C300425C44; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 344600AF2ECBA0C300425C44 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 344600AD2ECBA0C300425C44 /* EssentialsAISample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 344600AC2ECBA0C300425C44 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 344600AA2ECBA0C300425C44 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 344600C72ECBA0C400425C44 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 42GDTGK33W; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EssentialsAISample/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.maui.EssentialsAISample; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2,3,7"; + }; + name = Debug; + }; + 344600C82ECBA0C400425C44 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 42GDTGK33W; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = EssentialsAISample/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.maui.EssentialsAISample; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2,3,7"; + }; + name = Release; + }; + 344600C92ECBA0C400425C44 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 344600CA2ECBA0C400425C44 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 344600A92ECBA0C300425C44 /* Build configuration list for PBXProject "EssentialsAISample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 344600C92ECBA0C400425C44 /* Debug */, + 344600CA2ECBA0C400425C44 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 344600C62ECBA0C400425C44 /* Build configuration list for PBXNativeTarget "EssentialsAISample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 344600C72ECBA0C400425C44 /* Debug */, + 344600C82ECBA0C400425C44 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 344600A62ECBA0C300425C44 /* Project object */; +} diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample.xcodeproj/xcshareddata/xcschemes/EssentialsAISample.xcscheme b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample.xcodeproj/xcshareddata/xcschemes/EssentialsAISample.xcscheme new file mode 100644 index 000000000000..c2d6bb707979 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample.xcodeproj/xcshareddata/xcschemes/EssentialsAISample.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/AppDelegate.h b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/AppDelegate.h new file mode 100644 index 000000000000..a73691ea8798 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/AppDelegate.h @@ -0,0 +1,14 @@ +// +// AppDelegate.h +// EssentialsAISample +// +// Created by Matthew Leibowitz on 2025/11/17. +// + +#import + +@interface AppDelegate : UIResponder + + +@end + diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/AppDelegate.m b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/AppDelegate.m new file mode 100644 index 000000000000..1e00653cbf2e --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/AppDelegate.m @@ -0,0 +1,40 @@ +// +// AppDelegate.m +// EssentialsAISample +// +// Created by Matthew Leibowitz on 2025/11/17. +// + +#import "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + + +#pragma mark - UISceneSession lifecycle + + +- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; +} + + +- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. +} + + +@end diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Assets.xcassets/AccentColor.colorset/Contents.json b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000000..eb8789700816 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Assets.xcassets/AppIcon.appiconset/Contents.json b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..2305880107db --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Assets.xcassets/Contents.json b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Assets.xcassets/Contents.json new file mode 100644 index 000000000000..73c00596a7fc --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Base.lproj/LaunchScreen.storyboard b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..865e9329f376 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Base.lproj/Main.storyboard b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..808a21ce779b --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Info.plist b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Info.plist new file mode 100644 index 000000000000..81ed29b76cfb --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/SceneDelegate.h b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/SceneDelegate.h new file mode 100644 index 000000000000..9c6cb452c260 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/SceneDelegate.h @@ -0,0 +1,15 @@ +// +// SceneDelegate.h +// EssentialsAISample +// +// Created by Matthew Leibowitz on 2025/11/17. +// + +#import + +@interface SceneDelegate : UIResponder + +@property (strong, nonatomic) UIWindow * window; + +@end + diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/SceneDelegate.m b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/SceneDelegate.m new file mode 100644 index 000000000000..07ddaa782b02 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/SceneDelegate.m @@ -0,0 +1,57 @@ +// +// SceneDelegate.m +// EssentialsAISample +// +// Created by Matthew Leibowitz on 2025/11/17. +// + +#import "SceneDelegate.h" + +@interface SceneDelegate () + +@end + +@implementation SceneDelegate + + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). +} + + +- (void)sceneDidDisconnect:(UIScene *)scene { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). +} + + +- (void)sceneDidBecomeActive:(UIScene *)scene { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. +} + + +- (void)sceneWillResignActive:(UIScene *)scene { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). +} + + +- (void)sceneWillEnterForeground:(UIScene *)scene { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. +} + + +- (void)sceneDidEnterBackground:(UIScene *)scene { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. +} + + +@end diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/ViewController.h b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/ViewController.h new file mode 100644 index 000000000000..083682194106 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/ViewController.h @@ -0,0 +1,14 @@ +// +// ViewController.h +// EssentialsAISample +// +// Created by Matthew Leibowitz on 2025/11/17. +// + +#import + +@interface ViewController : UIViewController + + +@end + diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/ViewController.m b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/ViewController.m new file mode 100644 index 000000000000..85b26a190d08 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/ViewController.m @@ -0,0 +1,194 @@ +// +// ViewController.m +// EssentialsAISample +// +// Created by Matthew Leibowitz on 2025/11/17. +// + +#import "ViewController.h" + +#import "EssentialsAI/EssentialsAI-Swift.h" + +// Block typedefs for AI callbacks +typedef void (^AIUpdateBlock)(ResponseUpdateNative *_Nullable result); +typedef void (^AICompletionBlock)(ChatResponseNative *_Nullable result, + NSError *_Nullable error); + +#pragma mark - Tools + +// Find points of interest tool for Maui itinerary +@interface FindPointsOfInterestTool : NSObject +@end + +@implementation FindPointsOfInterestTool + +- (NSString *)name { + return @"findPointsOfInterest"; +} + +- (NSString *)desc { + return @"Finds points of interest for a landmark."; +} + +- (NSString *)argumentsSchema { + return @"{" + "\"type\":\"object\"," + "\"properties\":{" + "\"pointOfInterest\":{" + "\"description\":\"This is the type of destination to look up for.\"," + "\"type\":\"string\"," + "\"enum\":[\"Cafe\",\"Campground\",\"Hotel\",\"Marina\",\"Museum\",\"NationalMonument\",\"Restaurant\"]" + "}," + "\"naturalLanguageQuery\":{" + "\"description\":\"The natural language query of what to search for.\"," + "\"type\":\"string\"" + "}" + "}," + "\"required\":[\"pointOfInterest\",\"naturalLanguageQuery\"]" + "}"; +} + +- (NSString *)outputSchema { + return @"{\"type\":\"string\"}"; +} + +- (void)callWithArguments:(NSString *)arguments + completion:(void (^)(NSString *))completion { + // Parse the arguments + NSError *error = nil; + NSDictionary *argsDict = [NSJSONSerialization JSONObjectWithData:[arguments dataUsingEncoding:NSUTF8StringEncoding] + options:0 + error:&error]; + + NSString *poiType = argsDict[@"pointOfInterest"] ?: @"unknown"; + + // Generate results matching the C# implementation format + NSString *result; + if ([poiType isEqualToString:@"Cafe"]) { + result = @"There are these Cafe in Maui: Cafe 1, Cafe 2, Cafe 3"; + } else if ([poiType isEqualToString:@"Campground"]) { + result = @"There are these Campground in Maui: Campground 1, Campground 2, Campground 3"; + } else if ([poiType isEqualToString:@"Hotel"]) { + result = @"There are these Hotel in Maui: Hotel 1, Hotel 2, Hotel 3"; + } else if ([poiType isEqualToString:@"Marina"]) { + result = @"There are these Marina in Maui: Marina 1, Marina 2, Marina 3"; + } else if ([poiType isEqualToString:@"Museum"]) { + result = @"There are these Museum in Maui: Museum 1, Museum 2, Museum 3"; + } else if ([poiType isEqualToString:@"NationalMonument"]) { + result = @"There are these NationalMonument in Maui: The National Rock 1, The National Rock 2, The National Rock 3"; + } else if ([poiType isEqualToString:@"Restaurant"]) { + result = @"There are these Restaurant in Maui: Restaurant 1, Restaurant 2, Restaurant 3"; + } else { + result = [NSString stringWithFormat:@"There are no %@ in Maui", poiType]; + } + + completion(result); +} + +@end + +#pragma mark - View Controller + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + // Set up logging using simple block + AppleIntelligenceLogger.log = ^(NSString *message) { + NSLog(@"[Native] %@", message); + }; + + // 1. Create the client + ChatClientNative *client = [[ChatClientNative alloc] init]; + + // 2. Create system message with Maui description + ChatMessageNative *systemMessage = [[ChatMessageNative alloc] init]; + systemMessage.role = ChatRoleNativeSystem; + systemMessage.contents = @[ + [[TextContentNative alloc] initWithText:@"Your job is to create an itinerary for the person."], + [[TextContentNative alloc] initWithText:@"Each day needs an activity, hotel and restaurant."], + [[TextContentNative alloc] initWithText:@"Always use the findPointsOfInterest tool to find businesses and activities in Maui, especially hotels and restaurants.\n\nThe point of interest categories may include:"], + [[TextContentNative alloc] initWithText:@"Cafe, Campground, Hotel, Marina, Museum, NationalMonument, Restaurant"], + [[TextContentNative alloc] initWithText:@"Here is a description of Maui for your reference when considering what activities to generate:"], + [[TextContentNative alloc] initWithText:@"The second-largest island in Hawaii, Maui offers a stunning tapestry of volcanic landscapes, lush rainforests, pristine beaches, and dramatic coastal cliffs. Known as the \"Valley Isle,\" Maui is dominated by two massive volcanoes: the dormant Haleakalā in the east and the older, eroded West Maui Mountains. Haleakalā National Park features a massive volcanic crater that reaches over 10,000 feet in elevation, offering breathtaking sunrise views and unique high-altitude ecosystems.\n\nThe island's diverse geography creates distinct climate zones, from arid leeward coasts to verdant windward rainforests receiving over 400 inches of annual rainfall. The famous Road to Hana winds through tropical paradise, passing countless waterfalls, bamboo forests, and dramatic ocean vistas. Maui's volcanic soil supports rich agriculture, including sugarcane, pineapple, coffee, and exotic tropical fruits.\n\nMarine life thrives in Maui's warm Pacific waters. Humpback whales migrate to the shallow channels between the Hawaiian islands from December to May, making Maui one of the world's premier whale-watching destinations. Hawaiian green sea turtles, spinner dolphins, and vibrant coral reefs attract snorkelers and divers year-round. The island's beaches range from golden sand to unique red and black volcanic shores. Native Hawaiian plants such as the silversword, which grows only on Haleakalā's slopes, and endemic bird species like the nēnē (Hawaiian goose) highlight the island's ecological significance and ongoing conservation efforts."] + ]; + + // 3. Create user message + ChatMessageNative *userMessage = [[ChatMessageNative alloc] init]; + userMessage.role = ChatRoleNativeUser; + userMessage.contents = @[ + [[TextContentNative alloc] initWithText:@"Generate a 3-day itinerary to Maui."], + [[TextContentNative alloc] initWithText:@"Give it a fun title and description."], + [[TextContentNative alloc] initWithText:@"Here is an example, but don't copy it:"], + [[TextContentNative alloc] initWithText:@"{\"title\":\"Onsen Trip to Japan\",\"destinationName\":\"Mt. Fuji\",\"description\":\"Sushi, hot springs, and ryokan with a toddler!\",\"rationale\":\"You are traveling with a child, so climbing Mt. Fuji is probably not an option,\nbut there is lots to do around Kawaguchiko Lake, including Fujikyu.\nI recommend staying in a ryokan because you love hotsprings.\",\"days\":[{\"title\":\"Sushi and Shopping Near Kawaguchiko\",\"subtitle\":\"Spend your final day enjoying sushi and souvenir shopping.\",\"destination\":\"Kawaguchiko Lake\",\"activities\":[{\"type\":\"FoodAndDining\",\"title\":\"The Restaurant serving Sushi\",\"description\":\"Visit an authentic sushi restaurant for lunch.\"},{\"type\":\"Shopping\",\"title\":\"The Plaza\",\"description\":\"Enjoy souvenir shopping at various shops.\"},{\"type\":\"Sightseeing\",\"title\":\"The Beautiful Cherry Blossom Park\",\"description\":\"Admire the beautiful cherry blossom trees in the park.\"},{\"type\":\"HotelAndLodging\",\"title\":\"The Hotel\",\"description\":\"Spend one final evening in the hotspring before heading home.\"}]}]}"] + ]; + + // 4. Create options with JSON schema and tool + ChatOptionsNative *options = [[ChatOptionsNative alloc] init]; + + // JSON schema for structured itinerary output + options.responseJsonSchema = @"{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"description\":\"A travel itinerary with days and activities.\",\"type\":\"object\",\"properties\":{\"title\":{\"description\":\"An exciting name for the trip.\",\"type\":\"string\"},\"destinationName\":{\"type\":\"string\"},\"description\":{\"type\":\"string\"},\"rationale\":{\"description\":\"An explanation of how the itinerary meets the person's special requests.\",\"type\":\"string\"},\"days\":{\"description\":\"A list of day-by-day plans.\",\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"title\":{\"description\":\"A unique and exciting title for this day plan.\",\"type\":\"string\"},\"subtitle\":{\"type\":\"string\"},\"destination\":{\"type\":\"string\"},\"activities\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"Sightseeing\",\"FoodAndDining\",\"Shopping\",\"HotelAndLodging\"]},\"title\":{\"type\":\"string\"},\"description\":{\"type\":\"string\"}},\"required\":[\"type\",\"title\",\"description\"],\"title\":\"Activity\",\"additionalProperties\":false},\"minItems\":3,\"maxItems\":3}},\"required\":[\"title\",\"subtitle\",\"destination\",\"activities\"],\"title\":\"DayPlan\",\"additionalProperties\":false},\"minItems\":3,\"maxItems\":3}},\"required\":[\"title\",\"destinationName\",\"description\",\"rationale\",\"days\"],\"title\":\"Itinerary\",\"additionalProperties\":false}"; + + // Create tool instance + FindPointsOfInterestTool *poiTool = [[FindPointsOfInterestTool alloc] init]; + options.tools = @[ poiTool ]; + + // 5. Define callbacks + AICompletionBlock completion = + ^(ChatResponseNative *_Nullable result, NSError *_Nullable error) { + if (error) { + NSLog(@"Error from AI: %@", error); + return; + } + + NSLog(@"AI Response received with %lu messages", (unsigned long)result.messages.count); + + for (ChatMessageNative *msg in result.messages) { + NSLog(@"Message role: %ld", (long)msg.role); + + for (AIContentNative *content in msg.contents) { + if ([content isKindOfClass:[TextContentNative class]]) { + TextContentNative *text = (TextContentNative *)content; + NSLog(@" Text: %@", text.text); + } else if ([content isKindOfClass:[FunctionCallContentNative class]]) { + FunctionCallContentNative *funcCall = (FunctionCallContentNative *)content; + NSLog(@" Function Call: %@ (%@)", funcCall.name, funcCall.callId); + NSLog(@" Arguments: %@", funcCall.arguments); + } else if ([content isKindOfClass:[FunctionResultContentNative class]]) { + FunctionResultContentNative *funcResult = (FunctionResultContentNative *)content; + NSLog(@" Function Result (%@): %@", funcResult.callId, funcResult.result); + } + } + } + }; + + AIUpdateBlock update = ^(ResponseUpdateNative *_Nullable result) { + if (result.text) { + NSLog(@"Stream update: text = %@", result.text); + } + if (result.toolCallName) { + NSLog(@"Stream update: tool = %@ (id=%@)", result.toolCallName, result.toolCallId); + if (result.toolCallArguments) { + NSLog(@" arguments = %@", result.toolCallArguments); + } + if (result.toolCallResult) { + NSLog(@" result = %@", result.toolCallResult); + } + } + }; + + // 6. Call streamResponse with system and user messages + NSArray *messages = @[ systemMessage, userMessage ]; + + CancellationTokenNative *streamToken = + [client streamResponseWithMessages:messages + options:options + onUpdate:update + onComplete:completion]; + + // Keep reference to prevent deallocation + (void)streamToken; +} + +@end diff --git a/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/main.m b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/main.m new file mode 100644 index 000000000000..1bc39bfbad03 --- /dev/null +++ b/src/AI/samples/AppleNative/EssentialsAISample/EssentialsAISample/main.m @@ -0,0 +1,18 @@ +// +// main.m +// EssentialsAISample +// +// Created by Matthew Leibowitz on 2025/11/17. +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + NSString * appDelegateClassName; + @autoreleasepool { + // Setup code that might create autoreleased objects goes here. + appDelegateClassName = NSStringFromClass([AppDelegate class]); + } + return UIApplicationMain(argc, argv, nil, appDelegateClassName); +} diff --git a/src/AI/samples/Directory.Build.props b/src/AI/samples/Directory.Build.props new file mode 100644 index 000000000000..41af9256608e --- /dev/null +++ b/src/AI/samples/Directory.Build.props @@ -0,0 +1,10 @@ + + + true + true + Maui + $(WarningsNotAsErrors);XC0022;XC0023 + true + + + diff --git a/src/AI/samples/Directory.Build.targets b/src/AI/samples/Directory.Build.targets new file mode 100644 index 000000000000..68be46514e92 --- /dev/null +++ b/src/AI/samples/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/AI/samples/Essentials.AI.Sample/App.xaml b/src/AI/samples/Essentials.AI.Sample/App.xaml new file mode 100644 index 000000000000..bcb92097e3ff --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/App.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/src/AI/samples/Essentials.AI.Sample/App.xaml.cs b/src/AI/samples/Essentials.AI.Sample/App.xaml.cs new file mode 100644 index 000000000000..c82cafb92a64 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/App.xaml.cs @@ -0,0 +1,14 @@ +namespace Maui.Controls.Sample; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + } + + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new AppShell()); + } +} \ No newline at end of file diff --git a/src/AI/samples/Essentials.AI.Sample/AppShell.xaml b/src/AI/samples/Essentials.AI.Sample/AppShell.xaml new file mode 100644 index 000000000000..036ee279df00 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/AppShell.xaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/AI/samples/Essentials.AI.Sample/AppShell.xaml.cs b/src/AI/samples/Essentials.AI.Sample/AppShell.xaml.cs new file mode 100644 index 000000000000..d9d0dee6c438 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/AppShell.xaml.cs @@ -0,0 +1,15 @@ +using Maui.Controls.Sample.Pages; + +namespace Maui.Controls.Sample; + +public partial class AppShell : Shell +{ + public AppShell() + { + InitializeComponent(); + + // Register routes for navigation + // Only TripPlanningPage is navigable - LandmarkTripView is a child component + Routing.RegisterRoute(nameof(TripPlanningPage), typeof(TripPlanningPage)); + } +} diff --git a/src/AI/samples/Essentials.AI.Sample/Converters/InvertedBoolConverter.cs b/src/AI/samples/Essentials.AI.Sample/Converters/InvertedBoolConverter.cs new file mode 100644 index 000000000000..3862bbd20460 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Converters/InvertedBoolConverter.cs @@ -0,0 +1,16 @@ +using System.Globalization; + +namespace Maui.Controls.Sample.Converters; + +public class InvertedBoolConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is bool boolValue && !boolValue; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is bool boolValue && !boolValue; + } +} diff --git a/src/AI/samples/Essentials.AI.Sample/Converters/IsNotNullConverter.cs b/src/AI/samples/Essentials.AI.Sample/Converters/IsNotNullConverter.cs new file mode 100644 index 000000000000..d81d737ce8f8 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Converters/IsNotNullConverter.cs @@ -0,0 +1,16 @@ +using System.Globalization; + +namespace Maui.Controls.Sample.Converters; + +public class IsNotNullConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is not null; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/src/AI/samples/Essentials.AI.Sample/Converters/IsNotNullOrEmptyConverter.cs b/src/AI/samples/Essentials.AI.Sample/Converters/IsNotNullOrEmptyConverter.cs new file mode 100644 index 000000000000..650da7bb383d --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Converters/IsNotNullOrEmptyConverter.cs @@ -0,0 +1,16 @@ +using System.Globalization; + +namespace Maui.Controls.Sample.Converters; + +public class IsNotNullOrEmptyConverter : IValueConverter +{ + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value is string str && !string.IsNullOrWhiteSpace(str); + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/src/AI/samples/Essentials.AI.Sample/Directory.Build.targets b/src/AI/samples/Essentials.AI.Sample/Directory.Build.targets new file mode 100644 index 000000000000..fcc6017c6249 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Directory.Build.targets @@ -0,0 +1,4 @@ + + + + diff --git a/src/AI/samples/Essentials.AI.Sample/Essentials.AI.Sample.csproj b/src/AI/samples/Essentials.AI.Sample/Essentials.AI.Sample.csproj new file mode 100644 index 000000000000..c27e64c3a63b --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Essentials.AI.Sample.csproj @@ -0,0 +1,90 @@ + + + + $(MauiSamplePlatforms) + $(TargetFrameworks);$(MauiSamplePreviousPlatforms) + Maui.Essentials.AI.Sample + Microsoft.Maui.Essentials.AI.Sample + Exe + true + true + false + enable + enable + preview + SourceGen + + maccatalyst-x64 + maccatalyst-arm64 + 2727d4aa-a3a5-484b-9447-91604761972b + + + + Essentials AI + com.microsoft.maui.essentials.ai + 1.0 + 1 + + + + + + + + + + + + + + + + + + $(DefineConstants);ENABLE_OPENAI_CLIENT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $([System.Environment]::GetFolderPath(SpecialFolder.UserProfile))\AppData\Roaming\Microsoft\UserSecrets\$(UserSecretsId)\secrets.json + + + $([System.Environment]::GetFolderPath(SpecialFolder.UserProfile))/.microsoft/usersecrets/$(UserSecretsId)/secrets.json + + + + + + + + + + diff --git a/src/AI/samples/Essentials.AI.Sample/MauiProgram.cs b/src/AI/samples/Essentials.AI.Sample/MauiProgram.cs new file mode 100644 index 000000000000..f95bc153fe11 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/MauiProgram.cs @@ -0,0 +1,104 @@ +using System.Reflection; +using Maui.Controls.Sample.Pages; +using Maui.Controls.Sample.Services; +using Maui.Controls.Sample.ViewModels; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Maui.Essentials.AI; +#if ENABLE_OPENAI_CLIENT +using System.ClientModel; +using OpenAI; +using OpenAI.Chat; +#endif + +namespace Maui.Controls.Sample; + +public static class MauiProgram +{ + public static bool UseCloudAI = false; + + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + + builder.Configuration + .AddJsonStream(GetUserSecretsStream() ?? throw new InvalidOperationException("User secrets file not found as embedded resource.")); + + builder.UseMauiApp(); + +#if IOS || ANDROID || MACCATALYST + builder.UseMauiMaps(); +#endif + + builder.ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + + // Register AI + #if ENABLE_OPENAI_CLIENT + if (UseCloudAI) + { + var aiSection = builder.Configuration.GetSection("AI"); + var client = new ChatClient( + credential: new ApiKeyCredential(aiSection["ApiKey"] ?? throw new InvalidOperationException("API Key not found in user secrets.")), + model: aiSection["DeploymentName"] ?? throw new InvalidOperationException("Deployment Name not found in user secrets."), + options: new OpenAIClientOptions() + { + Endpoint = new(aiSection["Endpoint"] ?? throw new InvalidOperationException("Endpoint not found in user secrets.")), + }); + var ichatClient = client.AsIChatClient(); + + builder.Services.AddSingleton(provider => + { + var lf = provider.GetRequiredService(); + var realClient = ichatClient + .AsBuilder() + .UseLogging(lf) + .UseFunctionInvocation() + .Build(); + return realClient; + }); + } + else + #endif + { + builder.Services.AddPlatformChatClient(); + } + + // Register Pages + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + // Register ViewModels + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + // Register Services + builder.Services.AddSingleton(sp => LandmarkDataService.Instance); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddHttpClient(); + + // Configure Logging + builder.Services.AddLogging(); + builder.Logging.AddDebug(); + builder.Logging.AddConsole(); +#if DEBUG + builder.Logging.SetMinimumLevel(LogLevel.Debug); +#else + builder.Logging.SetMinimumLevel(LogLevel.Information); +#endif + + return builder.Build(); + } + + private static Stream? GetUserSecretsStream() + { + var assembly = Assembly.GetExecutingAssembly(); + var stream = assembly.GetManifestResourceStream("Maui.Essentials.AI.Sample.secrets.json"); + return stream; + } +} diff --git a/src/AI/samples/Essentials.AI.Sample/Models/Itinerary.cs b/src/AI/samples/Essentials.AI.Sample/Models/Itinerary.cs new file mode 100644 index 000000000000..d88be040c450 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Models/Itinerary.cs @@ -0,0 +1,101 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Maui.Controls.Sample.Models; + +[DisplayName("Itinerary")] +[Description("A travel itinerary with days and activities.")] +public record Itinerary +{ + [Description("An exciting name for the trip.")] + public required string Title { get; init; } + + public required string DestinationName { get; init; } + + public required string Description { get; init; } + + [Description("An explanation of how the itinerary meets the person's special requests.")] + public required string Rationale { get; init; } + + [Description("A list of day-by-day plans.")] + [Length(3, 3)] + public required List Days { get; init; } + + public static Itinerary GetExampleTripToJapan() => + new() + { + Title = "Onsen Trip to Japan", + DestinationName = "Mt. Fuji", + Description = "Sushi, hot springs, and ryokan with a toddler!", + Rationale = + """ + You are traveling with a child, so climbing Mt. Fuji is probably not an option, + but there is lots to do around Kawaguchiko Lake, including Fujikyu. + I recommend staying in a ryokan because you love hotsprings. + """, + Days = [ + new DayPlan + { + Title = "Sushi and Shopping Near Kawaguchiko", + Subtitle = "Spend your final day enjoying sushi and souvenir shopping.", + Destination = "Kawaguchiko Lake", + Activities = [ + new Activity + { + Type = ActivityKind.FoodAndDining, + Title = "The Restaurant serving Sushi", + Description = "Visit an authentic sushi restaurant for lunch." + }, + new Activity + { + Type = ActivityKind.Shopping, + Title = "The Plaza", + Description = "Enjoy souvenir shopping at various shops." + }, + new Activity + { + Type = ActivityKind.Sightseeing, + Title = "The Beautiful Cherry Blossom Park", + Description = "Admire the beautiful cherry blossom trees in the park." + }, + new Activity + { + Type = ActivityKind.HotelAndLodging, + Title = "The Hotel", + Description = "Spend one final evening in the hotspring before heading home." + }] + }] + }; +} + +[DisplayName("DayPlan")] +public record DayPlan +{ + [Description("A unique and exciting title for this day plan.")] + public required string Title { get; init; } + + public required string Subtitle { get; init; } + + public required string Destination { get; init; } + + [Length(3, 3)] + public required List Activities { get; init; } +} + +[DisplayName("Activity")] +public record Activity +{ + public required ActivityKind Type { get; init; } + + public required string Title { get; init; } + + public required string Description { get; init; } +} + +public enum ActivityKind +{ + Sightseeing, + FoodAndDining, + Shopping, + HotelAndLodging +} diff --git a/src/AI/samples/Essentials.AI.Sample/Models/Landmark.cs b/src/AI/samples/Essentials.AI.Sample/Models/Landmark.cs new file mode 100644 index 000000000000..3d7aafd3b114 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Models/Landmark.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; + +namespace Maui.Controls.Sample.Models; + +public record Landmark +{ + [JsonPropertyName("id")] + public int Id { get; init; } + + [JsonPropertyName("name")] + public required string Name { get; init; } + + [JsonPropertyName("continent")] + public required string Continent { get; init; } + + [JsonPropertyName("description")] + public required string Description { get; init; } + + [JsonPropertyName("shortDescription")] + public required string ShortDescription { get; init; } + + [JsonPropertyName("latitude")] + public double Latitude { get; init; } + + [JsonPropertyName("longitude")] + public double Longitude { get; init; } + + [JsonPropertyName("span")] + public double Span { get; init; } + + [JsonPropertyName("placeID")] + public string? PlaceId { get; init; } + + [JsonIgnore] + public string BackgroundImageName => $"{Id}"; + + [JsonIgnore] + public string ThumbnailImageName => $"{Id}_thumb"; + + [JsonIgnore] + public Location Location => new(Latitude, Longitude); +} diff --git a/src/AI/samples/Essentials.AI.Sample/Models/Weather.cs b/src/AI/samples/Essentials.AI.Sample/Models/Weather.cs new file mode 100644 index 000000000000..a2c2c8646a07 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Models/Weather.cs @@ -0,0 +1,86 @@ +using System.Text.Json.Serialization; + +namespace Maui.Controls.Sample.Models; + +public record WeatherForecast +{ + [JsonPropertyName("latitude")] + public double Latitude { get; init; } + + [JsonPropertyName("longitude")] + public double Longitude { get; init; } + + [JsonPropertyName("daily")] + public DailyWeather Daily { get; init; } = new(); +} + +public record DailyWeather +{ + [JsonPropertyName("time")] + public List Time { get; init; } = []; + + [JsonPropertyName("temperature_2m_mean")] + public List TemperatureMean { get; init; } = []; + + [JsonPropertyName("weather_code")] + public List WeatherCode { get; init; } = []; +} + +public static class WeatherCodeExtensions +{ + public static string GetWeatherEmoji(int code) + { + return code switch + { + 0 => "☀️", + 1 or 2 => "🌤️", + 3 => "☁️", + 45 or 48 => "🌫️", + 51 or 53 or 55 or 56 or 57 => "🌧️", + 61 or 63 or 65 => "🌧️", + 66 or 67 => "🌧️", + 71 or 73 or 75 or 77 => "❄️", + 80 or 81 or 82 => "🌧️", + 85 or 86 => "❄️", + 95 => "⛈️", + 96 or 99 => "⛈️", + _ => "☁️" + }; + } + + public static string GetWeatherDescription(int code) + { + return code switch + { + 0 => "Clear sky", + 1 => "Mainly clear", + 2 => "Partly cloudy", + 3 => "Overcast", + 45 => "Fog", + 48 => "Depositing rime fog", + 51 => "Light drizzle", + 53 => "Moderate drizzle", + 55 => "Dense drizzle", + 56 => "Light freezing drizzle", + 57 => "Dense freezing drizzle", + 61 => "Slight rain", + 63 => "Moderate rain", + 65 => "Heavy rain", + 66 => "Light freezing rain", + 67 => "Heavy freezing rain", + 71 => "Slight snow", + 73 => "Moderate snow", + 75 => "Heavy snow", + 77 => "Snow grains", + 80 => "Slight rain showers", + 81 => "Moderate rain showers", + 82 => "Violent rain showers", + 85 => "Slight snow showers", + 86 => "Heavy snow showers", + 95 => "Thunderstorm", + 96 => "Thunderstorm with slight hail", + 99 => "Thunderstorm with heavy hail", + _ => "Unknown" + }; + } +} diff --git a/src/AI/samples/Essentials.AI.Sample/Pages/LandmarksPage.xaml b/src/AI/samples/Essentials.AI.Sample/Pages/LandmarksPage.xaml new file mode 100644 index 000000000000..6b5c624b5604 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Pages/LandmarksPage.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + diff --git a/src/AI/samples/Essentials.AI.Sample/Pages/LandmarksPage.xaml.cs b/src/AI/samples/Essentials.AI.Sample/Pages/LandmarksPage.xaml.cs new file mode 100644 index 000000000000..71fb097ad41e --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Pages/LandmarksPage.xaml.cs @@ -0,0 +1,26 @@ +using Maui.Controls.Sample.Models; +using Maui.Controls.Sample.ViewModels; + +namespace Maui.Controls.Sample.Pages; + +public partial class LandmarksPage : ContentPage +{ + public LandmarksPage(LandmarksViewModel viewModel) + { + InitializeComponent(); + + BindingContext = viewModel; + + Loaded += async (_, _) => await viewModel.InitializeAsync(); + } + + private async void OnLandmarkTapped(object? sender, Landmark landmark) + { + var parameters = new Dictionary + { + { "Landmark", landmark } + }; + + await Shell.Current.GoToAsync(nameof(TripPlanningPage), parameters); + } +} diff --git a/src/AI/samples/Essentials.AI.Sample/Pages/TripPlanningPage.xaml b/src/AI/samples/Essentials.AI.Sample/Pages/TripPlanningPage.xaml new file mode 100644 index 000000000000..1ce3711ec370 --- /dev/null +++ b/src/AI/samples/Essentials.AI.Sample/Pages/TripPlanningPage.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + +