diff --git a/.github/workflows/discord-bot_discord-app.yml b/.github/workflows/discord-bot_discord-app.yml new file mode 100644 index 00000000..2e495c5a --- /dev/null +++ b/.github/workflows/discord-bot_discord-app.yml @@ -0,0 +1,74 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - discord-app + +on: + push: + branches: + - discord-bot + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: '18.x' + + - name: npm install, build, and test + run: | + cd discord + npm install + npm run build --if-present + npm run test --if-present + + - name: Zip artifact for deployment + run: | + cd discord + zip -r ../release.zip ./* + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write #This is required for requesting the JWT + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_F79B5525A24E4DD1B0474AE8E88AC734 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_82993AB6E0F44B589C524B05273985DB }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_BD59F4177F7646289B41D15D6119C77B }} + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'discord-app' + slot-name: 'Production' + package: . + \ No newline at end of file diff --git a/discord/.gitignore b/discord/.gitignore new file mode 100644 index 00000000..b21c85fd --- /dev/null +++ b/discord/.gitignore @@ -0,0 +1,3 @@ +.env +./node_modules/ +package-lock.json \ No newline at end of file diff --git a/discord/index.js b/discord/index.js new file mode 100644 index 00000000..2533e4e9 --- /dev/null +++ b/discord/index.js @@ -0,0 +1,196 @@ +require('dotenv').config(); +const { Client, GatewayIntentBits, ChannelType } = require('discord.js'); +const express = require('express'); +const app = express(); +const port = process.env.PORT || 3000; + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMessageReactions, + GatewayIntentBits.GuildMembers + ] +}); + +// Parse the GUILD_AGENT_MAP from the environment variable +const guildAgentMap = JSON.parse(process.env.GUILD_AGENT_MAP); + +client.once('ready', () => { + console.log(`Logged in as ${client.user.tag}!`); +}); + +client.on('messageCreate', async message => { + console.log('message received'); + + // Ignore messages from the bot itself + if (message.author.bot) return; + + // Check if the bot is tagged in the message + if (message.mentions.has(client.user)) { + const fetch = (await import('node-fetch')).default; + + // Function to split message into chunks of 2000 characters or less + function splitMessage(message, maxLength = 2000) { + if (message.length <= maxLength) return [message]; + const parts = []; + let currentPart = ''; + let inCodeBlock = false; + + const lines = message.split('\n'); + for (const line of lines) { + if (line.startsWith('```')) { + inCodeBlock = !inCodeBlock; + } + + if (currentPart.length + line.length + 1 > maxLength) { + if (inCodeBlock) { + currentPart += '\n```'; + parts.push(currentPart); + currentPart = '```'; + } else { + parts.push(currentPart); + currentPart = ''; + } + } + + currentPart += (currentPart.length > 0 ? '\n' : '') + line; + + if (!inCodeBlock && currentPart.length >= maxLength) { + parts.push(currentPart); + currentPart = ''; + } + } + + if (currentPart.length > 0) { + parts.push(currentPart); + } + + return parts; + } + + // Function to keep typing indicator active + function keepTyping(channel) { + const interval = setInterval(() => { + channel.sendTyping(); + }, 5000); // Typing indicator lasts for 10 seconds, so we refresh it every 5 seconds + return interval; + } + + // Function to stop typing indicator + function stopTyping(interval) { + clearInterval(interval); + } + + // Determine the channel (thread or main channel) + let channel; + if (message.channel.type === ChannelType.PublicThread || message.channel.type === ChannelType.PrivateThread) { + channel = message.channel + } else { + channel = await message.startThread({ + name: `Thread for ${message.author.username}`, + autoArchiveDuration: 60, + }); + + // await channel.send("Hold tight, I'm preparing your answer!\n\nQuick tip ⚡️, I can help you better from your IDE. Install the [VSCode extension](https://marketplace.visualstudio.com/items?itemName=WelltestedAI.fluttergpt)"); + } + + // Start typing indicator + const typingInterval = keepTyping(channel); + + // Fetch agent details + const guildId = message.guild.id; + const agentName = guildAgentMap[guildId]; + if (!agentName) { + channel.send('Sorry, I could not find the agent for this guild.'); + stopTyping(typingInterval); // Stop typing indicator + return; + } + + let agentDetails; + try { + const response = await fetch("https://api.commanddash.dev/agent/get-latest-agent", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name: agentName }), + }); + + if (!response.ok) { + throw new Error(`Failed to load the agent: ${await response.json()}`); + } + + agentDetails = await response.json(); + console.log(agentDetails) + } catch (error) { + console.error('Error fetching agent details:', error); + channel.send('Sorry, I could not fetch the agent details.'); + stopTyping(typingInterval); // Stop typing indicator + return; + } + + try { + // Fetch all messages in the thread if it's a thread + let history = []; + history.push({ "role": "user", "text": "This conversation is happening on Discord, so please keep response concise and quote snippets only when necessary (unless of course explicity requested) " }); + if (channel.type === ChannelType.PublicThread || channel.type === ChannelType.PrivateThread) { + const messages = await channel.messages.fetch({ limit: 100 }); + history = messages.map(msg => ({ + "role": msg.author.id === client.user.id ? "model" : "user", + "text": msg.content + })); + } + + history.push({ "role": "user", "text": message.content }); + + // Prepare data for agent answer API + const agentData = { + agent_name: agentDetails.name, + agent_version: agentDetails.version, + chat_history: history, + included_references: [], + private: agentDetails.testing, + }; + + // Get answer from agent + const response = await fetch("https://api.commanddash.dev/v2/ai/agent/answer", { + method: "POST", + body: JSON.stringify(agentData), + headers: { + "Content-Type": "application/json", + }, + }); + const modelResponse = await response.json(); + console.log(modelResponse); + + // Split the response into chunks and send each chunk + const responseChunks = splitMessage(modelResponse.response); + for (const chunk of responseChunks) { + await channel.send(chunk); + } + } catch (error) { + console.error('Error getting answer from agent:', error); + channel.send('Sorry, I could not get an answer from the agent.'); + } finally { + stopTyping(typingInterval); // Stop typing indicator + } + } +}); + +// Handle errors to prevent the bot from crashing +client.on('error', error => { + console.error('An error occurred:', error); +}); + +client.login(process.env.DISCORD_TOKEN); + +// API endpoint to return the Discord token +app.get('/api/token', (req, res) => { + res.json({ token: process.env.DISCORD_TOKEN }); +}); + +app.listen(port, () => { + console.log(`Server is running on port ${port}`); +}); \ No newline at end of file diff --git a/discord/package.json b/discord/package.json new file mode 100644 index 00000000..3e73e7c1 --- /dev/null +++ b/discord/package.json @@ -0,0 +1,18 @@ +{ + "name": "discord", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "discord.js": "^14.15.3", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "node-fetch": "^3.3.2" + } +} diff --git a/discord/web.config b/discord/web.config new file mode 100644 index 00000000..8113473f --- /dev/null +++ b/discord/web.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/readme.md b/readme.md index a3823c13..ad80b5fc 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,8 @@

CommandDash

-[![VScode Downloads](https://img.shields.io/visual-studio-marketplace/d/WelltestedAI.fluttergpt)](https://marketplace.visualstudio.com/items?itemName=WelltestedAI.fluttergpt&ssr=false#overview) [![License: APACHE](https://img.shields.io/badge/License-APACHE%202.0-yellow)](/LICENSE) +[![VScode Downloads](https://img.shields.io/visual-studio-marketplace/d/WelltestedAI.fluttergpt?style=for-the-badge)](https://marketplace.visualstudio.com/items?itemName=WelltestedAI.fluttergpt&ssr=false#overview) [![License: APACHE](https://img.shields.io/badge/License-APACHE%202.0-yellow?style=for-the-badge)](/LICENSE) +

Integrate APIs, SDKs or Packages with AI Agents

diff --git a/vscode/media/agent-ui-builder/agent-ui-builder.js b/vscode/media/agent-ui-builder/agent-ui-builder.js index a8c3df08..90f3c52a 100644 --- a/vscode/media/agent-ui-builder/agent-ui-builder.js +++ b/vscode/media/agent-ui-builder/agent-ui-builder.js @@ -19,7 +19,7 @@ class AgentUIBuilder { textHtml = textHtml.replace(`<${input.id}>`, inputElement.outerHTML); }); - activeCommandsAttach.textContent = `${slug}`; + // activeCommandsAttach.textContent = `${slug}`; this.container.innerHTML = `${textHtml}`; this.ref.appendChild(this.container); this.registerCodeInputListener(); diff --git a/vscode/media/command-deck/command-deck.js b/vscode/media/command-deck/command-deck.js index b4e5afe9..23feeec0 100644 --- a/vscode/media/command-deck/command-deck.js +++ b/vscode/media/command-deck/command-deck.js @@ -133,12 +133,19 @@ class CommandDeck { this.ref.innerHTML = textContent.substring(0, atIndex) + textContent.substring(atIndex + 1); } if (option?.name.startsWith('@')) { + console.log('agents options', option?.metadata); activeAgentAttach.style = "color: #497BEF; !important"; - activeAgentAttach.textContent = `@${option?.metadata.display_name}`; + + agentName = option?.metadata.display_name; + headerLogo.src = option.metadata.avatar_id; + headerText.classList.add("hidden"); + headerAgentName.classList.remove("hidden"); + headerAgentName.textContent = option?.metadata.display_name; + headerAgentDescription.classList.remove("hidden"); + headerAgentDescription.textContent = option?.metadata.description; activeAgent = true; commandEnable = false; - activeCommandsAttach.style = "color: var(--vscode-input-placeholderForeground); !important"; - activeCommandsAttach.textContent = "/"; + currentActiveAgent = option.name; this.closeMenu(); // Move the cursor to the end of the word @@ -164,7 +171,7 @@ class CommandDeck { setTimeout(() => { adjustHeight(); commandEnable = true; - activeCommandsAttach.style = "color: rgb(236 72 153); !important"; + // activeCommandsAttach.style = "color: rgb(236 72 153); !important"; }, 0); } }; @@ -247,10 +254,10 @@ class CommandDeck { setTimeout(() => { if (this.ref.textContent.trim() === "") { commandEnable = false; - activeCommandsAttach.style = "color: var(--vscode-input-placeholderForeground); !important"; + // activeCommandsAttach.style = "color: var(--vscode-input-placeholderForeground); !important"; agentInputsJson.length = 0; codeInputId = 0; - activeCommandsAttach.textContent = "/"; + // activeCommandsAttach.textContent = "/"; } }, 0); } diff --git a/vscode/media/market-place/market-place.html b/vscode/media/market-place/market-place.html index e70a5098..48d53ae5 100644 --- a/vscode/media/market-place/market-place.html +++ b/vscode/media/market-place/market-place.html @@ -30,10 +30,14 @@ - + + + + Create new agent + diff --git a/vscode/media/market-place/market-place.js b/vscode/media/market-place/market-place.js index dadea02a..c0c39636 100644 --- a/vscode/media/market-place/market-place.js +++ b/vscode/media/market-place/market-place.js @@ -132,7 +132,7 @@ function renderAgentsList(_agents) { // topDiv.appendChild(installContainer); const pDescription = document.createElement("p"); - pDescription.className = "text-xs truncate text-gray-500 my-1 description"; + pDescription.className = "text-xs truncate text-gray-400 my-1 description"; pDescription.style.color = "rgb(148 163 184)"; pDescription.style.marginTop = "0.5rem"; pDescription.style.marginBottom = "0.5rem"; @@ -157,7 +157,7 @@ function renderAgentsList(_agents) { divRowInner.className = "inline-flex flex-row"; const spanAuthor = document.createElement("span"); - spanAuthor.className = "text-xs text-gray-500 px-1 border-b border-gray-500"; + spanAuthor.className = "text-xs text-gray-400 px-1 border-b border-gray-500"; spanAuthor.innerHTML = `${agent.author.github_id}`; const divAuthor = document.createElement("div"); diff --git a/vscode/media/onboarding/onboarding.html b/vscode/media/onboarding/onboarding.html index 437d8402..b512aef6 100644 --- a/vscode/media/onboarding/onboarding.html +++ b/vscode/media/onboarding/onboarding.html @@ -19,20 +19,25 @@
-
- +
-
+
@@ -90,14 +96,14 @@

# Ask Dash

-

- @ + Type "@" to switch agent

-

/ -

+

-->