diff --git a/build.psm1 b/build.psm1 index b521571..f84f5de 100644 --- a/build.psm1 +++ b/build.psm1 @@ -6,6 +6,7 @@ $metadata = Get-Content $PSScriptRoot/tools/metadata.json | ConvertFrom-Json $dotnetSDKVersion = $(Get-Content $PSScriptRoot/global.json | ConvertFrom-Json).Sdk.Version $dotnetLocalDir = if ($IsWindows) { "$env:LocalAppData\Microsoft\dotnet" } else { "$env:HOME/.dotnet" } +$windowsOnlyAgents = @('phisilica') function Start-Build { @@ -20,7 +21,7 @@ function Start-Build [string] $Runtime = [NullString]::Value, [Parameter()] - [ValidateSet('openai-gpt', 'msaz', 'interpreter', 'ollama')] + [ValidateSet('openai-gpt', 'msaz', 'interpreter', 'ollama', 'phisilica')] [string[]] $AgentToInclude, [Parameter()] @@ -43,13 +44,18 @@ function Start-Build $MyInvocation.MyCommand.Parameters["AgentToInclude"].Attributes | Where-Object { $_ -is [ValidateSet] } | Select-Object -First 1 | - ForEach-Object ValidValues + ForEach-Object ValidValues | + Skip-Unapplicable } else { $agents.Split(",", [System.StringSplitOptions]::TrimEntries) Write-Verbose "Include agents specified in Metadata.json" } } + if (HasUnapplicableAgent $AgentToInclude) { + throw "The following specified agent(s) cannot be built on the current platform: $($windowsOnlyAgents -join ', ')." + } + $RID = $Runtime ?? (dotnet --info | Select-String '^\s*RID:\s+(\w+-\w+)$' | Select-Object -First 1 | @@ -69,6 +75,7 @@ function Start-Build $msaz_dir = Join-Path $agent_dir "Microsoft.Azure.Agent" $interpreter_agent_dir = Join-Path $agent_dir "AIShell.Interpreter.Agent" $ollama_agent_dir = Join-Path $agent_dir "AIShell.Ollama.Agent" + $phisilica_agent_dir = Join-Path $agent_dir "AIShell.PhiSilica.Agent" $config = $Configuration.ToLower() $out_dir = Join-Path $PSScriptRoot "out" @@ -79,6 +86,7 @@ function Start-Build $msaz_out_dir = Join-Path $app_out_dir "agents" "Microsoft.Azure.Agent" $interpreter_out_dir = Join-Path $app_out_dir "agents" "AIShell.Interpreter.Agent" $ollama_out_dir = Join-Path $app_out_dir "agents" "AIShell.Ollama.Agent" + $phisilica_out_dir = Join-Path $app_out_dir "agents" "AIShell.PhiSilica.Agent" if ($Clean) { if (Test-Path $out_dir) { @@ -152,6 +160,12 @@ function Start-Build dotnet publish $ollama_csproj -c $Configuration -o $ollama_out_dir } + if ($LASTEXITCODE -eq 0 -and $AgentToInclude -contains 'phisilica') { + Write-Host "`n[Build the PhiSilica agent ...]`n" -ForegroundColor Green + $phisilica_csproj = GetProjectFile $phisilica_agent_dir + dotnet publish $phisilica_csproj -c $Configuration -o $phisilica_out_dir + } + if ($LASTEXITCODE -eq 0 -and -not $NotIncludeModule) { Write-Host "`n[Build the AIShell module ...]`n" -ForegroundColor Green $aish_module_csproj = GetProjectFile $module_dir @@ -175,6 +189,26 @@ function Start-Build } } +filter Skip-Unapplicable { + if ($IsWindows -or $windowsOnlyAgents -notcontains $_) { + $_ + } +} + +function HasUnapplicableAgent($specifiedAgents) { + if ($IsWindows) { + return $false + } + + foreach ($agent in $windowsOnlyAgents) { + if ($specifiedAgents -contains $agent) { + return $true + } + } + + return $false +} + function GetProjectFile($dir) { return Get-Item "$dir/*.csproj" | ForEach-Object FullName diff --git a/shell/agents/AIShell.PhiSilica.Agent/AIShell.PhiSilica.Agent.csproj b/shell/agents/AIShell.PhiSilica.Agent/AIShell.PhiSilica.Agent.csproj new file mode 100644 index 0000000..abc3f55 --- /dev/null +++ b/shell/agents/AIShell.PhiSilica.Agent/AIShell.PhiSilica.Agent.csproj @@ -0,0 +1,29 @@ + + + Library + net8.0-windows10.0.26100.0 + enable + CS8305 + AnyCPU + None + true + true + true + win-arm64 + true + + + + + + false + + runtime + + + + + + + + diff --git a/shell/agents/AIShell.PhiSilica.Agent/PhiSilicaAgent.cs b/shell/agents/AIShell.PhiSilica.Agent/PhiSilicaAgent.cs new file mode 100644 index 0000000..f10a415 --- /dev/null +++ b/shell/agents/AIShell.PhiSilica.Agent/PhiSilicaAgent.cs @@ -0,0 +1,97 @@ +using AIShell.Abstraction; +using Microsoft.Windows.AI; +using Microsoft.Windows.AI.Generative; + +namespace AIShell.PhiSilica.Agent; + +public sealed partial class PhiSilicaAgent : ILLMAgent +{ + private readonly Task _initTask; + private LanguageModel _model; + + public string Name => "PhiSilica"; + public string Description => "This is the AI Shell Agent for talking to the inbox Phi Silica model on Copilot+ PCs."; + public string SettingFile => null; + + public IEnumerable GetCommands() => null; + public bool CanAcceptFeedback(UserAction action) => false; + public Task RefreshChatAsync(IShell shell, bool force) => Task.CompletedTask; + public void OnUserAction(UserActionPayload actionPayload) { } + public void Initialize(AgentConfig config) { } + public void Dispose() { } + + public PhiSilicaAgent() + { + // Start the initialization for AI feature and model on a background thread. + _initTask = Task.Run(InitFeatureAndModelAsync); + } + + private async Task InitFeatureAndModelAsync() + { + AIFeatureReadyState state = LanguageModel.GetReadyState(); + if (state is AIFeatureReadyState.NotSupportedOnCurrentSystem) + { + throw new PlatformNotSupportedException("The Phi Silica feature is not supported on current system."); + } + + if (state is AIFeatureReadyState.DisabledByUser) + { + throw new PlatformNotSupportedException("The Phi Silica feature is currently disabled."); + } + + if (state is AIFeatureReadyState.EnsureNeeded) + { + // Initialize the WinRT runtime. + AIFeatureReadyResult result = await LanguageModel.EnsureReadyAsync(); + // Do not proceed if it failed to get the feature ready. + if (result.Status is not AIFeatureReadyResultState.Success) + { + throw new InvalidOperationException(result.ErrorDisplayText, result.Error); + } + } + + _model = await LanguageModel.CreateAsync(); + } + + public async Task ChatAsync(string input, IShell shell) + { + IHost host = shell.Host; + + try + { + // Wait for the init task to finish. Once it's finished, calling this again is a non-op. + await _initTask; + } + catch (Exception e) + { + host.WriteErrorLine(e.Message); + if (e is InvalidOperationException && e.InnerException is not null) + { + host.WriteErrorLine(e.InnerException.StackTrace); + } + else if (e is not PlatformNotSupportedException) + { + // Show stack trace for non-PNS exception. + host.WriteErrorLine(e.StackTrace); + } + + return false; + } + + var result = await host.RunWithSpinnerAsync( + status: "Thinking ...", + func: async () => await _model.GenerateResponseAsync(input) + ); + + if (result is not null && !string.IsNullOrEmpty(result.Text)) + { + host.RenderFullResponse(result.Text); + } + else + { + host.WriteErrorLine("No response received from the language model."); + } + + return true; + } +}