Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Models from [OpenAI](https://openai.com) are used and can be customized for prom

## Setup
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note - still deciding how to restructure README.md to make setup easier. I think the most obvious point of confusion is deciding whether to use CLI or terminal to setup, i think a toggle like

Details
might make things simpler 🤔 ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@srtaalej Thanks for calling this out! 📚 ✨ I agree it's a confusing and hope we can encourage the CLI most-

IIRC @lukegalbraithrussell had thoughts on related changes as well and we should match such in slack-samples/bolt-python-assistant-template#42 too!


Before getting started, make sure you have a development workspace where you have permissions to install apps. If you dont have one setup, go ahead and [create one](https://slack.com/create).
Before getting started, make sure you have a development workspace where you have permissions to install apps. If you don't have one setup, go ahead and [create one](https://slack.com/create).

### Developer Program

Expand All @@ -16,8 +16,7 @@ Join the [Slack Developer Program](https://api.slack.com/developer-program) for

Add this app to your workspace using either the Slack CLI or other development tooling, then read ahead to configuring LLM responses in the **[Providers](#providers)** section.

### Using Slack CLI

<details><summary><strong> Using Slack CLI </strong></summary>
Install the latest version of the Slack CLI for your operating system:

- [Slack CLI for macOS & Linux](https://docs.slack.dev/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux/)
Expand Down Expand Up @@ -45,9 +44,11 @@ slack install
```

After the Slack app has been created you're all set to configure the LLM provider!
</details>

### Using Terminal
<details><summary><strong> Using Terminal </strong></summary>

#### Create Your Slack App
1. Open [https://api.slack.com/apps/new](https://api.slack.com/apps/new) and choose "From an app manifest"
2. Choose the workspace you want to install the application to
3. Copy the contents of [manifest.json](./manifest.json) into the text box that says `*Paste your manifest code here*` (within the JSON tab) and click _Next_
Expand Down Expand Up @@ -83,6 +84,7 @@ cd my-bolt-js-assistant
```sh
npm install
```
</details>

## Providers

Expand Down Expand Up @@ -110,6 +112,8 @@ slack run
npm start
```

Start talking to the bot! Start a new DM or thread and click the feedback button when it responds.

### Linting

```zsh
Expand Down Expand Up @@ -138,6 +142,8 @@ Configures the new Slack Assistant features, providing a dedicated side panel UI
- The `assistant_thread_started.js` file, which responds to new app threads with a list of suggested prompts.
- The `message.js` file, which responds to user messages sent to app threads or from the **Chat** and **History** tab with an LLM generated response.

### `/ai`
### `/agent`

The `llm-caller.js` file calls the OpenAI API and streams the generated response into a Slack conversation.

The `index.js` file handles the OpenAI API initialization and configuration.
The `tools` directory contains app-specific functions for the LLM to call.
112 changes: 112 additions & 0 deletions agent/llm-caller.js
Comment thread
srtaalej marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { OpenAI } from 'openai';
import { rollDice, rollDiceDefinition } from './tools/dice.js';

// OpenAI LLM client
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
Comment on lines +4 to +7
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌚 question: Is the openai client still used outside of this package, or could we remove the export here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! it is not


/**
* Stream an LLM response to prompts with an example dice rolling function
*
* @param {import("@slack/web-api").ChatStreamer} streamer - Slack chat stream

Check failure on line 12 in agent/llm-caller.js

View workflow job for this annotation

GitHub Actions / build (22.x)

Namespace '"/home/runner/work/bolt-js-assistant-template/bolt-js-assistant-template/node_modules/@slack/web-api/dist/index"' has no exported member 'ChatStreamer'.

Check failure on line 12 in agent/llm-caller.js

View workflow job for this annotation

GitHub Actions / build (20.x)

Namespace '"/home/runner/work/bolt-js-assistant-template/bolt-js-assistant-template/node_modules/@slack/web-api/dist/index"' has no exported member 'ChatStreamer'.

Check failure on line 12 in agent/llm-caller.js

View workflow job for this annotation

GitHub Actions / build (18.x)

Namespace '"/home/runner/work/bolt-js-assistant-template/bolt-js-assistant-template/node_modules/@slack/web-api/dist/index"' has no exported member 'ChatStreamer'.
Comment thread
srtaalej marked this conversation as resolved.
* @param {Array} prompts - OpenAI ResponseInputParam messages
*
* @see {@link https://docs.slack.dev/tools/bolt-js/web#sending-streaming-messages}
* @see {@link https://platform.openai.com/docs/guides/text}
* @see {@link https://platform.openai.com/docs/guides/streaming-responses}
* @see {@link https://platform.openai.com/docs/guides/function-calling}
*/
export async function callLLM(streamer, prompts) {
const toolCalls = [];

const response = await openai.responses.create({
model: 'gpt-4o-mini',
input: prompts,
tools: [rollDiceDefinition],
tool_choice: 'auto',
stream: true,
});

for await (const event of response) {
// Stream markdown text from the LLM response as it arrives
if (event.type === 'response.output_text.delta' && event.delta) {
await streamer.append({
markdown_text: event.delta,
});
}

// Save function calls for later computation and a new task is shown
if (event.type === 'response.output_item.done') {
if (event.item.type === 'function_call') {
toolCalls.push(event.item);

if (event.item.name === 'roll_dice') {
const args = JSON.parse(event.item.arguments);
await streamer.append({
chunks: [
{
type: 'task_update',
id: event.item.call_id,
title: `Rolling a ${args.count}d${args.sides}...`,
status: 'in_progress',
},
],
});
}
}
}
}

// Perform tool calls and marks tasks as completed
if (toolCalls.length > 0) {
for (const call of toolCalls) {
if (call.name === 'roll_dice') {
const args = JSON.parse(call.arguments);

prompts.push({
id: call.id,
call_id: call.call_id,
type: 'function_call',
name: 'roll_dice',
arguments: call.arguments,
});

const result = rollDice(args);

prompts.push({
type: 'function_call_output',
call_id: call.call_id,
output: JSON.stringify(result),
});

if (result.error != null) {
await streamer.append({
chunks: [
{
type: 'task_update',
id: call.call_id,
title: result.error,
status: 'error',
},
],
});
} else {
await streamer.append({
chunks: [
{
type: 'task_update',
id: call.call_id,
title: result.description,
status: 'complete',
},
],
});
}
}
}

// complete the llm response after making tool calls
await callLLM(streamer, prompts);
}
}
64 changes: 64 additions & 0 deletions agent/tools/dice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Roll one or more dice with a specified number of sides.
*
* @param {Object} options - The roll options
* @param {number} [options.sides=6] - The number of sides on the die
* @param {number} [options.count=1] - The number of dice to roll
* @returns {Object} The roll results with rolls array, total, and description
*/
export function rollDice({ sides = 6, count = 1 } = {}) {
if (sides < 2) {
return {
error: 'A die must have at least 2 sides',
rolls: [],
total: 0,
};
}

if (count < 1) {
return {
error: 'Must roll at least 1 die',
rolls: [],
total: 0,
};
}

const rolls = Array.from({ length: count }, () => Math.floor(Math.random() * sides) + 1);
const total = rolls.reduce((sum, roll) => sum + roll, 0);

return {
rolls,
total,
description: `Rolled a ${count}d${sides} to total ${total}`,
};
}

/**
* Tool definition for OpenAI API
*
* @type {import('openai/resources/responses/responses').Tool}
* @see {@link https://platform.openai.com/docs/guides/function-calling}
*/
export const rollDiceDefinition = {
Comment on lines +36 to +42
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🪬 note: Using jsdoc to match the expected type of the openai.responses.create arguments might be required since we're exporting from this package? I'm unsure what that type might be, but am hoping it'd address the error above!

type: 'function',
name: 'roll_dice',
description:
'Roll one or more dice with a specified number of sides. Use this when the user wants to roll dice or generate random numbers within a range.',
parameters: {
type: 'object',
properties: {
sides: {
type: 'integer',
description: 'The number of sides on the die (e.g., 6 for a standard die, 20 for a d20)',
default: 6,
},
count: {
type: 'integer',
description: 'The number of dice to roll',
default: 1,
},
},
required: ['sides', 'count'],
},
strict: false,
};
13 changes: 0 additions & 13 deletions ai/index.js

This file was deleted.

25 changes: 4 additions & 21 deletions listeners/assistant/assistant_thread_started.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,12 @@ export const assistantThreadStarted = async ({ event, logger, say, setSuggestedP
title: 'Start with this suggested prompt:',
prompts: [
{
title: 'This is a suggested prompt',
message:
'When a user clicks a prompt, the resulting prompt message text ' +
'can be passed directly to your LLM for processing.\n\n' +
'Assistant, please create some helpful prompts I can provide to ' +
'my users.',
title: 'Prompt a task with thinking steps',
message: 'Wonder a few deep thoughts.',
},
],
});
}

/**
* If the user opens the Assistant container in a channel, additional
* context is available. This can be used to provide conditional prompts
* that only make sense to appear in that context.
*/
if (context.channel_id) {
await setSuggestedPrompts({
title: 'Perform an action based on the channel',
prompts: [
{
title: 'Summarize channel',
message: 'Assistant, please summarize the activity in this channel!',
title: 'Roll dice for a random number',
message: 'Roll two 12-sided dice and three 6-sided dice for a pseudo-random score.',
},
],
});
Expand Down
Loading
Loading