├── .devcontainer ├── devcontainer.json └── on-create.sh ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ ├── QUEST_COMPLETION.yaml.nouse │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── azure-dev-build-only.yml │ ├── azure-dev.yml │ ├── clear-deployment-history.yml.nouse │ ├── clear-resource-group.yml.nouse │ ├── on-quest-submitted.yml.nouse │ ├── on-quest-verified.yml.nouse │ └── reset-resource-group.yml.nouse ├── .gitignore ├── Directory.Build.props ├── Dockerfile ├── Dockerfile.azure ├── LICENSE ├── OpenChatPlayground.sln ├── README.md ├── assets ├── favicon.zip ├── icon-inverted-transparent.png ├── icon-inverted-transparent.svg ├── icon-inverted.png ├── icon-inverted.svg ├── icon-transparent.png ├── icon-transparent.svg ├── icon.png ├── icon.pptx ├── icon.svg ├── logo-inverted.png └── logo.png ├── azure.yaml ├── docs ├── README.md ├── azure-ai-foundry.md ├── github-models.md ├── hugging-face.md ├── lg.md ├── openai.md └── upstage.md ├── global.json ├── images └── ocp-hero.png ├── infra ├── abbreviations.json ├── main.bicep ├── main.parameters.json ├── modules │ └── fetch-container-image.bicep └── resources.bicep ├── licenses ├── AWSSDK.Extensions.Bedrock.MEAI.md └── Mscc.GenerativeAI.Microsoft.md ├── src ├── OpenChat.ConsoleApp │ ├── Clients │ │ └── ApiClient.cs │ ├── Configurations │ │ └── AppSettings.cs │ ├── Models │ │ └── ChatMessage.cs │ ├── OpenChat.ConsoleApp.csproj │ ├── Options │ │ └── ArgumentOptions.cs │ ├── Program.cs │ ├── Services │ │ └── PlaygroundService.cs │ └── appsettings.json └── OpenChat.PlaygroundApp │ ├── Abstractions │ ├── ArgumentOptions.cs │ ├── LanguageModelConnector.cs │ └── LanguageModelSettings.cs │ ├── Components │ ├── App.razor │ ├── Layout │ │ ├── LoadingSpinner.razor │ │ ├── LoadingSpinner.razor.css │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── SurveyPrompt.razor │ │ └── SurveyPrompt.razor.css │ ├── Pages │ │ ├── Chat │ │ │ ├── Chat.razor │ │ │ ├── Chat.razor.cs │ │ │ ├── Chat.razor.css │ │ │ ├── ChatHeader.razor │ │ │ ├── ChatHeader.razor.cs │ │ │ ├── ChatHeader.razor.css │ │ │ ├── ChatInput.razor │ │ │ ├── ChatInput.razor.cs │ │ │ ├── ChatInput.razor.css │ │ │ ├── ChatInput.razor.js │ │ │ ├── ChatMessageItem.razor │ │ │ ├── ChatMessageItem.razor.cs │ │ │ ├── ChatMessageItem.razor.css │ │ │ ├── ChatMessageList.razor │ │ │ ├── ChatMessageList.razor.css │ │ │ └── ChatMessageList.razor.js │ │ └── Error.razor │ ├── Routes.razor │ └── _Imports.razor │ ├── Configurations │ ├── AmazonBedrockSettings.cs │ ├── AnthropicSettings.cs │ ├── AppSettings.cs │ ├── AzureAIFoundrySettings.cs │ ├── DockerModelRunnerSettings.cs │ ├── FoundryLocalSettings.cs │ ├── GitHubModelsSettings.cs │ ├── GoogleVertexAISettings.cs │ ├── HuggingFaceSettings.cs │ ├── LGSettings.cs │ ├── OllamaSettings.cs │ ├── OpenAISettings.cs │ └── UpstageSettings.cs │ ├── Connectors │ ├── AzureAIFoundryConnector.cs │ ├── ConnectorType.cs │ ├── GitHubModelsConnector.cs │ ├── HuggingFaceConnector.cs │ ├── LGConnector.cs │ ├── OpenAIConnector.cs │ └── UpstageConnector.cs │ ├── Constants │ ├── AppSettingConstants.cs │ └── ArgumentOptionConstants.cs │ ├── Endpoints │ ├── ChatResponseEndpoint.cs │ ├── EndpointExtensions.cs │ └── IEndpoint.cs │ ├── Models │ ├── ChatRequest.cs │ └── ChatResponse.cs │ ├── OpenApi │ └── OpenApiDocumentTransformer.cs │ ├── OpenChat.PlaygroundApp.csproj │ ├── OpenChat.PlaygroundApp.http │ ├── Options │ ├── AmazonBedrockArgumentOptions.cs │ ├── AnthropicArgumentOptions.cs │ ├── AzureAIFoundryArgumentOptions.cs │ ├── DockerModelRunnerArgumentOptions.cs │ ├── FoundryLocalArgumentOptions.cs │ ├── GitHubModelsArgumentOptions.cs │ ├── GoogleVertexAIArgumentOptions.cs │ ├── HuggingFaceArgumentOptions.cs │ ├── LGArgumentOptions.cs │ ├── OllamaArgumentOptions.cs │ ├── OpenAIArgumentOptions.cs │ └── UpstageArgumentOptions.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Services │ └── ChatService.cs │ ├── appsettings.json │ └── wwwroot │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── app.css │ ├── app.js │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── lib │ ├── dompurify │ │ ├── README.md │ │ └── dist │ │ │ └── purify.es.mjs │ ├── marked │ │ ├── README.md │ │ └── dist │ │ │ └── marked.esm.js │ └── tailwindcss │ │ ├── README.md │ │ └── dist │ │ └── preflight.css │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png └── test ├── OpenChat.ConsoleApp.Tests ├── Clients │ └── ApiClientTests.cs ├── OpenChat.ConsoleApp.Tests.csproj ├── Options │ └── ArgumentOptionsTests.cs └── Services │ └── PlaygroundServiceTests.cs └── OpenChat.PlaygroundApp.Tests ├── Abstractions ├── ArgumentOptionsTests.cs ├── LanguageModelConnectorTests.cs └── LanguageModelSettingsTests.cs ├── Components └── Pages │ └── Chat │ ├── ChatHeaderUITests.cs │ ├── ChatInputImeE2ETests.cs │ ├── ChatInputUITests.cs │ ├── ChatMessageItemUITests.cs │ ├── ChatStreamingUITest.cs │ └── ChatUITests.cs ├── Connectors ├── AzureAIFoundryConnectorTests.cs ├── GitHubModelsConnectorTests.cs ├── HuggingFaceConnectorTests.cs ├── LGConnectorTests.cs ├── OpenAIConnectorTests.cs └── UpstageConnectorTests.cs ├── Endpoints ├── ChatResponseEndpointTests.cs ├── EndpointExtensionsTests.cs └── TestEndpoint.cs ├── OpenChat.PlaygroundApp.Tests.csproj ├── Options ├── AmazonBedrockArgumentOptionsTests.cs ├── AnthropicArgumentOptionsTests.cs ├── AzureAIFoundryArgumentOptionsTests.cs ├── DockerModelRunnerArgumentOptionsTests.cs ├── FoundryLocalArgumentOptionsTests.cs ├── GitHubModelsArgumentOptionsTests.cs ├── GoogleVertexAIArgumentOptionsTests.cs ├── HuggingFaceArgumentOptionsTests.cs ├── LGArgumentOptionsTests.cs ├── OllamaArgumentOptionsTests.cs ├── OpenAIArgumentOptionsTests.cs └── UpstageArgumentOptionsTests.cs ├── Services └── ChatServiceTests.cs ├── TestConstants.cs └── appsettings.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Open Chat Playground", 3 | 4 | "image": "mcr.microsoft.com/devcontainers/dotnet:8.0-noble", 5 | 6 | "features": { 7 | "ghcr.io/azure/azure-dev/azd:latest": {}, 8 | "ghcr.io/devcontainers/features/common-utils:latest": {}, 9 | "ghcr.io/devcontainers/features/dotnet:latest": { 10 | "version": "9.0" 11 | }, 12 | "ghcr.io/devcontainers/features/azure-cli:latest": { 13 | "extensions": "account,containerapp,deploy-to-azure,subscription" 14 | }, 15 | "ghcr.io/devcontainers/features/docker-in-docker:latest": {}, 16 | "ghcr.io/devcontainers/features/github-cli:latest": {}, 17 | "ghcr.io/devcontainers/features/kubectl-helm-minikube:latest": {}, 18 | "ghcr.io/devcontainers/features/node:latest": {}, 19 | "ghcr.io/prom3theu5/aspirational-manifests/aspirate:latest": {} 20 | }, 21 | 22 | "overrideFeatureInstallOrder": [ 23 | "ghcr.io/devcontainers/features/common-utils" 24 | ], 25 | 26 | "customizations": { 27 | "vscode": { 28 | "extensions": [ 29 | "dbaeumer.vscode-eslint", 30 | "EditorConfig.EditorConfig", 31 | "GitHub.copilot", 32 | "GitHub.copilot-chat", 33 | "GitHub.vscode-github-actions", 34 | "GitHub.vscode-pull-request-github", 35 | "ms-azuretools.azure-dev", 36 | "ms-azuretools.vscode-bicep", 37 | "ms-azuretools.vscode-docker", 38 | "ms-dotnettools.csharp", 39 | "ms-dotnettools.csdevkit", 40 | "ms-vscode.vscode-node-azure-pack", 41 | "redhat.vscode-yaml" 42 | ], 43 | "settings": { 44 | "editor.minimap.enabled": false, 45 | 46 | "editor.fontFamily": "D2Coding, Consolas, 'Courier New', monospace", 47 | 48 | "terminal.integrated.fontFamily": "D2CodingLigature Nerd Font", 49 | 50 | "explorer.sortOrder": "type", 51 | "explorer.fileNesting.enabled": true, 52 | "explorer.fileNesting.patterns": { 53 | "*.bicep": "${capture}.json, ${capture}.parameters.json", 54 | "*.razor": "${capture}.razor.css, ${capture}.razor.cs", 55 | "*.js": "${capture}.js.map" 56 | } 57 | } 58 | } 59 | }, 60 | 61 | "remoteUser": "vscode", 62 | 63 | "onCreateCommand": "/bin/bash ./.devcontainer/on-create.sh > ~/on-create.log", 64 | 65 | "hostRequirements": { 66 | "memory": "8gb" 67 | } 68 | } -------------------------------------------------------------------------------- /.devcontainer/on-create.sh: -------------------------------------------------------------------------------- 1 | ## Install additional apt packages 2 | sudo apt-get update && \ 3 | sudo apt upgrade -y && \ 4 | sudo apt-get install -y dos2unix libsecret-1-0 xdg-utils fonts-naver-d2coding && \ 5 | sudo apt-get clean -y && \ 6 | sudo rm -rf /var/lib/apt/lists/* 7 | 8 | ## Configure git 9 | echo Configure git 10 | git config --global pull.rebase false 11 | git config --global core.autocrlf input 12 | 13 | ## Install .NET dev certs 14 | echo Install .NET dev certs 15 | dotnet dev-certs https --trust 16 | 17 | ## Add .NET Aspire workload 18 | echo Install .NET Aspire workload 19 | sudo dotnet workload update --from-previous-sdk 20 | sudo dotnet workload uninstall aspire 21 | dotnet new install Aspire.ProjectTemplates 22 | dotnet new install Microsoft.Extensions.AI.Templates 23 | dotnet tool install --global aspire.cli --prerelease 24 | 25 | # D2Coding Nerd Font 26 | echo Install D2Coding Nerd Font 27 | mkdir $HOME/.local 28 | mkdir $HOME/.local/share 29 | mkdir $HOME/.local/share/fonts 30 | wget https://github.com/ryanoasis/nerd-fonts/releases/latest/download/D2Coding.zip 31 | unzip D2Coding.zip -d $HOME/.local/share/fonts 32 | rm D2Coding.zip 33 | 34 | ## AZURE BICEP CLI ## 35 | echo Install Azure Bicep CLI 36 | az bicep install 37 | 38 | ## OH-MY-POSH ## 39 | echo Install oh-my-posh 40 | sudo wget https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/posh-linux-amd64 -O /usr/local/bin/oh-my-posh 41 | sudo chmod +x /usr/local/bin/oh-my-posh 42 | 43 | echo DONE! 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | *.sln text eol=crlf 5 | *.csproj text eol=crlf 6 | *.props text eol=crlf 7 | *.sh text eol=lf 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/QUEST_COMPLETION.yaml.nouse: -------------------------------------------------------------------------------- 1 | name: '과제 완료 인증' 2 | description: '과제 완료 인증용 템플릿' 3 | title: "과제 완료 인증" 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 과제를 완료하신 분들은 이슈를 통해 인증을 받을 수 있습니다. 아래의 템플릿을 작성하여 이슈를 등록해 주세요. 10 | 11 | ## 공통 ## 12 | - type: dropdown 13 | id: title 14 | attributes: 15 | label: '제목' 16 | options: 17 | # - '과제 1: C# Certificate' 18 | # - '과제 2: Blazor - Connect Four' 19 | # - '과제 3: Unit Tests' 20 | - '과제 4: GenAI for Beginners - .NET' 21 | default: 0 22 | 23 | ## 공통 ## 24 | - type: input 25 | id: github_id 26 | attributes: 27 | label: 'GitHub 프로필 URL' 28 | description: 'GitHub 프로필 주소를 입력하세요. (예: https://github.com/aliencube)' 29 | placeholder: 'GitHub 프로필 주소를 입력하세요. (예: https://github.com/aliencube)' 30 | validations: 31 | required: true 32 | 33 | # ## 과제 1 ## 34 | # - type: input 35 | # id: english_name 36 | # attributes: 37 | # label: '영문 이름' 38 | # description: 'C# 자격증에 표기된 영문 이름을 입력하세요. (예: `Gaebal Kim`)' 39 | # placeholder: 'C# 자격증에 표기된 영문 이름을 입력하세요. (예: Gaebal Kim)' 40 | # validations: 41 | # required: true 42 | 43 | # ## 과제 1 ## 44 | # - type: input 45 | # id: certification_id 46 | # attributes: 47 | # label: 'C# 자격증 ID' 48 | # description: 'C# 자격증의 고유 GUID를 입력하세요. 자신의 자격증에서 GUID를 찾을 수 있습니다. (예: https://www.freecodecamp.org/certification/`{{THIS_GUID}}`/foundational-c-sharp-with-microsoft)' 49 | # placeholder: 'C# 자격증의 고유 GUID를 입력하세요. 자신의 자격증에서 GUID를 찾을 수 있습니다. (예: https://www.freecodecamp.org/certification/{{THIS_GUID}}/foundational-c-sharp-with-microsoft)' 50 | # validations: 51 | # required: true 52 | 53 | # ## 과제 2 ## 54 | # - type: input 55 | # id: github_repository 56 | # attributes: 57 | # label: 'GitHub 리포지토리 URL' 58 | # description: '과제를 저장한 GitHub 리포지토리 주소를 입력하세요. (예: `https://github.com/aliencube/open-chat-playground`)' 59 | # placeholder: '과제를 저장한 GitHub 리포지토리 주소를 입력하세요. (예: https://github.com/aliencube/open-chat-playground)' 60 | # validations: 61 | # required: true 62 | 63 | # ## 과제 3 ## 64 | # - type: input 65 | # id: mslearn_profile 66 | # attributes: 67 | # label: 'MS Learn 프로필 URL' 68 | # description: 'MS Learn 프로필 주소를 입력하세요. (예: https://learn.microsoft.com/users/aliencube)' 69 | # placeholder: 'MS Learn 프로필 주소를 입력하세요. (예: https://learn.microsoft.com/users/aliencube)' 70 | # validations: 71 | # required: true 72 | 73 | ## 과제 4 ## 74 | - type: input 75 | id: youtube_url 76 | attributes: 77 | label: 'YouTube 동영상 링크' 78 | description: '과제를 저장한 YouTube 동영상 링크를 입력하세요. (예: `https://youtube.com/watch?v=4zkIBMFdL2w`)' 79 | placeholder: '과제를 저장한 YouTube 동영상 링크를 입력하세요. (예: https://youtube.com/watch?v=4zkIBMFdL2w)' 80 | validations: 81 | required: true 82 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] New feature 19 | [ ] Refactoring (no functional changes, no api changes) 20 | [ ] Documentation content changes 21 | [ ] Other... Please describe: 22 | ``` 23 | 24 | ## README updated? 25 | 26 | The top-level readme for this repo contains a link to each sample in the repo. If you're adding a new sample did you update the readme? 27 | 28 | ``` 29 | [ ] Yes 30 | [ ] No 31 | [ ] N/A 32 | ``` 33 | 34 | ## How to Test 35 | * Get the code 36 | 37 | ``` 38 | git clone [repo-address] 39 | cd [repo-name] 40 | git checkout [branch-name] 41 | ``` 42 | 43 | * Test the code 44 | 45 | ``` 46 | ``` 47 | 48 | ## What to Check 49 | Verify that the following are valid 50 | * ... 51 | 52 | ## Other Information 53 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev-build-only.yml: -------------------------------------------------------------------------------- 1 | name: Azure Dev - Build Only 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'feature/*' 7 | paths-ignore: 8 | - '.github/**' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | id-token: write 15 | contents: read 16 | issues: write 17 | pull-requests: write 18 | 19 | jobs: 20 | build-test: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Setup .NET SDK 29 | uses: actions/setup-dotnet@v4 30 | with: 31 | dotnet-version: 9.x 32 | 33 | - name: Install local certs 34 | shell: bash 35 | run: | 36 | dotnet dev-certs https --trust 37 | 38 | - name: Restore NuGet packages 39 | shell: bash 40 | run: | 41 | dotnet restore 42 | 43 | - name: Build solution 44 | shell: bash 45 | run: | 46 | dotnet build . --no-restore --no-incremental 47 | 48 | - name: Run unit tests 49 | shell: bash 50 | run: | 51 | dotnet test . --no-build --logger "trx" --collect:"XPlat Code Coverage" --filter "Category=UnitTest" 52 | 53 | - name: Install playwright 54 | shell: pwsh 55 | run: | 56 | $playwright = Get-ChildItem -File Microsoft.Playwright.dll -Path . -Recurse 57 | $installer = "$($playwright[0].Directory.FullName)/playwright.ps1" 58 | & "$installer" install 59 | 60 | - name: Run app in background 61 | shell: bash 62 | run: | 63 | dotnet user-secrets --project ./src/OpenChat.PlaygroundApp \ 64 | set GitHubModels:Token "temporary-dummy-token" 65 | 66 | dotnet run --project ./src/OpenChat.PlaygroundApp & 67 | 68 | sleep 30 69 | 70 | - name: Run integration tests - LLM not required 71 | shell: bash 72 | run: | 73 | dotnet test . --no-build --logger "trx" --collect:"XPlat Code Coverage" --filter "Category=IntegrationTest & Category!=LLMRequired" 74 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev.yml: -------------------------------------------------------------------------------- 1 | name: Azure Dev 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - '.github/**' 10 | 11 | permissions: 12 | contents: read 13 | packages: write 14 | attestations: write 15 | id-token: write 16 | 17 | env: 18 | REGISTRY: ghcr.io 19 | REPOSITORY: ${{ github.repository }} 20 | IMAGE_NAME: openchat-playground 21 | 22 | jobs: 23 | build-test-deploy: 24 | 25 | runs-on: ubuntu-latest 26 | 27 | env: 28 | AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} 29 | AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} 30 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} 31 | AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} 32 | AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} 33 | GH_MODELS_TOKEN: ${{ secrets.GH_MODELS_TOKEN }} 34 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 35 | 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | 40 | - name: Setup .NET SDK 41 | uses: actions/setup-dotnet@v4 42 | with: 43 | dotnet-version: 9.x 44 | 45 | - name: Install local certs 46 | shell: bash 47 | run: | 48 | dotnet dev-certs https --trust 49 | 50 | - name: Restore NuGet packages 51 | shell: bash 52 | run: | 53 | dotnet restore 54 | 55 | - name: Build solution 56 | shell: bash 57 | run: | 58 | dotnet build -c Release --no-restore --no-incremental 59 | 60 | - name: Run unit tests 61 | shell: bash 62 | run: | 63 | dotnet test . -c Release --no-build --logger "trx" --collect:"XPlat Code Coverage" --filter "Category=UnitTest" 64 | 65 | - name: Install playwright 66 | shell: pwsh 67 | run: | 68 | $playwright = Get-ChildItem -File Microsoft.Playwright.dll -Path . -Recurse 69 | $installer = "$($playwright[0].Directory.FullName)/playwright.ps1" 70 | & "$installer" install 71 | 72 | - name: Run app in background 73 | shell: bash 74 | run: | 75 | dotnet user-secrets --project ./src/OpenChat.PlaygroundApp \ 76 | set GitHubModels:Token "temporary-dummy-token" 77 | 78 | dotnet run --project ./src/OpenChat.PlaygroundApp & 79 | 80 | sleep 30 81 | 82 | - name: Run integration tests - LLM not required 83 | shell: bash 84 | run: | 85 | dotnet test . -c Release --no-build --logger "trx" --collect:"XPlat Code Coverage" --filter "Category=IntegrationTest & Category!=LLMRequired" 86 | 87 | - name: Log in to the Container registry 88 | uses: docker/login-action@v3 89 | with: 90 | registry: ${{ env.REGISTRY }} 91 | username: ${{ github.actor }} 92 | password: ${{ secrets.GITHUB_TOKEN }} 93 | 94 | - name: Extract metadata (tags, labels) for Docker 95 | id: meta 96 | uses: docker/metadata-action@v5 97 | with: 98 | images: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }} 99 | 100 | - name: Set up Docker Buildx 101 | uses: docker/setup-buildx-action@v3 102 | 103 | - name: Build and push Docker image 104 | id: push 105 | uses: docker/build-push-action@v6 106 | with: 107 | platforms: linux/amd64,linux/arm64 108 | push: true 109 | context: ${{ github.workspace }} 110 | file: ${{ github.workspace }}/Dockerfile 111 | tags: '${{ steps.meta.outputs.tags }},${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:latest' 112 | labels: ${{ steps.meta.outputs.labels }} 113 | 114 | - name: Generate artifact attestation 115 | uses: actions/attest-build-provenance@v2 116 | with: 117 | subject-name: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }} 118 | subject-digest: ${{ steps.push.outputs.digest }} 119 | push-to-registry: true 120 | 121 | - name: Install azd 122 | uses: Azure/setup-azd@v2 123 | 124 | - name: Log in with Azure 125 | shell: pwsh 126 | run: | 127 | azd auth login ` 128 | --client-id "$env:AZURE_CLIENT_ID" ` 129 | --federated-credential-provider "github" ` 130 | --tenant-id "$env:AZURE_TENANT_ID" 131 | 132 | - name: Provision Infrastructure 133 | shell: bash 134 | env: 135 | GitHubModels__Token: ${{ env.GH_MODELS_TOKEN }} 136 | OpenAI__ApiKey: ${{ env.OPENAI_API_KEY }} 137 | run: | 138 | azd provision --no-prompt 139 | 140 | - name: Deploy Application 141 | shell: bash 142 | run: | 143 | azd deploy --no-prompt 144 | -------------------------------------------------------------------------------- /.github/workflows/clear-deployment-history.yml.nouse: -------------------------------------------------------------------------------- 1 | name: Clear Deployment History 2 | 3 | on: 4 | workflow_dispatch: 5 | # schedule: 6 | # - cron: "0 0/4 * * *" 7 | 8 | permissions: 9 | contents: read 10 | id-token: write 11 | issues: write 12 | pull-requests: write 13 | 14 | jobs: 15 | delete-deployment-history: 16 | name: "Deleting Deployment History" 17 | 18 | if: github.repository_owner == 'aliencube' 19 | 20 | runs-on: ubuntu-latest 21 | 22 | env: 23 | AZURE_CLIENT_ID: ${{ vars.AZM_CLIENT_ID }} 24 | AZURE_CLIENT_SECRET: ${{ secrets.AZM_CLIENT_SECRET }} 25 | AZURE_TENANT_ID: ${{ vars.AZM_TENANT_ID }} 26 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZM_SUBSCRIPTION_ID }} 27 | 28 | steps: 29 | - name: Azure login 30 | uses: azure/login@v2 31 | with: 32 | creds: '{ "clientId": "${{ env.AZURE_CLIENT_ID }}", "clientSecret": "${{ env.AZURE_CLIENT_SECRET }}", "tenantId": "${{ env.AZURE_TENANT_ID }}", "subscriptionId": "${{ env.AZURE_SUBSCRIPTION_ID }}" }' 33 | # client-id: ${{ env.AZURE_CLIENT_ID }} 34 | # tenant-id: ${{ env.AZURE_TENANT_ID }} 35 | # subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} 36 | 37 | - name: Delete all deployment history 38 | shell: pwsh 39 | run: | 40 | $history = az deployment sub list --query "[].name" | ConvertFrom-Json 41 | $history | ForEach-Object { 42 | $name = $_ 43 | az deployment sub delete --name $name --no-wait 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/clear-resource-group.yml.nouse: -------------------------------------------------------------------------------- 1 | name: Clear Resource Group 2 | 3 | on: 4 | workflow_dispatch: 5 | # schedule: 6 | # # Run every day at 4:30 AM (KST) 7 | # - cron: "30 19 * * *" 8 | 9 | permissions: 10 | contents: read 11 | id-token: write 12 | issues: write 13 | pull-requests: write 14 | 15 | jobs: 16 | clear-resource-group: 17 | name: "Clearing Resource Groups" 18 | 19 | if: github.repository_owner == 'aliencube' 20 | 21 | runs-on: ubuntu-latest 22 | 23 | env: 24 | AZURE_CLIENT_ID: ${{ vars.AZM_CLIENT_ID }} 25 | AZURE_TENANT_ID: ${{ vars.AZM_TENANT_ID }} 26 | AZURE_SUBSCRIPTION_ID: ${{ vars.AZM_SUBSCRIPTION_ID }} 27 | AZURE_APP_NAMES: ${{ vars.AZM_APP_NAMES }} 28 | 29 | steps: 30 | - name: Azure login (Federated Credentials) 31 | if: env.AZURE_CLIENT_ID != '' 32 | uses: azure/login@v2 33 | with: 34 | client-id: ${{ env.AZURE_CLIENT_ID }} 35 | tenant-id: ${{ env.AZURE_TENANT_ID }} 36 | subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} 37 | 38 | - name: Delete all resource groups 39 | shell: pwsh 40 | run: | 41 | $groups = az group list --query "[].name" | ConvertFrom-Json 42 | $groups | ForEach-Object { 43 | $name = $_ 44 | az group delete -g $name -y --no-wait 45 | } 46 | 47 | - name: Delete all apps 48 | shell: pwsh 49 | run: | 50 | $excludes = "${{ env.AZURE_APP_NAMES }}" -split ',' 51 | 52 | $apps = az ad app list --query "[].{id: id, displayName:displayName, appId: appId}" | ConvertFrom-Json 53 | $apps | ForEach-Object { 54 | $id = $_.id 55 | $name = $_.displayName 56 | 57 | if ($excludes -notcontains $name) { 58 | az ad app delete --id $id 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/reset-resource-group.yml.nouse: -------------------------------------------------------------------------------- 1 | name: Reset Resource Group 2 | 3 | on: 4 | workflow_dispatch: 5 | # schedule: 6 | # # Run every day at 5:30 AM (KST) 7 | # - cron: "30 20 * * *" 8 | 9 | permissions: 10 | contents: read 11 | id-token: write 12 | issues: write 13 | pull-requests: write 14 | 15 | jobs: 16 | reset-resource-group: 17 | name: "Resetting Resource Groups" 18 | 19 | if: github.repository_owner == 'aliencube' 20 | 21 | runs-on: ubuntu-latest 22 | 23 | env: 24 | PAU_ON_RESET_RESOURCE_GROUP_REQUEST_URL: ${{ secrets.PAU_ON_RESET_RESOURCE_GROUP_REQUEST_URL }} 25 | PAU_API_KEY: ${{ secrets.PAU_API_KEY }} 26 | 27 | steps: 28 | - name: Call Power Automate to reset resource groups 29 | shell: pwsh 30 | run: | 31 | $uri = "${{ env.PAU_ON_RESET_RESOURCE_GROUP_REQUEST_URL }}" 32 | $body = @{ 33 | "apiKey" = "${{ env.PAU_API_KEY }}" 34 | } | ConvertTo-Json 35 | 36 | Invoke-RestMethod -Uri $uri -Method Post -Body $body -ContentType "application/json" 37 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | latest 6 | 7 | enable 8 | enable 9 | 10 | 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build 4 | 5 | COPY ./src/OpenChat.PlaygroundApp /source/OpenChat.PlaygroundApp 6 | 7 | WORKDIR /source/OpenChat.PlaygroundApp 8 | 9 | ARG TARGETARCH 10 | RUN case "$TARGETARCH" in \ 11 | "amd64") RID="linux-musl-x64" ;; \ 12 | "arm64") RID="linux-musl-arm64" ;; \ 13 | *) RID="linux-musl-x64" ;; \ 14 | esac && \ 15 | dotnet publish -c Release -o /app -r $RID --self-contained false 16 | 17 | FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final 18 | 19 | WORKDIR /app 20 | 21 | COPY --from=build /app . 22 | 23 | RUN chown $APP_UID /app 24 | 25 | USER $APP_UID 26 | 27 | ENTRYPOINT ["dotnet", "OpenChat.PlaygroundApp.dll"] -------------------------------------------------------------------------------- /Dockerfile.azure: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build 4 | 5 | COPY ./src/OpenChat.PlaygroundApp /source/OpenChat.PlaygroundApp 6 | 7 | WORKDIR /source/OpenChat.PlaygroundApp 8 | 9 | RUN dotnet publish -c Release -o /app 10 | 11 | FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final 12 | 13 | WORKDIR /app 14 | 15 | COPY --from=build /app . 16 | 17 | RUN chown $APP_UID /app 18 | 19 | USER $APP_UID 20 | 21 | ENTRYPOINT ["dotnet", "OpenChat.PlaygroundApp.dll"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 aliencube.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/favicon.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/assets/favicon.zip -------------------------------------------------------------------------------- /assets/icon-inverted-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/assets/icon-inverted-transparent.png -------------------------------------------------------------------------------- /assets/icon-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/assets/icon-inverted.png -------------------------------------------------------------------------------- /assets/icon-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/assets/icon-transparent.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/assets/icon.png -------------------------------------------------------------------------------- /assets/icon.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/assets/icon.pptx -------------------------------------------------------------------------------- /assets/logo-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/assets/logo-inverted.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/assets/logo.png -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: open-chat-playground 4 | 5 | metadata: 6 | template: azd-init@1.18.0 7 | 8 | services: 9 | openchat-playgroundapp: 10 | project: src/OpenChat.PlaygroundApp 11 | host: containerapp 12 | language: dotnet 13 | docker: 14 | path: ../../Dockerfile.azure 15 | context: ../../ 16 | remoteBuild: true 17 | 18 | hooks: 19 | preprovision: 20 | posix: 21 | shell: sh 22 | continueOnError: false 23 | interactive: false 24 | run: | 25 | CONNECTOR_TYPE=$(azd env get-value CONNECTOR_TYPE) 26 | AZURE_LOCATION=$(azd env get-value AZURE_LOCATION) 27 | 28 | if [ "$CONNECTOR_TYPE" = "HuggingFace" ] || [ "$CONNECTOR_TYPE" = "Ollama" ] || [ "$CONNECTOR_TYPE" = "LG" ]; 29 | then 30 | if [ "$AZURE_LOCATION" != "australiaeast" ] && [ "$AZURE_LOCATION" != "swedencentral" ] && [ "$AZURE_LOCATION" != "westus3" ]; 31 | then 32 | echo "Selected location, $AZURE_LOCATION is invalid for the connector, $CONNECTOR_TYPE. Please select one of the supported locations: Australia East, Sweden Central, West US 3." 33 | exit 1 34 | fi 35 | fi 36 | 37 | windows: 38 | shell: pwsh 39 | continueOnError: false 40 | interactive: false 41 | run: | 42 | $CONNECTOR_TYPE = azd env get-value CONNECTOR_TYPE 43 | $AZURE_LOCATION = azd env get-value AZURE_LOCATION 44 | 45 | if (($CONNECTOR_TYPE -eq "HuggingFace") -or ($CONNECTOR_TYPE -eq "Ollama") -or ($CONNECTOR_TYPE -eq "LG")) { 46 | if (($AZURE_LOCATION -ne "australiaeast") -and ($AZURE_LOCATION -ne "swedencentral") -and ($AZURE_LOCATION -ne "westus3")) { 47 | Write-Host "Selected location, $AZURE_LOCATION is invalid for the connector, $CONNECTOR_TYPE. Please select one of the supported locations: Australia East, Sweden Central, West US 3." 48 | exit 1 49 | } 50 | } -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with LLM 2 | 3 | - [Azure AI Foundry](azure-ai-foundry.md) 4 | - [GitHub Models](github-models.md) 5 | - [Hugging Face](hugging-face.md) 6 | - [LG](lg.md) 7 | - [OpenAI](openai.md) 8 | - [Upstage](upstage.md) 9 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "allowPrerelease": false 4 | } 5 | } -------------------------------------------------------------------------------- /images/ocp-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/images/ocp-hero.png -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysisServicesServers": "as", 3 | "apiManagementService": "apim-", 4 | "appConfigurationStores": "appcs-", 5 | "appManagedEnvironments": "cae-", 6 | "appContainerApps": "ca-", 7 | "authorizationPolicyDefinitions": "policy-", 8 | "automationAutomationAccounts": "aa-", 9 | "blueprintBlueprints": "bp-", 10 | "blueprintBlueprintsArtifacts": "bpa-", 11 | "cacheRedis": "redis-", 12 | "cdnProfiles": "cdnp-", 13 | "cdnProfilesEndpoints": "cdne-", 14 | "cognitiveServicesAccounts": "cog-", 15 | "cognitiveServicesFormRecognizer": "cog-fr-", 16 | "cognitiveServicesTextAnalytics": "cog-ta-", 17 | "computeAvailabilitySets": "avail-", 18 | "computeCloudServices": "cld-", 19 | "computeDiskEncryptionSets": "des", 20 | "computeDisks": "disk", 21 | "computeDisksOs": "osdisk", 22 | "computeGalleries": "gal", 23 | "computeSnapshots": "snap-", 24 | "computeVirtualMachines": "vm", 25 | "computeVirtualMachineScaleSets": "vmss-", 26 | "containerInstanceContainerGroups": "ci", 27 | "containerRegistryRegistries": "cr", 28 | "containerServiceManagedClusters": "aks-", 29 | "databricksWorkspaces": "dbw-", 30 | "dataFactoryFactories": "adf-", 31 | "dataLakeAnalyticsAccounts": "dla", 32 | "dataLakeStoreAccounts": "dls", 33 | "dataMigrationServices": "dms-", 34 | "dBforMySQLServers": "mysql-", 35 | "dBforPostgreSQLServers": "psql-", 36 | "devicesIotHubs": "iot-", 37 | "devicesProvisioningServices": "provs-", 38 | "devicesProvisioningServicesCertificates": "pcert-", 39 | "documentDBDatabaseAccounts": "cosmos-", 40 | "documentDBMongoDatabaseAccounts": "cosmon-", 41 | "eventGridDomains": "evgd-", 42 | "eventGridDomainsTopics": "evgt-", 43 | "eventGridEventSubscriptions": "evgs-", 44 | "eventHubNamespaces": "evhns-", 45 | "eventHubNamespacesEventHubs": "evh-", 46 | "hdInsightClustersHadoop": "hadoop-", 47 | "hdInsightClustersHbase": "hbase-", 48 | "hdInsightClustersKafka": "kafka-", 49 | "hdInsightClustersMl": "mls-", 50 | "hdInsightClustersSpark": "spark-", 51 | "hdInsightClustersStorm": "storm-", 52 | "hybridComputeMachines": "arcs-", 53 | "insightsActionGroups": "ag-", 54 | "insightsComponents": "appi-", 55 | "keyVaultVaults": "kv-", 56 | "kubernetesConnectedClusters": "arck", 57 | "kustoClusters": "dec", 58 | "kustoClustersDatabases": "dedb", 59 | "logicIntegrationAccounts": "ia-", 60 | "logicWorkflows": "logic-", 61 | "machineLearningServicesWorkspaces": "mlw-", 62 | "managedIdentityUserAssignedIdentities": "id-", 63 | "managementManagementGroups": "mg-", 64 | "migrateAssessmentProjects": "migr-", 65 | "networkApplicationGateways": "agw-", 66 | "networkApplicationSecurityGroups": "asg-", 67 | "networkAzureFirewalls": "afw-", 68 | "networkBastionHosts": "bas-", 69 | "networkConnections": "con-", 70 | "networkDnsZones": "dnsz-", 71 | "networkExpressRouteCircuits": "erc-", 72 | "networkFirewallPolicies": "afwp-", 73 | "networkFirewallPoliciesWebApplication": "waf", 74 | "networkFirewallPoliciesRuleGroups": "wafrg", 75 | "networkFrontDoors": "fd-", 76 | "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", 77 | "networkLoadBalancersExternal": "lbe-", 78 | "networkLoadBalancersInternal": "lbi-", 79 | "networkLoadBalancersInboundNatRules": "rule-", 80 | "networkLocalNetworkGateways": "lgw-", 81 | "networkNatGateways": "ng-", 82 | "networkNetworkInterfaces": "nic-", 83 | "networkNetworkSecurityGroups": "nsg-", 84 | "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", 85 | "networkNetworkWatchers": "nw-", 86 | "networkPrivateDnsZones": "pdnsz-", 87 | "networkPrivateLinkServices": "pl-", 88 | "networkPublicIPAddresses": "pip-", 89 | "networkPublicIPPrefixes": "ippre-", 90 | "networkRouteFilters": "rf-", 91 | "networkRouteTables": "rt-", 92 | "networkRouteTablesRoutes": "udr-", 93 | "networkTrafficManagerProfiles": "traf-", 94 | "networkVirtualNetworkGateways": "vgw-", 95 | "networkVirtualNetworks": "vnet-", 96 | "networkVirtualNetworksSubnets": "snet-", 97 | "networkVirtualNetworksVirtualNetworkPeerings": "peer-", 98 | "networkVirtualWans": "vwan-", 99 | "networkVpnGateways": "vpng-", 100 | "networkVpnGatewaysVpnConnections": "vcn-", 101 | "networkVpnGatewaysVpnSites": "vst-", 102 | "notificationHubsNamespaces": "ntfns-", 103 | "notificationHubsNamespacesNotificationHubs": "ntf-", 104 | "operationalInsightsWorkspaces": "log-", 105 | "portalDashboards": "dash-", 106 | "powerBIDedicatedCapacities": "pbi-", 107 | "purviewAccounts": "pview-", 108 | "recoveryServicesVaults": "rsv-", 109 | "resourcesResourceGroups": "rg-", 110 | "searchSearchServices": "srch-", 111 | "serviceBusNamespaces": "sb-", 112 | "serviceBusNamespacesQueues": "sbq-", 113 | "serviceBusNamespacesTopics": "sbt-", 114 | "serviceEndPointPolicies": "se-", 115 | "serviceFabricClusters": "sf-", 116 | "signalRServiceSignalR": "sigr", 117 | "sqlManagedInstances": "sqlmi-", 118 | "sqlServers": "sql-", 119 | "sqlServersDataWarehouse": "sqldw-", 120 | "sqlServersDatabases": "sqldb-", 121 | "sqlServersDatabasesStretch": "sqlstrdb-", 122 | "storageStorageAccounts": "st", 123 | "storageStorageAccountsVm": "stvm", 124 | "storSimpleManagers": "ssimp", 125 | "streamAnalyticsCluster": "asa-", 126 | "synapseWorkspaces": "syn", 127 | "synapseWorkspacesAnalyticsWorkspaces": "synw", 128 | "synapseWorkspacesSqlPoolsDedicated": "syndp", 129 | "synapseWorkspacesSqlPoolsSpark": "synsp", 130 | "timeSeriesInsightsEnvironments": "tsi-", 131 | "webServerFarms": "plan-", 132 | "webSitesAppService": "app-", 133 | "webSitesAppServiceEnvironment": "ase-", 134 | "webSitesFunctions": "func-", 135 | "webStaticSites": "stapp-" 136 | } 137 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the environment that can be used as part of naming resource convention') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('Primary location for all resources') 10 | param location string 11 | 12 | param connectorType string = '' 13 | 14 | // Amazon Bedrock 15 | // Azure AI Foundry 16 | param azureAIFoundryEndpoint string = '' 17 | @secure() 18 | param azureAIFoundryApiKey string = '' 19 | param azureAIFoundryDeploymentName string = '' 20 | // GitHub Models 21 | param githubModelsModel string = '' 22 | @secure() 23 | param githubModelsToken string = '' 24 | // Google Vertex AI 25 | // Docker Model Runner 26 | // Foundry Local 27 | // Hugging Face 28 | param huggingFaceModel string = '' 29 | // Ollama 30 | param ollamaModel string = '' 31 | // Anthropic 32 | // LG 33 | param lgModel string = '' 34 | // Naver 35 | // OpenAI 36 | param openAIModel string = '' 37 | @secure() 38 | param openAIApiKey string = '' 39 | // Upstage 40 | param upstageModel string = '' 41 | param upstageBaseUrl string = '' 42 | @secure() 43 | param upstageApiKey string = '' 44 | 45 | @allowed([ 46 | 'NC24-A100' 47 | 'NC8as-T4' 48 | ]) 49 | @description('The GPU profile name for Container Apps environment when using Ollama, Hugging Face or LG connectors. Supported values are NC24-A100 and NC8as-T4.') 50 | param gpuProfileName string = 'NC8as-T4' 51 | 52 | param openchatPlaygroundAppExists bool 53 | 54 | @description('Id of the user or app to assign application roles') 55 | param principalId string 56 | 57 | @description('Principal type of user or app') 58 | param principalType string 59 | 60 | // Tags that should be applied to all resources. 61 | // 62 | // Note that 'azd-service-name' tags should be applied separately to service host resources. 63 | // Example usage: 64 | // tags: union(tags, { 'azd-service-name': }) 65 | var tags = { 66 | 'azd-env-name': environmentName 67 | } 68 | 69 | // Organize resources in a resource group 70 | resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { 71 | name: 'rg-${environmentName}' 72 | location: location 73 | tags: tags 74 | } 75 | 76 | module resources 'resources.bicep' = { 77 | scope: rg 78 | name: 'resources' 79 | params: { 80 | location: location 81 | tags: tags 82 | principalId: principalId 83 | principalType: principalType 84 | connectorType: connectorType 85 | azureAIFoundryEndpoint: azureAIFoundryEndpoint 86 | azureAIFoundryApiKey: azureAIFoundryApiKey 87 | azureAIFoundryDeploymentName: azureAIFoundryDeploymentName 88 | githubModelsModel: githubModelsModel 89 | githubModelsToken: githubModelsToken 90 | huggingFaceModel: huggingFaceModel 91 | ollamaModel: ollamaModel 92 | lgModel: lgModel 93 | openAIModel: openAIModel 94 | openAIApiKey: openAIApiKey 95 | upstageModel: upstageModel 96 | upstageBaseUrl: upstageBaseUrl 97 | upstageApiKey: upstageApiKey 98 | gpuProfileName: gpuProfileName 99 | openchatPlaygroundAppExists: openchatPlaygroundAppExists 100 | } 101 | } 102 | 103 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT 104 | output AZURE_RESOURCE_OPENCHAT_PLAYGROUNDAPP_ID string = resources.outputs.AZURE_RESOURCE_OPENCHAT_PLAYGROUNDAPP_ID 105 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "connectorType": { 12 | "value": "${CONNECTOR_TYPE}" 13 | }, 14 | "azureAIFoundryEndpoint": { 15 | "value": "${AZURE_AI_FOUNDRY_ENDPOINT}" 16 | }, 17 | "azureAIFoundryApiKey": { 18 | "value": "${AZURE_AI_FOUNDRY_API_KEY}" 19 | }, 20 | "azureAIFoundryDeploymentName": { 21 | "value": "${AZURE_AI_FOUNDRY_DEPLOYMENT_NAME=gpt-4o-mini}" 22 | }, 23 | "githubModelsModel": { 24 | "value": "${GH_MODELS_MODEL=openai/gpt-4o-mini}" 25 | }, 26 | "githubModelsToken": { 27 | "value": "${GH_MODELS_TOKEN}" 28 | }, 29 | "huggingFaceModel": { 30 | "value": "${HUGGING_FACE_MODEL=hf.co/Qwen/Qwen3-0.6B-GGUF}" 31 | }, 32 | "lgModel": { 33 | "value": "${LG_MODEL=hf.co/LGAI-EXAONE/EXAONE-4.0-1.2B-GGUF}" 34 | }, 35 | "openAIModel": { 36 | "value": "${OPENAI_MODEL=gpt-4.1-mini}" 37 | }, 38 | "openAIApiKey": { 39 | "value": "${OPENAI_API_KEY}" 40 | }, 41 | "upstageModel": { 42 | "value": "${UPSTAGE_MODEL}" 43 | }, 44 | "upstageBaseUrl": { 45 | "value": "${UPSTAGE_BASE_URL}" 46 | }, 47 | "upstageApiKey": { 48 | "value": "${UPSTAGE_API_KEY}" 49 | }, 50 | "gpuProfileName": { 51 | "value": "${GPU_PROFILE_NAME=NC8as-T4}" 52 | }, 53 | "openchatPlaygroundAppExists": { 54 | "value": "${SERVICE_OPENCHAT_PLAYGROUNDAPP_RESOURCE_EXISTS=false}" 55 | }, 56 | "principalId": { 57 | "value": "${AZURE_PRINCIPAL_ID}" 58 | }, 59 | "principalType": { 60 | "value": "${AZURE_PRINCIPAL_TYPE}" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /infra/modules/fetch-container-image.bicep: -------------------------------------------------------------------------------- 1 | param exists bool 2 | param name string 3 | 4 | resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) { 5 | name: name 6 | } 7 | 8 | output containers array = exists ? existingApp!.properties.template.containers : [] 9 | -------------------------------------------------------------------------------- /licenses/AWSSDK.Extensions.Bedrock.MEAI.md: -------------------------------------------------------------------------------- 1 | # License Disclosure for `AWSSDK.Extensions.Bedrock.MEAI` 2 | 3 | The [`AWSSDK.Extensions.Bedrock.MEAI`](https://www.nuget.org/packages/AWSSDK.Extensions.Bedrock.MEAI) package is released under Apache 2.0 by [Amazon Web Services](https://aws.amazon.com). More details about the license can be found at [https://github.com/aws/aws-sdk-net/blob/main/License.txt](https://github.com/aws/aws-sdk-net/blob/main/License.txt). -------------------------------------------------------------------------------- /licenses/Mscc.GenerativeAI.Microsoft.md: -------------------------------------------------------------------------------- 1 | # License Disclosure for `Mscc.GenerativeAI.Microsoft` 2 | 3 | The [`Mscc.GenerativeAI.Microsoft`](https://www.nuget.org/packages/Mscc.GenerativeAI.Microsoft) package is released under the Apache 2.0 License by [mscraftsman](https://github.com/mscraftsman/generative-ai). 4 | More details about the license can be found at [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) and [https://github.com/mscraftsman/generative-ai/blob/main/LICENSE](https://github.com/mscraftsman/generative-ai/blob/main/LICENSE). 5 | -------------------------------------------------------------------------------- /src/OpenChat.ConsoleApp/Clients/ApiClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | 3 | using OpenChat.ConsoleApp.Models; 4 | 5 | namespace OpenChat.ConsoleApp.Clients; 6 | 7 | /// 8 | /// This provides interfaces to the . 9 | /// 10 | public interface IApiClient 11 | { 12 | /// 13 | /// Invokes the chat API and streams the responses. 14 | /// 15 | /// List of objects. 16 | /// Returns the object containing the response details. 17 | Task> InvokeStreamAsync(IEnumerable messages); 18 | } 19 | 20 | /// 21 | /// This represents the API client entity for the Playground app. 22 | /// 23 | /// instance. 24 | public class ApiClient(HttpClient http) : IApiClient 25 | { 26 | private const string REQUEST_URI = "api/chat/responses"; 27 | 28 | private readonly HttpClient _http = http ?? throw new ArgumentNullException(nameof(http)); 29 | 30 | /// 31 | public async Task> InvokeStreamAsync(IEnumerable messages) 32 | { 33 | var response = await this._http.PostAsJsonAsync(REQUEST_URI, messages); 34 | response.EnsureSuccessStatusCode(); 35 | 36 | var result = response.Content.ReadFromJsonAsAsyncEnumerable(); 37 | 38 | return result!; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/OpenChat.ConsoleApp/Configurations/AppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace OpenChat.ConsoleApp.Configurations; 2 | 3 | /// 4 | /// This represents the app settings entity from appsettings.json. 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public ApiAppSettings ApiApp { get; set; } = new(); 12 | 13 | /// 14 | /// Gets or sets the value indicating whether to display help information or not. 15 | /// 16 | public bool Help { get; set; } 17 | } 18 | 19 | /// 20 | /// This represents the API app settings entity from appsettings.json. 21 | /// 22 | public class ApiAppSettings 23 | { 24 | /// 25 | /// Gets or sets the API endpoint. 26 | /// 27 | public string? Endpoint { get; set; } 28 | } -------------------------------------------------------------------------------- /src/OpenChat.ConsoleApp/Models/ChatMessage.cs: -------------------------------------------------------------------------------- 1 | namespace OpenChat.ConsoleApp.Models; 2 | 3 | /// 4 | /// This represents the chat message entity. 5 | /// 6 | public class ChatMessage 7 | { 8 | /// 9 | /// Gets or sets the role of the message. 10 | /// 11 | public required string Role { get; set; } = string.Empty; 12 | 13 | /// 14 | /// Gets or sets the message content. 15 | /// 16 | public required string Message { get; set; } = string.Empty; 17 | } 18 | -------------------------------------------------------------------------------- /src/OpenChat.ConsoleApp/OpenChat.ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | enable 8 | 9 | OpenChat.ConsoleApp 10 | OpenChat.ConsoleApp 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/OpenChat.ConsoleApp/Options/ArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | using OpenChat.ConsoleApp.Configurations; 4 | 5 | namespace OpenChat.ConsoleApp.Options; 6 | 7 | /// 8 | /// This represents the base argument options settings entity for all arguments options to inherit. 9 | /// 10 | public class ArgumentOptions 11 | { 12 | private const string ENDPOINT_ARGUMENT = "--endpoint"; 13 | private const string HELP_ARGUMENT = "--help"; 14 | private const string HELP_ARGUMENT_IN_SHORT = "-h"; 15 | 16 | /// 17 | /// Parses the command line arguments into the specified options type. 18 | /// 19 | /// instance. 20 | /// List of arguments from the command line. 21 | /// The parsed options. 22 | public static AppSettings Parse(IConfiguration config, string[] args) 23 | { 24 | var settings = new AppSettings(); 25 | config.Bind(settings); 26 | 27 | for (var i = 0; i < args.Length; i++) 28 | { 29 | switch (args[i]) 30 | { 31 | case ENDPOINT_ARGUMENT: 32 | if (i + 1 < args.Length) 33 | { 34 | settings.ApiApp.Endpoint = args[++i]; 35 | } 36 | break; 37 | 38 | case HELP_ARGUMENT: 39 | case HELP_ARGUMENT_IN_SHORT: 40 | default: 41 | settings.Help = true; 42 | break; 43 | } 44 | } 45 | 46 | return settings; 47 | } 48 | 49 | /// 50 | /// Displays the help information for the command line arguments. 51 | /// 52 | public static void DisplayHelp() 53 | { 54 | var foregroundColor = Console.ForegroundColor; 55 | 56 | Console.ForegroundColor = ConsoleColor.Cyan; 57 | Console.WriteLine("OpenChat Playground"); 58 | Console.ForegroundColor = foregroundColor; 59 | 60 | Console.WriteLine("Usage: [options]"); 61 | Console.WriteLine(); 62 | Console.WriteLine("Options:"); 63 | Console.WriteLine($" {ENDPOINT_ARGUMENT} The API endpoint. Default is http://localhost:5280"); 64 | Console.WriteLine($" {HELP_ARGUMENT}|{HELP_ARGUMENT_IN_SHORT} Show this help message."); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/OpenChat.ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | 6 | using OpenChat.ConsoleApp.Clients; 7 | using OpenChat.ConsoleApp.Options; 8 | using OpenChat.ConsoleApp.Services; 9 | 10 | var config = new ConfigurationBuilder() 11 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 12 | .Build(); 13 | var settings = ArgumentOptions.Parse(config, args); 14 | if (settings.Help == true) 15 | { 16 | ArgumentOptions.DisplayHelp(); 17 | return; 18 | } 19 | 20 | var host = Host.CreateDefaultBuilder(args) 21 | .UseConsoleLifetime() 22 | .ConfigureLogging(logging => 23 | { 24 | logging.ClearProviders(); 25 | logging.AddConsole(); 26 | logging.SetMinimumLevel(LogLevel.Warning); 27 | }) 28 | .ConfigureServices(services => 29 | { 30 | services.AddSingleton(settings); 31 | services.AddHttpClient(client => 32 | { 33 | client.BaseAddress = new Uri(settings.ApiApp.Endpoint!); 34 | }); 35 | services.AddSingleton(); 36 | }) 37 | .Build(); 38 | 39 | var service = host.Services.GetRequiredService(); 40 | await service.RunAsync(); 41 | -------------------------------------------------------------------------------- /src/OpenChat.ConsoleApp/Services/PlaygroundService.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.ConsoleApp.Clients; 2 | using OpenChat.ConsoleApp.Models; 3 | 4 | namespace OpenChat.ConsoleApp.Services; 5 | 6 | /// 7 | /// This provides interfaces to the . 8 | /// 9 | public interface IPlaygroundService 10 | { 11 | /// 12 | /// Runs the playground service. 13 | /// 14 | Task RunAsync(); 15 | } 16 | 17 | /// 18 | /// This represents the service entity for the Playground operation. 19 | /// 20 | /// The instance. 21 | public class PlaygroundService(IApiClient client) : IPlaygroundService 22 | { 23 | private const string SYSTEM_ROLE = "system"; 24 | private const string USER_ROLE = "user"; 25 | private const string ASSISTANT_ROLE = "assistant"; 26 | 27 | private readonly IApiClient _client = client ?? throw new ArgumentNullException(nameof(client)); 28 | 29 | private readonly List _messages = []; 30 | 31 | /// 32 | public async Task RunAsync() 33 | { 34 | try 35 | { 36 | while (true) 37 | { 38 | Console.Write("User: "); 39 | var input = Console.ReadLine(); 40 | if (string.IsNullOrWhiteSpace(input)) 41 | { 42 | break; 43 | } 44 | 45 | var system = new ChatMessage() 46 | { 47 | Role = SYSTEM_ROLE, 48 | Message = "You are a helpful assistant." 49 | }; 50 | 51 | if (this._messages.Count == 0) 52 | { 53 | this._messages.Add(system); 54 | } 55 | 56 | var user = new ChatMessage() 57 | { 58 | Role = USER_ROLE, 59 | Message = input 60 | }; 61 | this._messages.Add(user); 62 | 63 | Console.Write("Assistant: "); 64 | 65 | var assistant = new ChatMessage() 66 | { 67 | Role = ASSISTANT_ROLE, 68 | Message = string.Empty 69 | }; 70 | this._messages.Add(assistant); 71 | 72 | var prompt = this._messages.Take(this._messages.Count - 1); 73 | var result = await this._client.InvokeStreamAsync([.. prompt]); 74 | await foreach (var item in result) 75 | { 76 | await Task.Delay(20); 77 | 78 | if (item is { Role: ASSISTANT_ROLE }) 79 | { 80 | Console.Write(item!.Message); 81 | assistant.Message += item.Message; 82 | } 83 | } 84 | 85 | Console.WriteLine(); 86 | Console.WriteLine(); 87 | } 88 | } 89 | catch (Exception ex) 90 | { 91 | Console.WriteLine(ex.Message); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/OpenChat.ConsoleApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApiApp": { 3 | "Endpoint": "http://localhost:5280" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Abstractions/LanguageModelConnector.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | 3 | using OpenChat.PlaygroundApp.Configurations; 4 | using OpenChat.PlaygroundApp.Connectors; 5 | 6 | namespace OpenChat.PlaygroundApp.Abstractions; 7 | 8 | /// 9 | /// This represents the base language model connector entity for all language model connectors to inherit. 10 | /// 11 | public abstract class LanguageModelConnector(LanguageModelSettings? settings) 12 | { 13 | /// 14 | /// Gets the instance. 15 | /// 16 | protected LanguageModelSettings? Settings { get; } = settings; 17 | 18 | /// 19 | /// Ensures that the language model settings are valid or not. 20 | /// 21 | /// Returns True if the settings are valid; otherwise, throws . 22 | public abstract bool EnsureLanguageModelSettingsValid(); 23 | 24 | /// 25 | /// Gets an instance. 26 | /// 27 | /// Returns instance. 28 | public abstract Task GetChatClientAsync(); 29 | 30 | /// 31 | /// Gets an instance based on the app settings provided. 32 | /// 33 | /// instance. 34 | /// Returns instance. 35 | public static async Task CreateChatClientAsync(AppSettings settings) 36 | { 37 | LanguageModelConnector connector = settings.ConnectorType switch 38 | { 39 | ConnectorType.AzureAIFoundry => new AzureAIFoundryConnector(settings), 40 | ConnectorType.GitHubModels => new GitHubModelsConnector(settings), 41 | ConnectorType.HuggingFace => new HuggingFaceConnector(settings), 42 | ConnectorType.LG => new LGConnector(settings), 43 | ConnectorType.OpenAI => new OpenAIConnector(settings), 44 | ConnectorType.Upstage => new UpstageConnector(settings), 45 | _ => throw new NotSupportedException($"Connector type '{settings.ConnectorType}' is not supported.") 46 | }; 47 | 48 | connector.EnsureLanguageModelSettingsValid(); 49 | 50 | return await connector.GetChatClientAsync().ConfigureAwait(false); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Abstractions/LanguageModelSettings.cs: -------------------------------------------------------------------------------- 1 | namespace OpenChat.PlaygroundApp.Abstractions; 2 | 3 | /// 4 | /// This represents the base language model settings entity for all language model settings to inherit. 5 | /// 6 | public abstract class LanguageModelSettings { } 7 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | @code { 41 | private readonly IComponentRenderMode renderMode = new InteractiveServerRenderMode(prerender: false); 42 | } 43 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Layout/LoadingSpinner.razor: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Layout/LoadingSpinner.razor.css: -------------------------------------------------------------------------------- 1 | /* Used under CC0 license */ 2 | 3 | .lds-ellipsis { 4 | color: #666; 5 | animation: fade-in 1s; 6 | } 7 | 8 | @keyframes fade-in { 9 | 0% { 10 | opacity: 0; 11 | } 12 | 13 | 100% { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .lds-ellipsis, 19 | .lds-ellipsis div { 20 | box-sizing: border-box; 21 | } 22 | 23 | .lds-ellipsis { 24 | margin: auto; 25 | display: block; 26 | position: relative; 27 | width: 80px; 28 | height: 80px; 29 | } 30 | 31 | .lds-ellipsis div { 32 | position: absolute; 33 | top: 33.33333px; 34 | width: 10px; 35 | height: 10px; 36 | border-radius: 50%; 37 | background: currentColor; 38 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 39 | } 40 | 41 | .lds-ellipsis div:nth-child(1) { 42 | left: 8px; 43 | animation: lds-ellipsis1 0.6s infinite; 44 | } 45 | 46 | .lds-ellipsis div:nth-child(2) { 47 | left: 8px; 48 | animation: lds-ellipsis2 0.6s infinite; 49 | } 50 | 51 | .lds-ellipsis div:nth-child(3) { 52 | left: 32px; 53 | animation: lds-ellipsis2 0.6s infinite; 54 | } 55 | 56 | .lds-ellipsis div:nth-child(4) { 57 | left: 56px; 58 | animation: lds-ellipsis3 0.6s infinite; 59 | } 60 | 61 | @keyframes lds-ellipsis1 { 62 | 0% { 63 | transform: scale(0); 64 | } 65 | 66 | 100% { 67 | transform: scale(1); 68 | } 69 | } 70 | 71 | @keyframes lds-ellipsis3 { 72 | 0% { 73 | transform: scale(1); 74 | } 75 | 76 | 100% { 77 | transform: scale(0); 78 | } 79 | } 80 | 81 | @keyframes lds-ellipsis2 { 82 | 0% { 83 | transform: translate(0, 0); 84 | } 85 | 86 | 100% { 87 | transform: translate(24px, 0); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | @Body 4 | 5 |
6 | An unhandled error has occurred. 7 | Reload 8 | 🗙 9 |
10 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | #blazor-error-ui { 2 | color-scheme: light only; 3 | background: lightyellow; 4 | bottom: 0; 5 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 6 | box-sizing: border-box; 7 | display: none; 8 | left: 0; 9 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 10 | position: fixed; 11 | width: 100%; 12 | z-index: 1000; 13 | } 14 | 15 | #blazor-error-ui .dismiss { 16 | cursor: pointer; 17 | position: absolute; 18 | right: 0.75rem; 19 | top: 0.5rem; 20 | } 21 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Layout/SurveyPrompt.razor: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 |
9 | How well is this template working for you? Please take a 10 | brief survey 11 | and tell us what you think. 12 |
13 |
14 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Layout/SurveyPrompt.razor.css: -------------------------------------------------------------------------------- 1 | .surveyContainer { 2 | display: flex; 3 | justify-content: center; 4 | gap: 0.5rem; 5 | font-size: 0.9em; 6 | margin: 0.5rem auto -0.7rem auto; 7 | max-width: 1024px; 8 | color: #444; 9 | } 10 | 11 | .surveyContainer a { 12 | text-decoration: underline; 13 | } 14 | 15 | .surveyContainer .tool-icon { 16 | margin-top: 0.15rem; 17 | width: 1.25rem; 18 | height: 1.25rem; 19 | flex-shrink: 0; 20 | } 21 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/Chat.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | OpenChat Playground 4 | 5 | 6 | 7 | 8 | 9 |
To get started, try asking about anything.
10 |
11 |
12 | 13 |
14 | 15 | @* Remove this line to eliminate the template survey message *@ 16 |
17 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/Chat.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.Extensions.AI; 3 | 4 | using OpenChat.PlaygroundApp.Services; 5 | 6 | namespace OpenChat.PlaygroundApp.Components.Pages.Chat; 7 | 8 | public partial class Chat : ComponentBase, IDisposable 9 | { 10 | private const string SystemPrompt = @" 11 | You are an assistant who answers questions about anything. 12 | Do not answer questions about anything else. 13 | Use only simple markdown to format your responses. 14 | "; 15 | 16 | private readonly ChatOptions chatOptions = new(); 17 | private readonly List messages = new(); 18 | private CancellationTokenSource? currentResponseCancellation; 19 | private ChatMessage? currentResponseMessage; 20 | private ChatInput? chatInput; 21 | 22 | [Inject] 23 | public required IChatService ChatService { get; set; } 24 | 25 | [Inject] 26 | public required NavigationManager Nav { get; set; } 27 | 28 | protected override void OnInitialized() 29 | { 30 | messages.Add(new(ChatRole.System, SystemPrompt)); 31 | } 32 | 33 | private async Task AddUserMessageAsync(ChatMessage userMessage) 34 | { 35 | CancelAnyCurrentResponse(); 36 | 37 | // Add the user message to the conversation 38 | messages.Add(userMessage); 39 | await chatInput!.FocusAsync(); 40 | 41 | // Stream and display a new response from the IChatClient 42 | var responseText = new TextContent(""); 43 | currentResponseMessage = new ChatMessage(ChatRole.Assistant, [responseText]); 44 | currentResponseCancellation = new(); 45 | 46 | await InvokeAsync(StateHasChanged); 47 | 48 | await foreach (var update in ChatService.GetStreamingResponseAsync([.. messages], chatOptions, currentResponseCancellation.Token)) 49 | { 50 | messages.AddMessages(update, filter: c => c is not TextContent); 51 | responseText.Text += update.Text; 52 | ChatMessageItem.NotifyChanged(currentResponseMessage); 53 | } 54 | 55 | // Store the final response in the conversation, and begin getting suggestions 56 | messages.Add(currentResponseMessage!); 57 | currentResponseMessage = null; 58 | } 59 | 60 | private void CancelAnyCurrentResponse() 61 | { 62 | // If a response was cancelled while streaming, include it in the conversation so it's not lost 63 | if (currentResponseMessage is not null) 64 | { 65 | messages.Add(currentResponseMessage); 66 | } 67 | 68 | currentResponseCancellation?.Cancel(); 69 | currentResponseMessage = null; 70 | } 71 | 72 | private async Task ResetConversationAsync() 73 | { 74 | CancelAnyCurrentResponse(); 75 | messages.Clear(); 76 | messages.Add(new(ChatRole.System, SystemPrompt)); 77 | await chatInput!.FocusAsync(); 78 | } 79 | 80 | public void Dispose() 81 | => currentResponseCancellation?.Cancel(); 82 | } 83 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/Chat.razor.css: -------------------------------------------------------------------------------- 1 | .chat-container { 2 | position: sticky; 3 | bottom: 0; 4 | padding-left: 1.5rem; 5 | padding-right: 1.5rem; 6 | padding-top: 0.75rem; 7 | padding-bottom: 1.5rem; 8 | border-top-width: 1px; 9 | background-color: #F3F4F6; 10 | border-color: #E5E7EB; 11 | } 12 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 |
10 |

11 | OpenChat Playground 12 | 13 | 14 | @Settings.ConnectorType 15 | | 16 | @Settings.Model 17 | 18 |

19 |
20 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | using OpenChat.PlaygroundApp.Configurations; 4 | 5 | namespace OpenChat.PlaygroundApp.Components.Pages.Chat; 6 | 7 | public partial class ChatHeader : ComponentBase 8 | { 9 | [Parameter] 10 | public EventCallback OnNewChat { get; set; } 11 | 12 | [Inject] 13 | public required AppSettings Settings { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatHeader.razor.css: -------------------------------------------------------------------------------- 1 | .chat-header-container { 2 | top: 0; 3 | padding: 1.5rem; 4 | } 5 | 6 | .chat-header-controls { 7 | margin-bottom: 1.5rem; 8 | } 9 | 10 | h1 { 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | } 14 | 15 | .new-chat-icon { 16 | width: 1.25rem; 17 | height: 1.25rem; 18 | color: rgb(55, 65, 81); 19 | } 20 | 21 | @media (min-width: 768px) { 22 | .chat-header-container { 23 | position: sticky; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatInput.razor: -------------------------------------------------------------------------------- 1 | 2 | 13 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatInput.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.Extensions.AI; 3 | using Microsoft.JSInterop; 4 | 5 | namespace OpenChat.PlaygroundApp.Components.Pages.Chat; 6 | 7 | public partial class ChatInput : ComponentBase 8 | { 9 | private ElementReference textArea; 10 | private string? messageText; 11 | private bool hasJustSent; 12 | 13 | [Parameter] 14 | public EventCallback OnSend { get; set; } 15 | 16 | [Inject] 17 | public required IJSRuntime JS { get; set; } 18 | 19 | public ValueTask FocusAsync() 20 | => textArea.FocusAsync(); 21 | 22 | private async Task SendMessageAsync() 23 | { 24 | if (messageText is { Length: > 0 } rawText) 25 | { 26 | var trimmedText = rawText.Trim(); 27 | if (trimmedText.Length == 0) 28 | { 29 | messageText = null; 30 | return; 31 | } 32 | hasJustSent = true; 33 | messageText = null; 34 | await OnSend.InvokeAsync(new ChatMessage(ChatRole.User, trimmedText)); 35 | } 36 | } 37 | 38 | private Task HandleKeyDown(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) 39 | { 40 | // Ignore while composing (IME) to avoid duplicate Enter keydown on macOS 41 | if (e.IsComposing == true) 42 | { 43 | return Task.CompletedTask; 44 | } 45 | 46 | // Ignore auto-repeat from holding the Enter key 47 | if (e.Repeat == true) 48 | { 49 | return Task.CompletedTask; 50 | } 51 | 52 | var isEnter = e.Key == "Enter" && !e.ShiftKey; 53 | if (isEnter == false) 54 | { 55 | // Any non-Enter key press clears the justSent guard 56 | hasJustSent = false; 57 | return Task.CompletedTask; 58 | } 59 | 60 | // Ignore immediate Enter after a send until user types again 61 | if (hasJustSent == true) 62 | { 63 | return Task.CompletedTask; 64 | } 65 | 66 | // Don't submit on empty Enter 67 | var isEmpty = string.IsNullOrWhiteSpace(messageText); 68 | if (isEmpty == true) 69 | { 70 | return Task.CompletedTask; 71 | } 72 | return SendMessageAsync(); 73 | } 74 | 75 | protected override async Task OnAfterRenderAsync(bool firstRender) 76 | { 77 | if (firstRender == false) 78 | { 79 | return; 80 | } 81 | 82 | try 83 | { 84 | var module = await JS.InvokeAsync("import", "./Components/Pages/Chat/ChatInput.razor.js"); 85 | await module.InvokeVoidAsync("init", textArea); 86 | await module.DisposeAsync(); 87 | } 88 | catch (JSDisconnectedException) 89 | { 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatInput.razor.css: -------------------------------------------------------------------------------- 1 | .input-box { 2 | display: flex; 3 | flex-direction: column; 4 | background: white; 5 | border: 1px solid rgb(229, 231, 235); 6 | border-radius: 8px; 7 | padding: 0.5rem 0.75rem; 8 | margin-top: 0.75rem; 9 | } 10 | 11 | .input-box:focus-within { 12 | outline: 2px solid #4152d5; 13 | } 14 | 15 | textarea { 16 | resize: none; 17 | border: none; 18 | outline: none; 19 | flex-grow: 1; 20 | } 21 | 22 | .tools { 23 | display: flex; 24 | margin-top: 1rem; 25 | align-items: center; 26 | } 27 | 28 | .tool-icon { 29 | width: 1.25rem; 30 | height: 1.25rem; 31 | } 32 | 33 | .send-button { 34 | color: black; 35 | margin-left: auto; 36 | } 37 | 38 | .send-button:disabled { 39 | color: #aaa; 40 | } 41 | 42 | .attach { 43 | background-color: white; 44 | border-style: dashed; 45 | color: #888; 46 | border-color: #888; 47 | padding: 3px 8px; 48 | } 49 | 50 | .attach:hover { 51 | background-color: #f0f0f0; 52 | color: black; 53 | } 54 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatMessageItem.razor: -------------------------------------------------------------------------------- 1 | @if (Message.Role == ChatRole.User) 2 | { 3 |
4 | @Message.Text 5 |
6 | } 7 | else if (Message.Role == ChatRole.Assistant) 8 | { 9 | foreach (var content in Message.Contents) 10 | { 11 | if (content is TextContent { Text: { Length: > 0 } text }) 12 | { 13 |
14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 |
Assistant
22 |
23 | 24 |
25 |
26 | } 27 | else if (InProgress && content is TextContent { Text: "" }) 28 | { 29 | 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatMessageItem.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.Extensions.AI; 5 | 6 | namespace OpenChat.PlaygroundApp.Components.Pages.Chat; 7 | 8 | public partial class ChatMessageItem : ComponentBase 9 | { 10 | private static readonly ConditionalWeakTable SubscribersLookup = new(); 11 | 12 | [Parameter, EditorRequired] 13 | public required ChatMessage Message { get; set; } 14 | 15 | [Parameter] 16 | public bool InProgress { get; set; } 17 | 18 | protected override void OnInitialized() 19 | { 20 | SubscribersLookup.AddOrUpdate(Message, this); 21 | } 22 | 23 | public static void NotifyChanged(ChatMessage source) 24 | { 25 | if (SubscribersLookup.TryGetValue(source, out var subscriber)) 26 | { 27 | subscriber.StateHasChanged(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatMessageItem.razor.css: -------------------------------------------------------------------------------- 1 | .user-message { 2 | background: rgb(182 215 232); 3 | align-self: flex-end; 4 | min-width: 25%; 5 | max-width: calc(100% - 5rem); 6 | padding: 0.5rem 1.25rem; 7 | border-radius: 0.25rem; 8 | color: #1F2937; 9 | white-space: pre-wrap; 10 | } 11 | 12 | .assistant-message, .assistant-search { 13 | display: grid; 14 | grid-template-rows: min-content; 15 | grid-template-columns: 2rem minmax(0, 1fr); 16 | gap: 0.25rem; 17 | } 18 | 19 | .assistant-message-header { 20 | font-weight: 600; 21 | } 22 | 23 | .assistant-message-text { 24 | grid-column-start: 2; 25 | } 26 | 27 | .assistant-message-icon { 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | border-radius: 9999px; 32 | width: 1.5rem; 33 | height: 1.5rem; 34 | color: #ffffff; 35 | background: #9b72ce; 36 | } 37 | 38 | .assistant-message-icon svg { 39 | width: 1rem; 40 | height: 1rem; 41 | } 42 | 43 | .assistant-search { 44 | font-size: 0.875rem; 45 | line-height: 1.25rem; 46 | } 47 | 48 | .assistant-search-icon { 49 | display: flex; 50 | justify-content: center; 51 | align-items: center; 52 | width: 1.5rem; 53 | height: 1.5rem; 54 | } 55 | 56 | .assistant-search-icon svg { 57 | width: 1rem; 58 | height: 1rem; 59 | } 60 | 61 | .assistant-search-content { 62 | align-content: center; 63 | } 64 | 65 | .assistant-search-phrase { 66 | font-weight: 600; 67 | } 68 | 69 | /* Default styling for markdown-formatted assistant messages */ 70 | ::deep ul { 71 | list-style-type: disc; 72 | margin-left: 1.5rem; 73 | } 74 | 75 | ::deep ol { 76 | list-style-type: decimal; 77 | margin-left: 1.5rem; 78 | } 79 | 80 | ::deep li { 81 | margin: 0.5rem 0; 82 | } 83 | 84 | ::deep strong { 85 | font-weight: 600; 86 | } 87 | 88 | ::deep h3 { 89 | margin: 1rem 0; 90 | font-weight: 600; 91 | } 92 | 93 | ::deep p + p { 94 | margin-top: 1rem; 95 | } 96 | 97 | ::deep table { 98 | margin: 1rem 0; 99 | } 100 | 101 | ::deep th { 102 | text-align: left; 103 | border-bottom: 1px solid silver; 104 | } 105 | 106 | ::deep th, ::deep td { 107 | padding: 0.1rem 0.5rem; 108 | } 109 | 110 | ::deep th, ::deep tr:nth-child(even) { 111 | background-color: rgba(0, 0, 0, 0.05); 112 | } 113 | 114 | ::deep pre > code { 115 | background-color: white; 116 | display: block; 117 | padding: 0.5rem 1rem; 118 | margin: 1rem 0; 119 | overflow-x: auto; 120 | } 121 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatMessageList.razor: -------------------------------------------------------------------------------- 1 | @inject IJSRuntime JS 2 | 3 |
4 | 5 | @foreach (var message in Messages) 6 | { 7 | 8 | } 9 | 10 | @if (InProgressMessage is not null) 11 | { 12 | 13 | } 14 | else if (IsEmpty) 15 | { 16 |
@NoMessagesContent
17 | } 18 |
19 |
20 | 21 | @code { 22 | [Parameter] 23 | public required IEnumerable Messages { get; set; } 24 | 25 | [Parameter] 26 | public ChatMessage? InProgressMessage { get; set; } 27 | 28 | [Parameter] 29 | public RenderFragment? NoMessagesContent { get; set; } 30 | 31 | private bool IsEmpty => !Messages.Any(m => (m.Role == ChatRole.User || m.Role == ChatRole.Assistant) && !string.IsNullOrEmpty(m.Text)); 32 | 33 | protected override async Task OnAfterRenderAsync(bool firstRender) 34 | { 35 | if (firstRender) 36 | { 37 | // Activates the auto-scrolling behavior 38 | await JS.InvokeVoidAsync("import", "./Components/Pages/Chat/ChatMessageList.razor.js"); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Chat/ChatMessageList.razor.css: -------------------------------------------------------------------------------- 1 | .message-list-container { 2 | margin: 2rem 1.5rem; 3 | flex-grow: 1; 4 | } 5 | 6 | .message-list { 7 | display: flex; 8 | flex-direction: column; 9 | gap: 1.25rem; 10 | } 11 | 12 | .no-messages { 13 | text-align: center; 14 | font-size: 1.25rem; 15 | color: #999; 16 | margin-top: calc(40vh - 18rem); 17 | } 18 | 19 | chat-messages > ::deep div:last-of-type { 20 | /* Adds some vertical buffer to so that suggestions don't overlap the output when they appear */ 21 | margin-bottom: 2rem; 22 | } 23 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | 27 | @code{ 28 | [CascadingParameter] 29 | private HttpContext? HttpContext { get; set; } 30 | 31 | private string? RequestId { get; set; } 32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/Routes.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.Extensions.AI 9 | @using Microsoft.JSInterop 10 | @using OpenChat.PlaygroundApp 11 | @using OpenChat.PlaygroundApp.Components 12 | @using OpenChat.PlaygroundApp.Components.Layout -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/AmazonBedrockSettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public AmazonBedrockSettings? AmazonBedrock { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for Amazon Bedrock. 16 | /// 17 | public class AmazonBedrockSettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the AWSCredentials Access Key ID for the Amazon Bedrock service. 21 | /// 22 | public string? AccessKeyId { get; set; } 23 | 24 | /// 25 | /// Gets or sets the AWSCredentials Secret Access Key for the Amazon Bedrock service. 26 | /// 27 | public string? SecretAccessKey { get; set; } 28 | 29 | /// 30 | /// Gets or sets the AWS region for the Amazon Bedrock service. 31 | /// 32 | public string? Region { get; set; } 33 | 34 | /// 35 | /// Gets or sets the model ID for the Amazon Bedrock service. 36 | /// 37 | public string? ModelId { get; set; } 38 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/AnthropicSettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public AnthropicSettings? Anthropic { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for Anthropic Claude. 16 | /// 17 | public class AnthropicSettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the API key for Anthropic Claude. 21 | /// 22 | public string? ApiKey { get; set; } 23 | 24 | /// 25 | /// Gets or sets the model name of Anthropic Claude. 26 | /// 27 | public string? Model { get; set; } 28 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/AppSettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Connectors; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | /// This represents the app settings entity from appsettings.json. 7 | /// 8 | public partial class AppSettings 9 | { 10 | /// 11 | /// Gets or sets the connector type to use. 12 | /// 13 | public ConnectorType ConnectorType { get; set; } 14 | 15 | /// 16 | /// Gets or sets the model name to use. 17 | /// 18 | public string? Model { get; set; } 19 | 20 | /// 21 | /// Gets or sets the value indicating whether to display help information or not. 22 | /// 23 | public bool Help { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/AzureAIFoundrySettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public AzureAIFoundrySettings? AzureAIFoundry { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for Azure AI Foundry. 16 | /// 17 | public class AzureAIFoundrySettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the endpoint URL of Azure AI Foundry API. 21 | /// 22 | public string? Endpoint { get; set; } 23 | 24 | /// 25 | /// Gets or sets the Azure AI Foundry API Access Token. 26 | /// 27 | public string? ApiKey { get; set; } 28 | 29 | /// 30 | /// Gets or sets the model name of Azure AI Foundry. 31 | /// 32 | public string? DeploymentName { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/DockerModelRunnerSettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public DockerModelRunnerSettings? DockerModelRunner { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for Docker Model Runner. 16 | /// 17 | public class DockerModelRunnerSettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the base URL of the Docker Model Runner API. 21 | /// 22 | public string? BaseUrl { get; set; } 23 | 24 | /// 25 | /// Gets or sets the model name of Docker Model Runner. 26 | /// 27 | public string? Model { get; set; } 28 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/FoundryLocalSettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public FoundryLocalSettings? FoundryLocal { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for FoundryLocal. 16 | /// 17 | public class FoundryLocalSettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the alias of FoundryLocal. 21 | /// 22 | public string? Alias { get; set; } 23 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/GitHubModelsSettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public GitHubModelsSettings? GitHubModels { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for GitHub Models. 16 | /// 17 | public class GitHubModelsSettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the endpoint URL of GitHub Models API. 21 | /// 22 | public string? Endpoint { get; set; } 23 | 24 | /// 25 | /// Gets or sets the GitHub Personal Access Token (PAT). 26 | /// 27 | public string? Token { get; set; } 28 | 29 | /// 30 | /// Gets or sets the model name of GitHub Models. 31 | /// 32 | public string? Model { get; set; } 33 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/GoogleVertexAISettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public GoogleVertexAISettings? GoogleVertexAI { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for Google Vertex AI. 16 | /// 17 | public class GoogleVertexAISettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the Google Vertex AI API Key. 21 | /// 22 | public string? ApiKey { get; set; } 23 | 24 | /// 25 | /// Gets or sets the model name of Google Vertex AI. 26 | /// 27 | public string? Model { get; set; } 28 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/HuggingFaceSettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public HuggingFaceSettings? HuggingFace { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for Hugging Face. 16 | /// 17 | public class HuggingFaceSettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the base URL of the Hugging Face API. 21 | /// 22 | public string? BaseUrl { get; set; } 23 | 24 | /// 25 | /// Gets or sets the model name for Hugging Face. 26 | /// 27 | public string? Model { get; set; } 28 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/LGSettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public LGSettings? LG { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for LG AI EXAONE. 16 | /// 17 | public class LGSettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the base URL of the LG AI EXAONE API. 21 | /// 22 | public string? BaseUrl { get; set; } 23 | 24 | /// 25 | /// Gets or sets the model name for LG AI EXAONE. 26 | /// 27 | public string? Model { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/OllamaSettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public OllamaSettings? Ollama { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for Ollama. 16 | /// 17 | public class OllamaSettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the base URL of the Ollama API. 21 | /// 22 | public string? BaseUrl { get; set; } 23 | 24 | /// 25 | /// Gets or sets the model name for Ollama. 26 | /// 27 | public string? Model { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/OpenAISettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public OpenAISettings? OpenAI { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for OpenAI. 16 | /// 17 | public class OpenAISettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the OpenAI API key. 21 | /// 22 | public string? ApiKey { get; set; } 23 | 24 | /// 25 | /// Gets or sets the model name for OpenAI. 26 | /// 27 | public string? Model { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Configurations/UpstageSettings.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Configurations; 4 | 5 | /// 6 | public partial class AppSettings 7 | { 8 | /// 9 | /// Gets or sets the instance. 10 | /// 11 | public UpstageSettings? Upstage { get; set; } 12 | } 13 | 14 | /// 15 | /// This represents the app settings entity for Upstage. 16 | /// 17 | public class UpstageSettings : LanguageModelSettings 18 | { 19 | /// 20 | /// Gets or sets the base URL of Upstage API. 21 | /// 22 | public string? BaseUrl { get; set; } 23 | 24 | /// 25 | /// Gets or sets the Upstage API key. 26 | /// 27 | public string? ApiKey { get; set; } 28 | 29 | /// 30 | /// Gets or sets the model name of Upstage. 31 | /// 32 | public string? Model { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Connectors/AzureAIFoundryConnector.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | 3 | using OpenChat.PlaygroundApp.Configurations; 4 | using OpenChat.PlaygroundApp.Abstractions; 5 | 6 | using Azure; 7 | using Azure.AI.OpenAI; 8 | 9 | namespace OpenChat.PlaygroundApp.Connectors; 10 | 11 | /// 12 | /// This represents the connector entity for Azure AI Foundry. 13 | /// 14 | public class AzureAIFoundryConnector(AppSettings settings) : LanguageModelConnector(settings.AzureAIFoundry) 15 | { 16 | private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings)); 17 | 18 | /// 19 | public override bool EnsureLanguageModelSettingsValid() 20 | { 21 | if (this.Settings is not AzureAIFoundrySettings settings) 22 | { 23 | throw new InvalidOperationException("Missing configuration: AzureAIFoundry."); 24 | } 25 | 26 | if (string.IsNullOrWhiteSpace(settings.Endpoint?.Trim())) 27 | { 28 | throw new InvalidOperationException("Missing configuration: AzureAIFoundry:Endpoint."); 29 | } 30 | 31 | if (string.IsNullOrWhiteSpace(settings.ApiKey?.Trim())) 32 | { 33 | throw new InvalidOperationException("Missing configuration: AzureAIFoundry:ApiKey."); 34 | } 35 | 36 | if (string.IsNullOrWhiteSpace(settings.DeploymentName?.Trim())) 37 | { 38 | throw new InvalidOperationException("Missing configuration: AzureAIFoundry:DeploymentName."); 39 | } 40 | 41 | return true; 42 | } 43 | 44 | /// 45 | public override async Task GetChatClientAsync() 46 | { 47 | var settings = this.Settings as AzureAIFoundrySettings; 48 | 49 | var endpoint = new Uri(settings!.Endpoint!); 50 | var deploymentName = settings.DeploymentName!; 51 | var apiKey = settings.ApiKey!; 52 | 53 | var credential = new AzureKeyCredential(apiKey); 54 | var azureClient = new AzureOpenAIClient(endpoint, credential); 55 | 56 | var chatClient = azureClient.GetChatClient(deploymentName) 57 | .AsIChatClient(); 58 | 59 | Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.DeploymentName}"); 60 | 61 | return await Task.FromResult(chatClient).ConfigureAwait(false); 62 | } 63 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Connectors/ConnectorType.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace OpenChat.PlaygroundApp.Connectors; 4 | 5 | /// 6 | /// This specifies the type of connector to use. 7 | /// 8 | [JsonConverter(typeof(JsonStringEnumConverter))] 9 | public enum ConnectorType 10 | { 11 | /// 12 | /// Identifies the unknown connector type. 13 | /// 14 | Unknown, 15 | 16 | /// 17 | /// Identifies the Amazon Bedrock connector type. 18 | /// 19 | AmazonBedrock, 20 | 21 | /// 22 | /// Identifies the Azure AI Foundry connector type. 23 | /// 24 | AzureAIFoundry, 25 | 26 | /// 27 | /// Identifies the GitHub Models connector type. 28 | /// 29 | GitHubModels, 30 | 31 | /// 32 | /// Identifies the Google Vertex AI connector type. 33 | /// 34 | GoogleVertexAI, 35 | 36 | /// 37 | /// Identifies the Docker Model Runner connector type. 38 | /// 39 | DockerModelRunner, 40 | 41 | /// 42 | /// Identifies the Foundry Local connector type. 43 | /// 44 | FoundryLocal, 45 | 46 | /// 47 | /// Identifies the Hugging Face connector type. 48 | /// 49 | HuggingFace, 50 | 51 | /// 52 | /// Identifies the Ollama connector type. 53 | /// 54 | Ollama, 55 | 56 | /// 57 | /// Identifies the Anthropic connector type. 58 | /// 59 | Anthropic, 60 | 61 | /// 62 | /// Identifies the LG connector type. 63 | /// 64 | LG, 65 | 66 | /// 67 | /// Identifies the Naver connector type. 68 | /// 69 | Naver, 70 | 71 | /// 72 | /// Identifies the OpenAI connector type. 73 | /// 74 | OpenAI, 75 | 76 | /// 77 | /// Identifies the Upstage connector type. 78 | /// 79 | Upstage, 80 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Connectors/GitHubModelsConnector.cs: -------------------------------------------------------------------------------- 1 | using System.ClientModel; 2 | 3 | using Microsoft.Extensions.AI; 4 | 5 | using OpenAI; 6 | 7 | using OpenChat.PlaygroundApp.Abstractions; 8 | using OpenChat.PlaygroundApp.Configurations; 9 | 10 | namespace OpenChat.PlaygroundApp.Connectors; 11 | 12 | /// 13 | /// This represents the connector entity for GitHub Models. 14 | /// 15 | /// instance. 16 | public class GitHubModelsConnector(AppSettings settings) : LanguageModelConnector(settings.GitHubModels) 17 | { 18 | private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings)); 19 | 20 | /// 21 | public override bool EnsureLanguageModelSettingsValid() 22 | { 23 | if (this.Settings is not GitHubModelsSettings settings) 24 | { 25 | throw new InvalidOperationException("Missing configuration: GitHubModels."); 26 | } 27 | 28 | if (string.IsNullOrWhiteSpace(settings.Endpoint!.Trim()) == true) 29 | { 30 | throw new InvalidOperationException("Missing configuration: GitHubModels:Endpoint."); 31 | } 32 | 33 | if (string.IsNullOrWhiteSpace(settings.Token!.Trim()) == true) 34 | { 35 | throw new InvalidOperationException("Missing configuration: GitHubModels:Token."); 36 | } 37 | 38 | if (string.IsNullOrWhiteSpace(settings.Model!.Trim()) == true) 39 | { 40 | throw new InvalidOperationException("Missing configuration: GitHubModels:Model."); 41 | } 42 | 43 | return true; 44 | } 45 | 46 | /// 47 | public override async Task GetChatClientAsync() 48 | { 49 | var settings = this.Settings as GitHubModelsSettings; 50 | 51 | var credential = new ApiKeyCredential(settings?.Token ?? throw new InvalidOperationException("Missing configuration: GitHubModels:Token.")); 52 | var options = new OpenAIClientOptions() 53 | { 54 | Endpoint = new Uri(settings.Endpoint ?? throw new InvalidOperationException("Missing configuration: GitHubModels:Endpoint.")) 55 | }; 56 | 57 | var client = new OpenAIClient(credential, options); 58 | var chatClient = client.GetChatClient(settings.Model) 59 | .AsIChatClient(); 60 | 61 | Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.Model}"); 62 | 63 | return await Task.FromResult(chatClient).ConfigureAwait(false); 64 | } 65 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Connectors/HuggingFaceConnector.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | 3 | using OllamaSharp; 4 | 5 | using OpenChat.PlaygroundApp.Abstractions; 6 | using OpenChat.PlaygroundApp.Configurations; 7 | 8 | namespace OpenChat.PlaygroundApp.Connectors; 9 | 10 | /// 11 | /// This represents the connector entity for Hugging Face. 12 | /// 13 | /// instance. 14 | public class HuggingFaceConnector(AppSettings settings) : LanguageModelConnector(settings.HuggingFace) 15 | { 16 | private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings)); 17 | 18 | private const string HuggingFaceHost = "hf.co"; 19 | private const string ModelSuffix = "gguf"; 20 | 21 | /// 22 | public override bool EnsureLanguageModelSettingsValid() 23 | { 24 | if (this.Settings is not HuggingFaceSettings settings) 25 | { 26 | throw new InvalidOperationException("Missing configuration: HuggingFace."); 27 | } 28 | 29 | if (string.IsNullOrWhiteSpace(settings.BaseUrl!.Trim()) == true) 30 | { 31 | throw new InvalidOperationException("Missing configuration: HuggingFace:BaseUrl."); 32 | } 33 | 34 | if (string.IsNullOrWhiteSpace(settings.Model!.Trim()) == true) 35 | { 36 | throw new InvalidOperationException("Missing configuration: HuggingFace:Model."); 37 | } 38 | 39 | // Accepts formats like: 40 | // - hf.co/{org}/{model}gguf e.g hf.co/Qwen/Qwen3-0.6B-GGUF hf.co/Qwen/Qwen3-0.6B_GGUF 41 | if (IsValidModel(settings.Model!.Trim()) == false) 42 | { 43 | throw new InvalidOperationException("Invalid configuration: HuggingFace:Model format. Expected 'hf.co/{org}/{model}gguf' format."); 44 | } 45 | 46 | return true; 47 | } 48 | 49 | /// 50 | public override async Task GetChatClientAsync() 51 | { 52 | var settings = this.Settings as HuggingFaceSettings; 53 | var baseUrl = settings!.BaseUrl!; 54 | var model = settings!.Model!; 55 | 56 | var config = new OllamaApiClient.Configuration 57 | { 58 | Uri = new Uri(baseUrl), 59 | Model = model, 60 | }; 61 | 62 | var chatClient = new OllamaApiClient(config); 63 | 64 | var pulls = chatClient.PullModelAsync(model); 65 | await foreach (var pull in pulls) 66 | { 67 | Console.WriteLine($"Pull status: {pull!.Status}"); 68 | } 69 | 70 | Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.Model}"); 71 | 72 | return await Task.FromResult(chatClient).ConfigureAwait(false); 73 | } 74 | 75 | private static bool IsValidModel(string model) 76 | { 77 | var segments = model.Split([ '/' ], StringSplitOptions.RemoveEmptyEntries); 78 | 79 | if (segments.Length != 3) 80 | { 81 | return false; 82 | } 83 | 84 | if (segments.First().Equals(HuggingFaceHost, StringComparison.InvariantCultureIgnoreCase) == false) 85 | { 86 | return false; 87 | } 88 | 89 | if (segments.Last().EndsWith(ModelSuffix, StringComparison.InvariantCultureIgnoreCase) == false) 90 | { 91 | return false; 92 | } 93 | 94 | return true; 95 | } 96 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Connectors/LGConnector.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | 3 | using OllamaSharp; 4 | 5 | using OpenChat.PlaygroundApp.Abstractions; 6 | using OpenChat.PlaygroundApp.Configurations; 7 | 8 | namespace OpenChat.PlaygroundApp.Connectors; 9 | 10 | /// 11 | /// This represents the connector entity for LG AI EXAONE. 12 | /// 13 | public class LGConnector(AppSettings settings) : LanguageModelConnector(settings.LG) 14 | { 15 | private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings)); 16 | 17 | /// 18 | public override bool EnsureLanguageModelSettingsValid() 19 | { 20 | if (this.Settings is not LGSettings settings) 21 | { 22 | throw new InvalidOperationException("Missing configuration: LG."); 23 | } 24 | 25 | if (string.IsNullOrWhiteSpace(settings.BaseUrl!.Trim()) == true) 26 | { 27 | throw new InvalidOperationException("Missing configuration: LG:BaseUrl."); 28 | } 29 | 30 | if (string.IsNullOrWhiteSpace(settings.Model!.Trim()) == true) 31 | { 32 | throw new InvalidOperationException("Missing configuration: LG:Model."); 33 | } 34 | 35 | if (IsValidModel(settings.Model.Trim()) == false) 36 | { 37 | throw new InvalidOperationException("Invalid configuration: Expected 'hf.co/LGAI-EXAONE/EXAONE-*-GGUF' format."); 38 | } 39 | 40 | return true; 41 | } 42 | 43 | /// 44 | public override async Task GetChatClientAsync() 45 | { 46 | var settings = this.Settings as LGSettings; 47 | var baseUrl = settings!.BaseUrl!; 48 | var model = settings!.Model!; 49 | 50 | var config = new OllamaApiClient.Configuration 51 | { 52 | Uri = new Uri(baseUrl), 53 | Model = model, 54 | }; 55 | 56 | var chatClient = new OllamaApiClient(config); 57 | 58 | var pulls = chatClient.PullModelAsync(model); 59 | await foreach (var pull in pulls) 60 | { 61 | Console.WriteLine($"Pull status: {pull!.Status}"); 62 | } 63 | 64 | Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.Model}"); 65 | 66 | return await Task.FromResult(chatClient).ConfigureAwait(false); 67 | } 68 | 69 | private static bool IsValidModel(string model) 70 | { 71 | var segments = model.Split(['/'], StringSplitOptions.RemoveEmptyEntries); 72 | 73 | if (segments.Length != 3) 74 | { 75 | return false; 76 | } 77 | 78 | if (segments[0].Equals("hf.co", StringComparison.InvariantCultureIgnoreCase) == false) 79 | { 80 | return false; 81 | } 82 | 83 | if (segments[1].Equals("LGAI-EXAONE", StringComparison.InvariantCultureIgnoreCase) == false) 84 | { 85 | return false; 86 | } 87 | 88 | if (segments[2].StartsWith("EXAONE-", StringComparison.InvariantCultureIgnoreCase) == false) 89 | { 90 | return false; 91 | } 92 | 93 | if (segments[2].EndsWith("-GGUF", StringComparison.InvariantCultureIgnoreCase) == false) 94 | { 95 | return false; 96 | } 97 | 98 | return true; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Connectors/OpenAIConnector.cs: -------------------------------------------------------------------------------- 1 | using System.ClientModel; 2 | 3 | using Microsoft.Extensions.AI; 4 | 5 | using OpenAI; 6 | 7 | using OpenChat.PlaygroundApp.Abstractions; 8 | using OpenChat.PlaygroundApp.Configurations; 9 | 10 | namespace OpenChat.PlaygroundApp.Connectors; 11 | 12 | /// 13 | /// This represents the connector entity for OpenAI. 14 | /// 15 | /// instance. 16 | public class OpenAIConnector(AppSettings settings) : LanguageModelConnector(settings.OpenAI) 17 | { 18 | private readonly AppSettings _appSettings = settings ?? throw new ArgumentNullException(nameof(settings)); 19 | 20 | /// 21 | public override bool EnsureLanguageModelSettingsValid() 22 | { 23 | if (this.Settings is not OpenAISettings settings) 24 | { 25 | throw new InvalidOperationException("Missing configuration: OpenAI."); 26 | } 27 | 28 | if (string.IsNullOrWhiteSpace(settings.ApiKey!.Trim()) == true) 29 | { 30 | throw new InvalidOperationException("Missing configuration: OpenAI:ApiKey."); 31 | } 32 | 33 | if (string.IsNullOrWhiteSpace(settings.Model!.Trim()) == true) 34 | { 35 | throw new InvalidOperationException("Missing configuration: OpenAI:Model."); 36 | } 37 | 38 | return true; 39 | } 40 | 41 | /// 42 | public override async Task GetChatClientAsync() 43 | { 44 | var settings = this.Settings as OpenAISettings; 45 | 46 | var credential = new ApiKeyCredential(settings?.ApiKey ?? throw new InvalidOperationException("Missing configuration: OpenAI:ApiKey.")); 47 | 48 | var client = new OpenAIClient(credential); 49 | var chatClient = client.GetChatClient(settings.Model) 50 | .AsIChatClient(); 51 | 52 | Console.WriteLine($"The {this._appSettings.ConnectorType} connector created with model: {settings.Model}"); 53 | 54 | return await Task.FromResult(chatClient).ConfigureAwait(false); 55 | } 56 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Connectors/UpstageConnector.cs: -------------------------------------------------------------------------------- 1 | using System.ClientModel; 2 | 3 | using Microsoft.Extensions.AI; 4 | 5 | using OpenAI; 6 | 7 | using OpenChat.PlaygroundApp.Abstractions; 8 | using OpenChat.PlaygroundApp.Configurations; 9 | 10 | namespace OpenChat.PlaygroundApp.Connectors; 11 | 12 | /// 13 | /// This represents the connector entity for Upstage. 14 | /// 15 | public class UpstageConnector(AppSettings settings) : LanguageModelConnector(settings.Upstage) 16 | { 17 | /// 18 | public override bool EnsureLanguageModelSettingsValid() 19 | { 20 | var settings = this.Settings as UpstageSettings; 21 | if (settings is null) 22 | { 23 | throw new InvalidOperationException("Missing configuration: Upstage."); 24 | } 25 | 26 | if (string.IsNullOrWhiteSpace(settings.BaseUrl?.Trim()) == true) 27 | { 28 | throw new InvalidOperationException("Missing configuration: Upstage:BaseUrl."); 29 | } 30 | 31 | if (string.IsNullOrWhiteSpace(settings.ApiKey?.Trim()) == true) 32 | { 33 | throw new InvalidOperationException("Missing configuration: Upstage:ApiKey."); 34 | } 35 | 36 | if (string.IsNullOrWhiteSpace(settings.Model?.Trim()) == true) 37 | { 38 | throw new InvalidOperationException("Missing configuration: Upstage:Model."); 39 | } 40 | 41 | return true; 42 | } 43 | 44 | /// 45 | public override async Task GetChatClientAsync() 46 | { 47 | var settings = this.Settings as UpstageSettings; 48 | 49 | var credential = new ApiKeyCredential(settings?.ApiKey ?? 50 | throw new InvalidOperationException("Missing configuration: Upstage:ApiKey.")); 51 | 52 | var options = new OpenAIClientOptions 53 | { 54 | Endpoint = new Uri(settings.BaseUrl ?? 55 | throw new InvalidOperationException("Missing configuration: Upstage:BaseUrl.")) 56 | }; 57 | 58 | var client = new OpenAIClient(credential, options); 59 | var chatClient = client.GetChatClient(settings.Model) 60 | .AsIChatClient(); 61 | 62 | return await Task.FromResult(chatClient).ConfigureAwait(false); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Constants/AppSettingConstants.cs: -------------------------------------------------------------------------------- 1 | namespace OpenChat.PlaygroundApp.Constants; 2 | 3 | /// 4 | /// This represents the app settings argument constants for all app settings arguments to reference. 5 | /// 6 | public static class AppSettingConstants 7 | { 8 | /// 9 | /// Defines the constant for 'ConnectorType'. 10 | /// 11 | public const string ConnectorType = "ConnectorType"; 12 | } 13 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Endpoints/ChatResponseEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.AI; 5 | 6 | using OpenChat.PlaygroundApp.Models; 7 | using OpenChat.PlaygroundApp.Services; 8 | 9 | using ChatMessage = Microsoft.Extensions.AI.ChatMessage; 10 | using ChatResponse = OpenChat.PlaygroundApp.Models.ChatResponse; 11 | 12 | namespace OpenChat.PlaygroundApp.Endpoints; 13 | 14 | /// 15 | /// This represents the endpoint entity for chat operations. 16 | /// 17 | /// The . 18 | /// The . 19 | public class ChatResponseEndpoint(IChatService chatService, ILogger logger) : IEndpoint 20 | { 21 | private readonly IChatService _chatService = chatService ?? throw new ArgumentNullException(nameof(chatService)); 22 | private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 23 | 24 | /// 25 | public void MapEndpoint(IEndpointRouteBuilder app) 26 | { 27 | app.MapPost("/chat/responses", PostChatResponseAsync) 28 | .WithTags("Chat") 29 | .Accepts>(contentType: "application/json") 30 | .Produces>(statusCode: StatusCodes.Status200OK, contentType: "application/json") 31 | .WithName("PostChatResponses") 32 | .WithOpenApi(); 33 | } 34 | 35 | private async IAsyncEnumerable PostChatResponseAsync( 36 | [FromBody] IEnumerable request, 37 | [EnumeratorCancellation] CancellationToken cancellationToken) 38 | { 39 | var chats = request.ToList(); 40 | 41 | this._logger.LogInformation("Received {RequestCount} chat requests", chats.Count); 42 | 43 | var messages = chats.Select(chat => new ChatMessage(new(chat.Role), chat.Message)); 44 | var options = new ChatOptions(); 45 | 46 | var result = this._chatService.GetStreamingResponseAsync(messages, options, cancellationToken: cancellationToken); 47 | await foreach (var update in result) 48 | { 49 | yield return new ChatResponse { Role = update.Role?.Value ?? string.Empty, Message = update.Text }; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Endpoints/EndpointExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace OpenChat.PlaygroundApp.Endpoints; 6 | 7 | /// 8 | /// This represents the extension entity for handling endpoints. 9 | /// 10 | public static class EndpointExtensions 11 | { 12 | /// 13 | /// Adds the chat client to the service collection. 14 | /// 15 | /// The instance. 16 | /// The instance. 17 | /// Returns the modified instance. 18 | public static IServiceCollection AddEndpoints(this IServiceCollection services, Assembly assembly) 19 | { 20 | var descriptors = assembly.DefinedTypes 21 | .Where(type => type is { IsAbstract: false, IsInterface: false } 22 | && type.IsAssignableTo(typeof(IEndpoint))) 23 | .Select(type => ServiceDescriptor.Scoped(typeof(IEndpoint), type)); 24 | services.TryAddEnumerable(descriptors); 25 | 26 | return services; 27 | } 28 | 29 | /// 30 | /// Maps all the registered endpoints. 31 | /// 32 | /// instance. 33 | /// instance. 34 | /// Returns instance. 35 | public static IApplicationBuilder MapEndpoints(this WebApplication app, RouteGroupBuilder? group = default) 36 | { 37 | IEndpointRouteBuilder builder = group is null ? app : group; 38 | 39 | var endpoints = app.Services 40 | .GetRequiredService() 41 | .CreateScope() 42 | .ServiceProvider 43 | .GetRequiredService>(); 44 | foreach (var endpoint in endpoints) 45 | { 46 | endpoint.MapEndpoint(builder); 47 | } 48 | 49 | return app; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Endpoints/IEndpoint.cs: -------------------------------------------------------------------------------- 1 | namespace OpenChat.PlaygroundApp.Endpoints; 2 | 3 | /// 4 | /// This provides interfaces to the endpoints. 5 | /// 6 | public interface IEndpoint 7 | { 8 | /// 9 | /// Maps the endpoint to the specified . 10 | /// 11 | /// instance. 12 | void MapEndpoint(IEndpointRouteBuilder app); 13 | } 14 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Models/ChatRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OpenChat.PlaygroundApp.Models; 4 | 5 | /// 6 | /// This represents the chat request entity. 7 | /// 8 | public class ChatRequest 9 | { 10 | /// 11 | /// Gets or sets the role of the message sender. 12 | /// 13 | [Required] 14 | public string Role { get; set; } = string.Empty; 15 | 16 | /// 17 | /// Gets or sets the message content. 18 | /// 19 | [Required] 20 | public string Message { get; set; } = string.Empty; 21 | } 22 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Models/ChatResponse.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OpenChat.PlaygroundApp.Models; 4 | 5 | /// 6 | /// This represents the chat response entity. 7 | /// 8 | public class ChatResponse 9 | { 10 | /// 11 | /// Gets or sets the role of the message content. 12 | /// 13 | public string Role { get; set; } = string.Empty; 14 | 15 | /// 16 | /// Gets or sets the message content. 17 | /// 18 | [Required] 19 | public string Message { get; set; } = string.Empty; 20 | } 21 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/OpenApi/OpenApiDocumentTransformer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.OpenApi; 2 | using Microsoft.OpenApi.Models; 3 | 4 | namespace OpenChat.PlaygroundApp.OpenApi; 5 | 6 | /// 7 | /// This represents the transformer entity for OpenAPI document. 8 | /// 9 | public class OpenApiDocumentTransformer(IHttpContextAccessor accessor) : IOpenApiDocumentTransformer 10 | { 11 | /// 12 | public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken) 13 | { 14 | document.Info = new OpenApiInfo 15 | { 16 | Title = "OpenChat Playground API", 17 | Version = "1.0.0", 18 | Description = "An API for the OpenChat Playground." 19 | }; 20 | document.Servers = 21 | [ 22 | new OpenApiServer 23 | { 24 | Url = accessor.HttpContext != null 25 | ? $"{accessor.HttpContext.Request.Scheme}://{accessor.HttpContext.Request.Host}/" 26 | : "http://localhost:5280/" 27 | } 28 | ]; 29 | 30 | return Task.CompletedTask; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/OpenChat.PlaygroundApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | latest 6 | 7 | enable 8 | enable 9 | 10 | OpenChat.PlaygroundApp 11 | OpenChat.PlaygroundApp 12 | 13 | 6bf996dc-c60b-4c4e-b34f-0e06358687bf 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/OpenChat.PlaygroundApp.http: -------------------------------------------------------------------------------- 1 | @OpenChat.PlaygroundApp_HostAddress = http://localhost:5280 2 | 3 | POST {{OpenChat.PlaygroundApp_HostAddress}}/api/chat/responses 4 | Accept: application/json 5 | Content-Type: application/json 6 | 7 | [ 8 | { 9 | "role": "system", 10 | "message": "You're a helpful assistant." 11 | }, 12 | { 13 | "role": "user", 14 | "message": "Why is the sky blue?" 15 | } 16 | ] 17 | 18 | ### 19 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/AmazonBedrockArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | /// 8 | /// This represents the argument options entity for Amazon Bedrock. 9 | /// 10 | public class AmazonBedrockArgumentOptions : ArgumentOptions 11 | { 12 | /// 13 | /// Gets or sets the AWSCredentials Access Key ID for the Amazon Bedrock service. 14 | /// 15 | public string? AccessKeyId { get; set; } 16 | 17 | /// 18 | /// Gets or sets the AWSCredentials Secret Access Key for the Amazon Bedrock service. 19 | /// 20 | public string? SecretAccessKey { get; set; } 21 | 22 | /// 23 | /// Gets or sets the AWS region for the Amazon Bedrock service. 24 | /// 25 | public string? Region { get; set; } 26 | 27 | /// 28 | /// Gets or sets the model for the Amazon Bedrock service. 29 | /// 30 | public string? ModelId { get; set; } 31 | 32 | /// 33 | protected override void ParseOptions(IConfiguration config, string[] args) 34 | { 35 | var settings = new AppSettings(); 36 | config.Bind(settings); 37 | 38 | var amazonBedrock = settings.AmazonBedrock; 39 | 40 | this.AccessKeyId ??= amazonBedrock?.AccessKeyId; 41 | this.SecretAccessKey ??= amazonBedrock?.SecretAccessKey; 42 | this.Region ??= amazonBedrock?.Region; 43 | this.ModelId ??= amazonBedrock?.ModelId; 44 | 45 | for (var i = 0; i < args.Length; i++) 46 | { 47 | switch (args[i]) 48 | { 49 | case ArgumentOptionConstants.AmazonBedrock.AccessKeyId: 50 | if (i + 1 < args.Length) 51 | { 52 | this.AccessKeyId = args[++i]; 53 | } 54 | break; 55 | 56 | case ArgumentOptionConstants.AmazonBedrock.SecretAccessKey: 57 | if (i + 1 < args.Length) 58 | { 59 | this.SecretAccessKey = args[++i]; 60 | } 61 | break; 62 | 63 | case ArgumentOptionConstants.AmazonBedrock.Region: 64 | if (i + 1 < args.Length) 65 | { 66 | this.Region = args[++i]; 67 | } 68 | break; 69 | 70 | case ArgumentOptionConstants.AmazonBedrock.ModelId: 71 | if (i + 1 < args.Length) 72 | { 73 | this.ModelId = args[++i]; 74 | } 75 | break; 76 | 77 | default: 78 | break; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/AnthropicArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | /// 8 | /// This represents the argument options entity for Anthropic Claude. 9 | /// 10 | public class AnthropicArgumentOptions : ArgumentOptions 11 | { 12 | /// 13 | /// Gets or sets the API key for Anthropic Claude. 14 | /// 15 | public string? ApiKey { get; set; } 16 | 17 | /// 18 | /// Gets or sets the model name of Anthropic Claude. 19 | /// 20 | public string? Model { get; set; } 21 | 22 | /// 23 | protected override void ParseOptions(IConfiguration config, string[] args) 24 | { 25 | var settings = new AppSettings(); 26 | config.Bind(settings); 27 | 28 | var anthropic = settings.Anthropic; 29 | 30 | this.ApiKey ??= anthropic?.ApiKey; 31 | this.Model ??= anthropic?.Model; 32 | 33 | for (var i = 0; i < args.Length; i++) 34 | { 35 | switch (args[i]) 36 | { 37 | case ArgumentOptionConstants.Anthropic.ApiKey: 38 | if (i + 1 < args.Length) 39 | { 40 | this.ApiKey = args[++i]; 41 | } 42 | break; 43 | 44 | case ArgumentOptionConstants.Anthropic.Model: 45 | if (i + 1 < args.Length) 46 | { 47 | this.Model = args[++i]; 48 | } 49 | break; 50 | 51 | default: 52 | break; 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/AzureAIFoundryArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | /// 8 | /// This represents the argument options entity for Azure AI Foundry. 9 | /// 10 | public class AzureAIFoundryArgumentOptions : ArgumentOptions 11 | { 12 | /// 13 | /// Gets or sets the endpoint URL for Azure AI Foundry API. 14 | /// 15 | public string? Endpoint { get; set; } 16 | 17 | /// 18 | /// Gets or sets the personal access token for Azure AI Foundry. 19 | /// 20 | public string? ApiKey { get; set; } 21 | 22 | /// 23 | /// Gets or sets the model name of Azure AI Foundry. 24 | /// 25 | public string? DeploymentName { get; set; } 26 | 27 | /// 28 | protected override void ParseOptions(IConfiguration config, string[] args) 29 | { 30 | var settings = new AppSettings(); 31 | config.Bind(settings); 32 | 33 | var azureAIFoundry = settings.AzureAIFoundry; 34 | 35 | this.Endpoint ??= azureAIFoundry?.Endpoint; 36 | this.ApiKey ??= azureAIFoundry?.ApiKey; 37 | this.DeploymentName ??= azureAIFoundry?.DeploymentName; 38 | 39 | for (var i = 0; i < args.Length; i++) 40 | { 41 | switch (args[i]) 42 | { 43 | case ArgumentOptionConstants.AzureAIFoundry.Endpoint: 44 | if (i + 1 < args.Length) 45 | { 46 | this.Endpoint = args[++i]; 47 | } 48 | break; 49 | 50 | case ArgumentOptionConstants.AzureAIFoundry.ApiKey: 51 | if (i + 1 < args.Length) 52 | { 53 | this.ApiKey = args[++i]; 54 | } 55 | break; 56 | 57 | case ArgumentOptionConstants.AzureAIFoundry.DeploymentName: 58 | if (i + 1 < args.Length) 59 | { 60 | this.DeploymentName = args[++i]; 61 | } 62 | break; 63 | 64 | default: 65 | break; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/DockerModelRunnerArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | 3 | namespace OpenChat.PlaygroundApp.Options; 4 | 5 | /// 6 | /// This represents the argument options entity for Docker Model Runner. 7 | /// 8 | public class DockerModelRunnerArgumentOptions : ArgumentOptions 9 | { 10 | /// 11 | /// Gets or sets the Docker Model Runner Base URL. 12 | /// 13 | public string? BaseUrl { get; set; } 14 | 15 | /// 16 | /// Gets or sets the Docker Model Runner model/deployment name. 17 | /// 18 | public string? Model { get; set; } 19 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/FoundryLocalArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | /// 8 | /// This represents the argument options entity for Foundry Local. 9 | /// 10 | public class FoundryLocalArgumentOptions : ArgumentOptions 11 | { 12 | /// 13 | /// Gets or sets the alias of Foundry Local. 14 | /// 15 | public string? Alias { get; set; } 16 | 17 | /// 18 | protected override void ParseOptions(IConfiguration config, string[] args) 19 | { 20 | var settings = new AppSettings(); 21 | config.Bind(settings); 22 | 23 | var foundryLocal = settings.FoundryLocal; 24 | 25 | this.Alias ??= foundryLocal?.Alias; 26 | 27 | for (var i = 0; i < args.Length; i++) 28 | { 29 | switch (args[i]) 30 | { 31 | case ArgumentOptionConstants.FoundryLocal.Alias: 32 | if (i + 1 < args.Length) 33 | { 34 | this.Alias = args[++i]; 35 | } 36 | break; 37 | 38 | default: 39 | break; 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/GitHubModelsArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | /// 8 | /// This represents the argument options entity for GitHub Models. 9 | /// 10 | public class GitHubModelsArgumentOptions : ArgumentOptions 11 | { 12 | /// 13 | /// Gets or sets the endpoint URL for GitHub Models API. 14 | /// 15 | public string? Endpoint { get; set; } 16 | 17 | /// 18 | /// Gets or sets the personal access token for GitHub Models. 19 | /// 20 | public string? Token { get; set; } 21 | 22 | /// 23 | /// Gets or sets the model name of GitHub Models. 24 | /// 25 | public string? Model { get; set; } 26 | 27 | /// 28 | protected override void ParseOptions(IConfiguration config, string[] args) 29 | { 30 | var settings = new AppSettings(); 31 | config.Bind(settings); 32 | 33 | var github = settings.GitHubModels; 34 | 35 | this.Endpoint ??= github?.Endpoint; 36 | this.Token ??= github?.Token; 37 | this.Model ??= github?.Model; 38 | 39 | for (var i = 0; i < args.Length; i++) 40 | { 41 | switch (args[i]) 42 | { 43 | case ArgumentOptionConstants.GitHubModels.Endpoint: 44 | if (i + 1 < args.Length) 45 | { 46 | this.Endpoint = args[++i]; 47 | } 48 | break; 49 | 50 | case ArgumentOptionConstants.GitHubModels.Token: 51 | if (i + 1 < args.Length) 52 | { 53 | this.Token = args[++i]; 54 | } 55 | break; 56 | 57 | case ArgumentOptionConstants.GitHubModels.Model: 58 | if (i + 1 < args.Length) 59 | { 60 | this.Model = args[++i]; 61 | } 62 | break; 63 | 64 | default: 65 | break; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/GoogleVertexAIArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | /// 8 | /// This represents the argument options entity for Google Vertex AI. 9 | /// 10 | public class GoogleVertexAIArgumentOptions : ArgumentOptions 11 | { 12 | /// 13 | /// Gets or sets the Google Vertex AI API Key. 14 | /// 15 | public string? ApiKey { get; set; } 16 | 17 | /// 18 | /// Gets or sets the model name of Google Vertex AI. 19 | /// 20 | public string? Model { get; set; } 21 | 22 | 23 | /// 24 | protected override void ParseOptions(IConfiguration config, string[] args) 25 | { 26 | var settings = new AppSettings(); 27 | config.Bind(settings); 28 | 29 | var googleVertexAI = settings.GoogleVertexAI; 30 | 31 | this.ApiKey ??= googleVertexAI?.ApiKey; 32 | this.Model ??= googleVertexAI?.Model; 33 | 34 | for (var i = 0; i < args.Length; i++) 35 | { 36 | switch (args[i]) 37 | { 38 | case ArgumentOptionConstants.GoogleVertexAI.ApiKey: 39 | if (i + 1 < args.Length) 40 | { 41 | this.ApiKey = args[++i]; 42 | } 43 | break; 44 | 45 | case ArgumentOptionConstants.GoogleVertexAI.Model: 46 | if (i + 1 < args.Length) 47 | { 48 | this.Model = args[++i]; 49 | } 50 | break; 51 | 52 | default: 53 | break; 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/HuggingFaceArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | /// 8 | /// This represents the argument options entity for Hugging Face. 9 | /// 10 | public class HuggingFaceArgumentOptions : ArgumentOptions 11 | { 12 | /// 13 | /// Gets or sets the base URL for the Hugging Face API. 14 | /// 15 | public string? BaseUrl { get; set; } 16 | 17 | /// 18 | /// Gets or sets the model name for Hugging Face. 19 | /// 20 | public string? Model { get; set; } 21 | 22 | /// 23 | protected override void ParseOptions(IConfiguration config, string[] args) 24 | { 25 | var settings = new AppSettings(); 26 | config.Bind(settings); 27 | 28 | var huggingFace = settings.HuggingFace; 29 | 30 | this.BaseUrl ??= huggingFace?.BaseUrl; 31 | this.Model ??= huggingFace?.Model; 32 | 33 | for (var i = 0; i < args.Length; i++) 34 | { 35 | switch (args[i]) 36 | { 37 | case ArgumentOptionConstants.HuggingFace.BaseUrl: 38 | if (i + 1 < args.Length) 39 | { 40 | this.BaseUrl = args[++i]; 41 | } 42 | break; 43 | 44 | case ArgumentOptionConstants.HuggingFace.Model: 45 | if (i + 1 < args.Length) 46 | { 47 | this.Model = args[++i]; 48 | } 49 | break; 50 | 51 | default: 52 | break; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/LGArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | /// 8 | /// Represents the command-line argument options for LG AI EXAONE. 9 | /// 10 | public class LGArgumentOptions : ArgumentOptions 11 | { 12 | /// 13 | /// Gets or sets the base URL for LG AI EXAONE API. 14 | /// 15 | public string? BaseUrl { get; set; } 16 | 17 | /// 18 | /// Gets or sets the model name for LG AI EXAONE. 19 | /// 20 | public string? Model { get; set; } 21 | 22 | protected override void ParseOptions(IConfiguration config, string[] args) 23 | { 24 | var settings = new AppSettings(); 25 | config.Bind(settings); 26 | 27 | var lg = settings.LG; 28 | 29 | this.BaseUrl ??= lg?.BaseUrl; 30 | this.Model ??= lg?.Model; 31 | 32 | for (var i = 0; i < args.Length; i++) 33 | { 34 | switch (args[i]) 35 | { 36 | case ArgumentOptionConstants.LG.BaseUrl: 37 | if (i + 1 < args.Length) 38 | { 39 | this.BaseUrl = args[++i]; 40 | } 41 | break; 42 | 43 | case ArgumentOptionConstants.LG.Model: 44 | if (i + 1 < args.Length) 45 | { 46 | this.Model = args[++i]; 47 | } 48 | break; 49 | 50 | default: 51 | break; 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/OllamaArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | 8 | /// 9 | /// Represents the command-line argument options for Ollama. 10 | /// 11 | public class OllamaArgumentOptions : ArgumentOptions 12 | { 13 | /// 14 | /// Gets or sets the base URL for Ollama API. 15 | /// 16 | public string? BaseUrl { get; set; } 17 | 18 | /// 19 | /// Gets or sets the model name for Ollama. 20 | /// 21 | public string? Model { get; set; } 22 | 23 | protected override void ParseOptions(IConfiguration config, string[] args) 24 | { 25 | var settings = new AppSettings(); 26 | config.Bind(settings); 27 | 28 | var ollama = settings.Ollama; 29 | 30 | this.BaseUrl ??= ollama?.BaseUrl; 31 | this.Model ??= ollama?.Model; 32 | 33 | for (var i = 0; i < args.Length; i++) 34 | { 35 | switch (args[i]) 36 | { 37 | case ArgumentOptionConstants.Ollama.BaseUrl: 38 | if (i + 1 < args.Length) 39 | { 40 | this.BaseUrl = args[++i]; 41 | } 42 | break; 43 | case ArgumentOptionConstants.Ollama.Model: 44 | if (i + 1 < args.Length) 45 | { 46 | this.Model = args[++i]; 47 | } 48 | break; 49 | default: 50 | break; 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/OpenAIArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | /// 8 | /// This represents the argument options entity for OpenAI. 9 | /// 10 | public class OpenAIArgumentOptions : ArgumentOptions 11 | { 12 | /// 13 | /// Gets or sets the OpenAI API key. 14 | /// 15 | public string? ApiKey { get; set; } 16 | 17 | /// 18 | /// Gets or sets the OpenAI model name. 19 | /// 20 | public string? Model { get; set; } 21 | 22 | /// 23 | protected override void ParseOptions(IConfiguration config, string[] args) 24 | { 25 | var settings = new AppSettings(); 26 | config.Bind(settings); 27 | 28 | var openai = settings.OpenAI; 29 | 30 | this.ApiKey ??= openai?.ApiKey; 31 | this.Model ??= openai?.Model; 32 | 33 | for (var i = 0; i < args.Length; i++) 34 | { 35 | switch (args[i]) 36 | { 37 | case ArgumentOptionConstants.OpenAI.ApiKey: 38 | if (i + 1 < args.Length) 39 | { 40 | this.ApiKey = args[++i]; 41 | } 42 | break; 43 | 44 | case ArgumentOptionConstants.OpenAI.Model: 45 | if (i + 1 < args.Length) 46 | { 47 | this.Model = args[++i]; 48 | } 49 | break; 50 | 51 | default: 52 | break; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Options/UpstageArgumentOptions.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Constants; 4 | 5 | namespace OpenChat.PlaygroundApp.Options; 6 | 7 | /// 8 | /// This represents the argument options entity for Upstage. 9 | /// 10 | public class UpstageArgumentOptions : ArgumentOptions 11 | { 12 | /// 13 | /// Gets or sets the base URL for Upstage API. 14 | /// 15 | public string? BaseUrl { get; set; } 16 | 17 | /// 18 | /// Gets or sets the API key for Upstage. 19 | /// 20 | public string? ApiKey { get; set; } 21 | 22 | /// 23 | /// Gets or sets the model name of Upstage. 24 | /// 25 | public string? Model { get; set; } 26 | 27 | /// 28 | protected override void ParseOptions(IConfiguration config, string[] args) 29 | { 30 | var settings = new AppSettings(); 31 | config.Bind(settings); 32 | 33 | var upstage = settings.Upstage; 34 | 35 | this.BaseUrl ??= upstage?.BaseUrl; 36 | this.ApiKey ??= upstage?.ApiKey; 37 | this.Model ??= upstage?.Model; 38 | 39 | for (var i = 0; i < args.Length; i++) 40 | { 41 | switch (args[i]) 42 | { 43 | case ArgumentOptionConstants.Upstage.BaseUrl: 44 | if (i + 1 < args.Length) 45 | { 46 | this.BaseUrl = args[++i]; 47 | } 48 | break; 49 | 50 | case ArgumentOptionConstants.Upstage.ApiKey: 51 | if (i + 1 < args.Length) 52 | { 53 | this.ApiKey = args[++i]; 54 | } 55 | break; 56 | 57 | case ArgumentOptionConstants.Upstage.Model: 58 | if (i + 1 < args.Length) 59 | { 60 | this.Model = args[++i]; 61 | } 62 | break; 63 | 64 | default: 65 | break; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | 3 | using OpenChat.PlaygroundApp.Abstractions; 4 | using OpenChat.PlaygroundApp.Components; 5 | using OpenChat.PlaygroundApp.Endpoints; 6 | using OpenChat.PlaygroundApp.OpenApi; 7 | using OpenChat.PlaygroundApp.Services; 8 | 9 | var builder = WebApplication.CreateBuilder(args); 10 | 11 | var config = builder.Configuration; 12 | var settings = ArgumentOptions.Parse(config, args); 13 | if (settings.Help == true) 14 | { 15 | ArgumentOptions.DisplayHelp(); 16 | return; 17 | } 18 | 19 | builder.Services.AddSingleton(settings!); 20 | 21 | builder.Services.AddRazorComponents() 22 | .AddInteractiveServerComponents(); 23 | 24 | var chatClient = await LanguageModelConnector.CreateChatClientAsync(settings); 25 | 26 | builder.Services.AddChatClient(chatClient) 27 | .UseFunctionInvocation() 28 | .UseLogging(); 29 | 30 | builder.Services.AddHttpContextAccessor(); 31 | builder.Services.AddOpenApi("openapi", options => 32 | { 33 | options.AddDocumentTransformer(); 34 | }); 35 | 36 | builder.Services.AddScoped(); 37 | builder.Services.AddEndpoints(typeof(Program).Assembly); 38 | 39 | var app = builder.Build(); 40 | 41 | // Configure the HTTP request pipeline. 42 | if (!app.Environment.IsDevelopment()) 43 | { 44 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 45 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 46 | app.UseHsts(); 47 | 48 | app.UseHttpsRedirection(); 49 | } 50 | 51 | app.UseAntiforgery(); 52 | app.UseStaticFiles(); 53 | 54 | if (app.Environment.IsDevelopment()) 55 | { 56 | app.MapOpenApi("/{documentName}.json"); 57 | } 58 | 59 | var group = app.MapGroup("/api"); 60 | app.MapEndpoints(group); 61 | 62 | app.MapRazorComponents() 63 | .AddInteractiveServerRenderMode(); 64 | 65 | await app.RunAsync(); 66 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "http://localhost:5280", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "dotnetRunMessages": true, 16 | "launchBrowser": true, 17 | "applicationUrl": "https://localhost:45280;http://localhost:5280", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/Services/ChatService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | 3 | namespace OpenChat.PlaygroundApp.Services; 4 | 5 | /// 6 | /// This provides interfaces to the chat service. 7 | /// 8 | public interface IChatService 9 | { 10 | /// 11 | /// Sends chat messages and streams the response. 12 | /// 13 | /// The sequence of to send. 14 | /// The with which to configure the request. 15 | /// The to monitor for cancellation requests. The default is . 16 | /// The generated. 17 | IAsyncEnumerable GetStreamingResponseAsync( 18 | IEnumerable messages, 19 | ChatOptions? options = null, 20 | CancellationToken cancellationToken = default); 21 | } 22 | 23 | /// 24 | /// This represents the service entity for chat operations. 25 | /// 26 | /// The . 27 | /// The . 28 | public class ChatService(IChatClient chatClient, ILogger logger) : IChatService 29 | { 30 | private readonly IChatClient _chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient)); 31 | private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 32 | 33 | /// 34 | public IAsyncEnumerable GetStreamingResponseAsync( 35 | IEnumerable messages, 36 | ChatOptions? options = null, 37 | CancellationToken cancellationToken = default) 38 | { 39 | var chats = messages.ToList(); 40 | if (chats.Count < 2) 41 | { 42 | throw new ArgumentException("At least two messages are required", nameof(messages)); 43 | } 44 | 45 | if (chats.First().Role != ChatRole.System) 46 | { 47 | throw new ArgumentException("The first message must be a system message", nameof(messages)); 48 | } 49 | 50 | if (chats.ElementAt(1).Role != ChatRole.User) 51 | { 52 | throw new ArgumentException("The second message must be a user message", nameof(messages)); 53 | } 54 | 55 | this._logger.LogInformation("Requesting chat response with {MessageCount} messages", chats.Count); 56 | 57 | return this._chatClient.GetStreamingResponseAsync(chats, options, cancellationToken); 58 | } 59 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "Microsoft.EntityFrameworkCore": "Warning" 7 | } 8 | }, 9 | 10 | "AllowedHosts": "*", 11 | 12 | "ConnectorType": "GitHubModels", 13 | 14 | "AmazonBedrock": { 15 | "AccessKeyId": "{{AWS_ACCESS_KEY_ID}}", 16 | "SecretAccessKey": "{{AWS_SECRET_ACCESS_KEY}}", 17 | "Region": "{{AWS_REGION}}", 18 | "ModelId": "anthropic.claude-sonnet-4-20250514-v1:0" 19 | }, 20 | 21 | "AzureAIFoundry": { 22 | "Endpoint": "{{AZURE_ENDPOINT}}", 23 | "ApiKey": "{{AZURE_API_KEY}}", 24 | "DeploymentName": "gpt-4o-mini" 25 | }, 26 | 27 | "GitHubModels": { 28 | "Endpoint": "https://models.github.ai/inference", 29 | "Token": "{{GITHUB_PAT}}", 30 | "Model": "openai/gpt-4o-mini" 31 | }, 32 | 33 | "GoogleVertexAI": { 34 | "ApiKey": "{{GOOGLE_API_KEY}}", 35 | "Model": "gemini-2.5-flash-lite" 36 | }, 37 | 38 | "DockerModelRunner": { 39 | "BaseUrl": "http://localhost:12434", 40 | "Model": "ai/smollm2" 41 | }, 42 | 43 | "FoundryLocal": { 44 | "Alias": "phi-4-mini" 45 | }, 46 | 47 | "HuggingFace": { 48 | "BaseUrl": "http://localhost:11434", 49 | "Model": "hf.co/Qwen/Qwen3-0.6B-GGUF" 50 | }, 51 | 52 | "Ollama": { 53 | "BaseUrl": "http://localhost:11434", 54 | "Model": "llama3.2" 55 | }, 56 | 57 | "Anthropic": { 58 | "ApiKey": "{{ANTHROPIC_API_KEY}}", 59 | "Model": "claude-sonnet-4-0" 60 | }, 61 | 62 | "LG": { 63 | "BaseUrl": "http://localhost:11434", 64 | "Model": "hf.co/LGAI-EXAONE/EXAONE-4.0-1.2B-GGUF" 65 | }, 66 | 67 | "OpenAI": { 68 | "ApiKey": "{{OPENAI_API_KEY}}", 69 | "Model": "gpt-4.1-mini" 70 | }, 71 | 72 | "Upstage": { 73 | "BaseUrl": "https://api.upstage.ai/v1", 74 | "ApiKey": "{{UPSTAGE_API_KEY}}", 75 | "Model": "solar-mini" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-144x144.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-192x192.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-36x36.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-48x48.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-72x72.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/android-icon-96x96.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | @import url('lib/tailwindcss/dist/preflight.css'); 2 | 3 | html { 4 | min-height: 100vh; 5 | } 6 | 7 | html, .main-background-gradient { 8 | background: linear-gradient(to bottom, rgb(225 227 233), #f4f4f4 25rem); 9 | } 10 | 11 | body { 12 | display: flex; 13 | flex-direction: column; 14 | min-height: 100vh; 15 | } 16 | 17 | html::after { 18 | content: ''; 19 | background-image: linear-gradient(to right, #3a4ed5, #3acfd5 15%, #d53abf 85%, red); 20 | width: 100%; 21 | height: 2px; 22 | position: fixed; 23 | top: 0; 24 | } 25 | 26 | h1 { 27 | font-size: 2.25rem; 28 | line-height: 2.5rem; 29 | font-weight: 600; 30 | } 31 | 32 | h1:focus { 33 | outline: none; 34 | } 35 | 36 | .valid.modified:not([type=checkbox]) { 37 | outline: 1px solid #26b050; 38 | } 39 | 40 | .invalid { 41 | outline: 1px solid #e50000; 42 | } 43 | 44 | .validation-message { 45 | color: #e50000; 46 | } 47 | 48 | .blazor-error-boundary { 49 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 50 | padding: 1rem 1rem 1rem 3.7rem; 51 | color: white; 52 | } 53 | 54 | .blazor-error-boundary::after { 55 | content: "An error has occurred." 56 | } 57 | 58 | .btn-default { 59 | display: flex; 60 | padding: 0.25rem 0.75rem; 61 | gap: 0.25rem; 62 | align-items: center; 63 | border-radius: 0.25rem; 64 | border: 1px solid #9CA3AF; 65 | font-size: 0.875rem; 66 | line-height: 1.25rem; 67 | font-weight: 600; 68 | background-color: #D1D5DB; 69 | } 70 | 71 | .btn-default:hover { 72 | background-color: #E5E7EB; 73 | } 74 | 75 | .btn-subtle { 76 | display: flex; 77 | padding: 0.25rem 0.75rem; 78 | gap: 0.25rem; 79 | align-items: center; 80 | border-radius: 0.25rem; 81 | border: 1px solid #D1D5DB; 82 | font-size: 0.875rem; 83 | line-height: 1.25rem; 84 | } 85 | 86 | .btn-subtle:hover { 87 | border-color: #93C5FD; 88 | background-color: #DBEAFE; 89 | } 90 | 91 | .page-width { 92 | max-width: 1024px; 93 | margin: auto; 94 | } 95 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-114x114.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-120x120.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-144x144.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-152x152.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-180x180.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-57x57.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-60x60.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-72x72.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-76x76.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon-precomposed.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/apple-icon.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/favicon-16x16.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/favicon-32x32.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/favicon-96x96.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/lib/dompurify/README.md: -------------------------------------------------------------------------------- 1 | dompurify version 3.2.4 2 | https://github.com/cure53/DOMPurify 3 | License: Apache 2.0 and Mozilla Public License 2.0 4 | 5 | To update, replace the files with an updated build from https://www.npmjs.com/package/dompurify 6 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/lib/marked/README.md: -------------------------------------------------------------------------------- 1 | marked version 15.0.6 2 | https://github.com/markedjs/marked 3 | License: MIT 4 | 5 | To update, replace the files with with an updated build from https://www.npmjs.com/package/marked 6 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/lib/tailwindcss/README.md: -------------------------------------------------------------------------------- 1 | tailwindcss version 4.0.3 2 | https://github.com/tailwindlabs/tailwindcss 3 | License: MIT 4 | 5 | This template uses only `preflight.css`, the CSS reset stylesheet from Tailwind. For simplicity, this template doesn't use a complete deployment of Tailwind. If you want to use the full Tailwind library, remove `preflight.css` and reference the full version of Tailwind. 6 | 7 | To update, replace the `preflight.css` file with the one in an updated build from https://www.npmjs.com/package/tailwindcss 8 | -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenChat Playground", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-144x144.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-150x150.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-310x310.png -------------------------------------------------------------------------------- /src/OpenChat.PlaygroundApp/wwwroot/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aliencube/open-chat-playground/a6c9edf368a7df6a6cb3e25e2d17454b5bfe094a/src/OpenChat.PlaygroundApp/wwwroot/ms-icon-70x70.png -------------------------------------------------------------------------------- /test/OpenChat.ConsoleApp.Tests/OpenChat.ConsoleApp.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/OpenChat.ConsoleApp.Tests/Options/ArgumentOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using Microsoft.Extensions.Configuration; 4 | 5 | using OpenChat.ConsoleApp.Options; 6 | 7 | namespace OpenChat.ConsoleApp.Tests.Options; 8 | 9 | public class ArgumentOptionsTests 10 | { 11 | private static IConfiguration BuildConfig(string? endpoint = null) 12 | { 13 | var values = new Dictionary 14 | { 15 | ["ApiApp:Endpoint"] = endpoint 16 | }!; 17 | 18 | return new ConfigurationBuilder() 19 | .AddInMemoryCollection(values!) 20 | .Build(); 21 | } 22 | 23 | [Trait("Category", "UnitTest")] 24 | [Fact] 25 | public void Given_EndpointArgument_When_Parse_Then_Should_Set_Endpoint_And_Help_False() 26 | { 27 | // Arrange 28 | var config = BuildConfig(endpoint: "http://default"); 29 | var expected = "http://localhost:1234"; 30 | var args = new[] { "--endpoint", expected }; 31 | 32 | // Act 33 | var result = ArgumentOptions.Parse(config, args); 34 | 35 | // Assert 36 | result.ShouldNotBeNull(); 37 | result.ApiApp.Endpoint.ShouldBe(expected); 38 | result.Help.ShouldBeFalse(); 39 | } 40 | 41 | [Trait("Category", "UnitTest")] 42 | [Theory] 43 | [InlineData("--help")] 44 | [InlineData("-h")] 45 | public void Given_HelpArguments_When_Parse_Then_Should_Set_Help_True(string arg) 46 | { 47 | // Arrange 48 | var config = BuildConfig(); 49 | var args = new[] { arg }; 50 | 51 | // Act 52 | var result = ArgumentOptions.Parse(config, args); 53 | 54 | // Assert 55 | result.Help.ShouldBeTrue(); 56 | } 57 | 58 | [Trait("Category", "UnitTest")] 59 | [Fact] 60 | public void Given_UnknownArgument_When_Parse_Then_Should_Set_Help_True() 61 | { 62 | // Arrange 63 | var config = BuildConfig(); 64 | var args = new[] { "--unknown" }; 65 | 66 | // Act 67 | var result = ArgumentOptions.Parse(config, args); 68 | 69 | // Assert 70 | result.Help.ShouldBeTrue(); 71 | } 72 | 73 | [Trait("Category", "UnitTest")] 74 | [Fact] 75 | public void Given_EndpointArgumentWithoutValue_When_Parse_Then_Should_NotThrow_And_Help_False() 76 | { 77 | // Arrange 78 | var config = BuildConfig(); 79 | var args = new[] { "--endpoint" }; // Missing value intentionally. 80 | 81 | // Act 82 | var result = ArgumentOptions.Parse(config, args); 83 | 84 | // Assert 85 | result.Help.ShouldBeFalse(); 86 | result.ApiApp.Endpoint.ShouldBeNull(); 87 | } 88 | 89 | [Trait("Category", "UnitTest")] 90 | [Fact] 91 | public void Given_DisplayHelp_When_Called_Then_Should_Write_Expected_Lines() 92 | { 93 | // Arrange 94 | var output = new StringBuilder(); 95 | using var writer = new StringWriter(output); 96 | var originalOut = Console.Out; 97 | Console.SetOut(writer); 98 | 99 | try 100 | { 101 | // Act 102 | ArgumentOptions.DisplayHelp(); 103 | } 104 | finally 105 | { 106 | Console.SetOut(originalOut); 107 | } 108 | 109 | // Assert 110 | var text = output.ToString(); 111 | text.ShouldContain("OpenChat Playground"); 112 | text.ShouldContain("--endpoint"); 113 | text.ShouldContain("--help"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Abstractions/LanguageModelConnectorTests.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Connectors; 4 | 5 | namespace OpenChat.PlaygroundApp.Tests.Abstractions; 6 | 7 | public class LanguageModelConnectorTests 8 | { 9 | private static AppSettings BuildAppSettings( 10 | ConnectorType connectorType = ConnectorType.GitHubModels, 11 | string? endpoint = "https://models.github.ai/inference", 12 | string? token = "test-token", 13 | string? model = "openai/gpt-4o-mini") 14 | { 15 | return new AppSettings 16 | { 17 | ConnectorType = connectorType, 18 | GitHubModels = new GitHubModelsSettings 19 | { 20 | Endpoint = endpoint, 21 | Token = token, 22 | Model = model 23 | } 24 | }; 25 | } 26 | 27 | [Trait("Category", "UnitTest")] 28 | [Fact] 29 | public async Task Given_GitHubModels_Settings_When_CreateChatClient_Invoked_Then_It_Should_Return_ChatClient() 30 | { 31 | // Arrange 32 | var settings = BuildAppSettings(); 33 | 34 | // Act 35 | var client = await LanguageModelConnector.CreateChatClientAsync(settings); 36 | 37 | // Assert 38 | client.ShouldNotBeNull(); 39 | } 40 | 41 | [Trait("Category", "UnitTest")] 42 | [Theory] 43 | [InlineData(ConnectorType.Unknown)] 44 | public async Task Given_Unsupported_ConnectorType_When_CreateChatClient_Invoked_Then_It_Should_Throw(ConnectorType connectorType) 45 | { 46 | // Arrange 47 | var settings = BuildAppSettings(connectorType: connectorType); 48 | 49 | // Act 50 | var ex = await Assert.ThrowsAsync(() => LanguageModelConnector.CreateChatClientAsync(settings)); 51 | 52 | // Assert 53 | ex.Message.ShouldContain($"Connector type '{connectorType}'"); 54 | } 55 | 56 | [Trait("Category", "UnitTest")] 57 | [Theory] 58 | // [InlineData(typeof(AmazonBedrockConnector))] 59 | // [InlineData(typeof(AzureAIFoundryConnector))] 60 | [InlineData(typeof(GitHubModelsConnector))] 61 | // [InlineData(typeof(GoogleVertexAIConnector))] 62 | // [InlineData(typeof(DockerModelRunnerConnector))] 63 | // [InlineData(typeof(FoundryLocalConnector))] 64 | [InlineData(typeof(HuggingFaceConnector))] 65 | // [InlineData(typeof(OllamaConnector))] 66 | // [InlineData(typeof(AnthropicConnector))] 67 | // [InlineData(typeof(LGConnector))] 68 | // [InlineData(typeof(NaverConnector))] 69 | [InlineData(typeof(OpenAIConnector))] 70 | // [InlineData(typeof(UpstageConnector))] 71 | public void Given_Concrete_Connectors_When_Checking_Inheritance_Then_Should_Inherit_From_LanguageModelConnector(Type derivedType) 72 | { 73 | // Arrange 74 | var baseType = typeof(LanguageModelConnector); 75 | 76 | // Act 77 | var result = baseType.IsAssignableFrom(derivedType); 78 | 79 | // Assert 80 | result.ShouldBeTrue(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Abstractions/LanguageModelSettingsTests.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | 4 | namespace OpenChat.PlaygroundApp.Tests.Abstractions; 5 | 6 | public class LanguageModelSettingsTests 7 | { 8 | [Trait("Category", "UnitTest")] 9 | [Theory] 10 | [InlineData(typeof(AmazonBedrockSettings))] 11 | [InlineData(typeof(AzureAIFoundrySettings))] 12 | [InlineData(typeof(GitHubModelsSettings))] 13 | [InlineData(typeof(GoogleVertexAISettings))] 14 | [InlineData(typeof(DockerModelRunnerSettings))] 15 | [InlineData(typeof(FoundryLocalSettings))] 16 | [InlineData(typeof(HuggingFaceSettings))] 17 | [InlineData(typeof(OllamaSettings))] 18 | [InlineData(typeof(AnthropicSettings))] 19 | [InlineData(typeof(LGSettings))] 20 | // [InlineData(typeof(NaverSettings))] 21 | [InlineData(typeof(OpenAISettings))] 22 | [InlineData(typeof(UpstageSettings))] 23 | public void Given_Concrete_Settings_When_Checking_Inheritance_Then_Should_Inherit_From_LanguageModelSettings(Type type) 24 | { 25 | // Act 26 | var isSubclass = type.IsSubclassOf(typeof(LanguageModelSettings)); 27 | 28 | // Assert 29 | isSubclass.ShouldBeTrue(); 30 | } 31 | } -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatHeaderUITests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | using Microsoft.Playwright.Xunit; 3 | 4 | using OpenChat.PlaygroundApp.Connectors; 5 | 6 | namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat; 7 | 8 | public class ChatHeaderUITests : PageTest 9 | { 10 | public override async Task InitializeAsync() 11 | { 12 | await base.InitializeAsync(); 13 | await Page.GotoAsync(TestConstants.LocalhostUrl); 14 | await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); 15 | } 16 | 17 | [Trait("Category", "IntegrationTest")] 18 | [Theory] 19 | [InlineData("OpenChat Playground")] 20 | public async Task Given_Root_Page_When_Loaded_Then_Header_Should_Be_Visible(string expected) 21 | { 22 | // Act 23 | var title = await Page.Locator("span.app-title-text").InnerTextAsync(); 24 | 25 | // Assert 26 | title.ShouldBe(expected); 27 | } 28 | 29 | [Trait("Category", "IntegrationTest")] 30 | [Fact] 31 | public async Task Given_Root_Page_When_Loaded_Then_Header_Should_Display_ConnectorType_And_Model() 32 | { 33 | // Act 34 | var connector = await Page.Locator("span.app-connector").InnerTextAsync(); 35 | var model = await Page.Locator("span.app-model").InnerTextAsync(); 36 | 37 | // Assert 38 | connector.ShouldNotBeNullOrEmpty(); 39 | Enum.IsDefined(typeof(ConnectorType), connector).ShouldBeTrue(); 40 | model.ShouldNotBeNullOrEmpty(); 41 | } 42 | 43 | [Trait("Category", "IntegrationTest")] 44 | [Fact] 45 | public async Task Given_Root_Page_When_Loaded_Then_NewChat_Button_Should_Be_Visible() 46 | { 47 | // Arrange 48 | var newChatButton = Page.GetByRole(AriaRole.Button, new() { Name = "New chat" }); 49 | 50 | // Assert 51 | var isVisible = await newChatButton.IsVisibleAsync(); 52 | isVisible.ShouldBeTrue(); 53 | } 54 | 55 | [Trait("Category", "IntegrationTest")] 56 | [Fact] 57 | public async Task Given_Header_When_Loaded_Then_NewChat_Icon_Should_Be_Visible() 58 | { 59 | // Arrange 60 | var icon = Page.Locator("button svg.new-chat-icon"); 61 | 62 | // Assert 63 | var isVisible = await icon.IsVisibleAsync(); 64 | isVisible.ShouldBeTrue(); 65 | } 66 | 67 | [Trait("Category", "IntegrationTest")] 68 | [Trait("Category", "LLMRequired")] 69 | [Theory] 70 | [InlineData("1+1의 결과는 무엇인가요?")] 71 | [InlineData("what is the result of 1 + 1?")] 72 | public async Task Given_UserAndAssistantMessages_When_NewChat_Clicked_Then_Conversation_Should_Reset(string userMessage) 73 | { 74 | // Arrange 75 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 76 | var sendButton = Page.GetByRole(AriaRole.Button, new() { Name = "User Message Send Button" }); 77 | var newChatButton = Page.GetByRole(AriaRole.Button, new() { Name = "New chat" }); 78 | 79 | var loadingSpinner = Page.Locator(".lds-ellipsis"); 80 | var userMessages = Page.Locator(".user-message"); 81 | var assistantMessages = Page.Locator(".assistant-message-header"); 82 | var noMessagesPlaceholder = Page.Locator(".no-messages"); 83 | 84 | // Act 85 | await textArea.FillAsync(userMessage); 86 | await sendButton.ClickAsync(); 87 | await newChatButton.ClickAsync(); 88 | await noMessagesPlaceholder.WaitForAsync(); 89 | 90 | // Assert 91 | var userMessageCount = await userMessages.CountAsync(); 92 | userMessageCount.ShouldBe(0); 93 | 94 | var assistantMessageCount = await assistantMessages.CountAsync(); 95 | assistantMessageCount.ShouldBe(0); 96 | 97 | var placeholderVisible = await noMessagesPlaceholder.IsVisibleAsync(); 98 | placeholderVisible.ShouldBeTrue(); 99 | 100 | var spinnerVisible = await loadingSpinner.IsVisibleAsync(); 101 | spinnerVisible.ShouldBeFalse(); 102 | } 103 | 104 | public override async Task DisposeAsync() 105 | { 106 | await Page.CloseAsync(); 107 | await base.DisposeAsync(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatInputImeE2ETests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | using Microsoft.Playwright.Xunit; 3 | 4 | namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat; 5 | 6 | public class ChatInputImeE2ETests : PageTest 7 | { 8 | public override async Task InitializeAsync() 9 | { 10 | await base.InitializeAsync(); 11 | await Page.GotoAsync(TestConstants.LocalhostUrl); 12 | await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); 13 | } 14 | 15 | [Trait("Category", "IntegrationTest")] 16 | [Theory] 17 | [InlineData("안녕하세요")] 18 | [InlineData("테스트")] 19 | public async Task Given_Korean_IME_Composition_When_Enter_During_Composition_Then_It_Should_Not_Submit(string testMessage) 20 | { 21 | // Arrange 22 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 23 | await textArea.FocusAsync(); 24 | await textArea.FillAsync(testMessage); 25 | var userCountBefore = await Page.Locator(".user-message").CountAsync(); 26 | 27 | // Act: Enter during composition should NOT submit 28 | await Page.DispatchEventAsync("textarea", "compositionstart", new { }); 29 | await Page.DispatchEventAsync("textarea", "keydown", new { bubbles = true, cancelable = true, key = "Enter", isComposing = true }); 30 | 31 | // Assert: no user message added 32 | var userCountAfterComposeEnter = await Page.Locator(".user-message").CountAsync(); 33 | userCountAfterComposeEnter.ShouldBe(userCountBefore); 34 | } 35 | 36 | [Trait("Category", "IntegrationTest")] 37 | [Trait("Category", "LLMRequired")] 38 | [Theory] 39 | [InlineData("안녕하세요", "안")] 40 | [InlineData("테스트", "테")] 41 | public async Task Given_Korean_IME_Composition_Ended_When_Enter_Pressed_Then_It_Should_Submit_Once(string testMessage, string compositionData) 42 | { 43 | // Arrange 44 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 45 | await textArea.FocusAsync(); 46 | await textArea.FillAsync(testMessage); 47 | var userCountBefore = await Page.Locator(".user-message").CountAsync(); 48 | 49 | // Act: Composition ends, then Enter should submit once 50 | await Page.DispatchEventAsync("textarea", "compositionstart", new { }); 51 | await Page.DispatchEventAsync("textarea", "compositionend", new { data = compositionData }); 52 | var assistantCountBefore = await Page.Locator(".assistant-message-header").CountAsync(); 53 | await Page.DispatchEventAsync("textarea", "keydown", new { bubbles = true, cancelable = true, key = "Enter" }); 54 | 55 | // Assert: assistant response begins and user message added once 56 | await Page.WaitForFunctionAsync( 57 | "args => document.querySelectorAll(args.selector).length >= args.expected", 58 | new { selector = ".assistant-message-header", expected = assistantCountBefore + 1 } 59 | ); 60 | var userCountAfterSubmit = await Page.Locator(".user-message").CountAsync(); 61 | userCountAfterSubmit.ShouldBe(userCountBefore + 1); 62 | } 63 | 64 | [Trait("Category", "IntegrationTest")] 65 | [Trait("Category", "LLMRequired")] 66 | [Theory] 67 | [InlineData("테스트 메시지")] 68 | [InlineData("안녕하세요")] 69 | public async Task Given_Message_Sent_When_Enter_Pressed_Immediately_Then_It_Should_Not_Send_Twice(string testMessage) 70 | { 71 | // Arrange 72 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 73 | await textArea.FocusAsync(); 74 | await textArea.FillAsync(testMessage); 75 | var userCountBefore = await Page.Locator(".user-message").CountAsync(); 76 | 77 | // Act: Send via Enter 78 | var assistantCountBefore = await Page.Locator(".assistant-message-header").CountAsync(); 79 | await textArea.PressAsync("Enter"); 80 | 81 | // Assert: assistant response begins and one user message 82 | await Page.WaitForFunctionAsync( 83 | "args => document.querySelectorAll(args.selector).length >= args.expected", 84 | new { selector = ".assistant-message-header", expected = assistantCountBefore + 1 } 85 | ); 86 | var userCountAfterFirst = await Page.Locator(".user-message").CountAsync(); 87 | userCountAfterFirst.ShouldBe(userCountBefore + 1); 88 | 89 | // Act: Press Enter again immediately without typing 90 | await textArea.PressAsync("Enter"); 91 | 92 | // Assert: no additional user message 93 | var userCountAfterSecond = await Page.Locator(".user-message").CountAsync(); 94 | userCountAfterSecond.ShouldBe(userCountBefore + 1); 95 | } 96 | 97 | [Trait("Category", "IntegrationTest")] 98 | [Theory] 99 | [InlineData("첫 줄")] 100 | [InlineData("테스트")] 101 | public async Task Given_Text_Input_When_Shift_Enter_Pressed_Then_It_Should_Insert_Newline_Not_Submit(string initialText) 102 | { 103 | // Arrange 104 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 105 | await textArea.FocusAsync(); 106 | await textArea.FillAsync(initialText); 107 | var userCountBefore = await Page.Locator(".user-message").CountAsync(); 108 | 109 | // Act: Shift+Enter should insert newline (not submit) 110 | await textArea.PressAsync("Shift+Enter"); 111 | 112 | // Assert: value contains newline and no submission 113 | var value = await textArea.InputValueAsync(); 114 | value.ShouldContain("\n"); 115 | var userCountAfter = await Page.Locator(".user-message").CountAsync(); 116 | userCountAfter.ShouldBe(userCountBefore); 117 | } 118 | 119 | public override async Task DisposeAsync() 120 | { 121 | await Page.CloseAsync(); 122 | await base.DisposeAsync(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatMessageItemUITests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | using Microsoft.Playwright.Xunit; 3 | 4 | namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat; 5 | 6 | public class ChatMessageItemUITests : PageTest 7 | { 8 | public override async Task InitializeAsync() 9 | { 10 | await base.InitializeAsync(); 11 | await Page.GotoAsync(TestConstants.LocalhostUrl); 12 | await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); 13 | } 14 | 15 | [Trait("Category", "IntegrationTest")] 16 | [Theory] 17 | [InlineData("Input usermessage")] 18 | public async Task Given_UserMessage_When_Sent_Then_UserMessage_Count_Should_Increment(string userMessage) 19 | { 20 | // Arrange 21 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 22 | var userMessages = Page.Locator(".user-message"); 23 | var initialCount = await userMessages.CountAsync(); 24 | 25 | // Act 26 | await textArea.FillAsync(userMessage); 27 | await textArea.PressAsync("Enter"); 28 | var newUserMessage = userMessages.Nth(initialCount); 29 | await newUserMessage.WaitForAsync(new() { State = WaitForSelectorState.Attached }); 30 | 31 | // Assert 32 | var finalCount = await userMessages.CountAsync(); 33 | finalCount.ShouldBe(initialCount + 1); 34 | } 35 | 36 | [Trait("Category", "IntegrationTest")] 37 | [Theory] 38 | [InlineData("Input usermessage")] 39 | public async Task Given_UserMessage_When_Sent_Then_Rendered_Text_Should_Match_UserMessage(string userMessage) 40 | { 41 | // Arrange 42 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 43 | var userMessages = Page.Locator(".user-message"); 44 | var initialCount = await userMessages.CountAsync(); 45 | 46 | // Act 47 | await textArea.FillAsync(userMessage); 48 | await textArea.PressAsync("Enter"); 49 | var newUserMessage = userMessages.Nth(initialCount); 50 | await newUserMessage.WaitForAsync(new() { State = WaitForSelectorState.Attached }); 51 | 52 | // Assert 53 | var latestUserMessage = userMessages.Nth(initialCount); 54 | var renderedText = await latestUserMessage.InnerTextAsync(); 55 | renderedText.ShouldBe(userMessage); 56 | } 57 | 58 | [Trait("Category", "IntegrationTest")] 59 | [Trait("Category", "LLMRequired")] 60 | [Theory] 61 | [InlineData("Input usermessage")] 62 | public async Task Given_AssistantResponse_When_Streamed_Then_Latest_Text_Should_NotBeEmpty(string userMessage) 63 | { 64 | // Arrange 65 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 66 | var assistantTexts = Page.Locator(".assistant-message-text"); 67 | var initialTextCount = await assistantTexts.CountAsync(); 68 | 69 | // Act 70 | await textArea.FillAsync(userMessage); 71 | await textArea.PressAsync("Enter"); 72 | var newAssistantText = assistantTexts.Nth(initialTextCount); 73 | await newAssistantText.WaitForAsync(new() { State = WaitForSelectorState.Attached }); 74 | 75 | // Assert 76 | var finalContent = await newAssistantText.InnerTextAsync(); 77 | finalContent.ShouldNotBeNullOrWhiteSpace(); 78 | } 79 | 80 | [Trait("Category", "IntegrationTest")] 81 | [Trait("Category", "LLMRequired")] 82 | [Theory] 83 | [InlineData("Input usermessage")] 84 | public async Task Given_AssistantResponse_When_Message_Arrives_Then_Assistant_Icon_Should_Be_Visible(string userMessage) 85 | { 86 | // Arrange 87 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 88 | var assistantHeaders = Page.Locator(".assistant-message-header"); 89 | var assistantIcons = Page.Locator(".assistant-message-icon svg"); 90 | var initialHeaderCount = await assistantHeaders.CountAsync(); 91 | 92 | // Act 93 | await textArea.FillAsync(userMessage); 94 | await textArea.PressAsync("Enter"); 95 | var newAssistantHeader = assistantHeaders.Nth(initialHeaderCount); 96 | await newAssistantHeader.WaitForAsync(new() { State = WaitForSelectorState.Attached }); 97 | 98 | // Assert 99 | var finalIconCount = await assistantIcons.CountAsync(); 100 | var iconIndex = finalIconCount - 1; 101 | var iconVisible = await assistantIcons.Nth(iconIndex).IsVisibleAsync(); 102 | iconVisible.ShouldBeTrue(); 103 | } 104 | 105 | [Trait("Category", "IntegrationTest")] 106 | [Trait("Category", "LLMRequired")] 107 | [Theory] 108 | [InlineData("Input usermessage")] 109 | public async Task Given_Response_InProgress_When_Stream_Completes_Then_Spinner_Should_Toggle(string userMessage) 110 | { 111 | // Arrange 112 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 113 | var spinner = Page.Locator(".lds-ellipsis"); 114 | 115 | // Act 116 | await textArea.FillAsync(userMessage); 117 | await textArea.PressAsync("Enter"); 118 | await spinner.WaitForAsync(new() { State = WaitForSelectorState.Visible }); 119 | var visibleWhileStreaming = await spinner.IsVisibleAsync(); 120 | await spinner.WaitForAsync(new() { State = WaitForSelectorState.Hidden }); 121 | var visibleAfterComplete = await spinner.IsVisibleAsync(); 122 | 123 | // Assert 124 | visibleWhileStreaming.ShouldBeTrue(); 125 | visibleAfterComplete.ShouldBeFalse(); 126 | } 127 | 128 | public override async Task DisposeAsync() 129 | { 130 | await Page.CloseAsync(); 131 | await base.DisposeAsync(); 132 | } 133 | } -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatStreamingUITest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | using Microsoft.Playwright.Xunit; 3 | 4 | namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat; 5 | 6 | public class ChatStreamingUITest : PageTest 7 | { 8 | public override async Task InitializeAsync() 9 | { 10 | await base.InitializeAsync(); 11 | await Page.GotoAsync(TestConstants.LocalhostUrl); 12 | await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); 13 | } 14 | 15 | [Trait("Category", "IntegrationTest")] 16 | [Trait("Category", "LLMRequired")] 17 | [Theory] 18 | [InlineData("하늘은 왜 푸른 색인가요? 다섯 개의 단락으로 자세히 설명해주세요.")] 19 | [InlineData("Why is the sky blue? Please explain in five paragraphs.")] 20 | public async Task Given_UserMessage_When_SendButton_Clicked_Then_Response_Should_Stream_Progressively(string userMessage) 21 | { 22 | // Arrange 23 | const int timeoutMs = 5000; 24 | 25 | const string messageSelector = ".assistant-message-text"; 26 | 27 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 28 | var sendButton = Page.GetByRole(AriaRole.Button, new() { Name = "User Message Send Button" }); 29 | var message = Page.Locator(messageSelector); 30 | 31 | // Act 32 | await textArea.FillAsync(userMessage); 33 | await sendButton.ClickAsync(); 34 | 35 | // Assert 36 | await Expect(message).ToBeVisibleAsync(new() { Timeout = timeoutMs }); 37 | await Expect(message).Not.ToHaveTextAsync(string.Empty, new() { Timeout = timeoutMs }); 38 | 39 | var initialContent = await message.InnerTextAsync(); 40 | 41 | await Expect(message).Not.ToHaveTextAsync(initialContent, new() { Timeout = timeoutMs }); 42 | 43 | var finalContent = await message.InnerTextAsync(); 44 | 45 | finalContent.ShouldNotBe(initialContent); 46 | finalContent.ShouldStartWith(initialContent); 47 | finalContent.Length.ShouldBeGreaterThan(initialContent.Length); 48 | } 49 | 50 | [Trait("Category", "IntegrationTest")] 51 | [Trait("Category", "LLMRequired")] 52 | [Theory] 53 | [InlineData("하늘은 왜 푸른 색인가요?")] 54 | [InlineData("Why is the sky blue?")] 55 | public async Task Given_UserMessage_When_SendButton_Clicked_Then_LoadingSpinner_Should_Be_Visible_Before_Text_Arrives(string userMessage) 56 | { 57 | // Arrange 58 | const string spinnerSelector = ".lds-ellipsis"; 59 | 60 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 61 | var sendButton = Page.GetByRole(AriaRole.Button, new() { Name = "User Message Send Button" }); 62 | var loadingSpinner = Page.Locator(spinnerSelector); 63 | 64 | // Act 65 | await textArea.FillAsync(userMessage); 66 | await sendButton.ClickAsync(); 67 | 68 | // Assert 69 | await Page.WaitForSelectorAsync(spinnerSelector, new() { State = WaitForSelectorState.Visible }); 70 | 71 | var spinnerVisible = await loadingSpinner.IsVisibleAsync(); 72 | spinnerVisible.ShouldBeTrue(); 73 | } 74 | 75 | [Trait("Category", "IntegrationTest")] 76 | [Trait("Category", "LLMRequired")] 77 | [Theory] 78 | [InlineData("하늘은 왜 푸른 색인가요?")] 79 | [InlineData("Why is the sky blue?")] 80 | public async Task Given_UserMessage_When_Response_Text_Arrives_Then_LoadingSpinner_Should_Disappear(string userMessage) 81 | { 82 | // Arrange 83 | const string spinnerSelector = ".lds-ellipsis"; 84 | const string messageSelector = ".assistant-message-text"; 85 | 86 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 87 | var sendButton = Page.GetByRole(AriaRole.Button, new() { Name = "User Message Send Button" }); 88 | var loadingSpinner = Page.Locator(spinnerSelector); 89 | var message = Page.Locator(messageSelector); 90 | 91 | // Act 92 | await textArea.FillAsync(userMessage); 93 | await sendButton.ClickAsync(); 94 | 95 | // Assert 96 | var messageContent = await message.InnerTextAsync(); 97 | messageContent.ShouldNotBeEmpty(); 98 | 99 | var spinnerVisible = await loadingSpinner.IsVisibleAsync(); 100 | spinnerVisible.ShouldBeFalse(); 101 | } 102 | 103 | public override async Task DisposeAsync() 104 | { 105 | await Page.CloseAsync(); 106 | await base.DisposeAsync(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Components/Pages/Chat/ChatUITests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Playwright; 2 | using Microsoft.Playwright.Xunit; 3 | 4 | namespace OpenChat.PlaygroundApp.Tests.Components.Pages.Chat; 5 | 6 | public class ChatUITests : PageTest 7 | { 8 | public override async Task InitializeAsync() 9 | { 10 | await base.InitializeAsync(); 11 | await Page.GotoAsync(TestConstants.LocalhostUrl); 12 | await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); 13 | } 14 | 15 | [Trait("Category", "IntegrationTest")] 16 | [Fact] 17 | public async Task Given_Root_Page_When_Loaded_Then_NoMessagesContent_Should_Be_Visible() 18 | { 19 | // Arrange 20 | var noMessages = Page.Locator(".no-messages"); 21 | 22 | // Act 23 | var isVisible = await noMessages.IsVisibleAsync(); 24 | 25 | // Assert 26 | isVisible.ShouldBeTrue(); 27 | } 28 | 29 | [Trait("Category", "IntegrationTest")] 30 | [Fact] 31 | public async Task Given_Root_Page_When_Loaded_Then_NoMessagesContent_Text_Should_Match() 32 | { 33 | // Arrange 34 | var noMessages = Page.Locator(".no-messages"); 35 | 36 | // Act 37 | var text = await noMessages.InnerTextAsync(); 38 | 39 | // Assert 40 | text.ShouldBe("To get started, try asking about anything."); 41 | } 42 | 43 | [Trait("Category", "IntegrationTest")] 44 | [Fact] 45 | public async Task Given_Root_Page_When_Loaded_Then_PageTitle_Should_Be_Visible() 46 | { 47 | // Act 48 | var headTitle = Page.Locator("title"); 49 | var count = await headTitle.CountAsync(); 50 | 51 | // Assert 52 | count.ShouldBeGreaterThan(0); 53 | } 54 | 55 | 56 | [Trait("Category", "IntegrationTest")] 57 | [Fact] 58 | public async Task Given_Root_Page_When_Loaded_Then_PageTitle_Text_Should_Match() 59 | { 60 | // Act 61 | var title = await Page.TitleAsync(); 62 | 63 | // Assert 64 | title.ShouldBe("OpenChat Playground"); 65 | } 66 | 67 | [Trait("Category", "IntegrationTest")] 68 | [Fact] 69 | public async Task Given_NewChat_Clicked_Then_Input_Textarea_Should_Be_Focused() 70 | { 71 | // Arrange 72 | var newChatButton = Page.GetByRole(AriaRole.Button, new() { Name = "New chat" }); 73 | var textArea = Page.GetByRole(AriaRole.Textbox, new() { Name = "User Message Textarea" }); 74 | 75 | // Act 76 | await newChatButton.ClickAsync(); 77 | 78 | // Assert 79 | await Expect(textArea).ToBeFocusedAsync(); 80 | } 81 | 82 | [Trait("Category", "IntegrationTest")] 83 | [Fact] 84 | public async Task Given_NewChat_Clicked_Then_Conversation_Should_Reset_To_No_UserMessages() 85 | { 86 | // Arrange 87 | var newChatButton = Page.GetByRole(AriaRole.Button, new() { Name = "New chat" }); 88 | var userMessageLocator = Page.Locator(".user-message"); 89 | 90 | // Act 91 | await newChatButton.ClickAsync(); 92 | 93 | // Assert 94 | await Expect(userMessageLocator).ToHaveCountAsync(0); 95 | } 96 | 97 | public override async Task DisposeAsync() 98 | { 99 | await Page.CloseAsync(); 100 | await base.DisposeAsync(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Connectors/HuggingFaceConnectorTests.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Configurations; 3 | using OpenChat.PlaygroundApp.Connectors; 4 | 5 | namespace OpenChat.PlaygroundApp.Tests.Connectors; 6 | 7 | public class HuggingFaceConnectorTests 8 | { 9 | private const string BaseUrl = "https://test.huggingface.co/api"; 10 | private const string Model = "hf.co/test-org/model-gguf"; 11 | 12 | private static AppSettings BuildAppSettings(string? baseUrl = BaseUrl, string? model = Model) 13 | { 14 | return new AppSettings 15 | { 16 | ConnectorType = ConnectorType.HuggingFace, 17 | HuggingFace = new HuggingFaceSettings 18 | { 19 | BaseUrl = baseUrl, 20 | Model = model 21 | } 22 | }; 23 | } 24 | 25 | [Trait("Category", "UnitTest")] 26 | [Theory] 27 | [InlineData(typeof(LanguageModelConnector), typeof(HuggingFaceConnector), true)] 28 | [InlineData(typeof(HuggingFaceConnector), typeof(LanguageModelConnector), false)] 29 | public void Given_BaseType_Then_It_Should_Be_AssignableFrom_DerivedType(Type baseType, Type derivedType, bool expected) 30 | { 31 | // Act 32 | var result = baseType.IsAssignableFrom(derivedType); 33 | 34 | // Assert 35 | result.ShouldBe(expected); 36 | } 37 | 38 | [Trait("Category", "UnitTest")] 39 | [Fact] 40 | public void Given_Settings_Is_Null_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw() 41 | { 42 | // Arrange 43 | var settings = new AppSettings { ConnectorType = ConnectorType.HuggingFace, HuggingFace = null }; 44 | var connector = new HuggingFaceConnector(settings); 45 | 46 | // Act 47 | var ex = Assert.Throws(() => connector.EnsureLanguageModelSettingsValid()); 48 | 49 | // Assert 50 | ex.Message.ShouldContain("HuggingFace"); 51 | } 52 | 53 | [Trait("Category", "UnitTest")] 54 | [Theory] 55 | [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")] 56 | [InlineData("", typeof(InvalidOperationException), "HuggingFace:BaseUrl")] 57 | [InlineData(" ", typeof(InvalidOperationException), "HuggingFace:BaseUrl")] 58 | public void Given_Invalid_BaseUrl_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? baseUrl, Type expectedType, string expectedMessage) 59 | { 60 | // Arrange 61 | var settings = BuildAppSettings(baseUrl: baseUrl); 62 | var connector = new HuggingFaceConnector(settings); 63 | 64 | // Act 65 | var ex = Assert.Throws(expectedType, () => connector.EnsureLanguageModelSettingsValid()); 66 | 67 | // Assert 68 | ex.Message.ShouldContain(expectedMessage); 69 | } 70 | 71 | [Trait("Category", "UnitTest")] 72 | [Theory] 73 | [InlineData(null, typeof(NullReferenceException), "Object reference not set to an instance of an object")] 74 | [InlineData("", typeof(InvalidOperationException), "HuggingFace:Model")] 75 | [InlineData(" ", typeof(InvalidOperationException), "HuggingFace:Model")] 76 | [InlineData("hf.co/org/model", typeof(InvalidOperationException), "HuggingFace:Model format")] 77 | [InlineData("org/model-gguf", typeof(InvalidOperationException), "HuggingFace:Model format")] 78 | [InlineData("hf.co//model-gguf", typeof(InvalidOperationException), "HuggingFace:Model format")] 79 | public void Given_Invalid_Model_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Throw(string? model, Type expectedType, string expectedMessage) 80 | { 81 | // Arrange 82 | var settings = BuildAppSettings(model: model); 83 | var connector = new HuggingFaceConnector(settings); 84 | 85 | // Act 86 | var ex = Assert.Throws(expectedType, () => connector.EnsureLanguageModelSettingsValid()); 87 | 88 | // Assert 89 | ex.Message.ShouldContain(expectedMessage); 90 | } 91 | 92 | [Trait("Category", "UnitTest")] 93 | [Fact] 94 | public void Given_Valid_Settings_When_EnsureLanguageModelSettingsValid_Invoked_Then_It_Should_Return_True() 95 | { 96 | // Arrange 97 | var settings = BuildAppSettings(); 98 | var connector = new HuggingFaceConnector(settings); 99 | 100 | // Act 101 | var result = connector.EnsureLanguageModelSettingsValid(); 102 | 103 | // Assert 104 | result.ShouldBeTrue(); 105 | } 106 | 107 | [Trait("Category", "IntegrationTest")] 108 | [Trait("Category", "LLMRequired")] 109 | [Fact] 110 | public async Task Given_Valid_Settings_When_GetChatClient_Invoked_Then_It_Should_Return_ChatClient() 111 | { 112 | // Arrange 113 | var settings = BuildAppSettings(); 114 | var connector = new HuggingFaceConnector(settings); 115 | 116 | // Act 117 | var client = await connector.GetChatClientAsync(); 118 | 119 | // Assert 120 | client.ShouldNotBeNull(); 121 | } 122 | 123 | [Trait("Category", "UnitTest")] 124 | [Theory] 125 | [InlineData(null, typeof(ArgumentNullException), "null")] 126 | [InlineData("", typeof(UriFormatException), "empty")] 127 | public async Task Given_Missing_BaseUrl_When_GetChatClient_Invoked_Then_It_Should_Throw(string? baseUrl, Type expected, string message) 128 | { 129 | // Arrange 130 | var settings = BuildAppSettings(baseUrl: baseUrl); 131 | var connector = new HuggingFaceConnector(settings); 132 | 133 | // Act 134 | var ex = await Assert.ThrowsAsync(expected, connector.GetChatClientAsync); 135 | 136 | // Assert 137 | ex.Message.ShouldContain(message); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Endpoints/ChatResponseEndpointTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Routing; 3 | using Microsoft.Extensions.Logging; 4 | 5 | using OpenChat.PlaygroundApp.Endpoints; 6 | using OpenChat.PlaygroundApp.Services; 7 | 8 | namespace OpenChat.PlaygroundApp.Tests.Endpoints; 9 | 10 | public class ChatResponseEndpointTests 11 | { 12 | [Trait("Category", "UnitTest")] 13 | [Fact] 14 | public void Given_Null_IChatService_When_ChatResponseEndpoint_Instantiated_Then_It_Should_Throw() 15 | { 16 | // Arrange 17 | var logger = Substitute.For>(); 18 | 19 | // Act 20 | Action action = () => new ChatResponseEndpoint(default(IChatService)!, logger); 21 | 22 | // Assert 23 | action.ShouldThrow(); 24 | } 25 | 26 | [Trait("Category", "UnitTest")] 27 | [Fact] 28 | public void Given_Null_Logger_When_ChatResponseEndpoint_Instantiated_Then_It_Should_Throw() 29 | { 30 | // Arrange 31 | var service = Substitute.For(); 32 | 33 | // Act 34 | Action action = () => new ChatResponseEndpoint(service, default(ILogger)!); 35 | 36 | // Assert 37 | action.ShouldThrow(); 38 | } 39 | 40 | [Trait("Category", "UnitTest")] 41 | [Fact] 42 | public void Given_Both_Dependencies_When_ChatResponseEndpoint_Instantiated_Then_It_Should_Create() 43 | { 44 | // Arrange 45 | var service = Substitute.For(); 46 | var logger = Substitute.For>(); 47 | 48 | // Act 49 | var result = new ChatResponseEndpoint(service, logger); 50 | 51 | // Assert 52 | result.ShouldNotBeNull(); 53 | } 54 | 55 | [Trait("Category", "UnitTest")] 56 | [Theory] 57 | [InlineData("/chat/responses")] 58 | public void Given_Endpoint_When_MapEndpoint_Invoked_Then_It_Should_Contain(string pattern) 59 | { 60 | // Arrange 61 | var args = Array.Empty(); 62 | var app = WebApplication.CreateBuilder(args).Build(); 63 | var chatService = Substitute.For(); 64 | var logger = Substitute.For>(); 65 | var endpoint = new ChatResponseEndpoint(chatService, logger); 66 | 67 | // Act 68 | endpoint.MapEndpoint(app); 69 | var result = app.GetType() 70 | .GetProperty("DataSources", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)! 71 | .GetValue(app) as ICollection; 72 | 73 | // Assert 74 | result.ShouldNotBeNull() 75 | .First() 76 | .Endpoints.OfType() 77 | .Any(e => e.RoutePattern.RawText?.Equals(pattern, StringComparison.OrdinalIgnoreCase) ?? false) 78 | .ShouldBeTrue(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Endpoints/TestEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Routing; 2 | 3 | using OpenChat.PlaygroundApp.Endpoints; 4 | 5 | namespace OpenChat.PlaygroundApp.Tests.Endpoints; 6 | 7 | public partial class EndpointExtensionsTests 8 | { 9 | private class TestEndpoint : IEndpoint 10 | { 11 | public void MapEndpoint(IEndpointRouteBuilder app) 12 | { 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/OpenChat.PlaygroundApp.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | PreserveNewest 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Options/DockerModelRunnerArgumentOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using OpenChat.PlaygroundApp.Abstractions; 2 | using OpenChat.PlaygroundApp.Options; 3 | 4 | namespace OpenChat.PlaygroundApp.Tests.Options; 5 | 6 | public class DockerModelRunnerArgumentOptionsTests 7 | { 8 | [Trait("Category", "UnitTest")] 9 | [Theory] 10 | [InlineData(typeof(ArgumentOptions), typeof(DockerModelRunnerArgumentOptions), true)] 11 | [InlineData(typeof(DockerModelRunnerArgumentOptions), typeof(ArgumentOptions), false)] 12 | public void Given_BaseType_Then_It_Should_Be_AssignableFrom_DerivedType(Type baseType, Type derivedType, bool expected) 13 | { 14 | // Act 15 | var result = baseType.IsAssignableFrom(derivedType); 16 | 17 | // Assert 18 | result.ShouldBe(expected); 19 | } 20 | } -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/Services/ChatServiceTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.AI; 2 | using Microsoft.Extensions.Logging; 3 | 4 | using OpenChat.PlaygroundApp.Services; 5 | 6 | namespace OpenChat.PlaygroundApp.Tests.Services; 7 | 8 | public class ChatServiceTests 9 | { 10 | [Trait("Category", "UnitTest")] 11 | [Fact] 12 | public void Given_Null_IChatClient_When_ChatService_Instantiated_Then_It_Should_Throw() 13 | { 14 | // Arrange 15 | var logger = Substitute.For>(); 16 | 17 | // Act 18 | Action action = () => new ChatService(default(IChatClient)!, logger); 19 | 20 | // Assert 21 | action.ShouldThrow(); 22 | } 23 | 24 | [Trait("Category", "UnitTest")] 25 | [Fact] 26 | public void Given_Null_Logger_When_ChatService_Instantiated_Then_It_Should_Throw() 27 | { 28 | // Arrange 29 | var client = Substitute.For(); 30 | 31 | // Act 32 | Action action = () => new ChatService(client, default(ILogger)!); 33 | 34 | // Assert 35 | action.ShouldThrow(); 36 | } 37 | 38 | [Trait("Category", "UnitTest")] 39 | [Fact] 40 | public void Given_Both_Dependencies_When_ChatService_Instantiated_Then_It_Should_Create() 41 | { 42 | // Arrange 43 | var client = Substitute.For(); 44 | var logger = Substitute.For>(); 45 | 46 | // Act 47 | var result = new ChatService(client, logger); 48 | 49 | // Assert 50 | result.ShouldNotBeNull(); 51 | } 52 | 53 | [Trait("Category", "UnitTest")] 54 | [Fact] 55 | public void Given_Less_Than_Two_Messages_When_GetStreamingResponseAsync_Invoked_Then_It_Should_Throw() 56 | { 57 | // Arrange 58 | var chatClient = Substitute.For(); 59 | var logger = Substitute.For>(); 60 | var chatService = new ChatService(chatClient, logger); 61 | 62 | var messages = new List 63 | { 64 | new(ChatRole.User, "Hello") 65 | }; 66 | 67 | // Act 68 | Action action = () => chatService.GetStreamingResponseAsync(messages); 69 | 70 | // Assert 71 | action.ShouldThrow() 72 | .Message.ShouldContain("At least two messages are required"); 73 | } 74 | 75 | [Trait("Category", "UnitTest")] 76 | [Fact] 77 | public void Given_First_Message_Is_Not_System_When_GetStreamingResponseAsync_Invoked_Then_It_Should_Throw() 78 | { 79 | // Arrange 80 | var chatClient = Substitute.For(); 81 | var logger = Substitute.For>(); 82 | var chatService = new ChatService(chatClient, logger); 83 | 84 | var messages = new List 85 | { 86 | new(ChatRole.User, "Hello"), 87 | new(ChatRole.User, "How are you?") 88 | }; 89 | 90 | // Act 91 | Action action = () => chatService.GetStreamingResponseAsync(messages); 92 | 93 | // Assert 94 | action.ShouldThrow() 95 | .Message.ShouldContain("The first message must be a system message"); 96 | } 97 | 98 | [Trait("Category", "UnitTest")] 99 | [Fact] 100 | public void Given_Second_Message_Is_Not_User_When_GetStreamingResponseAsync_Invoked_Then_It_Should_Throw() 101 | { 102 | // Arrange 103 | var chatClient = Substitute.For(); 104 | var logger = Substitute.For>(); 105 | var chatService = new ChatService(chatClient, logger); 106 | 107 | var messages = new List 108 | { 109 | new(ChatRole.System, "You are a helpful assistant."), 110 | new(ChatRole.Assistant, "Why is the sky blue?") 111 | }; 112 | 113 | // Act 114 | Action action = () => chatService.GetStreamingResponseAsync(messages); 115 | 116 | // Assert 117 | action.ShouldThrow() 118 | .Message.ShouldContain("The second message must be a user message"); 119 | } 120 | 121 | [Trait("Category", "UnitTest")] 122 | [Theory] 123 | [InlineData("This ")] 124 | [InlineData("This ", "is ")] 125 | [InlineData("This ", "is ", "a ")] 126 | [InlineData("This ", "is ", "a ", "test.")] 127 | public async Task Given_Valid_Messages_When_GetStreamingResponseAsync_Invoked_Then_It_Should_Call_ChatClient(params string[] responseMessages) 128 | { 129 | // Arrange 130 | IEnumerable responses = responseMessages.Select(m => new ChatResponseUpdate(ChatRole.Assistant, m)); 131 | 132 | var chatClient = Substitute.For(); 133 | chatClient.GetStreamingResponseAsync(Arg.Any>(), Arg.Any(), Arg.Any()) 134 | .Returns(responses.ToAsyncEnumerable()); 135 | 136 | var logger = Substitute.For>(); 137 | var chatService = new ChatService(chatClient, logger); 138 | 139 | var messages = new List 140 | { 141 | new(ChatRole.System, "You are a helpful assistant."), 142 | new(ChatRole.User, "Why is the sky blue?") 143 | }; 144 | 145 | // Act 146 | var result = chatService.GetStreamingResponseAsync(messages); 147 | var count = await result.CountAsync(); 148 | 149 | // Assert 150 | result.ShouldNotBeNull(); 151 | count.ShouldBe(responseMessages.Length); 152 | } 153 | } -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/TestConstants.cs: -------------------------------------------------------------------------------- 1 | namespace OpenChat.PlaygroundApp.Tests; 2 | 3 | public class TestConstants 4 | { 5 | public const string LocalhostUrl = "http://localhost:5280"; 6 | } 7 | -------------------------------------------------------------------------------- /test/OpenChat.PlaygroundApp.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "GitHubModels": { 3 | "Endpoint": "http://github-models/endpoint", 4 | "Token": "{{GITHUB_PAT}}", 5 | "Model": "github-models" 6 | } 7 | } --------------------------------------------------------------------------------