Skip to content

Commit 0522432

Browse files
committed
Integrate AI Horde
1 parent 3ac8693 commit 0522432

9 files changed

Lines changed: 242 additions & 1 deletion

File tree

config.yml.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ clientSecret: your_client_secret
1414
callbackURI: https://callback_uri/callback
1515

1616
# The overlay Base URL to use for users using the overlay
17-
overlayBaseURL: https://overlay_base_url
17+
overlayBaseURL: https://overlay_base_url
18+
19+
# The AI Horde Base URL to use for users using AI with Nightbot
20+
aiHordeBaseURL: https://aihorde.net

pages/callback.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
GET https://nightbot.logicism.tv/commands/addquote?channel={channelname}&moderator={moderatorid}&text=%22This%20is%20an%20example%20quote%21%22%20-%20Logicism%202024<br><br>
3838
GET https://nightbot.logicism.tv/commands/delquote?channel={channelname}&moderator={moderatorid}&index=0<br><br>
3939
GET https://nightbot.logicism.tv/commands/quotes?channel={channelname}&moderator={moderatorid}<br><br>
40+
GET https://nightbot.logicism.tv/commands/ai?channel={channelname}&moderator={moderatorid}&username={username}&key={aihordekey}&text={Text to send to AI}&model={aihordemodel}<br><br>
4041
</div>
4142
</body>
4243
</html>

pages/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<ul style="display: inline-block; text-align: left; vertical-align: top;">
4949
<li>!delquote</li>
5050
<li>!quotes</li>
51+
<li>!ai (Powered by <a href="https://aihorde.net/">AI Horde</a>)</li>
5152
</ul>
5253
</div>
5354
</div>

src/me/Logicism/NightbotCommandsWebServer/OverlayConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public class OverlayConfig {
1414

1515
private String overlayBaseURL;
1616

17+
private String aiHordeBaseURL;
18+
1719
public String getHttpServerIP() {
1820
return httpServerIP;
1921
}
@@ -38,4 +40,7 @@ public String getOverlayBaseURL() {
3840
return overlayBaseURL;
3941
}
4042

43+
public String getAiHordeBaseURL() {
44+
return aiHordeBaseURL;
45+
}
4146
}

src/me/Logicism/NightbotCommandsWebServer/http/handlers/CommandsHandler.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import me.Logicism.NightbotCommandsWebServer.http.oauth.TokenHandle;
99
import me.Logicism.NightbotCommandsWebServer.util.HTTPUtils;
1010
import me.Logicism.NightbotCommandsWebServer.util.TextUtils;
11+
import me.Logicism.NightbotCommandsWebServer.util.ai.AIHordeClient;
12+
import me.Logicism.NightbotCommandsWebServer.util.ai.AIMemory;
13+
import me.Logicism.NightbotCommandsWebServer.util.ai.AISanitizer;
1114
import org.apache.commons.lang3.StringUtils;
1215
import org.json.JSONArray;
1316
import org.json.JSONObject;
@@ -23,6 +26,9 @@
2326
import java.util.*;
2427

