Skip to content

Commit 038d316

Browse files
srtaalejzimegmwbrooks
authored
feat: showcase thinking steps (#98)
Co-authored-by: Eden Zimbelman <eden.zimbelman@salesforce.com> Co-authored-by: Michael Brooks <mbrooks@slack-corp.com>
1 parent 8c12cde commit 038d316

8 files changed

Lines changed: 330 additions & 186 deletions

File tree

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Models from [OpenAI](https://openai.com) are used and can be customized for prom
66

77
## Setup
88

9-
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).
9+
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).
1010

1111
### Developer Program
1212

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

1717
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.
1818

19-
### Using Slack CLI
20-
19+
<details><summary><strong> Using Slack CLI </strong></summary>
2120
Install the latest version of the Slack CLI for your operating system:
2221

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

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

49-
### Using Terminal
49+
<details><summary><strong> Using Terminal </strong></summary>
5050

51+
#### Create Your Slack App
5152
1. Open [https://api.slack.com/apps/new](https://api.slack.com/apps/new) and choose "From an app manifest"
5253
2. Choose the workspace you want to install the application to
5354
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_
@@ -83,6 +84,7 @@ cd my-bolt-js-assistant
8384
```sh
8485
npm install
8586
```
87+
</details>
8688

8789
## Providers
8890

@@ -110,6 +112,8 @@ slack run
110112
npm start
111113
```
112114

115+
Start talking to the bot! Start a new DM or thread and click the feedback button when it responds.
116+
113117
### Linting
114118

115119
```zsh
@@ -138,6 +142,8 @@ Configures the new Slack Assistant features, providing a dedicated side panel UI
138142
- The `assistant_thread_started.js` file, which responds to new app threads with a list of suggested prompts.
139143
- 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.
140144

141-
### `/ai`
145+
### `/agent`
146+
147+
The `llm-caller.js` file calls the OpenAI API and streams the generated response into a Slack conversation.
142148

143-
The `index.js` file handles the OpenAI API initialization and configuration.
149+
The `tools` directory contains app-specific functions for the LLM to call.

agent/llm-caller.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { OpenAI } from 'openai';
2+
import { rollDice, rollDiceDefinition } from './tools/dice.js';
3+
4+
// OpenAI LLM client
5+
const openai = new OpenAI({
6+
apiKey: process.env.OPENAI_API_KEY,
7+
});
8+
9+
/**
10+
* Stream an LLM response to prompts with an example dice rolling function
11+
*
12+
* @param {import("@slack/web-api").ChatStreamer} streamer - Slack chat stream
13+
* @param {Array} prompts - OpenAI ResponseInputParam messages
14+
*
15+
* @see {@link https://docs.slack.dev/tools/bolt-js/web#sending-streaming-messages}
16+
* @see {@link https://platform.openai.com/docs/guides/text}
17+
* @see {@link https://platform.openai.com/docs/guides/streaming-responses}
18+
* @see {@link https://platform.openai.com/docs/guides/function-calling}
19+
*/
20+
export async function callLLM(streamer, prompts) {
21+
const toolCalls = [];
22+
23+
const response = await openai.responses.create({
24+
model: 'gpt-4o-mini',
25+
input: prompts,
26+
tools: [rollDiceDefinition],
27+
tool_choice: 'auto',
28+
stream: true,
29+
});
30+
31+
for await (const event of response) {
32+
// Stream markdown text from the LLM response as it arrives
33+
if (event.type === 'response.output_text.delta' && event.delta) {
34+
await streamer.append({
35+
markdown_text: event.delta,
36+
});
37+
}
38+
39+
// Save function calls for later computation and a new task is shown
40+
if (event.type === 'response.output_item.done') {
41+
if (event.item.type === 'function_call') {
42+
toolCalls.push(event.item);
43+
44+
if (event.item.name === 'roll_dice') {
45+
const args = JSON.parse(event.item.arguments);
46+
await streamer.append({
47+
chunks: [
48+
{
49+
type: 'task_update',
50+
id: event.item.call_id,
51+
title: `Rolling a ${args.count}d${args.sides}...`,
52+
status: 'in_progress',
53+
},
54+
],
55+
});
56+
}
57+
}
58+
}
59+
}
60+
61+
// Perform tool calls and marks tasks as completed
62+
if (toolCalls.length > 0) {
63+
for (const call of toolCalls) {
64+
if (call.name === 'roll_dice') {
65+
const args = JSON.parse(call.arguments);
66+
67+
prompts.push({
68+
id: call.id,
69+
call_id: call.call_id,
70+
type: 'function_call',
71+
name: 'roll_dice',
72+
arguments: call.arguments,
73+
});
74+
75+
const result = rollDice(args);
76+
77+
prompts.push({
78+
type: 'function_call_output',
79+
call_id: call.call_id,
80+
output: JSON.stringify(result),
81+
});
82+
83+
if (result.error != null) {
84+
await streamer.append({
85+
chunks: [
86+
{
87+
type: 'task_update',
88+
id: call.call_id,
89+
title: result.error,
90+
status: 'error',
91+
},
92+
],
93+
});
94+
} else {
95+
await streamer.append({
96+
chunks: [
97+
{
98+
type: 'task_update',
99+
id: call.call_id,
100+
title: result.description,
101+
status: 'complete',
102+
},
103+
],
104+
});
105+
}
106+
}
107+
}
108+
109+
// complete the llm response after making tool calls
110+
await callLLM(streamer, prompts);
111+
}
112+
}

agent/tools/dice.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Roll one or more dice with a specified number of sides.
3+
*
4+
* @param {Object} options - The roll options
5+
* @param {number} [options.sides=6] - The number of sides on the die
6+
* @param {number} [options.count=1] - The number of dice to roll
7+
* @returns {Object} The roll results with rolls array, total, and description
8+
*/
9+
export function rollDice({ sides = 6, count = 1 } = {}) {
10+
if (sides < 2) {
11+
return {
12+
error: 'A die must have at least 2 sides',
13+
rolls: [],
14+
total: 0,
15+
};
16+
}
17+
18+
if (count < 1) {
19+
return {
20+
error: 'Must roll at least 1 die',
21+
rolls: [],
22+
total: 0,
23+
};
24+
}
25+
26+
const rolls = Array.from({ length: count }, () => Math.floor(Math.random() * sides) + 1);
27+
const total = rolls.reduce((sum, roll) => sum + roll, 0);
28+
29+
return {
30+
rolls,
31+
total,
32+
description: `Rolled a ${count}d${sides} to total ${total}`,
33+
};
34+
}
35+
36+
/**
37+
* Tool definition for OpenAI API
38+
*
39+
* @type {import('openai/resources/responses/responses').Tool}
40+
* @see {@link https://platform.openai.com/docs/guides/function-calling}
41+
*/
42+
export const rollDiceDefinition = {
43+
type: 'function',
44+
name: 'roll_dice',
45+
description:
46+
'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.',
47+
parameters: {
48+
type: 'object',
49+
properties: {
50+
sides: {
51+
type: 'integer',
52+
description: 'The number of sides on the die (e.g., 6 for a standard die, 20 for a d20)',
53+
default: 6,
54+
},
55+
count: {
56+
type: 'integer',
57+
description: 'The number of dice to roll',
58+
default: 1,
59+
},
60+
},
61+
required: ['sides', 'count'],
62+
},
63+
strict: false,
64+
};

ai/index.js

Lines changed: 0 additions & 13 deletions
This file was deleted.

listeners/assistant/assistant_thread_started.js

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,29 +40,12 @@ export const assistantThreadStarted = async ({ event, logger, say, setSuggestedP
4040
title: 'Start with this suggested prompt:',
4141
prompts: [
4242
{
43-
title: 'This is a suggested prompt',
44-
message:
45-
'When a user clicks a prompt, the resulting prompt message text ' +
46-
'can be passed directly to your LLM for processing.\n\n' +
47-
'Assistant, please create some helpful prompts I can provide to ' +
48-
'my users.',
43+
title: 'Prompt a task with thinking steps',
44+
message: 'Wonder a few deep thoughts.',
4945
},
50-
],
51-
});
52-
}
53-
54-
/**
55-
* If the user opens the Assistant container in a channel, additional
56-
* context is available. This can be used to provide conditional prompts
57-
* that only make sense to appear in that context.
58-
*/
59-
if (context.channel_id) {
60-
await setSuggestedPrompts({
61-
title: 'Perform an action based on the channel',
62-
prompts: [
6346
{
64-
title: 'Summarize channel',
65-
message: 'Assistant, please summarize the activity in this channel!',
47+
title: 'Roll dice for a random number',
48+
message: 'Roll two 12-sided dice and three 6-sided dice for a pseudo-random score.',
6649
},
6750
],
6851
});

0 commit comments

Comments
 (0)