├── .gitignore ├── LICENSE.txt ├── README.md ├── assets ├── chat1.png ├── chat2.png ├── mathchat.png └── role-play.png ├── azure.yaml ├── infra ├── abbreviations.json ├── core │ ├── ai │ │ └── cognitiveservices.bicep │ ├── host │ │ ├── appservice.bicep │ │ ├── appserviceplan.bicep │ │ └── staticwebapp.bicep │ ├── search │ │ └── search-services.bicep │ ├── security │ │ └── role.bicep │ └── storage │ │ └── storage-account.bicep ├── main.bicep └── main.parameters.json ├── package-lock.json ├── package.json └── src ├── .eslintrc.json ├── __mocks__ ├── react-markdown.tsx └── remark-gfm.tsx ├── agent ├── agentProvider.ts ├── gptAgent.test.ts ├── gptAgent.ts ├── gptAgentConfigPanel.tsx ├── index.ts ├── type.ts └── userProxyAgent.ts ├── chat ├── group.test.ts ├── group.ts ├── groupConfigModal.tsx └── type.ts ├── components ├── Agent │ ├── agent.tsx │ └── agentListItem.tsx ├── Chat │ ├── Chat.tsx │ ├── ChatInput.tsx │ ├── ChatLoader.tsx │ ├── ChatMessage.tsx │ ├── Conversation.tsx │ ├── CopyButton.tsx │ ├── ErrorMessageDiv.tsx │ ├── GroupListItem.tsx │ ├── ModelSelect.tsx │ ├── PromptList.tsx │ ├── Regenerate.tsx │ ├── SystemPrompt.tsx │ └── VariableModal.tsx ├── Global │ ├── Badge.tsx │ ├── DeleteConfirmationDialog.tsx │ ├── EditableSavableTextField.tsx │ ├── Hover.tsx │ ├── Markdown.tsx │ └── Spinner.tsx ├── Markdown │ ├── CodeBlock.tsx │ ├── Image.tsx │ └── MemoizedReactMarkdown.tsx └── Mobile │ └── Navbar.tsx ├── jest.config.js ├── memory ├── chatMemory.test.ts ├── chatMemory.ts ├── chatMemoryConfigPanel.tsx ├── inMemorySavableVectorStore.ts ├── index.tsx ├── memoryProvider.ts └── type.ts ├── message ├── LogMessage.tsx ├── MarkdownMessage.tsx ├── index.tsx ├── messageProvider.ts └── type.ts ├── model ├── azure │ ├── ConfigPanel.tsx │ ├── GPT.ts │ └── index.ts ├── index.tsx ├── llmprovider.ts ├── openai │ ├── GPT.ts │ ├── ModelConfig.tsx │ └── index.ts ├── type.ts └── utils.ts ├── next-i18next.config.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx └── index.tsx ├── postcss.config.js ├── prettier.config.js ├── public ├── favicon.ico ├── locales │ ├── bn │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── de │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── en │ │ └── common.json │ ├── es │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── fr │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── he │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── id │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── ja │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── ko │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── pt │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── ru │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── sv │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── te │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ ├── vi │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json │ └── zh │ │ ├── chat.json │ │ ├── common.json │ │ ├── markdown.json │ │ └── sidebar.json └── screenshot.png ├── styles └── globals.css ├── tailwind.config.js ├── test └── jest.setup.ts ├── tsconfig.json ├── tsconfig.test.json ├── types ├── data.ts ├── env.ts ├── error.ts ├── folder.ts ├── group.ts ├── index.ts ├── openai.ts ├── prompt.ts └── storage.ts └── utils ├── app ├── agentReducer.ts ├── clean.ts ├── codeblock.ts ├── const.ts ├── conversation.ts ├── convertJson.ts ├── folders.ts ├── groupReducer.ts ├── importExport.ts ├── prompts.ts ├── provider.ts ├── recordProvider.ts ├── setup.ts └── storageReducer.ts ├── blobStorage.test.ts ├── blobStorage.ts ├── index.ts └── logger.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | .next/ 6 | out/ 7 | wwwroot/ 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | /dist 18 | 19 | # production 20 | build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | .pnpm-debug.log* 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | .idea 42 | .azure 43 | 44 | # doxfx 45 | **/DROP/ 46 | **/TEMP/ 47 | **/packages/ 48 | **/bin/ 49 | **/obj/ 50 | _site 51 | 52 | .env 53 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 LittleLittleCloud 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ![Note] 2 | > # This repository has been archived. The project has been moved into [Agent ChatRoom](https://github.com/LittleLittleCloud/Agent-ChatRoom/tree/main/chatroom-ui) 3 | 4 | 5 | --- 6 | --- 7 | --- 8 | --- 9 | 10 | # [Multi-Agent ChatUI](https://www.llmchat.me) - Chat with multiple agents in a role-playing game style 11 | 12 | ## Multi-agent role-playing algorithm 13 | ![Multi-agent role-playing algorithm](./assets/role-play.png) 14 | 15 | 16 | ## Support LLM 17 | - OpenAI.GPT 18 | - Azure.GPT 19 | 20 | ## Run locally ## 21 | ### Prerequisites 22 | - Node.js 23 | - NPM 24 | 25 | ### Clone 26 | ```bash 27 | git clone https://github.com/LittleLittleCloud/Multi-agent-ChatUI.git 28 | ``` 29 | ### Install dependencies 30 | ```bash 31 | cd ./src 32 | npm install 33 | ``` 34 | 35 | ### Build && Run 36 | ```bash 37 | npm run build 38 | npm run start 39 | ``` 40 | 41 | ## Example ## 42 | ### Note 43 | - All agents are powered by OpenAI GPT3.5 turbo. 44 | - Credit on my cat, PanPan, for contributing his profile picture as agent avatar on a 100% voluntary basis. 45 | ### Math Chat ### 46 | ![Math Chat](./assets/mathchat.png) 47 | 48 | ## License 49 | [MIT](./LICENSE.txt) 50 | -------------------------------------------------------------------------------- /assets/chat1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittleLittleCloud/Multi-Agent-ChatUI/47d8feddddff477e1d2fde287c81bfdb00d5dfe9/assets/chat1.png -------------------------------------------------------------------------------- /assets/chat2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittleLittleCloud/Multi-Agent-ChatUI/47d8feddddff477e1d2fde287c81bfdb00d5dfe9/assets/chat2.png -------------------------------------------------------------------------------- /assets/mathchat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittleLittleCloud/Multi-Agent-ChatUI/47d8feddddff477e1d2fde287c81bfdb00d5dfe9/assets/mathchat.png -------------------------------------------------------------------------------- /assets/role-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LittleLittleCloud/Multi-Agent-ChatUI/47d8feddddff477e1d2fde287c81bfdb00d5dfe9/assets/role-play.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: chatroom 4 | services: 5 | frontend: 6 | project: ./src 7 | host: staticwebapp 8 | dist: ./build 9 | language: ts -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysisServicesServers": "as", 3 | "apiManagementService": "apim-", 4 | "appConfigurationConfigurationStores": "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 | "eventGridDomains": "evgd-", 41 | "eventGridDomainsTopics": "evgt-", 42 | "eventGridEventSubscriptions": "evgs-", 43 | "eventHubNamespaces": "evhns-", 44 | "eventHubNamespacesEventHubs": "evh-", 45 | "hdInsightClustersHadoop": "hadoop-", 46 | "hdInsightClustersHbase": "hbase-", 47 | "hdInsightClustersKafka": "kafka-", 48 | "hdInsightClustersMl": "mls-", 49 | "hdInsightClustersSpark": "spark-", 50 | "hdInsightClustersStorm": "storm-", 51 | "hybridComputeMachines": "arcs-", 52 | "insightsActionGroups": "ag-", 53 | "insightsComponents": "appi-", 54 | "keyVaultVaults": "kv-", 55 | "kubernetesConnectedClusters": "arck", 56 | "kustoClusters": "dec", 57 | "kustoClustersDatabases": "dedb", 58 | "logicIntegrationAccounts": "ia-", 59 | "logicWorkflows": "logic-", 60 | "machineLearningServicesWorkspaces": "mlw-", 61 | "managedIdentityUserAssignedIdentities": "id-", 62 | "managementManagementGroups": "mg-", 63 | "migrateAssessmentProjects": "migr-", 64 | "networkApplicationGateways": "agw-", 65 | "networkApplicationSecurityGroups": "asg-", 66 | "networkAzureFirewalls": "afw-", 67 | "networkBastionHosts": "bas-", 68 | "networkConnections": "con-", 69 | "networkDnsZones": "dnsz-", 70 | "networkExpressRouteCircuits": "erc-", 71 | "networkFirewallPolicies": "afwp-", 72 | "networkFirewallPoliciesWebApplication": "waf", 73 | "networkFirewallPoliciesRuleGroups": "wafrg", 74 | "networkFrontDoors": "fd-", 75 | "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", 76 | "networkLoadBalancersExternal": "lbe-", 77 | "networkLoadBalancersInternal": "lbi-", 78 | "networkLoadBalancersInboundNatRules": "rule-", 79 | "networkLocalNetworkGateways": "lgw-", 80 | "networkNatGateways": "ng-", 81 | "networkNetworkInterfaces": "nic-", 82 | "networkNetworkSecurityGroups": "nsg-", 83 | "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", 84 | "networkNetworkWatchers": "nw-", 85 | "networkPrivateDnsZones": "pdnsz-", 86 | "networkPrivateLinkServices": "pl-", 87 | "networkPublicIPAddresses": "pip-", 88 | "networkPublicIPPrefixes": "ippre-", 89 | "networkRouteFilters": "rf-", 90 | "networkRouteTables": "rt-", 91 | "networkRouteTablesRoutes": "udr-", 92 | "networkTrafficManagerProfiles": "traf-", 93 | "networkVirtualNetworkGateways": "vgw-", 94 | "networkVirtualNetworks": "vnet-", 95 | "networkVirtualNetworksSubnets": "snet-", 96 | "networkVirtualNetworksVirtualNetworkPeerings": "peer-", 97 | "networkVirtualWans": "vwan-", 98 | "networkVpnGateways": "vpng-", 99 | "networkVpnGatewaysVpnConnections": "vcn-", 100 | "networkVpnGatewaysVpnSites": "vst-", 101 | "notificationHubsNamespaces": "ntfns-", 102 | "notificationHubsNamespacesNotificationHubs": "ntf-", 103 | "operationalInsightsWorkspaces": "log-", 104 | "portalDashboards": "dash-", 105 | "powerBIDedicatedCapacities": "pbi-", 106 | "purviewAccounts": "pview-", 107 | "recoveryServicesVaults": "rsv-", 108 | "resourcesResourceGroups": "rg-", 109 | "searchSearchServices": "srch-", 110 | "serviceBusNamespaces": "sb-", 111 | "serviceBusNamespacesQueues": "sbq-", 112 | "serviceBusNamespacesTopics": "sbt-", 113 | "serviceEndPointPolicies": "se-", 114 | "serviceFabricClusters": "sf-", 115 | "signalRServiceSignalR": "sigr", 116 | "sqlManagedInstances": "sqlmi-", 117 | "sqlServers": "sql-", 118 | "sqlServersDataWarehouse": "sqldw-", 119 | "sqlServersDatabases": "sqldb-", 120 | "sqlServersDatabasesStretch": "sqlstrdb-", 121 | "storageStorageAccounts": "st", 122 | "storageStorageAccountsVm": "stvm", 123 | "storSimpleManagers": "ssimp", 124 | "streamAnalyticsCluster": "asa-", 125 | "synapseWorkspaces": "syn", 126 | "synapseWorkspacesAnalyticsWorkspaces": "synw", 127 | "synapseWorkspacesSqlPoolsDedicated": "syndp", 128 | "synapseWorkspacesSqlPoolsSpark": "synsp", 129 | "timeSeriesInsightsEnvironments": "tsi-", 130 | "webServerFarms": "plan-", 131 | "webSitesAppService": "app-", 132 | "webSitesAppServiceEnvironment": "ase-", 133 | "webSitesFunctions": "func-", 134 | "webStaticSites": "stapp-" 135 | } 136 | -------------------------------------------------------------------------------- /infra/core/ai/cognitiveservices.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param customSubDomainName string = name 6 | param deployments array = [] 7 | param kind string = 'OpenAI' 8 | param publicNetworkAccess string = 'Enabled' 9 | param sku object = { 10 | name: 'S0' 11 | } 12 | 13 | resource account 'Microsoft.CognitiveServices/accounts@2022-10-01' = { 14 | name: name 15 | location: location 16 | tags: tags 17 | kind: kind 18 | properties: { 19 | customSubDomainName: customSubDomainName 20 | publicNetworkAccess: publicNetworkAccess 21 | } 22 | sku: sku 23 | } 24 | 25 | @batchSize(1) 26 | resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2022-10-01' = [for deployment in deployments: { 27 | parent: account 28 | name: deployment.name 29 | properties: { 30 | model: deployment.model 31 | raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null 32 | scaleSettings: deployment.scaleSettings 33 | } 34 | }] 35 | 36 | output endpoint string = account.properties.endpoint 37 | output id string = account.id 38 | output name string = account.name 39 | -------------------------------------------------------------------------------- /infra/core/host/appservice.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | // Reference Properties 6 | param applicationInsightsName string = '' 7 | param appServicePlanId string 8 | param keyVaultName string = '' 9 | param managedIdentity bool = !empty(keyVaultName) 10 | 11 | // Runtime Properties 12 | @allowed([ 13 | 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' 14 | ]) 15 | param runtimeName string 16 | param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' 17 | param runtimeVersion string 18 | 19 | // Microsoft.Web/sites Properties 20 | param kind string = 'app,linux' 21 | 22 | // Microsoft.Web/sites/config 23 | param allowedOrigins array = [] 24 | param alwaysOn bool = true 25 | param appCommandLine string = '' 26 | param appSettings object = {} 27 | param clientAffinityEnabled bool = false 28 | param enableOryxBuild bool = contains(kind, 'linux') 29 | param functionAppScaleLimit int = -1 30 | param linuxFxVersion string = runtimeNameAndVersion 31 | param minimumElasticInstanceCount int = -1 32 | param numberOfWorkers int = -1 33 | param scmDoBuildDuringDeployment bool = false 34 | param use32BitWorkerProcess bool = false 35 | param ftpsState string = 'FtpsOnly' 36 | param healthCheckPath string = '' 37 | 38 | resource appService 'Microsoft.Web/sites@2022-03-01' = { 39 | name: name 40 | location: location 41 | tags: tags 42 | kind: kind 43 | properties: { 44 | serverFarmId: appServicePlanId 45 | siteConfig: { 46 | linuxFxVersion: linuxFxVersion 47 | alwaysOn: alwaysOn 48 | ftpsState: ftpsState 49 | appCommandLine: appCommandLine 50 | numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null 51 | minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null 52 | use32BitWorkerProcess: use32BitWorkerProcess 53 | functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null 54 | healthCheckPath: healthCheckPath 55 | cors: { 56 | allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) 57 | } 58 | } 59 | clientAffinityEnabled: clientAffinityEnabled 60 | httpsOnly: true 61 | } 62 | 63 | identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } 64 | 65 | resource configAppSettings 'config' = { 66 | name: 'appsettings' 67 | properties: union(appSettings, 68 | { 69 | SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) 70 | ENABLE_ORYX_BUILD: string(enableOryxBuild) 71 | }, 72 | !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, 73 | !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) 74 | } 75 | 76 | resource configLogs 'config' = { 77 | name: 'logs' 78 | properties: { 79 | applicationLogs: { fileSystem: { level: 'Verbose' } } 80 | detailedErrorMessages: { enabled: true } 81 | failedRequestsTracing: { enabled: true } 82 | httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } 83 | } 84 | dependsOn: [ 85 | configAppSettings 86 | ] 87 | } 88 | } 89 | 90 | resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { 91 | name: keyVaultName 92 | } 93 | 94 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { 95 | name: applicationInsightsName 96 | } 97 | 98 | output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' 99 | output name string = appService.name 100 | output uri string = 'https://${appService.properties.defaultHostName}' 101 | -------------------------------------------------------------------------------- /infra/core/host/appserviceplan.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param kind string = '' 6 | param reserved bool = true 7 | param sku object 8 | 9 | resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { 10 | name: name 11 | location: location 12 | tags: tags 13 | sku: sku 14 | kind: kind 15 | properties: { 16 | reserved: reserved 17 | } 18 | } 19 | 20 | output id string = appServicePlan.id 21 | output name string = appServicePlan.name 22 | -------------------------------------------------------------------------------- /infra/core/host/staticwebapp.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | // Microsoft.Web/staticSites/config 6 | param appSettings object={} 7 | 8 | param sku object = { 9 | name: 'Free' 10 | tier: 'Free' 11 | } 12 | 13 | resource web 'Microsoft.Web/staticSites@2022-03-01' = { 14 | name: name 15 | location: location 16 | tags: tags 17 | sku: sku 18 | properties: { 19 | provider: 'Custom' 20 | } 21 | 22 | resource configAppSettings 'config' = { 23 | name: 'appsettings' 24 | properties: appSettings 25 | } 26 | } 27 | 28 | output name string = web.name 29 | output uri string = 'https://${web.properties.defaultHostname}' 30 | -------------------------------------------------------------------------------- /infra/core/search/search-services.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param sku object = { 6 | name: 'standard' 7 | } 8 | 9 | param authOptions object = {} 10 | param semanticSearch string = 'disabled' 11 | 12 | resource search 'Microsoft.Search/searchServices@2021-04-01-preview' = { 13 | name: name 14 | location: location 15 | tags: tags 16 | identity: { 17 | type: 'SystemAssigned' 18 | } 19 | properties: { 20 | authOptions: authOptions 21 | disableLocalAuth: false 22 | disabledDataExfiltrationOptions: [] 23 | encryptionWithCmk: { 24 | enforcement: 'Unspecified' 25 | } 26 | hostingMode: 'default' 27 | networkRuleSet: { 28 | bypass: 'None' 29 | ipRules: [] 30 | } 31 | partitionCount: 1 32 | publicNetworkAccess: 'Enabled' 33 | replicaCount: 1 34 | semanticSearch: semanticSearch 35 | } 36 | sku: sku 37 | } 38 | 39 | output id string = search.id 40 | output endpoint string = 'https://${name}.search.windows.net/' 41 | output name string = search.name 42 | -------------------------------------------------------------------------------- /infra/core/security/role.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | 3 | @allowed([ 4 | 'Device' 5 | 'ForeignGroup' 6 | 'Group' 7 | 'ServicePrincipal' 8 | 'User' 9 | ]) 10 | param principalType string = 'ServicePrincipal' 11 | param roleDefinitionId string 12 | 13 | resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 14 | name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) 15 | properties: { 16 | principalId: principalId 17 | principalType: principalType 18 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /infra/core/storage/storage-account.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | @allowed([ 'Hot', 'Cool', 'Premium' ]) 6 | param accessTier string = 'Hot' 7 | param allowBlobPublicAccess bool = false 8 | param allowCrossTenantReplication bool = true 9 | param allowSharedKeyAccess bool = true 10 | param defaultToOAuthAuthentication bool = false 11 | param deleteRetentionPolicy object = {} 12 | @allowed([ 'AzureDnsZone', 'Standard' ]) 13 | param dnsEndpointType string = 'Standard' 14 | param kind string = 'StorageV2' 15 | param minimumTlsVersion string = 'TLS1_2' 16 | @allowed([ 'Enabled', 'Disabled' ]) 17 | param publicNetworkAccess string = 'Disabled' 18 | param sku object = { name: 'Standard_LRS' } 19 | 20 | param containers array = [] 21 | 22 | resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { 23 | name: name 24 | location: location 25 | tags: tags 26 | kind: kind 27 | sku: sku 28 | properties: { 29 | accessTier: accessTier 30 | allowBlobPublicAccess: allowBlobPublicAccess 31 | allowCrossTenantReplication: allowCrossTenantReplication 32 | allowSharedKeyAccess: allowSharedKeyAccess 33 | defaultToOAuthAuthentication: defaultToOAuthAuthentication 34 | dnsEndpointType: dnsEndpointType 35 | minimumTlsVersion: minimumTlsVersion 36 | networkAcls: { 37 | bypass: 'AzureServices' 38 | defaultAction: 'Allow' 39 | } 40 | publicNetworkAccess: publicNetworkAccess 41 | } 42 | 43 | resource blobServices 'blobServices' = if (!empty(containers)) { 44 | name: 'default' 45 | properties: { 46 | deleteRetentionPolicy: deleteRetentionPolicy 47 | } 48 | resource container 'containers' = [for container in containers: { 49 | name: container.name 50 | properties: { 51 | publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' 52 | } 53 | }] 54 | } 55 | } 56 | 57 | output name string = storage.name 58 | output primaryEndpoints object = storage.properties.primaryEndpoints 59 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the the environment which is used to generate a short unique hash used in all resources.') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('Primary location for all resources') 10 | param location string 11 | 12 | param backendServiceName string = '' 13 | param resourceGroupName string = '' 14 | 15 | var abbrs = loadJsonContent('abbreviations.json') 16 | var tags = { 'azd-env-name': environmentName } 17 | // Organize resources in a resource group 18 | resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { 19 | name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' 20 | location: location 21 | tags: tags 22 | } 23 | 24 | var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) 25 | 26 | // The application frontend 27 | module frontend 'core/host/staticwebapp.bicep' = { 28 | name: 'frontend' 29 | scope: resourceGroup 30 | params: { 31 | name: !empty(backendServiceName) ? backendServiceName : '${abbrs.webSitesAppService}frontend-${resourceToken}' 32 | location: location 33 | tags: union(tags, { 'azd-service-name': 'frontend' }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 | "principalId": { 12 | "value": "${AZURE_PRINCIPAL_ID}" 13 | }, 14 | "openAiServiceName": { 15 | "value": "${AZURE_OPENAI_SERVICE}" 16 | }, 17 | "openAiResourceGroupName": { 18 | "value": "${AZURE_OPENAI_RESOURCE_GROUP}" 19 | }, 20 | "openAiSkuName": { 21 | "value": "S0" 22 | }, 23 | "storageAccountName": { 24 | "value": "${AZURE_STORAGE_ACCOUNT}" 25 | }, 26 | "storageResourceGroupName": { 27 | "value": "${AZURE_STORAGE_RESOURCE_GROUP}" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "llm-chatroom", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/__mocks__/react-markdown.tsx: -------------------------------------------------------------------------------- 1 | function ReactMarkdown({ children }){ 2 | return <>{children}; 3 | } 4 | 5 | export default ReactMarkdown; -------------------------------------------------------------------------------- /src/__mocks__/remark-gfm.tsx: -------------------------------------------------------------------------------- 1 | function remarkGfm(){ 2 | return () => {} 3 | } -------------------------------------------------------------------------------- /src/agent/agentProvider.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from "@/utils/app/provider"; 2 | import { IAgentRecord, IAgent } from "./type"; 3 | 4 | export const AgentProvider = new Provider IAgent>(); 5 | -------------------------------------------------------------------------------- /src/agent/gptAgent.test.ts: -------------------------------------------------------------------------------- 1 | import { IOpenAIGPTRecord } from "@/model/openai/GPT"; 2 | import { IChatMessageRecord } from "@/message/type"; 3 | import { Logger } from "@/utils/logger"; 4 | import { AzureGPT, IAzureGPTRecord } from "@/model/azure/GPT"; 5 | import { GPTAgent, IGPTAgentRecord } from "./gptAgent"; 6 | import { AzureKeyCredential, FunctionDefinition, OpenAIClient } from "@azure/openai"; 7 | import { LLMProvider } from "@/model/llmprovider"; 8 | 9 | test('gpt agent callAsync test', async () => { 10 | const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY; 11 | const AZURE_API_ENDPOINT = process.env.AZURE_API_ENDPOINT; 12 | const AZURE_GPT_3_5_TURBO_16K = process.env.AZURE_GPT_3_5_TURBO_16K; 13 | var llm = new AzureGPT( 14 | { 15 | type: "azure.gpt", 16 | deploymentID: AZURE_GPT_3_5_TURBO_16K, 17 | isChatModel: true, 18 | apiKey: AZURE_OPENAI_API_KEY, 19 | isStreaming: true, 20 | maxTokens: 64, 21 | temperature: 0.5, 22 | topP: 1, 23 | endpoint: AZURE_API_ENDPOINT, 24 | frequencyPenalty: 0, 25 | } as IAzureGPTRecord, 26 | ); 27 | 28 | expect(LLMProvider.getDefaultValue("azure.gpt") instanceof AzureGPT).toBe(true); 29 | 30 | var agent = new GPTAgent( 31 | { 32 | name: "alice", 33 | system_message: `Just say 'hello world' to every message 34 | e.g. 35 | hello world`, 36 | avatar: "test", 37 | llm: llm, 38 | }); 39 | 40 | var userMessage = { 41 | role: 'user', 42 | content: 'hello', 43 | } as IChatMessageRecord; 44 | 45 | var response = await agent.callAsync({ 46 | messages: [userMessage], 47 | temperature: 0, 48 | }); 49 | 50 | expect(response.content).toBe("hello world"); 51 | }) 52 | 53 | test('gpt agent callAsync function_call test', async () => { 54 | const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY; 55 | const AZURE_API_ENDPOINT = process.env.AZURE_API_ENDPOINT; 56 | const AZURE_GPT_3_5_TURBO_16K = process.env.AZURE_GPT_3_5_TURBO_16K; 57 | var llm = new AzureGPT( 58 | { 59 | type: "azure.gpt", 60 | deploymentID: AZURE_GPT_3_5_TURBO_16K, 61 | isChatModel: true, 62 | apiKey: AZURE_OPENAI_API_KEY, 63 | isStreaming: true, 64 | maxTokens: 64, 65 | temperature: 0.5, 66 | topP: 1, 67 | endpoint: AZURE_API_ENDPOINT, 68 | frequencyPenalty: 0, 69 | } as IAzureGPTRecord, 70 | ); 71 | 72 | var say_hi_function_definition: FunctionDefinition = { 73 | name: "say_hi", 74 | description: "say hi", 75 | parameters: { 76 | type: "object", 77 | properties: { 78 | name:{ 79 | type: "string", 80 | description: "name of the person", 81 | }, 82 | }, 83 | required: ["name"], 84 | } 85 | }; 86 | 87 | var say_hi_function = async (args: string) => { 88 | var name = JSON.parse(args).name; 89 | return `[SAY_HI_FUNCTION] hi ${name}`; 90 | } 91 | 92 | var agent = new GPTAgent( 93 | { 94 | name: "alice", 95 | system_message: `replying using say_hi function`, 96 | avatar: "test", 97 | llm: llm, 98 | function_map: new Map Promise>([ 99 | [say_hi_function_definition, say_hi_function] 100 | ]), 101 | }); 102 | 103 | var userMessage = { 104 | role: 'user', 105 | content: 'hi I am you dad', 106 | } as IChatMessageRecord; 107 | 108 | var response = await agent.callAsync({ 109 | messages: [userMessage], 110 | temperature: 0, 111 | }); 112 | expect(response.functionCall?.name).toBe("say_hi"); 113 | expect(response.role).toBe("assistant"); 114 | expect(response.content).toContain("[SAY_HI_FUNCTION]"); 115 | expect(response.from).toBe("alice"); 116 | }) 117 | 118 | -------------------------------------------------------------------------------- /src/agent/gptAgent.ts: -------------------------------------------------------------------------------- 1 | import { IAgentRecord, IAgent, AgentCallParams } from "./type"; 2 | import { IChatMessageRecord } from "@/message/type"; 3 | import { IEmbeddingModel, IChatModelRecord } from "@/model/type"; 4 | import { IMemory } from "@/memory/type"; 5 | import { Logger } from "@/utils/logger"; 6 | import { AzureGPT, IAzureGPTRecord } from "@/model/azure/GPT"; 7 | import { IOpenAIGPTRecord } from "@/model/openai/GPT"; 8 | import { LLMProvider } from "@/model/llmprovider"; 9 | import { FunctionDefinition } from "@azure/openai"; 10 | 11 | export interface IGPTAgentRecord extends IAgentRecord { 12 | type: 'agent.gpt'; 13 | llm?: IAzureGPTRecord | IOpenAIGPTRecord; 14 | memory?: IMemory; 15 | embedding?: IEmbeddingModel; 16 | name: string; 17 | group_message?: string; 18 | system_message: string; 19 | avatar: string; // url 20 | }; 21 | 22 | 23 | export class GPTAgent implements IAgent, IGPTAgentRecord { 24 | name: string 25 | type: "agent.gpt"; 26 | llm?: IAzureGPTRecord | IOpenAIGPTRecord | undefined; 27 | memory?: IMemory | undefined; 28 | embedding?: IEmbeddingModel | undefined; 29 | system_message: string; 30 | group_message?: string; 31 | avatar: string; 32 | function_map?: Map Promise>; 33 | 34 | constructor(agent: Partial Promise>}>) { 35 | Logger.debug("initialize chat agent executor"); 36 | this.name = agent.name ?? "GPT"; 37 | this.type = 'agent.gpt'; 38 | this.llm = agent.llm; 39 | this.memory = agent.memory; 40 | this.embedding = agent.embedding; 41 | this.system_message = agent.system_message ?? "You are a helpful AI assistant"; 42 | this.avatar = agent.avatar ?? "GPT"; 43 | this.function_map = agent.function_map; 44 | this.group_message = agent.group_message ?? "Hey"; 45 | } 46 | 47 | async callAsync(params: AgentCallParams): Promise { 48 | var llmRecord = this.llm; 49 | if (!llmRecord) { 50 | throw new Error("No llm provided"); 51 | } 52 | var system_msg = { 53 | role: "system", 54 | content: `Your name is ${this.name}, ${this.system_message}`, 55 | } as IChatMessageRecord; 56 | var llmProvider = LLMProvider.getProvider(llmRecord); 57 | var llm = llmProvider(llmRecord); 58 | var msg = await llm.getChatCompletion({ 59 | messages: [system_msg, ...params.messages], 60 | temperature: params.temperature, 61 | maxTokens: params.maxTokens, 62 | stop: params.stopWords, 63 | functions: this.function_map ? Array.from(this.function_map.keys()) : undefined, 64 | }); 65 | msg.from = this.name; 66 | // if message is a function_call, execute the function 67 | if(msg.functionCall != undefined && this.function_map != undefined){ 68 | var functionDefinitions = Array.from(this.function_map?.keys() ?? []); 69 | var functionDefinition = functionDefinitions.find(f => f.name == msg.functionCall!.name); 70 | var func = functionDefinition ? this.function_map.get(functionDefinition) : undefined; 71 | if(func){ 72 | try{ 73 | var result = await func(msg.functionCall.arguments); 74 | msg.content = result; 75 | msg.name = msg.functionCall.name; 76 | } 77 | catch(e){ 78 | var errorMsg = `Error executing function ${msg.functionCall.name}: ${e}`; 79 | msg.content = errorMsg; 80 | msg.name = msg.functionCall.name; 81 | } 82 | } 83 | else{ 84 | var availableFunctions = Array.from(this.function_map?.keys() ?? []); 85 | var errorMsg = `Function ${msg.functionCall.name} not found. Available functions: ${availableFunctions.map(f => f.name).join(", ")}`; 86 | msg.content = errorMsg; 87 | msg.functionCall = undefined; 88 | } 89 | 90 | return msg; 91 | } 92 | else{ 93 | return msg; 94 | } 95 | } 96 | } 97 | 98 | export function initializeGPTAgent(agent: IGPTAgentRecord, history?: IChatMessageRecord[]): IAgent { 99 | if (!agent.llm) { 100 | throw new Error("No llm provided"); 101 | } 102 | 103 | var agentExecutor = new GPTAgent(agent); 104 | 105 | return agentExecutor; 106 | } 107 | -------------------------------------------------------------------------------- /src/agent/index.ts: -------------------------------------------------------------------------------- 1 | import {IGPTAgentRecord, initializeGPTAgent } from "./gptAgent"; 2 | import { GPTAgentConfigPanel } from "./gptAgentConfigPanel"; 3 | import { AgentProvider } from "./agentProvider"; 4 | 5 | // register gptAgent 6 | AgentProvider.registerProvider( 7 | "agent.gpt", 8 | (agent) => { 9 | if (agent.type != "agent.gpt") { 10 | throw new Error("Invalid agent type"); 11 | } else { 12 | const gpt_record: IGPTAgentRecord = { 13 | ...agent, 14 | type: "agent.gpt", 15 | }; 16 | 17 | return initializeGPTAgent(gpt_record); 18 | } 19 | }, 20 | (agent, onConfigChange) => { 21 | if (agent.type != "agent.gpt") { 22 | throw new Error("Invalid agent type"); 23 | } 24 | 25 | var gpt_record: IGPTAgentRecord = { 26 | ...agent, 27 | type: "agent.gpt", 28 | }; 29 | 30 | return GPTAgentConfigPanel(gpt_record, onConfigChange); 31 | }, 32 | { 33 | type: "agent.gpt", 34 | name: "GPT Agent", 35 | system_message: "you are a helpful ai assistant", 36 | } as IGPTAgentRecord); -------------------------------------------------------------------------------- /src/agent/type.ts: -------------------------------------------------------------------------------- 1 | import { IRecord } from "@/types/storage"; 2 | import { IChatMessageRecord } from "@/message/type"; 3 | 4 | export interface IAgentRecord extends IRecord{ 5 | name: string, 6 | system_message: string, 7 | avatar: string, 8 | } 9 | export interface AgentCallParams{ 10 | messages: IChatMessageRecord[], 11 | maxTokens?: number, 12 | temperature?: number, 13 | stopWords?: string[], 14 | } 15 | export interface IAgent{ 16 | name: string; 17 | 18 | callAsync(params: AgentCallParams): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /src/agent/userProxyAgent.ts: -------------------------------------------------------------------------------- 1 | import { IChatMessageRecord } from "@/message/type"; 2 | import { AgentCallParams, IAgent } from "./type"; 3 | 4 | export class UserProxyAgent implements IAgent{ 5 | public name: string; 6 | 7 | constructor( 8 | name: string, 9 | ){ 10 | this.name = name; 11 | } 12 | 13 | public async callAsync(params: AgentCallParams): Promise{ 14 | throw new Error("Method not implemented."); 15 | } 16 | } -------------------------------------------------------------------------------- /src/chat/group.test.ts: -------------------------------------------------------------------------------- 1 | import { GroupChat } from "./group"; 2 | import { Logger } from "@/utils/logger"; 3 | import { AgentProvider } from "@/agent/agentProvider"; 4 | import { IMarkdownMessageRecord } from "@/message/MarkdownMessage"; 5 | import { IAgentRecord } from "@/agent/type"; 6 | import { AzureGPT, IAzureGPTRecord } from "@/model/azure/GPT"; 7 | import { GPTAgent } from "@/agent/gptAgent"; 8 | import { IChatMessageRecord } from "@/message/type"; 9 | 10 | test('multi-agent response test', async () => { 11 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY; 12 | const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY; 13 | const AZURE_API_ENDPOINT = process.env.AZURE_API_ENDPOINT; 14 | const AZURE_GPT_3_5_TURBO_16K = process.env.AZURE_GPT_3_5_TURBO_16K; 15 | var llm = new AzureGPT({ 16 | deploymentID: AZURE_GPT_3_5_TURBO_16K, 17 | apiKey: AZURE_OPENAI_API_KEY, 18 | endpoint: AZURE_API_ENDPOINT, 19 | temperature: 0, 20 | }); 21 | 22 | var alice = new GPTAgent( 23 | { 24 | name: "alice", 25 | system_message: 'say hello', 26 | llm: llm, 27 | } 28 | ); 29 | 30 | var bob = new GPTAgent( 31 | { 32 | name: "bob", 33 | system_message: 'say hi', 34 | llm: llm, 35 | } 36 | ); 37 | 38 | var groupChat = new GroupChat({ 39 | name: "group", 40 | llm: llm, 41 | admin: alice, 42 | agents: [bob], 43 | }); 44 | 45 | groupChat.addInitialConversation("hello", alice); 46 | groupChat.addInitialConversation("hi", bob); 47 | var nextMessage = { 48 | from: alice.name, 49 | role: 'user', 50 | content: 'hello bob', 51 | } as IChatMessageRecord; 52 | var nextMessages = await groupChat.callAsync([nextMessage], 1); 53 | expect(nextMessages.length).toBe(2); 54 | expect(nextMessages[0].from).toBe(alice.name); 55 | expect(nextMessages[0].content).toBe("hello bob"); 56 | expect(nextMessages[1].from).toBe(bob.name); 57 | }) 58 | -------------------------------------------------------------------------------- /src/chat/groupConfigModal.tsx: -------------------------------------------------------------------------------- 1 | import { IAgentRecord } from "@/agent/type"; 2 | import { SmallTextField, SmallMultipleSelectField, SmallLabel, LargeLabel } from "@/components/Global/EditableSavableTextField"; 3 | import { Dialog, DialogTitle, DialogContent, Stack, DialogActions, Button } from "@mui/material"; 4 | import { FC, useState, useEffect } from "react"; 5 | import { IGroupRecord, SelectSpeakerMode } from "./type"; 6 | import { LogMessageLevel } from "@/message/LogMessage"; 7 | import { IMessageRecord } from "@/message/type"; 8 | 9 | export const GroupConfigModal: FC<{ 10 | open: boolean, 11 | group?: IGroupRecord, 12 | agents: IAgentRecord[], 13 | onSaved: (group: IGroupRecord) => void, 14 | onCancel: () => void, 15 | messageHandler?: (msg: IMessageRecord) => void,}> = ({ 16 | open, 17 | group, 18 | agents, 19 | onSaved, 20 | onCancel, 21 | messageHandler 22 | }) => { 23 | const [groupName, setGroupName] = useState(group?.name); 24 | const [selectAgents, setSelectAgents] = useState(group?.agentNames ?? []); // [agentId] 25 | const [speakerSelectionModel, setSpeakerSelectionModel] = useState(group?.selectSpeakerMode ?? 'semi-auto'); 26 | const [maxRound, setMaxRound] = useState(group?.maxRound ?? 10); 27 | const [logLevel, setLogLevel] = useState(group?.logLevel ?? 'info'); 28 | const availableLogLevel: LogMessageLevel[] = ['verbose', 'debug', 'info', 'warning', 'error']; 29 | const availableMode: SelectSpeakerMode[] = ['auto', 'semi-auto', 'manual']; 30 | const availableAgents: IAgentRecord[] = agents; 31 | useEffect(() => { 32 | setGroupName(group?.name); 33 | setSelectAgents(group?.agentNames ?? []); 34 | }, [group]); 35 | 36 | const onSavedHandler = () => { 37 | // verification 38 | if(groupName == undefined || groupName == ''){ 39 | alert('Group name is required'); 40 | return; 41 | } 42 | 43 | if(selectAgents.length == 0){ 44 | alert('At least one agent is required'); 45 | return; 46 | } 47 | 48 | if(maxRound == undefined || maxRound < 1){ 49 | alert('Max round must be greater than 0'); 50 | return; 51 | } 52 | 53 | if(availableMode.indexOf(speakerSelectionModel) < 0){ 54 | alert('Invalid speaker selection model'); 55 | return; 56 | } 57 | 58 | if(availableLogLevel.indexOf(logLevel) < 0){ 59 | alert('Invalid log level'); 60 | return; 61 | } 62 | 63 | onSaved({ 64 | ...group ?? {}, 65 | name: groupName, 66 | agentNames: selectAgents, 67 | logLevel: logLevel, 68 | maxRound: maxRound, 69 | selectSpeakerMode: speakerSelectionModel, 70 | } as IGroupRecord); 71 | }; 72 | 73 | 74 | return (!open ? <> : 75 |
79 | 80 |
82 |
83 |
84 |
85 |
86 | Edit Group 87 |
88 | Group Name 91 |
92 | setGroupName((e.target as HTMLInputElement).value)} /> 97 |
98 | Max round 101 |
102 | setMaxRound(parseInt((e.target as HTMLInputElement).value))} /> 107 |
108 | Add/Remove Agents 111 |
112 | { 113 | availableAgents.length > 0 && 114 | availableAgents.map((agent, index) => ( 115 |
118 | { 125 | if(selectAgents.includes(agent.name)){ 126 | setSelectAgents(selectAgents.filter(name => name != agent.name)); 127 | } 128 | else{ 129 | setSelectAgents([...selectAgents, agent.name]); 130 | } 131 | }} 132 | /> 133 | {agent.name} 134 |
135 | )) 136 | } 137 |
138 | Speaker Selection Mode 141 |
142 | { 143 | availableMode.map((mode, index) => ( 144 |
147 | setSpeakerSelectionModel(mode)} 154 | /> 155 | {mode} 156 |
157 | )) 158 | } 159 |
160 | Log Level 163 |
164 | { 165 | availableLogLevel.map((level, index) => ( 166 |
169 | setLogLevel(level)} 176 | /> 177 | {level} 178 |
179 | )) 180 | } 181 |
182 |
183 |
185 | 191 | 197 |
198 |
199 |
200 |
201 |
); 202 | }; -------------------------------------------------------------------------------- /src/chat/type.ts: -------------------------------------------------------------------------------- 1 | import { LogMessageLevel } from "@/message/LogMessage"; 2 | import { IChatMessageRecord, IMessageRecord } from "@/message/type"; 3 | import { IChatModelRecord } from "@/model/type"; 4 | import { IRecord } from "@/types/storage"; 5 | 6 | export const GroupTypeString: GroupType = 'group'; 7 | export type GroupType = 'group'; 8 | export type SelectSpeakerMode = 'auto' | 'manual' | 'semi-auto' 9 | export interface IGroupRecord extends IRecord{ 10 | type: GroupType; 11 | name: string; 12 | agentNames: string[]; 13 | conversation: IMessageRecord[]; 14 | llmModel?: IChatModelRecord; 15 | logLevel?: LogMessageLevel; 16 | maxRound?: number; 17 | selectSpeakerMode?: SelectSpeakerMode; 18 | initialMessages?: IChatMessageRecord[]; 19 | } 20 | 21 | export interface IGroup extends IGroupRecord{ 22 | callAsync( 23 | messages: IChatMessageRecord[], 24 | max_round?: number) : Promise; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Agent/agentListItem.tsx: -------------------------------------------------------------------------------- 1 | import { IAgentRecord } from "@/agent/type"; 2 | import { Tooltip } from "@mui/material"; 3 | import { FC } from "react"; 4 | import { SmallLabel } from "../Global/EditableSavableTextField"; 5 | import DeleteIcon from '@mui/icons-material/Delete'; 6 | import AddIcon from '@mui/icons-material/Add'; 7 | 8 | export interface AgentListItemProps { 9 | agent: IAgentRecord; 10 | selected: boolean; 11 | onClick?: (agent: IAgentRecord) => void; 12 | onDeleted?: (agent: IAgentRecord) => void; 13 | onCloned?: (agent: IAgentRecord) => void; 14 | } 15 | 16 | export const AgentListItem: FC = (props) => { 17 | const selected = props.selected; 18 | const agent = props.agent; 19 | 20 | const Element = ( 21 |
props.onClick?.(agent)}> 24 |
25 | {agent.name} 26 |
27 | 28 |
29 | 30 | { 32 | e.stopPropagation(); 33 | props.onCloned?.(agent); 34 | }}/> 35 | 36 | 37 | { 40 | e.stopPropagation(); 41 | props.onDeleted?.(agent); 42 | }}/> 43 | 44 |
45 |
46 | ) 47 | 48 | return selected ? ( 49 |
51 | {Element} 52 |
53 | ) : ( 54 |
56 | {Element} 57 |
58 | ) 59 | } -------------------------------------------------------------------------------- /src/components/Chat/ChatLoader.tsx: -------------------------------------------------------------------------------- 1 | import { IconDots } from '@tabler/icons-react'; 2 | import { FC } from 'react'; 3 | 4 | interface Props {} 5 | 6 | export const ChatLoader: FC = () => { 7 | return ( 8 |
12 |
13 |
AI:
14 | 15 |
16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/Chat/ChatMessage.tsx: -------------------------------------------------------------------------------- 1 | import { IconEdit, IconRefresh } from '@tabler/icons-react'; 2 | import { useTranslation } from 'next-i18next'; 3 | import { FC, memo, useEffect, useRef, useState } from 'react'; 4 | import { Avatar, Box, IconButton, Stack, Tooltip, Typography } from '@mui/material'; 5 | import { SmallAvatar, SmallLabel, TinyAvatar, TinyLabel } from '../Global/EditableSavableTextField'; 6 | import DeleteIcon from '@mui/icons-material/Delete'; 7 | import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; 8 | import RefreshIcon from '@mui/icons-material/Refresh'; 9 | import { IChatMessageRecord, IMessageRecord, IsUserMessage } from '@/message/type'; 10 | import { IAgentRecord } from '@/agent/type'; 11 | import { MessageProvider } from '@/message/messageProvider'; 12 | import { MessageElement } from '@/message'; 13 | 14 | interface Props { 15 | message: IChatMessageRecord; 16 | agent?: IAgentRecord; 17 | onDeleteMessage?: (message: IChatMessageRecord) => void; 18 | onResendMessage?: (message: IChatMessageRecord) => void; 19 | } 20 | 21 | export const ChatMessage: FC = memo( 22 | ({ message, agent, onDeleteMessage, onResendMessage}) => { 23 | const { t } = useTranslation('chat'); 24 | const [isEditing, setIsEditing] = useState(false); 25 | const [messageContent, setMessageContent] = useState(message.content); 26 | const [messagedCopied, setMessageCopied] = useState(false); 27 | const isUser = IsUserMessage(message); 28 | const textareaRef = useRef(null); 29 | 30 | // const copyOnClick = () => { 31 | // if (!navigator.clipboard) return; 32 | 33 | // navigator.clipboard.writeText(message.content.toString()).then(() => { 34 | // setMessageCopied(true); 35 | // setTimeout(() => { 36 | // setMessageCopied(false); 37 | // }, 2000); 38 | // }); 39 | // }; 40 | 41 | useEffect(() => { 42 | if (textareaRef.current) { 43 | textareaRef.current.style.height = 'inherit'; 44 | textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; 45 | } 46 | }, [isEditing]); 47 | 48 | const onDeleteMessageHandler = () => { 49 | if (confirm('Are you sure you want to delete this message???') && onDeleteMessage != undefined) { 50 | onDeleteMessage(message); 51 | } 52 | }; 53 | 54 | const onResendMessageHandler = () => { 55 | if (confirm('Are you sure you want to resend this message?') && onResendMessage) { 56 | onResendMessage(message); 57 | } 58 | }; 59 | 60 | return ( 61 | 71 | 77 | 84 | 90 | {!isUser && } 91 | {isUser ? 92 | You : 93 | {message.from} 94 | } 95 | { 96 | message.timestamp && 97 | {new Date(message.timestamp).toLocaleString()} 98 | } 99 | 108 | {isUser && 109 | 111 | 113 | 118 | 119 | 120 | } 121 | 123 | 126 | 131 | 132 | 133 | 134 | 135 | 139 | 140 | 141 | 142 | 143 | 144 | ); 145 | }); 146 | ChatMessage.displayName = 'ChatMessage'; 147 | -------------------------------------------------------------------------------- /src/components/Chat/Conversation.tsx: -------------------------------------------------------------------------------- 1 | import { IChatMessageRecord, IMessageRecord, IsChatMessage } from '@/message/type'; 2 | import { Box, List } from '@mui/material'; 3 | import React from 'react'; 4 | import { ChatMessage } from './ChatMessage'; 5 | import { IAgentRecord } from '@/agent/type'; 6 | import { ILogMessageRecord, LogLevelsToPresent, LogMessage, LogMessageLevel, LogMessageType, LogMessageTypeString } from '@/message/LogMessage'; 7 | import { MessageElement } from '@/message'; 8 | 9 | interface ConversationProps { 10 | conversation: IMessageRecord[]; 11 | agents: IAgentRecord[]; 12 | onResendMessage: (message: IChatMessageRecord, index: number) => void; 13 | onDeleteMessage: (message: IChatMessageRecord, index: number) => void; 14 | logLevel?: LogMessageLevel; 15 | } 16 | 17 | export const Conversation: React.FC = ({ conversation, onDeleteMessage, onResendMessage, agents, logLevel }) => { 18 | const presentLogLoevels = LogLevelsToPresent(logLevel ?? 'info'); 19 | return ( 20 | 24 | {conversation?.map((message, index) => ( 25 | 31 | { 32 | message.type === LogMessageTypeString && 33 | presentLogLoevels.includes((message as ILogMessageRecord).level) && 34 | 35 | } 36 | { 37 | IsChatMessage(message) && 38 | agent.name === (message as IChatMessageRecord).from)} 42 | onDeleteMessage={(message) => onDeleteMessage(message, index)} 43 | onResendMessage={(message) => onResendMessage(message, index)} 44 | /> 45 | } 46 | 47 | ))} 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/Chat/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconCheck, IconCopy } from '@tabler/icons-react'; 2 | import { FC } from 'react'; 3 | 4 | type Props = { 5 | messagedCopied: boolean; 6 | copyOnClick: () => void; 7 | }; 8 | 9 | export const CopyButton: FC = ({ messagedCopied, copyOnClick }) => ( 10 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/components/Chat/ErrorMessageDiv.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorMessage } from '@/types/error'; 2 | import { IconCircleX } from '@tabler/icons-react'; 3 | import { FC } from 'react'; 4 | 5 | interface Props { 6 | error: ErrorMessage; 7 | } 8 | 9 | export const ErrorMessageDiv: FC = ({ error }) => { 10 | return ( 11 |
12 |
13 | 14 |
15 |
{error.title}
16 | {error.messageLines.map((line, index) => ( 17 |
18 | {' '} 19 | {line}{' '} 20 |
21 | ))} 22 |
23 | {error.code ? Code: {error.code} : ''} 24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/Chat/GroupListItem.tsx: -------------------------------------------------------------------------------- 1 | import { IAgentRecord } from "@/agent/type"; 2 | import { IGroupRecord } from "@/chat/type"; 3 | import { Tooltip, Divider, AvatarGroup } from "@mui/material"; 4 | import { FC } from "react"; 5 | import { MediumLabel, TinyAvatar } from "../Global/EditableSavableTextField"; 6 | import DeleteIcon from '@mui/icons-material/Delete'; 7 | import AddIcon from '@mui/icons-material/Add'; 8 | import SettingsIcon from '@mui/icons-material/Settings'; 9 | 10 | export interface GroupListItemProps{ 11 | group: IGroupRecord; 12 | agents: IAgentRecord[]; 13 | selected: boolean; 14 | onClick?: (group: IGroupRecord) => void; 15 | onDeleted?: (group: IGroupRecord) => void; 16 | onCloned?: (group: IGroupRecord) => void; 17 | onUpdated?: (group: IGroupRecord) => void; 18 | } 19 | 20 | export const GroupListItem: FC = (props) => { 21 | const selected = props.selected; 22 | const group = props.group; 23 | 24 | const Element = ( 25 |
props.onClick?.(group)}> 28 |
30 |
31 | {group.name} 32 |
33 | 34 |
35 | 36 | { 39 | e.stopPropagation(); 40 | props.onDeleted?.(group); 41 | }}/> 42 | 43 |
44 | 45 |
46 | 47 |
49 |
50 | a.name)}`} placement="top"> 51 | 53 | {props.agents.map((agentRecord, index) => { 54 | return ( 55 | 59 | ) 60 | })} 61 | 62 | 63 |
64 | 65 |
66 | 67 | { 69 | e.stopPropagation(); 70 | var clonedGroup = {...group}; 71 | clonedGroup.name = `${clonedGroup.name}(1)`; 72 | props.onCloned?.(clonedGroup); 73 | }}/> 74 | 75 | 76 | { 78 | e.stopPropagation(); 79 | props.onUpdated?.(group); 80 | }}/> 81 | 82 | 83 |
84 |
85 |
86 | ) 87 | 88 | return selected ? ( 89 |
91 | {Element} 92 |
93 | ) : ( 94 |
96 | {Element} 97 |
98 | ) 99 | } -------------------------------------------------------------------------------- /src/components/Chat/ModelSelect.tsx: -------------------------------------------------------------------------------- 1 | import { OpenAIModel } from '@/types/openai'; 2 | import { useTranslation } from 'next-i18next'; 3 | import { FC } from 'react'; 4 | 5 | interface Props { 6 | model: OpenAIModel; 7 | models: OpenAIModel[]; 8 | onModelChange: (model: OpenAIModel) => void; 9 | } 10 | 11 | export const ModelSelect: FC = ({ model, models, onModelChange }) => { 12 | const { t } = useTranslation('chat'); 13 | return ( 14 |
15 | 18 |
19 | 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/Chat/PromptList.tsx: -------------------------------------------------------------------------------- 1 | import { Prompt } from '@/types/prompt'; 2 | import { FC, MutableRefObject } from 'react'; 3 | 4 | interface Props { 5 | prompts: Prompt[]; 6 | activePromptIndex: number; 7 | onSelect: () => void; 8 | onMouseOver: (index: number) => void; 9 | promptListRef: MutableRefObject; 10 | } 11 | 12 | export const PromptList: FC = ({ 13 | prompts, 14 | activePromptIndex, 15 | onSelect, 16 | onMouseOver, 17 | promptListRef, 18 | }) => { 19 | return ( 20 |
    30 | {prompts.map((prompt, index) => ( 31 |
  • { 39 | e.preventDefault(); 40 | e.stopPropagation(); 41 | onSelect(); 42 | }} 43 | onMouseEnter={() => onMouseOver(index)} 44 | > 45 | {prompt.name} 46 |
  • 47 | ))} 48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/Chat/Regenerate.tsx: -------------------------------------------------------------------------------- 1 | import { IconRefresh } from '@tabler/icons-react'; 2 | import { useTranslation } from 'next-i18next'; 3 | import { FC } from 'react'; 4 | 5 | interface Props { 6 | onRegenerate: () => void; 7 | } 8 | 9 | export const Regenerate: FC = ({ onRegenerate }) => { 10 | const { t } = useTranslation('chat'); 11 | return ( 12 |
13 |
14 | {t('Sorry, there was an error.')} 15 |
16 | 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Chat/SystemPrompt.tsx: -------------------------------------------------------------------------------- 1 | import { Conversation } from '@/types/chat'; 2 | import { OpenAIModelID } from '@/types/openai'; 3 | import { Prompt } from '@/types/prompt'; 4 | import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const'; 5 | import { useTranslation } from 'next-i18next'; 6 | import { 7 | FC, 8 | KeyboardEvent, 9 | useCallback, 10 | useEffect, 11 | useRef, 12 | useState, 13 | } from 'react'; 14 | import { PromptList } from './PromptList'; 15 | import { VariableModal } from './VariableModal'; 16 | 17 | interface Props { 18 | conversation: Conversation; 19 | prompts: Prompt[]; 20 | onChangePrompt: (prompt: string) => void; 21 | } 22 | 23 | export const SystemPrompt: FC = ({ 24 | conversation, 25 | prompts, 26 | onChangePrompt, 27 | }) => { 28 | const { t } = useTranslation('chat'); 29 | 30 | const [value, setValue] = useState(''); 31 | const [activePromptIndex, setActivePromptIndex] = useState(0); 32 | const [showPromptList, setShowPromptList] = useState(false); 33 | const [promptInputValue, setPromptInputValue] = useState(''); 34 | const [variables, setVariables] = useState([]); 35 | const [isModalVisible, setIsModalVisible] = useState(false); 36 | 37 | const textareaRef = useRef(null); 38 | const promptListRef = useRef(null); 39 | 40 | const filteredPrompts = prompts.filter((prompt) => 41 | prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()), 42 | ); 43 | 44 | const handleChange = (e: React.ChangeEvent) => { 45 | const value = e.target.value; 46 | const maxLength = 47 | conversation.model.id === OpenAIModelID.GPT_3_5 ? 12000 : 24000; 48 | 49 | if (value.length > maxLength) { 50 | alert( 51 | t( 52 | `Prompt limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`, 53 | { maxLength, valueLength: value.length }, 54 | ), 55 | ); 56 | return; 57 | } 58 | 59 | setValue(value); 60 | updatePromptListVisibility(value); 61 | 62 | if (value.length > 0) { 63 | onChangePrompt(value); 64 | } 65 | }; 66 | 67 | const handleInitModal = () => { 68 | const selectedPrompt = filteredPrompts[activePromptIndex]; 69 | setValue((prevVal) => { 70 | const newContent = prevVal?.replace(/\/\w*$/, selectedPrompt.content); 71 | return newContent; 72 | }); 73 | handlePromptSelect(selectedPrompt); 74 | setShowPromptList(false); 75 | }; 76 | 77 | const parseVariables = (content: string) => { 78 | const regex = /{{(.*?)}}/g; 79 | const foundVariables = []; 80 | let match; 81 | 82 | while ((match = regex.exec(content)) !== null) { 83 | foundVariables.push(match[1]); 84 | } 85 | 86 | return foundVariables; 87 | }; 88 | 89 | const updatePromptListVisibility = useCallback((text: string) => { 90 | const match = text.match(/\/\w*$/); 91 | 92 | if (match) { 93 | setShowPromptList(true); 94 | setPromptInputValue(match[0].slice(1)); 95 | } else { 96 | setShowPromptList(false); 97 | setPromptInputValue(''); 98 | } 99 | }, []); 100 | 101 | const handlePromptSelect = (prompt: Prompt) => { 102 | const parsedVariables = parseVariables(prompt.content); 103 | setVariables(parsedVariables); 104 | 105 | if (parsedVariables.length > 0) { 106 | setIsModalVisible(true); 107 | } else { 108 | const updatedContent = value?.replace(/\/\w*$/, prompt.content); 109 | 110 | setValue(updatedContent); 111 | onChangePrompt(updatedContent); 112 | 113 | updatePromptListVisibility(prompt.content); 114 | } 115 | }; 116 | 117 | const handleSubmit = (updatedVariables: string[]) => { 118 | const newContent = value?.replace(/{{(.*?)}}/g, (match, variable) => { 119 | const index = variables.indexOf(variable); 120 | return updatedVariables[index]; 121 | }); 122 | 123 | setValue(newContent); 124 | onChangePrompt(newContent); 125 | 126 | if (textareaRef && textareaRef.current) { 127 | textareaRef.current.focus(); 128 | } 129 | }; 130 | 131 | const handleKeyDown = (e: KeyboardEvent) => { 132 | if (showPromptList) { 133 | if (e.key === 'ArrowDown') { 134 | e.preventDefault(); 135 | setActivePromptIndex((prevIndex) => 136 | prevIndex < prompts.length - 1 ? prevIndex + 1 : prevIndex, 137 | ); 138 | } else if (e.key === 'ArrowUp') { 139 | e.preventDefault(); 140 | setActivePromptIndex((prevIndex) => 141 | prevIndex > 0 ? prevIndex - 1 : prevIndex, 142 | ); 143 | } else if (e.key === 'Tab') { 144 | e.preventDefault(); 145 | setActivePromptIndex((prevIndex) => 146 | prevIndex < prompts.length - 1 ? prevIndex + 1 : 0, 147 | ); 148 | } else if (e.key === 'Enter') { 149 | e.preventDefault(); 150 | handleInitModal(); 151 | } else if (e.key === 'Escape') { 152 | e.preventDefault(); 153 | setShowPromptList(false); 154 | } else { 155 | setActivePromptIndex(0); 156 | } 157 | } 158 | }; 159 | 160 | useEffect(() => { 161 | if (textareaRef && textareaRef.current) { 162 | textareaRef.current.style.height = 'inherit'; 163 | textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`; 164 | } 165 | }, [value]); 166 | 167 | useEffect(() => { 168 | if (conversation.prompt) { 169 | setValue(conversation.prompt); 170 | } else { 171 | setValue(DEFAULT_SYSTEM_PROMPT); 172 | } 173 | }, [conversation]); 174 | 175 | useEffect(() => { 176 | const handleOutsideClick = (e: MouseEvent) => { 177 | if ( 178 | promptListRef.current && 179 | !promptListRef.current.contains(e.target as Node) 180 | ) { 181 | setShowPromptList(false); 182 | } 183 | }; 184 | 185 | window.addEventListener('click', handleOutsideClick); 186 | 187 | return () => { 188 | window.removeEventListener('click', handleOutsideClick); 189 | }; 190 | }, []); 191 | 192 | return ( 193 |
194 | 197 |