@elizaos/plugin-twitch 2.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/integration.test.ts +889 -0
- package/build.ts +17 -0
- package/dist/index.js +1021 -0
- package/package.json +35 -0
- package/src/actions/joinChannel.ts +147 -0
- package/src/actions/leaveChannel.ts +172 -0
- package/src/actions/listChannels.ts +100 -0
- package/src/actions/sendMessage.ts +177 -0
- package/src/index.ts +117 -0
- package/src/providers/channelState.ts +98 -0
- package/src/providers/userContext.ts +117 -0
- package/src/service.ts +488 -0
- package/src/types.ts +340 -0
- package/tsconfig.json +21 -0
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elizaos/plugin-twitch",
|
|
3
|
+
"version": "2.0.0-alpha.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "bun run build.ts",
|
|
9
|
+
"test": "bun test"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@twurple/auth": "^7.2.0",
|
|
13
|
+
"@twurple/chat": "^7.2.0",
|
|
14
|
+
"zod": "^4.3.6"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@elizaos/core": "2.0.0-alpha.3"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/bun": "^1.1.0",
|
|
21
|
+
"typescript": "^5.3.0"
|
|
22
|
+
},
|
|
23
|
+
"milaidy": {
|
|
24
|
+
"platforms": [
|
|
25
|
+
"node"
|
|
26
|
+
],
|
|
27
|
+
"runtime": "node",
|
|
28
|
+
"platformDetails": {
|
|
29
|
+
"node": "Node.js via main entry point"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Join channel action for Twitch plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Action,
|
|
7
|
+
ActionResult,
|
|
8
|
+
IAgentRuntime,
|
|
9
|
+
Memory,
|
|
10
|
+
State,
|
|
11
|
+
} from "@elizaos/core";
|
|
12
|
+
import {
|
|
13
|
+
composePromptFromState,
|
|
14
|
+
ModelType,
|
|
15
|
+
parseJSONObjectFromText,
|
|
16
|
+
} from "@elizaos/core";
|
|
17
|
+
import type { TwitchService } from "../service.js";
|
|
18
|
+
import { normalizeChannel, TWITCH_SERVICE_NAME } from "../types.js";
|
|
19
|
+
|
|
20
|
+
const JOIN_CHANNEL_TEMPLATE = `You are helping to extract a Twitch channel name.
|
|
21
|
+
|
|
22
|
+
The user wants to join a Twitch channel.
|
|
23
|
+
|
|
24
|
+
Recent conversation:
|
|
25
|
+
{{recentMessages}}
|
|
26
|
+
|
|
27
|
+
Extract the channel name to join (without the # prefix).
|
|
28
|
+
|
|
29
|
+
Respond with a JSON object like:
|
|
30
|
+
{
|
|
31
|
+
"channel": "channelname"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Only respond with the JSON object, no other text.`;
|
|
35
|
+
|
|
36
|
+
export const joinChannel: Action = {
|
|
37
|
+
name: "TWITCH_JOIN_CHANNEL",
|
|
38
|
+
similes: ["JOIN_TWITCH_CHANNEL", "ENTER_CHANNEL", "CONNECT_CHANNEL"],
|
|
39
|
+
description: "Join a Twitch channel to listen and send messages",
|
|
40
|
+
|
|
41
|
+
validate: async (
|
|
42
|
+
_runtime: IAgentRuntime,
|
|
43
|
+
message: Memory,
|
|
44
|
+
_state?: State,
|
|
45
|
+
): Promise<boolean> => {
|
|
46
|
+
return message.content.source === "twitch";
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
handler: async (
|
|
50
|
+
runtime: IAgentRuntime,
|
|
51
|
+
message: Memory,
|
|
52
|
+
state?: State,
|
|
53
|
+
_options?: Record<string, unknown>,
|
|
54
|
+
callback?: (response: { text: string; source?: string }) => void,
|
|
55
|
+
): Promise<ActionResult> => {
|
|
56
|
+
const twitchService =
|
|
57
|
+
runtime.getService<TwitchService>(TWITCH_SERVICE_NAME);
|
|
58
|
+
|
|
59
|
+
if (!twitchService || !twitchService.isConnected()) {
|
|
60
|
+
if (callback) {
|
|
61
|
+
callback({
|
|
62
|
+
text: "Twitch service is not available.",
|
|
63
|
+
source: "twitch",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return { success: false, error: "Twitch service not available" };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Compose prompt
|
|
70
|
+
const currentState = state ?? (await runtime.composeState(message));
|
|
71
|
+
const prompt = await composePromptFromState({
|
|
72
|
+
template: JOIN_CHANNEL_TEMPLATE,
|
|
73
|
+
state: currentState,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Extract channel name using LLM
|
|
77
|
+
let channelName: string | null = null;
|
|
78
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
79
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
80
|
+
prompt,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const parsed = parseJSONObjectFromText(String(response));
|
|
84
|
+
if (parsed?.channel) {
|
|
85
|
+
channelName = normalizeChannel(String(parsed.channel));
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!channelName) {
|
|
91
|
+
if (callback) {
|
|
92
|
+
callback({
|
|
93
|
+
text: "I couldn't understand which channel you want me to join. Please specify the channel name.",
|
|
94
|
+
source: "twitch",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return { success: false, error: "Could not extract channel name" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if already joined
|
|
101
|
+
if (twitchService.getJoinedChannels().includes(channelName)) {
|
|
102
|
+
if (callback) {
|
|
103
|
+
callback({
|
|
104
|
+
text: `Already in channel #${channelName}.`,
|
|
105
|
+
source: "twitch",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
data: { channel: channelName, alreadyJoined: true },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Join channel
|
|
115
|
+
await twitchService.joinChannel(channelName);
|
|
116
|
+
|
|
117
|
+
if (callback) {
|
|
118
|
+
callback({
|
|
119
|
+
text: `Joined channel #${channelName}.`,
|
|
120
|
+
source: message.content.source as string,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
data: {
|
|
127
|
+
channel: channelName,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
examples: [
|
|
133
|
+
[
|
|
134
|
+
{
|
|
135
|
+
name: "{{user1}}",
|
|
136
|
+
content: { text: "Join the channel shroud" },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "{{agent}}",
|
|
140
|
+
content: {
|
|
141
|
+
text: "I'll join that channel.",
|
|
142
|
+
actions: ["TWITCH_JOIN_CHANNEL"],
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
],
|
|
147
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leave channel action for Twitch plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Action,
|
|
7
|
+
ActionResult,
|
|
8
|
+
IAgentRuntime,
|
|
9
|
+
Memory,
|
|
10
|
+
State,
|
|
11
|
+
} from "@elizaos/core";
|
|
12
|
+
import {
|
|
13
|
+
composePromptFromState,
|
|
14
|
+
ModelType,
|
|
15
|
+
parseJSONObjectFromText,
|
|
16
|
+
} from "@elizaos/core";
|
|
17
|
+
import type { TwitchService } from "../service.js";
|
|
18
|
+
import { normalizeChannel, TWITCH_SERVICE_NAME } from "../types.js";
|
|
19
|
+
|
|
20
|
+
const LEAVE_CHANNEL_TEMPLATE = `You are helping to extract a Twitch channel name.
|
|
21
|
+
|
|
22
|
+
The user wants to leave a Twitch channel.
|
|
23
|
+
|
|
24
|
+
Recent conversation:
|
|
25
|
+
{{recentMessages}}
|
|
26
|
+
|
|
27
|
+
Currently joined channels: {{joinedChannels}}
|
|
28
|
+
|
|
29
|
+
Extract the channel name to leave (without the # prefix).
|
|
30
|
+
|
|
31
|
+
Respond with a JSON object like:
|
|
32
|
+
{
|
|
33
|
+
"channel": "channelname"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Only respond with the JSON object, no other text.`;
|
|
37
|
+
|
|
38
|
+
export const leaveChannel: Action = {
|
|
39
|
+
name: "TWITCH_LEAVE_CHANNEL",
|
|
40
|
+
similes: [
|
|
41
|
+
"LEAVE_TWITCH_CHANNEL",
|
|
42
|
+
"EXIT_CHANNEL",
|
|
43
|
+
"PART_CHANNEL",
|
|
44
|
+
"DISCONNECT_CHANNEL",
|
|
45
|
+
],
|
|
46
|
+
description: "Leave a Twitch channel",
|
|
47
|
+
|
|
48
|
+
validate: async (
|
|
49
|
+
_runtime: IAgentRuntime,
|
|
50
|
+
message: Memory,
|
|
51
|
+
_state?: State,
|
|
52
|
+
): Promise<boolean> => {
|
|
53
|
+
return message.content.source === "twitch";
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
handler: async (
|
|
57
|
+
runtime: IAgentRuntime,
|
|
58
|
+
message: Memory,
|
|
59
|
+
state?: State,
|
|
60
|
+
_options?: Record<string, unknown>,
|
|
61
|
+
callback?: (response: { text: string; source?: string }) => void,
|
|
62
|
+
): Promise<ActionResult> => {
|
|
63
|
+
const twitchService =
|
|
64
|
+
runtime.getService<TwitchService>(TWITCH_SERVICE_NAME);
|
|
65
|
+
|
|
66
|
+
if (!twitchService || !twitchService.isConnected()) {
|
|
67
|
+
if (callback) {
|
|
68
|
+
callback({
|
|
69
|
+
text: "Twitch service is not available.",
|
|
70
|
+
source: "twitch",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return { success: false, error: "Twitch service not available" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const joinedChannels = twitchService.getJoinedChannels();
|
|
77
|
+
|
|
78
|
+
// Get or compose state
|
|
79
|
+
const currentState = state ?? (await runtime.composeState(message));
|
|
80
|
+
|
|
81
|
+
// Build state with joined channels
|
|
82
|
+
const enrichedState = {
|
|
83
|
+
...currentState,
|
|
84
|
+
joinedChannels: joinedChannels.join(", "),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Compose prompt
|
|
88
|
+
const prompt = await composePromptFromState({
|
|
89
|
+
template: LEAVE_CHANNEL_TEMPLATE,
|
|
90
|
+
state: enrichedState,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Extract channel name using LLM
|
|
94
|
+
let channelName: string | null = null;
|
|
95
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
96
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
97
|
+
prompt,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const parsed = parseJSONObjectFromText(String(response));
|
|
101
|
+
if (parsed?.channel) {
|
|
102
|
+
channelName = normalizeChannel(String(parsed.channel));
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!channelName) {
|
|
108
|
+
if (callback) {
|
|
109
|
+
callback({
|
|
110
|
+
text: "I couldn't understand which channel you want me to leave. Please specify the channel name.",
|
|
111
|
+
source: "twitch",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return { success: false, error: "Could not extract channel name" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check if we're in that channel
|
|
118
|
+
if (!joinedChannels.includes(channelName)) {
|
|
119
|
+
if (callback) {
|
|
120
|
+
callback({
|
|
121
|
+
text: `Not currently in channel #${channelName}.`,
|
|
122
|
+
source: "twitch",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return { success: false, error: "Not in that channel" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Prevent leaving primary channel
|
|
129
|
+
if (channelName === twitchService.getPrimaryChannel()) {
|
|
130
|
+
if (callback) {
|
|
131
|
+
callback({
|
|
132
|
+
text: `Cannot leave the primary channel #${channelName}.`,
|
|
133
|
+
source: "twitch",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return { success: false, error: "Cannot leave primary channel" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Leave channel
|
|
140
|
+
await twitchService.leaveChannel(channelName);
|
|
141
|
+
|
|
142
|
+
if (callback) {
|
|
143
|
+
callback({
|
|
144
|
+
text: `Left channel #${channelName}.`,
|
|
145
|
+
source: message.content.source as string,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
data: {
|
|
152
|
+
channel: channelName,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
examples: [
|
|
158
|
+
[
|
|
159
|
+
{
|
|
160
|
+
name: "{{user1}}",
|
|
161
|
+
content: { text: "Leave the channel shroud" },
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "{{agent}}",
|
|
165
|
+
content: {
|
|
166
|
+
text: "I'll leave that channel.",
|
|
167
|
+
actions: ["TWITCH_LEAVE_CHANNEL"],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
],
|
|
172
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List channels action for Twitch plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Action,
|
|
7
|
+
ActionResult,
|
|
8
|
+
IAgentRuntime,
|
|
9
|
+
Memory,
|
|
10
|
+
State,
|
|
11
|
+
} from "@elizaos/core";
|
|
12
|
+
import type { TwitchService } from "../service.js";
|
|
13
|
+
import { formatChannelForDisplay, TWITCH_SERVICE_NAME } from "../types.js";
|
|
14
|
+
|
|
15
|
+
export const listChannels: Action = {
|
|
16
|
+
name: "TWITCH_LIST_CHANNELS",
|
|
17
|
+
similes: [
|
|
18
|
+
"LIST_TWITCH_CHANNELS",
|
|
19
|
+
"SHOW_CHANNELS",
|
|
20
|
+
"GET_CHANNELS",
|
|
21
|
+
"CURRENT_CHANNELS",
|
|
22
|
+
],
|
|
23
|
+
description: "List all Twitch channels the bot is currently in",
|
|
24
|
+
|
|
25
|
+
validate: async (
|
|
26
|
+
_runtime: IAgentRuntime,
|
|
27
|
+
message: Memory,
|
|
28
|
+
_state?: State,
|
|
29
|
+
): Promise<boolean> => {
|
|
30
|
+
return message.content.source === "twitch";
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
handler: async (
|
|
34
|
+
runtime: IAgentRuntime,
|
|
35
|
+
message: Memory,
|
|
36
|
+
_state?: State,
|
|
37
|
+
_options?: Record<string, unknown>,
|
|
38
|
+
callback?: (response: { text: string; source?: string }) => void,
|
|
39
|
+
): Promise<ActionResult> => {
|
|
40
|
+
const twitchService =
|
|
41
|
+
runtime.getService<TwitchService>(TWITCH_SERVICE_NAME);
|
|
42
|
+
|
|
43
|
+
if (!twitchService || !twitchService.isConnected()) {
|
|
44
|
+
if (callback) {
|
|
45
|
+
callback({
|
|
46
|
+
text: "Twitch service is not available.",
|
|
47
|
+
source: "twitch",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return { success: false, error: "Twitch service not available" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const joinedChannels = twitchService.getJoinedChannels();
|
|
54
|
+
const primaryChannel = twitchService.getPrimaryChannel();
|
|
55
|
+
|
|
56
|
+
// Format channel list
|
|
57
|
+
const channelList = joinedChannels.map((channel) => {
|
|
58
|
+
const displayName = formatChannelForDisplay(channel);
|
|
59
|
+
const isPrimary = channel === primaryChannel;
|
|
60
|
+
return isPrimary ? `${displayName} (primary)` : displayName;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const responseText =
|
|
64
|
+
joinedChannels.length > 0
|
|
65
|
+
? `Currently in ${joinedChannels.length} channel(s):\n${channelList.map((c) => `• ${c}`).join("\n")}`
|
|
66
|
+
: "Not currently in any channels.";
|
|
67
|
+
|
|
68
|
+
if (callback) {
|
|
69
|
+
callback({
|
|
70
|
+
text: responseText,
|
|
71
|
+
source: message.content.source as string,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
data: {
|
|
78
|
+
channelCount: joinedChannels.length,
|
|
79
|
+
channels: joinedChannels,
|
|
80
|
+
primaryChannel,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
examples: [
|
|
86
|
+
[
|
|
87
|
+
{
|
|
88
|
+
name: "{{user1}}",
|
|
89
|
+
content: { text: "What channels are you in?" },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "{{agent}}",
|
|
93
|
+
content: {
|
|
94
|
+
text: "I'll list the channels I'm currently in.",
|
|
95
|
+
actions: ["TWITCH_LIST_CHANNELS"],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
],
|
|
100
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send message action for Twitch plugin.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
Action,
|
|
7
|
+
ActionResult,
|
|
8
|
+
IAgentRuntime,
|
|
9
|
+
Memory,
|
|
10
|
+
State,
|
|
11
|
+
} from "@elizaos/core";
|
|
12
|
+
import {
|
|
13
|
+
composePromptFromState,
|
|
14
|
+
ModelType,
|
|
15
|
+
parseJSONObjectFromText,
|
|
16
|
+
} from "@elizaos/core";
|
|
17
|
+
import type { TwitchService } from "../service.js";
|
|
18
|
+
import { normalizeChannel, TWITCH_SERVICE_NAME } from "../types.js";
|
|
19
|
+
|
|
20
|
+
const SEND_MESSAGE_TEMPLATE = `You are helping to extract send message parameters for Twitch chat.
|
|
21
|
+
|
|
22
|
+
The user wants to send a message to a Twitch channel.
|
|
23
|
+
|
|
24
|
+
Recent conversation:
|
|
25
|
+
{{recentMessages}}
|
|
26
|
+
|
|
27
|
+
Extract the following:
|
|
28
|
+
1. text: The message text to send
|
|
29
|
+
2. channel: The channel name to send to (without # prefix), or "current" for the current channel
|
|
30
|
+
|
|
31
|
+
Respond with a JSON object like:
|
|
32
|
+
{
|
|
33
|
+
"text": "The message to send",
|
|
34
|
+
"channel": "current"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Only respond with the JSON object, no other text.`;
|
|
38
|
+
|
|
39
|
+
interface SendMessageParams {
|
|
40
|
+
text: string;
|
|
41
|
+
channel: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const sendMessage: Action = {
|
|
45
|
+
name: "TWITCH_SEND_MESSAGE",
|
|
46
|
+
similes: [
|
|
47
|
+
"SEND_TWITCH_MESSAGE",
|
|
48
|
+
"TWITCH_CHAT",
|
|
49
|
+
"CHAT_TWITCH",
|
|
50
|
+
"SAY_IN_TWITCH",
|
|
51
|
+
],
|
|
52
|
+
description: "Send a message to a Twitch channel",
|
|
53
|
+
|
|
54
|
+
validate: async (
|
|
55
|
+
_runtime: IAgentRuntime,
|
|
56
|
+
message: Memory,
|
|
57
|
+
_state?: State,
|
|
58
|
+
): Promise<boolean> => {
|
|
59
|
+
return message.content.source === "twitch";
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
handler: async (
|
|
63
|
+
runtime: IAgentRuntime,
|
|
64
|
+
message: Memory,
|
|
65
|
+
state?: State,
|
|
66
|
+
_options?: Record<string, unknown>,
|
|
67
|
+
callback?: (response: { text: string; source?: string }) => void,
|
|
68
|
+
): Promise<ActionResult> => {
|
|
69
|
+
const twitchService =
|
|
70
|
+
runtime.getService<TwitchService>(TWITCH_SERVICE_NAME);
|
|
71
|
+
|
|
72
|
+
if (!twitchService || !twitchService.isConnected()) {
|
|
73
|
+
if (callback) {
|
|
74
|
+
callback({
|
|
75
|
+
text: "Twitch service is not available.",
|
|
76
|
+
source: "twitch",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { success: false, error: "Twitch service not available" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Get or compose state
|
|
83
|
+
const currentState = state ?? (await runtime.composeState(message));
|
|
84
|
+
|
|
85
|
+
// Compose prompt
|
|
86
|
+
const prompt = await composePromptFromState({
|
|
87
|
+
template: SEND_MESSAGE_TEMPLATE,
|
|
88
|
+
state: currentState,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Extract parameters using LLM
|
|
92
|
+
let messageInfo: SendMessageParams | null = null;
|
|
93
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
94
|
+
const response = await runtime.useModel(ModelType.TEXT_SMALL, {
|
|
95
|
+
prompt,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const parsed = parseJSONObjectFromText(String(response));
|
|
99
|
+
if (parsed?.text) {
|
|
100
|
+
messageInfo = {
|
|
101
|
+
text: String(parsed.text),
|
|
102
|
+
channel: String(parsed.channel || "current"),
|
|
103
|
+
};
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!messageInfo || !messageInfo.text) {
|
|
109
|
+
if (callback) {
|
|
110
|
+
callback({
|
|
111
|
+
text: "I couldn't understand what message you want me to send. Please try again.",
|
|
112
|
+
source: "twitch",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return { success: false, error: "Could not extract message parameters" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Determine target channel
|
|
119
|
+
let targetChannel = twitchService.getPrimaryChannel();
|
|
120
|
+
if (messageInfo.channel && messageInfo.channel !== "current") {
|
|
121
|
+
targetChannel = normalizeChannel(messageInfo.channel);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Get channel from room context if available
|
|
125
|
+
if (currentState?.data?.room?.channelId) {
|
|
126
|
+
targetChannel = normalizeChannel(
|
|
127
|
+
currentState.data.room.channelId as string,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Send message
|
|
132
|
+
const result = await twitchService.sendMessage(messageInfo.text, {
|
|
133
|
+
channel: targetChannel,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!result.success) {
|
|
137
|
+
if (callback) {
|
|
138
|
+
callback({
|
|
139
|
+
text: `Failed to send message: ${result.error}`,
|
|
140
|
+
source: "twitch",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return { success: false, error: result.error };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (callback) {
|
|
147
|
+
callback({
|
|
148
|
+
text: "Message sent successfully.",
|
|
149
|
+
source: message.content.source as string,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
data: {
|
|
156
|
+
channel: targetChannel,
|
|
157
|
+
messageId: result.messageId,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
examples: [
|
|
163
|
+
[
|
|
164
|
+
{
|
|
165
|
+
name: "{{user1}}",
|
|
166
|
+
content: { text: "Send a message to chat saying 'Hello everyone!'" },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "{{agent}}",
|
|
170
|
+
content: {
|
|
171
|
+
text: "I'll send that message to the chat.",
|
|
172
|
+
actions: ["TWITCH_SEND_MESSAGE"],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
],
|
|
177
|
+
};
|