2528
public class CommandsHandler implements HttpHandler {
29+
private final AIHordeClient aiHordeClient = new AIHordeClient();
30+
private final Map<String, AIMemory> aiMemoryMap = new HashMap<>();
31+
2632
@Override
2733
public void handle(HttpExchange exchange) throws IOException {
2834
try {
@@ -334,6 +340,28 @@ public void handle(HttpExchange exchange) throws IOException {
334340
} else {
335341
String response = "Moderator is unauthenticated! Head to https://nightbot.logicism.tv/ to Authenticate!";
336342

343+
HTTPUtils.throwSuccessHTML(exchange, response);
344+
}
345+
} else if (exchange.getRequestURI().toString().startsWith("/commands/ai")) {
346+
if (queryMap.containsKey("text") && queryMap.containsKey("key") && queryMap.containsKey("username")) {
347+
String message = queryMap.get("text");
348+
String userName = queryMap.get("username");
349+
String channelName = queryMap.get("channel");
350+
String key = queryMap.get("key");
351+
String model = queryMap.get("model");
352+
353+
AIMemory memory = aiMemoryMap.computeIfAbsent(channelName, AIMemory::new);
354+
memory.addMessage(userName, message);
355+
356+
String fullPrompt = memory.toNativePrompt();
357+
String rawAiResponse = aiHordeClient.askNightbot(fullPrompt, userName, model, key);
358+
359+
String response = AISanitizer.clean(rawAiResponse, userName);
360+
361+
HTTPUtils.throwSuccessHTML(exchange, response);
362+
} else {
363+
String response = "Query requires 'text' and 'key'";
364+
337365
HTTPUtils.throwSuccessHTML(exchange, response);
338366
}
339367
} else {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package me.Logicism.NightbotCommandsWebServer.util.ai;
2+
3+
import org.json.JSONArray;
4+
import org.json.JSONObject; // Use a library like org.json or Gson
5+
6+
import java.io.BufferedReader;
7+
import java.io.InputStreamReader;
8+
import java.io.OutputStream;
9+
import java.net.HttpURLConnection;
10+
import java.net.URL;
11+
12+
public class AIHordeClient {
13+
private static final String API_KEY = "0000000000";
14+
private static final String SUBMIT_URL = "https://aihorde.net/api/v2/generate/text/async";
15+
private static final String STATUS_URL = "https://aihorde.net/api/v2/generate/text/status/";
16+
17+
public String askNightbot(String prompt, String userName, String model, String key) throws Exception {
18+
// 1. Submit the Request
19+
String id = submitPrompt(prompt, userName, model, key);
20+
21+
// 2. Poll for the Result
22+
while (true) {
23+
Thread.sleep(2000); // Wait 2 seconds between checks
24+
JSONObject status = checkStatus(id);
25+
System.out.println(status);
26+
if (status.getInt("finished") == 1) {
27+
return status.getJSONArray("generations").getJSONObject(0).getString("text");
28+
}
29+
30+
// Optional: Print wait-time or queue position
31+
System.out.println("Nightbot is thinking... Queue position: " + status.getInt("queue_position"));
32+
}
33+
}
34+
35+
private String submitPrompt(String promptString, String userName, String model, String key) throws Exception {
36+
URL url = new URL(SUBMIT_URL);
37+
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
38+
conn.setRequestMethod("POST");
39+
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36");
40+
conn.setRequestProperty("Content-Type", "application/json");
41+
conn.setRequestProperty("Client-Agent", "AIHordeNightbotIntegration:1:Logicism");
42+
conn.setRequestProperty("apikey", key != null ? key : API_KEY);
43+
conn.setDoOutput(true);
44+
45+
JSONObject payload = new JSONObject()
46+
.put("prompt", promptString)
47+
.put("params", new JSONObject().put("n", 1).put("frmtadsnsp", false).put("frmtrmblln", false)
48+
.put("frmtrmspch", false).put("frmttriminc", false)
49+
.put("max_context_length", 4096).put("max_length", 128).put("rep_pen", 1.1)
50+
.put("rep_pen_range", 1024).put("rep_pen_slope", 0.7).put("singleline", false)
51+
.put("temperature", 0.7).put("tfs", 0.97).put("top_a", 0.75).put("top_k", 0).put("top_p", 0.5)
52+
.put("typical", 0.19).put("dynatemp_exponent", 1).put("dynatemp_range", 0).put("min_p", 0).put("n", 1)
53+
.put("sampler_order", new JSONArray().put(6).put(5).put(4).put(3).put(2).put(1).put(0)))
54+
.put("stop_sequence", new JSONArray().put(userName + ": ").put("Nightbot: "))
55+
.put("models", new JSONArray().put(model));
56+
57+
try (OutputStream os = conn.getOutputStream()) {
58+
os.write(payload.toString().getBytes("utf-8"));
59+
}
60+
61+
return readResponse(conn).getString("id");
62+
}
63+
64+
private JSONObject checkStatus(String id) throws Exception {
65+
URL url = new URL(STATUS_URL + id);
66+
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
67+
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36");
68+
conn.setRequestMethod("GET");
69+
return readResponse(conn);
70+
}
71+
72+
private JSONObject readResponse(HttpURLConnection conn) throws Exception {
73+
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
74+
StringBuilder response = new StringBuilder();
75+
String line;
76+
while ((line = br.readLine()) != null) {
77+
response.append(line.trim());
78+
}
79+
return new JSONObject(response.toString());
80+
}
81+
}
82+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package me.Logicism.NightbotCommandsWebServer.util.ai;
2+
3+
import org.json.JSONArray;
4+
import org.json.JSONObject;
5+
6+
import java.io.File;
7+
import java.io.FileWriter;
8+
import java.io.IOException;
9+
import java.io.PrintWriter;
10+
import java.nio.file.Files;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.stream.Collectors;
14+
15+
public class AIMemory {
16+
private final List<ChatMessage> history = new ArrayList<>();
17+
private final File memoryDir = new File("memory");
18+
private final String filePath;
19+
20+
public AIMemory(String channelName) {
21+
this.filePath = channelName + ".json";
22+
if (!loadFromFile()) {
23+
// Default personality if no file exists
24+
history.add(new ChatMessage("System",
25+
"You are Nightbot, a knowledgeable, helpful, and non-judgmental Twitch bot."));
26+
}
27+
}
28+
29+
public void addMessage(String role, String content) {
30+
history.add(new ChatMessage(role, content));
31+
while (getTotalChars() > 4000 && history.size() > 1) {
32+
// Remove the oldest message (index 1), keeping the System Prompt (index 0)
33+
history.remove(1);
34+
}
35+
saveToFile();
36+
}
37+
38+
public String toNativePrompt() {
39+
return history.stream()
40+
.map(msg -> msg.role + ": " + msg.content)
41+
.collect(Collectors.joining("\n")) + "\nNightbot:";
42+
}
43+
44+
private void saveToFile() {
45+
try (PrintWriter out = new PrintWriter(new FileWriter(new File(memoryDir, filePath)))) {
46+
JSONArray array = new JSONArray();
47+
for (ChatMessage msg : history) {
48+
JSONObject obj = new JSONObject();
49+
obj.put("role", msg.role);
50+
obj.put("content", msg.content);
51+
array.put(obj);
52+
}
53+
out.write(array.toString(2)); // Indent for readability
54+
} catch (IOException e) {
55+
e.printStackTrace();
56+
}
57+
}
58+
59+
private boolean loadFromFile() {
60+
if (!memoryDir.exists()) {
61+
memoryDir.mkdir();
62+
}
63+
File file = new File(memoryDir, filePath);
64+
if (!file.exists()) return false;
65+
66+
try {
67+
String content = new String(Files.readAllBytes(file.toPath()));
68+
JSONArray array = new JSONArray(content);
69+
history.clear();
70+
for (int i = 0; i < array.length(); i++) {
71+
JSONObject obj = array.getJSONObject(i);
72+
history.add(new ChatMessage(obj.getString("role"), obj.getString("content")));
73+
}
74+
return true;
75+
} catch (Exception e) {
76+
System.err.println("Could not load memory: " + e.getMessage());
77+
return false;
78+
}
79+
}
80+
81+
private int getTotalChars() {
82+
return history.stream().filter(m -> m != null && m.content != null).mapToInt(m -> m.content.length()).sum();
83+
}
84+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package me.Logicism.NightbotCommandsWebServer.util.ai;
2+
3+
public class AISanitizer {
4+
5+
public static String clean(String rawText, String currentUserName) {
6+
if (rawText == null || rawText.trim().isEmpty()) {
7+
return "I'm having trouble thinking right now!";
8+
}
9+
10+
// 1. Strip common prefixes
11+
String clean = rawText.replaceAll("(?i)^(Nightbot|Assistant|System):\\s*", "");
12+
13+
// 2. Dynamic Stop-Sequence: Cut off if AI tries to speak FOR the user
14+
// We look for both "User:" and the specific "LogicismTV:"
15+
String[] stopSequences = {"\nUser:", "\n" + currentUserName + ":"};
16+
17+
for (String stop : stopSequences) {
18+
int index = clean.indexOf(stop);
19+
if (index != -1) {
20+
clean = clean.substring(0, index);
21+
}
22+
}
23+
24+
return clean.trim();
25+
}
26+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package me.Logicism.NightbotCommandsWebServer.util.ai;
2+
3+
public class ChatMessage {
4+
String role; // "User", "Nightbot", or "System"
5+
String content;
6+
7+
public ChatMessage(String role, String content) {
8+
this.role = role;
9+
this.content = content;
10+
}
11+
}

0 commit comments

Comments
 (0)