broadcast
All checks were successful
Build Ollama-chat plugin / Build-latest-jar (push) Successful in 1m49s

This commit is contained in:
Martin Hoke 2025-07-21 11:35:35 +02:00
parent 460ba125a5
commit 8184cf771d
34 changed files with 2171 additions and 573 deletions

View File

@ -24,4 +24,4 @@ jobs:
./gradlew build && ls -lah && ls */ -lah && ls */* -lah&& ls */*/* -lah
- name: Build | publish jar
run: |
curl --insecure --user username:mypass -T build/libs/OllamaChat-1.0.2.jar ftp://192.168.10.133:/
curl --insecure --user username:mypass -T build/libs/OllamaChat-1.1.4.jar ftp://192.168.10.133:/

3
.gitignore vendored
View File

@ -22,6 +22,3 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
build
.gradle

8
.idea/.gitignore vendored
View File

@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -1 +0,0 @@
OllamaChat

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="APPLICATION" />
<option name="description" value="" />
<option name="applicationTheme" value="default" />
<option name="iconsTheme" value="default" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
<option name="customApplicationId" value="" />
</component>
</project>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTreeColorHighlighter">
<files />
<colors>
<color id="1" value="#4f060d" name="Color 1" enabled="true" />
<color id="2" value="#44220e" name="Color 2" enabled="true" />
<color id="3" value="#3f371b" name="Color 3" enabled="true" />
<color id="4" value="#162c16" name="Color 4" enabled="true" />
<color id="5" value="#0f2f47" name="Color 5" enabled="true" />
<color id="6" value="#171a34" name="Color 6" enabled="true" />
<color id="7" value="#311333" name="Color 7" enabled="true" />
<color id="8" value="#1e1e1e" name="Color 8" enabled="true" />
</colors>
<option name="marksForCollapsedHighlights" value="Dots" />
</component>
</project>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
</project>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/OllamaChat.main.iml" filepath="$PROJECT_DIR$/.idea/modules/OllamaChat.main.iml" />
</modules>
</component>
</project>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="FacetManager">
<facet type="minecraft" name="Minecraft">
<configuration>
<autoDetectTypes>
<platformType>PAPER</platformType>
<platformType>ADVENTURE</platformType>
</autoDetectTypes>
<projectReimportVersion>1</projectReimportVersion>
</configuration>
</facet>
</component>
</module>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

138
README.md
View File

@ -1,64 +1,101 @@
![Version](https://img.shields.io/badge/version-1.0.2-blue)
# Ollama-Chat
![Version](https://img.shields.io/badge/version-1.1.4-blue)
# OllamaChat
[![Download](https://github.com/gabrielvicenteYT/modrinth-icons/blob/main/Branding/Badge/badge-dark.svg)](https://modrinth.com/plugin/ollama-chat)
## Overview
**Ollama-Chat** is a cutting-edge Minecraft plugin that brings the power of Ollama and other AI models directly into your Minecraft world. This plugin enables players to interact with AI in real-time, creating a unique and immersive gameplay experience. Whether you want to chat with an AI companion, ask questions, or simply explore the capabilities of AI, Ollama-Chat makes it possible within the Minecraft universe.
**OllamaChat** is a cutting-edge Minecraft plugin that integrates Ollama and OpenAI-class APIs, enabling real-time AI interactions, multi-language support, and advanced prompt and conversation management for immersive in-game experiences.
## Features
- **AI-Powered Conversations**: Communicate with AI entities in Minecraft by sending messages prefixed with `@bot`. The AI will respond intelligently, providing a dynamic and engaging interaction.
- **Ollama Integration**: Leverage the advanced capabilities of Ollama to enhance your Minecraft experience.
- **Multi-Language Support**: Supports multiple languages (e.g., English, Simplified Chinese) through language files in the `lang` folder.
- **Toggle AI Services**: Enable or disable AI services dynamically using the `/ollamachat toggle <ai-name>` command.
- **Simple Commands**: Use the `/ollamachat reload` command to reload the plugin configuration instantly, ensuring seamless updates without server restarts.
- **AI-Powered Conversations**: Chat with AI using `@bot` or `@ai` prefixes for dynamic, intelligent responses.
- **Ollama & OpenAI Integration**: Leverage advanced AI models to enhance your Minecraft experience.
- **Multi-Language Support**: Supports multiple languages (e.g., English, Simplified Chinese) via `lang` folder files.
- **Toggle AI Services**: Enable/disable AI services with `/ollamachat toggle <ai-name>`.
- **Prompt Management**: Create, delete, list, select, or clear custom prompts to tailor AI interactions.
- **Conversation Management**: Start, switch, delete, or view player-specific conversations with AI entities.
- **Smart Response Suggestions**: Generate configurable, clickable follow-up suggestions with hover text and rate limiting.
- **Tab Completion**: Enhanced usability with Tab completion for `/ollamachat` subcommands.
- **Progress Display**: Visual status bar for prompt answer generation (0% to 100%).
- **Configurable Settings**: Customize suggestion count, prompt templates, preset responses, and model toggles.
## Usage
### Chatting with AI
To interact with the AI, simply type `@bot` followed by your message in the Minecraft chat. The AI will process your input and respond accordingly.
Type `@bot` or `@ai` followed by your message in Minecraft chat to interact with the AI.
**Example:**
```
@bot What is the weather like today?
@bot What's the best way to build a castle?
```
### Commands
- **/ollamachat reload**: Reloads the plugin configuration, including language files and AI settings.
- **/ollamachat toggle <ai-name>**: Enables or disables the specified AI service.
- **/aichat <ai-name> <prompt>**: Interacts with other AI services (e.g., OpenAI).
- **/ollamachat reload**: Reloads plugin configuration and language files (`ollamachat.reload`).
- **/ollamachat toggle <ai-name>**: Enables/disables specified AI service (`ollamachat.toggle`).
- **/aichat <ai-name> <prompt>**: Interacts with other AI services (`ollamachat.use`).
- **/ollamachat prompt set <promptName> <promptContent>**: Creates a new prompt (`ollamachat.prompt.set`).
- **/ollamachat prompt delete <promptName>**: Deletes a prompt (`ollamachat.prompt.delete`).
- **/ollamachat prompt list**: Lists all prompts (`ollamachat.prompt.list`).
- **/ollamachat prompt select <promptName>**: Sets default prompt (`ollamachat.prompt.select`).
- **/ollamachat prompt clear**: Resets default prompt (`ollamachat.prompt.select`).
- **/ollamachat conversation new <aiName> <convName>**: Starts a new conversation (`ollamachat.conversation.new`).
- **/ollamachat conversation select <aiName> <convName>**: Switches conversations (`ollamachat.conversation.select`).
- **/ollamachat conversation delete <aiName> <convName>**: Deletes a conversation (`ollamachat.conversation.delete`).
- **/ollamachat conversation list <aiName>**: Lists conversations for an AI (`ollamachat.conversation.list`).
- **/ollamachat suggests toggle**: Toggles suggested responses (`ollamachat.suggests.toggle`).
### Permissions
| Command | Permission | Description |
|---------|------------|-------------|
| `/ollamachat reload` | `ollamachat.reload` | Reloads plugin configuration. |
| `/ollamachat toggle <aiName>` | `ollamachat.toggle` | Toggles specified AI service. |
| `/aichat <aiName> <message>` | `ollamachat.use` | Sends a message to specified AI. |
| `/ollamachat prompt set <promptName> <promptContent>` | `ollamachat.prompt.set` | Creates and saves a new prompt. |
| `/ollamachat prompt delete <promptName>` | `ollamachat.prompt.delete` | Deletes a specified prompt. |
| `/ollamachat prompt list` | `ollamachat.prompt.list` | Lists all prompts and current default. |
| `/ollamachat prompt select <promptName>` | `ollamachat.prompt.select` | Sets a prompt as default. |
| `/ollamachat prompt clear` | `ollamachat.prompt.select` | Resets default prompt. |
| `/ollamachat conversation new <aiName> <convName>` | `ollamachat.conversation.new` | Starts a new conversation. |
| `/ollamachat conversation select <aiName> <convName>` | `ollamachat.conversation.select` | Switches to an existing conversation. |
| `/ollamachat conversation delete <aiName> <convName>` | `ollamachat.conversation.delete` | Deletes a conversation. |
| `/ollamachat conversation list <aiName>` | `ollamachat.conversation.list` | Lists all conversations for an AI. |
| `/ollamachat suggests toggle` | `ollamachat.suggests.toggle` | Toggles suggested responses. |
| `/ollamachat suggests-presets toggle` | `ollamachat.suggests-presets.toggle` | Toggles preset suggested responses. |
**Example:**
```
/aichat openai Tell me a joke
/aichat ollama Tell me about Redstone
/ollamachat prompt set creativePrompt "Act as a creative Minecraft builder"
/ollamachat suggests toggle
```
## Installation
1. **Download the Plugin**: Obtain the latest version of **Ollama-Chat** from the [official repository](https://github.com/mcraftbbs/Ollama-Chat).
2. **Install the Plugin**: Place the downloaded `.jar` file into the `plugins` folder of your Minecraft server.
3. **Configure the Plugin**: Modify the `config.yml` file to customize AI settings.
4. **Reload the Plugin**: Use the `/ollamachat reload` command to apply any configuration changes.
2. **Install**: Place the `.jar` file in your server's `plugins` folder.
3. **Configure**: Edit `config.yml` to customize AI settings, prompts, and suggestions.
4. **Reload**: Use `/ollamachat reload` to apply changes.
## Configuration
The plugin's configuration file (`config.yml`) allows you to customize various aspects of the AI interactions.
Customize AI interactions via `config.yml`:
Example `config.yml`:
```yaml
# Ollama API
ollama-api-url: "http://localhost:11434/api/generate"
model: "llama3"
ollama-enabled: true
# Streaming settings
stream-settings:
enabled: true
# Chat
trigger-prefix: "@bot "
response-prefix: "§b[AI] §r"
trigger-prefixes:
- "@bot"
- "@ai"
# Length
max-response-length: 500
@ -67,7 +104,48 @@ max-response-length: 500
max-history: 5
# Language Settings
language: "en" # Default language (en or zh_cn)
language: "en_us"
# Progress Display Settings
progress-display:
enabled: true
type: "bossbar"
color: "BLUE"
style: "SOLID"
update-interval: 1
# Suggested Response
suggested-responses-enabled: false
suggested-response-models:
- "llama3"
suggested-response-count: 3
suggested-response-prompt: "Conversation:\nUser: {prompt}\nAI: {response}\n\nBased on the above conversation, suggest {count} natural follow-up responses the user might want to say. They should be conversational in tone rather than questions. List them as:\n1. Response 1\n2. Response 2\n3. Response 3"
suggested-response-presets:
- "I see what you mean."
- "That's interesting!"
- "Tell me more about that."
suggested-response-presets-enabled: false
suggested-response-model-toggles:
- "llama3"
suggested-response-cooldown: 10
# Database (for mysql, set database.type: mysql.)
database:
type: sqlite
mysql:
host: localhost
port: 3306
database: ollamachat
username: root
password: ""
# Default prompt
default-prompt: ""
# Custom prompts
prompts:
# friendly: "You are a friendly assistant who responds in a cheerful tone."
# formal: "You are a professional assistant who responds formally."
# Other AI Configurations
other-ai-configs:
@ -75,21 +153,21 @@ other-ai-configs:
api-url: "https://api.openai.com/v1/chat/completions"
api-key: "your-openai-api-key"
model: "gpt-4"
enabled: true
enabled: false
messages-format: true
```
## Contributing
We welcome contributions from the community to improve **Ollama-Chat**! If you have ideas, bug reports, or feature requests, please open an issue or submit a pull request on our [GitHub repository](https://github.com/mcraftbbs/Ollama-Chat).
We welcome contributions! Submit issues or pull requests on our [GitHub repository](https://github.com/mcraftbbs/Ollama-Chat).
## License
**Ollama-Chat** is licensed under the MIT License. For more details, see the [LICENSE](LICENSE) file.
Licensed under the MIT License. See [LICENSE](https://github.com/mcraftbbs/Ollama-Chat?tab=MIT-1-ov-file).
## Support
For assistance, questions, or feedback, please visit our [GitHub repository](https://github.com/mcraftbbs/Ollama-Chat) or join our [community server](https://chat.sarskin.cn/invite/iHgI6LTX).
For help, visit our [GitHub repository](https://github.com/mcraftbbs/Ollama-Chat) or join our [community server](https://chat.sarskin.cn/invite/iHgI6LTX).
---
**Note**: **Ollama-Chat** is actively developed, with new features and improvements being added regularly. Stay tuned for updates!

View File

@ -3,7 +3,7 @@ plugins {
}
group = 'com'
version = '1.0.2'
version = '1.1.4'
repositories {
mavenCentral()
@ -19,6 +19,8 @@ repositories {
dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT")
compileOnly("mysql:mysql-connector-java:8.0.33")
compileOnly("org.xerial:sqlite-jdbc:3.46.0.0")
}
def targetJavaVersion = 21
@ -47,3 +49,5 @@ processResources {
expand props
}
}

0
gradlew vendored Executable file → Normal file
View File

View File

@ -1,15 +1,15 @@
package com.ollamachat;
import com.google.gson.Gson;
import org.bukkit.Bukkit;
import com.google.gson.JsonObject;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.concurrent.CompletableFuture;
public class AIService {
@ -22,14 +22,28 @@ public class AIService {
this.gson = new Gson();
}
public CompletableFuture<String> sendRequest(String apiUrl, String apiKey, String model, String prompt) {
public CompletableFuture<String> sendRequest(String apiUrl, String apiKey, String model, String prompt, boolean isMessagesFormat) {
return CompletableFuture.supplyAsync(() -> {
try {
Map<String, Object> requestBody = Map.of(
"model", model,
"prompt", prompt,
"stream", false
);
Map<String, Object> requestBody;
if (isMessagesFormat) {
requestBody = Map.of(
"model", model,
"messages", List.of(
Map.of(
"role", "user",
"content", prompt
)
),
"stream", false
);
} else {
requestBody = Map.of(
"model", model,
"prompt", prompt,
"stream", false
);
}
String jsonRequest = gson.toJson(requestBody);
@ -38,6 +52,10 @@ public class AIService {
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonRequest));
if (apiKey != null && !apiKey.isEmpty()) {
requestBuilder.header("Authorization", "Bearer " + apiKey);
}
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = httpClient.send(
@ -46,18 +64,102 @@ public class AIService {
);
if (response.statusCode() == 200) {
Bukkit.getConsoleSender().sendMessage("AI API Response: " + response.body());
return response.body();
} else {
Bukkit.getConsoleSender().sendMessage("AI API Response-ERR: " + response.body());
throw new RuntimeException("AI API Error: " + response.body());
}
} catch (Exception e) {
Bukkit.getConsoleSender().sendMessage("AI API Response-ERR: " + e.getMessage());
throw new RuntimeException("Failed to get response from AI: " + e.getMessage(), e);
}
});
}
public CompletableFuture<Void> sendStreamingRequest(String apiUrl, String apiKey, String model, String prompt, Consumer<String> responseConsumer, boolean isMessagesFormat) {
return CompletableFuture.runAsync(() -> {
try {
Map<String, Object> requestBody;
if (isMessagesFormat) {
requestBody = Map.of(
"model", model,
"messages", List.of(
Map.of(
"role", "user",
"content", prompt
)
),
"stream", true
);
} else {
requestBody = Map.of(
"model", model,
"prompt", prompt,
"stream", true
);
}
String jsonRequest = gson.toJson(requestBody);
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonRequest));
if (apiKey != null && !apiKey.isEmpty()) {
requestBuilder.header("Authorization", "Bearer " + apiKey);
}
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = httpClient.send(
request,
HttpResponse.BodyHandlers.ofString()
);
if (response.statusCode() == 200) {
StringBuilder buffer = new StringBuilder();
int minBufferLength = 50; // Minimum length before sending
String[] lines = response.body().split("\n");
for (String line : lines) {
if (!line.trim().isEmpty()) {
if (isMessagesFormat && line.startsWith("data: ")) {
String jsonData = line.substring(6); // Remove "data: " prefix
if (jsonData.equals("[DONE]")) continue;
JsonObject json = gson.fromJson(jsonData, JsonObject.class);
if (json.has("choices")) {
String partialResponse = json.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.get("delta").getAsJsonObject()
.get("content").getAsString();
buffer.append(partialResponse);
}
} else if (!isMessagesFormat) {
JsonObject json = gson.fromJson(line, JsonObject.class);
if (json.has("response")) {
String partialResponse = json.get("response").getAsString();
buffer.append(partialResponse);
}
}
// Check if buffer ends with a sentence boundary or is long enough
String currentBuffer = buffer.toString();
if (currentBuffer.endsWith(".") || currentBuffer.endsWith("?") ||
currentBuffer.endsWith("!") || currentBuffer.length() >= minBufferLength) {
responseConsumer.accept(currentBuffer);
buffer.setLength(0); // Clear buffer
}
}
}
// Send any remaining content in the buffer
if (buffer.length() > 0) {
responseConsumer.accept(buffer.toString());
}
} else {
throw new RuntimeException("AI API Error: " + response.body());
}
} catch (Exception e) {
throw new RuntimeException("Failed to get streaming response from AI: " + e.getMessage(), e);
}
});
}
}

View File

@ -2,6 +2,8 @@ package com.ollamachat;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class ChatHistoryManager {
@ -17,11 +19,35 @@ public class ChatHistoryManager {
databaseManager.savePlayerInfo(player.getUniqueId(), player.getName());
}
public void saveChatHistory(UUID playerUuid, String aiModel, String prompt, String response) {
databaseManager.saveChatHistory(playerUuid, aiModel, prompt, response);
public String createConversation(UUID playerUuid, String aiModel, String convName) {
return databaseManager.createConversation(playerUuid, aiModel, convName);
}
public String getChatHistory(UUID playerUuid, String aiModel) {
return databaseManager.getChatHistory(playerUuid, aiModel, maxHistory);
public boolean conversationExistsByName(UUID playerUuid, String aiModel, String convName) {
return databaseManager.conversationExistsByName(playerUuid, aiModel, convName);
}
}
public String getConversationId(UUID playerUuid, String aiModel, String convName) {
return databaseManager.getConversationId(playerUuid, aiModel, convName);
}
public boolean conversationExists(UUID playerUuid, String aiModel, String convId) {
return databaseManager.conversationExists(playerUuid, aiModel, convId);
}
public boolean deleteConversation(UUID playerUuid, String aiModel, String convId) {
return databaseManager.deleteConversation(playerUuid, aiModel, convId);
}
public Map<String, String> listConversations(UUID playerUuid, String aiModel) {
return databaseManager.listConversations(playerUuid, aiModel);
}
public void saveChatHistory(UUID playerUuid, String aiModel, String conversationId, String prompt, String response) {
databaseManager.saveChatHistory(playerUuid, aiModel, conversationId, prompt, response);
}
public String getChatHistory(UUID playerUuid, String aiModel, String conversationId) {
return databaseManager.getChatHistory(playerUuid, aiModel, conversationId, maxHistory);
}
}

View File

@ -1,81 +1,308 @@
package com.ollamachat;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Logger;
public class DatabaseManager {
private Connection connection;
private final JavaPlugin plugin;
private final Logger logger;
private String databaseType; // "sqlite" or "mysql"
private Connection sqliteConnection; // For SQLite, maintain a single connection
private final ClassLoader dependencyClassLoader;
private String mysqlUrl;
private String mysqlUsername;
private String mysqlPassword;
public DatabaseManager() {
public DatabaseManager(JavaPlugin plugin, ClassLoader dependencyClassLoader) {
this.plugin = plugin;
this.logger = plugin.getLogger();
this.dependencyClassLoader = dependencyClassLoader;
initializeDatabase();
}
private void initializeDatabase() {
FileConfiguration config = plugin.getConfig();
databaseType = config.getString("database.type", "sqlite").toLowerCase();
try {
Class.forName("org.sqlite.JDBC");
connection = DriverManager.getConnection("jdbc:sqlite:plugins/OllamaChat/chat_history.db");
if (databaseType.equals("mysql")) {
initializeMySQL();
} else {
initializeSQLite();
}
createTables();
} catch (Exception e) {
logger.severe("Failed to initialize database: " + e.getMessage());
e.printStackTrace();
throw new RuntimeException("Database initialization failed", e);
}
}
private void initializeSQLite() {
try {
// Use the dependency class loader to load SQLite driver
Class.forName("org.sqlite.JDBC", true, dependencyClassLoader);
} catch (ClassNotFoundException e) {
logger.severe("SQLite JDBC driver not found. Ensure 'sqlite-jdbc' dependency is included in the plugin.");
throw new RuntimeException("SQLite driver not found", e);
}
try {
databaseType = "sqlite";
sqliteConnection = DriverManager.getConnection("jdbc:sqlite:plugins/OllamaChat/chat_history.db");
sqliteConnection.setAutoCommit(true);
logger.info("SQLite database initialized successfully.");
} catch (SQLException e) {
logger.severe("Failed to initialize SQLite database: " + e.getMessage());
throw new RuntimeException("SQLite initialization failed", e);
}
}
private void initializeMySQL() {
try {
// Use the dependency class loader to load MySQL driver
Class.forName("com.mysql.cj.jdbc.Driver", true, dependencyClassLoader);
} catch (ClassNotFoundException e) {
logger.severe("MySQL JDBC driver not found. Ensure 'mysql-connector-java' dependency is included in the plugin.");
logger.warning("Falling back to SQLite due to missing MySQL driver.");
initializeSQLite();
return;
}
try {
FileConfiguration config = plugin.getConfig();
String host = config.getString("database.mysql.host", "localhost");
int port = config.getInt("database.mysql.port", 3306);
String database = config.getString("database.mysql.database", "ollamachat");
mysqlUsername = config.getString("database.mysql.username", "root");
mysqlPassword = config.getString("database.mysql.password", "");
mysqlUrl = String.format("jdbc:mysql://%s:%d/%s?useSSL=false&allowPublicKeyRetrieval=true&autoReconnect=true", host, port, database);
// Test connection
try (Connection conn = DriverManager.getConnection(mysqlUrl, mysqlUsername, mysqlPassword)) {
logger.info("MySQL database initialized successfully.");
}
} catch (SQLException e) {
logger.severe("Failed to initialize MySQL database: " + e.getMessage());
logger.warning("Falling back to SQLite due to MySQL initialization failure.");
initializeSQLite();
}
}
private Connection getConnection() throws SQLException {
if (databaseType.equals("sqlite")) {
if (sqliteConnection == null || sqliteConnection.isClosed()) {
sqliteConnection = DriverManager.getConnection("jdbc:sqlite:plugins/OllamaChat/chat_history.db");
sqliteConnection.setAutoCommit(true);
}
return sqliteConnection;
} else {
return DriverManager.getConnection(mysqlUrl, mysqlUsername, mysqlPassword);
}
}
private void createTables() throws SQLException {
try (Statement stmt = connection.createStatement()) {
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
stmt.execute("CREATE TABLE IF NOT EXISTS players (" +
"uuid TEXT PRIMARY KEY," +
"username TEXT NOT NULL)");
stmt.execute("CREATE TABLE IF NOT EXISTS chat_history (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
stmt.execute("CREATE TABLE IF NOT EXISTS conversations (" +
"conversation_id TEXT NOT NULL," +
"player_uuid TEXT NOT NULL," +
"ai_model TEXT NOT NULL," +
"conversation_name TEXT NOT NULL," +
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
"PRIMARY KEY (conversation_id, player_uuid, ai_model)," +
"FOREIGN KEY (player_uuid) REFERENCES players(uuid))");
stmt.execute("CREATE TABLE IF NOT EXISTS chat_history (" +
"id INTEGER PRIMARY KEY " + (databaseType.equals("sqlite") ? "AUTOINCREMENT" : "AUTO_INCREMENT") + "," +
"player_uuid TEXT NOT NULL," +
"ai_model TEXT NOT NULL," +
"conversation_id TEXT," +
"timestamp DATETIME DEFAULT CURRENT_TIMESTAMP," +
"prompt TEXT NOT NULL," +
"response TEXT NOT NULL," +
"FOREIGN KEY (player_uuid) REFERENCES players(uuid))");
"FOREIGN KEY (player_uuid) REFERENCES players(uuid)," +
"FOREIGN KEY (conversation_id, player_uuid, ai_model) REFERENCES conversations(conversation_id, player_uuid, ai_model))");
}
}
public void savePlayerInfo(UUID uuid, String username) {
String sql = "INSERT OR REPLACE INTO players(uuid, username) VALUES(?, ?)";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
String sql;
if (databaseType.equals("sqlite")) {
sql = "INSERT OR REPLACE INTO players (uuid, username) VALUES (?, ?)";
} else {
sql = "INSERT INTO players (uuid, username) VALUES (?, ?) ON DUPLICATE KEY UPDATE username = ?";
}
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, uuid.toString());
pstmt.setString(2, username);
if (databaseType.equals("mysql")) {
pstmt.setString(3, username); // For ON DUPLICATE KEY UPDATE
}
pstmt.executeUpdate();
} catch (SQLException e) {
logger.severe("Failed to save player info: " + e.getMessage());
e.printStackTrace();
}
}
public void saveChatHistory(UUID playerUuid, String aiModel, String prompt, String response) {
String sql = "INSERT INTO chat_history(player_uuid, ai_model, prompt, response) VALUES(?, ?, ?, ?)";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
public String createConversation(UUID playerUuid, String aiModel, String convName) {
String convId = UUID.randomUUID().toString();
String sql = "INSERT INTO conversations (conversation_id, player_uuid, ai_model, conversation_name) VALUES (?, ?, ?, ?)";
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, convId);
pstmt.setString(2, playerUuid.toString());
pstmt.setString(3, aiModel);
pstmt.setString(4, convName);
pstmt.executeUpdate();
return convId;
} catch (SQLException e) {
logger.severe("Failed to create conversation: " + e.getMessage());
e.printStackTrace();
return null;
}
}
public boolean conversationExistsByName(UUID playerUuid, String aiModel, String convName) {
String sql = "SELECT 1 FROM conversations WHERE conversation_name = ? AND player_uuid = ? AND ai_model = ?";
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, convName);
pstmt.setString(2, playerUuid.toString());
pstmt.setString(3, aiModel);
try (ResultSet rs = pstmt.executeQuery()) {
return rs.next();
}
} catch (SQLException e) {
logger.severe("Failed to check conversation existence: " + e.getMessage());
e.printStackTrace();
return false;
}
}
public String getConversationId(UUID playerUuid, String aiModel, String convName) {
String sql = "SELECT conversation_id FROM conversations WHERE conversation_name = ? AND player_uuid = ? AND ai_model = ?";
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, convName);
pstmt.setString(2, playerUuid.toString());
pstmt.setString(3, aiModel);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return rs.getString("conversation_id");
}
}
} catch (SQLException e) {
logger.severe("Failed to get conversation ID: " + e.getMessage());
e.printStackTrace();
}
return null;
}
public boolean conversationExists(UUID playerUuid, String aiModel, String convId) {
String sql = "SELECT 1 FROM conversations WHERE conversation_id = ? AND player_uuid = ? AND ai_model = ?";
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, convId);
pstmt.setString(2, playerUuid.toString());
pstmt.setString(3, aiModel);
try (ResultSet rs = pstmt.executeQuery()) {
return rs.next();
}
} catch (SQLException e) {
logger.severe("Failed to check conversation existence: " + e.getMessage());
e.printStackTrace();
return false;
}
}
public boolean deleteConversation(UUID playerUuid, String aiModel, String convId) {
String sqlDeleteHistory = "DELETE FROM chat_history WHERE conversation_id = ? AND player_uuid = ? AND ai_model = ?";
String sqlDeleteConv = "DELETE FROM conversations WHERE conversation_id = ? AND player_uuid = ? AND ai_model = ?";
try (Connection conn = getConnection();
PreparedStatement pstmtHistory = conn.prepareStatement(sqlDeleteHistory);
PreparedStatement pstmtConv = conn.prepareStatement(sqlDeleteConv)) {
pstmtHistory.setString(1, convId);
pstmtHistory.setString(2, playerUuid.toString());
pstmtHistory.setString(3, aiModel);
pstmtConv.setString(1, convId);
pstmtConv.setString(2, playerUuid.toString());
pstmtConv.setString(3, aiModel);
int rowsAffected = pstmtHistory.executeUpdate() + pstmtConv.executeUpdate();
return rowsAffected > 0;
} catch (SQLException e) {
logger.severe("Failed to delete conversation: " + e.getMessage());
e.printStackTrace();
return false;
}
}
public Map<String, String> listConversations(UUID playerUuid, String aiModel) {
Map<String, String> conversations = new HashMap<>();
String sql = "SELECT conversation_id, conversation_name FROM conversations WHERE player_uuid = ? AND ai_model = ?";
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, playerUuid.toString());
pstmt.setString(2, aiModel);
pstmt.setString(3, prompt);
pstmt.setString(4, response);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
conversations.put(rs.getString("conversation_id"), rs.getString("conversation_name"));
}
}
} catch (SQLException e) {
logger.severe("Failed to list conversations: " + e.getMessage());
e.printStackTrace();
}
return conversations;
}
public void saveChatHistory(UUID playerUuid, String aiModel, String conversationId, String prompt, String response) {
String sql = "INSERT INTO chat_history (player_uuid, ai_model, conversation_id, prompt, response) VALUES (?, ?, ?, ?, ?)";
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, playerUuid.toString());
pstmt.setString(2, aiModel);
if (conversationId != null) {
pstmt.setString(3, conversationId);
} else {
pstmt.setNull(3, Types.VARCHAR);
}
pstmt.setString(4, prompt);
pstmt.setString(5, response);
pstmt.executeUpdate();
} catch (SQLException e) {
logger.severe("Failed to save chat history: " + e.getMessage());
e.printStackTrace();
}
}
public String getChatHistory(UUID playerUuid, String aiModel, int maxHistory) {
public String getChatHistory(UUID playerUuid, String aiModel, String conversationId, int maxHistory) {
StringBuilder history = new StringBuilder();
String sql = "SELECT prompt, response FROM chat_history " +
"WHERE player_uuid = ? AND ai_model = ? " +
(conversationId != null ? "AND conversation_id = ? " : "AND conversation_id IS NULL ") +
"ORDER BY timestamp DESC LIMIT ?";
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, playerUuid.toString());
pstmt.setString(2, aiModel);
pstmt.setInt(3, maxHistory);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
history.insert(0, "User: " + rs.getString("prompt") + "\n");
history.insert(0, "AI: " + rs.getString("response") + "\n");
int index = 3;
if (conversationId != null) {
pstmt.setString(index++, conversationId);
}
pstmt.setInt(index, maxHistory);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
history.insert(0, "User: " + rs.getString("prompt") + "\n");
history.insert(0, "AI: " + rs.getString("response") + "\n");
}
}
} catch (SQLException e) {
logger.severe("Failed to get chat history: " + e.getMessage());
e.printStackTrace();
}
return history.toString();
@ -83,11 +310,20 @@ public class DatabaseManager {
public void close() {
try {
if (connection != null) {
connection.close();
if (databaseType.equals("sqlite") && sqliteConnection != null && !sqliteConnection.isClosed()) {
sqliteConnection.close();
}
// No need to close MySQL connections explicitly as they are managed per query
} catch (SQLException e) {
logger.severe("Failed to close database: " + e.getMessage());
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,114 @@
package com.ollamachat;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
public class DependencyLoader {
private final JavaPlugin plugin;
private final Logger logger;
private final File libDir;
private URLClassLoader dependencyClassLoader;
public DependencyLoader(JavaPlugin plugin) {
this.plugin = plugin;
this.logger = plugin.getLogger();
this.libDir = new File(plugin.getDataFolder(), "libs");
}
public boolean loadDependencies() {
try {
// Create libs directory if it doesn't exist
if (!libDir.exists()) {
libDir.mkdirs();
}
// Define dependencies to download
List<Dependency> dependencies = new ArrayList<>();
dependencies.add(new Dependency("com.mysql", "mysql-connector-j", "8.0.33"));
dependencies.add(new Dependency("org.xerial", "sqlite-jdbc", "3.46.0.0"));
// Download each dependency
List<URL> jarUrls = new ArrayList<>();
for (Dependency dep : dependencies) {
File jarFile = downloadDependency(dep);
if (jarFile != null) {
jarUrls.add(jarFile.toURI().toURL());
logger.info("Found dependency: " + jarFile.getName());
} else {
logger.warning("Failed to download dependency: " + dep.artifactId + "-" + dep.version);
}
}
if (jarUrls.isEmpty()) {
logger.warning("No dependencies were successfully downloaded.");
return false;
}
// Create a new classloader with the dependencies
dependencyClassLoader = new URLClassLoader(
jarUrls.toArray(new URL[0]),
plugin.getClass().getClassLoader()
);
// Set this classloader as the thread context classloader
Thread.currentThread().setContextClassLoader(dependencyClassLoader);
logger.info("Successfully loaded " + jarUrls.size() + " dependencies");
return true;
} catch (Exception e) {
logger.severe("Failed to load dependencies: " + e.getMessage());
e.printStackTrace();
return false;
}
}
public ClassLoader getDependencyClassLoader() {
return dependencyClassLoader;
}
private File downloadDependency(Dependency dep) {
String fileName = dep.artifactId + "-" + dep.version + ".jar";
File file = new File(libDir, fileName);
if (file.exists()) {
return file;
}
String mavenPath = String.format("%s/%s/%s/%s-%s.jar",
dep.groupId.replace(".", "/"), dep.artifactId, dep.version, dep.artifactId, dep.version);
String url = "https://repo1.maven.org/maven2/" + mavenPath;
try (InputStream in = new URL(url).openStream()) {
Files.copy(in, file.toPath());
logger.info("Downloaded dependency: " + fileName);
return file;
} catch (IOException e) {
logger.severe("Failed to download dependency " + fileName + ": " + e.getMessage());
return null;
}
}
private static class Dependency {
String groupId;
String artifactId;
String version;
Dependency(String groupId, String artifactId, String version) {
this.groupId = groupId;
this.artifactId = artifactId;
this.version = version;
}
}
}

View File

@ -1,352 +0,0 @@
package com.ollamachat;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class Ollamachat extends JavaPlugin implements Listener {
private AIService aiService;
private Gson gson;
private String ollamaApiUrl;
private String ollamaModel;
private String triggerPrefix;
private int maxResponseLength;
private Map<String, AIConfig> otherAIConfigs;
private boolean ollamaEnabled;
private Map<String, Boolean> otherAIEnabled;
private FileConfiguration langConfig;
private DatabaseManager databaseManager;
private ChatHistoryManager chatHistoryManager;
private int maxHistory;
@Override
public void onEnable() {
saveDefaultConfig();
reloadConfigValues();
String language = getConfig().getString("language", "en");
loadLanguageFile(language);
updateCommandUsages();
databaseManager = new DatabaseManager();
maxHistory = getConfig().getInt("max-history", 5);
chatHistoryManager = new ChatHistoryManager(databaseManager, maxHistory);
aiService = new AIService();
gson = new Gson();
getServer().getPluginManager().registerEvents(this, this);
getCommand("ollamachat").setExecutor(this);
getCommand("aichat").setExecutor(this);
}
private void updateCommandUsages() {
String usageOllamachat = getMessage("usage-ollamachat", null);
String usageAichat = getMessage("usage-aichat", null);
getCommand("ollamachat").setUsage(usageOllamachat);
getCommand("aichat").setUsage(usageAichat);
}
@Override
public void onDisable() {
databaseManager.close();
}
private void updateConfig() {
FileConfiguration config = getConfig();
if (!config.contains("ollama-enabled")) {
config.set("ollama-enabled", true);
}
if (!config.contains("language")) {
config.set("language", "en");
}
if (!config.contains("other-ai-configs")) {
config.createSection("other-ai-configs");
}
if (!config.contains("max-history")) {
config.set("max-history", 5);
}
saveConfig();
}
private void reloadConfigValues() {
File configFile = new File(getDataFolder(), "config.yml");
if (!configFile.exists()) {
saveDefaultConfig();
} else {
try {
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
if (!config.contains("ollama-api-url") || !config.contains("model")) {
getLogger().warning(getMessage("config-invalid", null));
configFile.delete();
saveDefaultConfig();
}
} catch (Exception e) {
getLogger().severe(getMessage("config-load-failed", Map.of("error", e.getMessage())));
configFile.delete();
saveDefaultConfig();
}
}
reloadConfig();
updateConfig();
FileConfiguration config = getConfig();
ollamaApiUrl = config.getString("ollama-api-url", "http://localhost:11434/api/generate");
ollamaModel = config.getString("model", "llama3");
triggerPrefix = config.getString("trigger-prefix", "@bot ");
maxResponseLength = config.getInt("max-response-length", 500);
ollamaEnabled = config.getBoolean("ollama-enabled", true);
maxHistory = config.getInt("max-history", 5);
otherAIConfigs = new HashMap<>();
otherAIEnabled = new HashMap<>();
if (config.contains("other-ai-configs")) {
for (String aiName : config.getConfigurationSection("other-ai-configs").getKeys(false)) {
String apiUrl = config.getString("other-ai-configs." + aiName + ".api-url");
String apiKey = config.getString("other-ai-configs." + aiName + ".api-key");
String model = config.getString("other-ai-configs." + aiName + ".model");
boolean enabled = config.getBoolean("other-ai-configs." + aiName + ".enabled", true);
otherAIConfigs.put(aiName, new AIConfig(apiUrl, apiKey, model));
otherAIEnabled.put(aiName, enabled);
}
}
}
private void loadLanguageFile(String language) {
File langFolder = new File(getDataFolder(), "lang");
if (!langFolder.exists()) {
langFolder.mkdirs();
}
File langFile = new File(langFolder, language + ".lang");
if (!langFile.exists()) {
saveResource("lang/" + language + ".lang", false);
}
try {
langConfig = YamlConfiguration.loadConfiguration(langFile);
} catch (Exception e) {
getLogger().severe("Failed to load language file: " + langFile.getName());
e.printStackTrace();
}
}
private String getMessage(String key, Map<String, String> placeholders) {
String message = langConfig.getString(key, "§cMissing language key: " + key);
if (placeholders != null) {
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
message = message.replace("{" + entry.getKey() + "}", entry.getValue());
}
}
return message;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
chatHistoryManager.savePlayerInfo(event.getPlayer());
}
@EventHandler
public void onPlayerChat(AsyncPlayerChatEvent event) {
if (!ollamaEnabled) return;
String message = event.getMessage();
Player player = event.getPlayer();
if (message.startsWith(triggerPrefix)) {
/*event.setCancelled(true);*/
String prompt = message.substring(triggerPrefix.length()).trim();
if (!prompt.isEmpty()) {
processOllamaQueryAsync(player, prompt);
}
}
}
private void processOllamaQueryAsync(Player player, String prompt) {
CompletableFuture.runAsync(() -> {
try {
String history = chatHistoryManager.getChatHistory(player.getUniqueId(), "ollama");
String context = history + "User: " + prompt;
String responseBody = aiService.sendRequest(ollamaApiUrl, null, ollamaModel, context).join();
OllamaResponse ollamaResponse = gson.fromJson(responseBody, OllamaResponse.class);
chatHistoryManager.saveChatHistory(player.getUniqueId(), "ollama", prompt, ollamaResponse.response);
sendFormattedResponse(player, ollamaResponse.response);
} catch (Exception e) {
getLogger().severe("Error processing Ollama request: " + e.getMessage());
sendErrorMessage(player, getMessage("error-prefix", null) + "Failed to get response from Ollama");
}
});
}
private void processOtherAIQueryAsync(Player player, String aiName, String prompt) {
if (!otherAIEnabled.getOrDefault(aiName, false)) {
sendErrorMessage(player, getMessage("error-prefix", null) + getMessage("toggle-disabled", Map.of("ai-name", aiName)));
return;
}
CompletableFuture.runAsync(() -> {
try {
String history = chatHistoryManager.getChatHistory(player.getUniqueId(), aiName);
String context = history + "User: " + prompt;
AIConfig aiConfig = otherAIConfigs.get(aiName);
String responseBody = aiService.sendRequest(
aiConfig.getApiUrl(),
aiConfig.getApiKey(),
aiConfig.getModel(),
context
).join();
String response = parseAIResponse(aiName, responseBody);
chatHistoryManager.saveChatHistory(
player.getUniqueId(),
aiName,
prompt,
response
);
sendFormattedResponse(player, response);
} catch (Exception e) {
getLogger().severe("Error processing " + aiName + " request: " + e.getMessage());
sendErrorMessage(player, getMessage("error-prefix", null) + "Failed to get response from " + aiName);
}
});
}
private String parseAIResponse(String aiName, String responseBody) {
switch (aiName.toLowerCase()) {
case "openai":
JsonObject json = gson.fromJson(responseBody, JsonObject.class);
return json.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.get("message").getAsJsonObject()
.get("content").getAsString();
default:
return gson.fromJson(responseBody, OllamaResponse.class).response;
}
}
private void sendFormattedResponse(Player player, String response) {
response = response.replaceAll("User:", "§7User:").replaceAll("AI:", "");
if (response.trim().isEmpty()) {
Bukkit.broadcast(Component.text(getMessage("response-prefix", null) + "§cNastala chyba, zkuste se zeptat znovu."));
return;
}
if (response.length() > maxResponseLength) {
response = response.substring(0, maxResponseLength) + "...";
}
Bukkit.broadcast(Component.text(getMessage("response-prefix", null) + response));
/*player.sendMessage(getMessage("response-prefix", null) + response);*/
}
private void sendErrorMessage(Player player, String errorMessage) {
player.sendMessage(getMessage("error-prefix", null) + errorMessage);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (command.getName().equalsIgnoreCase("ollamachat")) {
if (args.length > 0 && args[0].equalsIgnoreCase("reload")) {
reloadConfigValues();
loadLanguageFile(getConfig().getString("language", "en"));
sender.sendMessage(getMessage("reload-success", null));
return true;
} else if (args.length > 1 && args[0].equalsIgnoreCase("toggle")) {
String aiName = args[1];
if (aiName.equalsIgnoreCase("ollama")) {
ollamaEnabled = !ollamaEnabled;
sender.sendMessage(getMessage(ollamaEnabled ? "ollama-enabled" : "ollama-disabled", null));
} else if (otherAIConfigs.containsKey(aiName)) {
boolean newState = !otherAIEnabled.getOrDefault(aiName, false);
otherAIEnabled.put(aiName, newState);
sender.sendMessage(getMessage(newState ? "toggle-enabled" : "toggle-disabled", Map.of("ai-name", aiName)));
} else {
sender.sendMessage(getMessage("invalid-ai-name", Map.of("ai-list", String.join(", ", otherAIConfigs.keySet()))));
}
return true;
}
} else if (command.getName().equalsIgnoreCase("aichat")) {
if (args.length < 2) {
sender.sendMessage(getMessage("usage-aichat", null));
return true;
}
String aiName = args[0];
String prompt = String.join(" ", java.util.Arrays.copyOfRange(args, 1, args.length));
if (otherAIConfigs.containsKey(aiName)) {
if (sender instanceof Player) {
processOtherAIQueryAsync((Player) sender, aiName, prompt);
} else {
sender.sendMessage(getMessage("player-only", null));
}
} else {
sender.sendMessage(getMessage("invalid-ai-name", Map.of("ai-list", String.join(", ", otherAIConfigs.keySet()))));
}
return true;
}
return false;
}
private static class AIConfig {
private final String apiUrl;
private final String apiKey;
private final String model;
public AIConfig(String apiUrl, String apiKey, String model) {
this.apiUrl = apiUrl;
this.apiKey = apiKey;
this.model = model;
}
public String getApiUrl() {
return apiUrl;
}
public String getApiKey() {
return apiKey;
}
public String getModel() {
return model;
}
}
private static class OllamaResponse {
public String response;
}
}

View File

@ -0,0 +1,104 @@
package com.ollamachat;
import com.ollamachat.core.Ollamachat;
import com.ollamachat.core.ConfigManager;
import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.boss.BossBar;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class ProgressManager {
private final Ollamachat plugin;
private final ConfigManager configManager; // Add ConfigManager reference
private final Map<UUID, BossBar> bossBars = new HashMap<>();
private final Map<UUID, BukkitTask> tasks = new HashMap<>();
public ProgressManager(Ollamachat plugin) {
this.plugin = plugin;
this.configManager = plugin.getConfigManager(); // Initialize ConfigManager
}
public void startProgress(Player player, String title, BarColor color, BarStyle style) {
UUID uuid = player.getUniqueId();
cleanup(player);
if (plugin.getConfig().getString("progress-display.type", "bossbar").equalsIgnoreCase("bossbar")) {
handleBossBarProgress(player, uuid, title, color, style);
} else {
handleActionBarProgress(player, uuid);
}
}
private void handleBossBarProgress(Player player, UUID uuid, String title, BarColor color, BarStyle style) {
BossBar bossBar = Bukkit.createBossBar(
configManager.getMessage("generating-status", Map.of("progress", "0")), // Use configManager
color,
style
);
bossBar.addPlayer(player);
bossBar.setProgress(0.0);
bossBar.setVisible(true);
bossBars.put(uuid, bossBar);
long interval = 20L * plugin.getConfig().getInt("progress-display.update-interval", 1);
tasks.put(uuid, Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (!player.isOnline()) {
cleanup(player);
}
}, 0, interval));
}
private void handleActionBarProgress(Player player, UUID uuid) {
long interval = 20L * plugin.getConfig().getInt("progress-display.update-interval", 1);
tasks.put(uuid, Bukkit.getScheduler().runTaskTimer(plugin, () -> {
if (!player.isOnline()) {
cleanup(player);
return;
}
player.sendActionBar(configManager.getMessage("generating-status", Map.of("progress", "0"))); // Use configManager
}, 0, interval));
}
public void complete(Player player) {
UUID uuid = player.getUniqueId();
if (bossBars.containsKey(uuid)) {
BossBar bossBar = bossBars.get(uuid);
bossBar.setProgress(1.0);
bossBar.setTitle(configManager.getMessage("complete-status", null)); // Use configManager
bossBar.setColor(BarColor.GREEN);
Bukkit.getScheduler().runTaskLater(plugin, () -> cleanup(player), 40L);
} else {
Bukkit.getScheduler().runTaskLater(plugin, () -> cleanup(player), 40L);
}
}
public void error(Player player) {
UUID uuid = player.getUniqueId();
if (bossBars.containsKey(uuid)) {
BossBar bossBar = bossBars.get(uuid);
bossBar.setTitle(configManager.getMessage("error-status", null)); // Use configManager
bossBar.setColor(BarColor.RED);
Bukkit.getScheduler().runTaskLater(plugin, () -> cleanup(player), 40L);
}
}
public void cleanup(Player player) {
UUID uuid = player.getUniqueId();
if (tasks.containsKey(uuid)) {
tasks.get(uuid).cancel();
tasks.remove(uuid);
}
if (bossBars.containsKey(uuid)) {
bossBars.get(uuid).removeAll();
bossBars.remove(uuid);
}
}
}

View File

@ -0,0 +1,164 @@
package com.ollamachat.chat;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.ollamachat.AIService;
import com.ollamachat.core.ConfigManager;
import com.ollamachat.core.Ollamachat;
import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor;
import org.bukkit.boss.BarStyle;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
public class ChatTriggerHandler implements Listener {
private final Ollamachat plugin;
private final ConfigManager configManager;
private final AIService aiService;
private final SuggestedResponseHandler suggestedResponseHandler;
private final Gson gson;
public ChatTriggerHandler(Ollamachat plugin) {
this.plugin = plugin;
this.configManager = plugin.getConfigManager();
this.aiService = new AIService();
this.suggestedResponseHandler = new SuggestedResponseHandler(plugin);
this.gson = new Gson();
}
@EventHandler
public void onPlayerChat(AsyncPlayerChatEvent event) {
if (!configManager.isOllamaEnabled()) return;
String message = event.getMessage();
Player player = event.getPlayer();
for (String prefix : configManager.getTriggerPrefixes()) {
if (message.startsWith(prefix)) {
//event.setCancelled(true);
String prompt = message.substring(prefix.length()).trim();
if (!prompt.isEmpty()) {
processAIQuery(player, "ollama", prompt);
}
break;
}
}
}
public void processAIQuery(Player player, String aiName, String prompt) {
if (!aiName.equalsIgnoreCase("ollama") && !configManager.getOtherAIEnabled().getOrDefault(aiName, false)) {
sendErrorMessage(player, configManager.getMessage("error-prefix", null) + configManager.getMessage("toggle-disabled", Map.of("ai-name", aiName)));
return;
}
if (plugin.getConfig().getBoolean("progress-display.enabled", true)) {
BarColor color = BarColor.valueOf(plugin.getConfig().getString("progress-display.color", "BLUE"));
BarStyle style = BarStyle.valueOf(plugin.getConfig().getString("progress-display.style", "SOLID"));
plugin.getProgressManager().startProgress(player, configManager.getMessage("generating-status", null), color, style);
}
CompletableFuture.runAsync(() -> {
try {
UUID playerUuid = player.getUniqueId();
// Save player info to ensure uuid exists in players table
plugin.getChatHistoryManager().savePlayerInfo(player);
String conversationName = configManager.getSelectedConversations()
.computeIfAbsent(playerUuid, k -> new HashMap<>())
.getOrDefault(aiName, null);
String conversationId = conversationName != null
? plugin.getChatHistoryManager().getConversationId(playerUuid, aiName, conversationName)
: null;
String history = plugin.getChatHistoryManager().getChatHistory(playerUuid, aiName, conversationId);
String selectedPrompt = configManager.getPrompts().getOrDefault(configManager.getDefaultPrompt(), "");
String context = history + (selectedPrompt.isEmpty() ? "" : selectedPrompt + "\n") + "User: " + prompt;
String apiUrl, apiKey, model;
boolean isMessagesFormat;
if (aiName.equalsIgnoreCase("ollama")) {
apiUrl = configManager.getOllamaApiUrl();
apiKey = null;
model = configManager.getOllamaModel();
isMessagesFormat = false;
} else {
ConfigManager.AIConfig aiConfig = configManager.getOtherAIConfigs().get(aiName);
apiUrl = aiConfig.getApiUrl();
apiKey = aiConfig.getApiKey();
model = aiConfig.getModel();
isMessagesFormat = aiConfig.isMessagesFormat();
}
String finalResponse;
if (configManager.isStreamingEnabled()) {
StringBuilder fullResponse = new StringBuilder();
AtomicBoolean isFirstMessage = new AtomicBoolean(true);
aiService.sendStreamingRequest(apiUrl, apiKey, model, context, partialResponse -> {
if (player.isOnline()) {
String formattedPartial = partialResponse.length() > configManager.getMaxResponseLength()
? partialResponse.substring(0, configManager.getMaxResponseLength()) + "..."
: partialResponse;
String message = isFirstMessage.get()
? configManager.getMessage("response-prefix", null) + formattedPartial
: formattedPartial;
//player.sendMessage(message);
Bukkit.broadcastMessage(message);
isFirstMessage.set(false);
fullResponse.append(partialResponse);
}
}, isMessagesFormat).join();
finalResponse = fullResponse.toString();
} else {
String responseBody = aiService.sendRequest(apiUrl, apiKey, model, context, isMessagesFormat).join();
if (isMessagesFormat) {
JsonObject json = gson.fromJson(responseBody, JsonObject.class);
finalResponse = json.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.get("message").getAsJsonObject()
.get("content").getAsString();
} else {
JsonObject json = gson.fromJson(responseBody, JsonObject.class);
finalResponse = json.get("response").getAsString();
}
if (player.isOnline()) {
sendFormattedResponse(player, finalResponse);
}
}
if (!finalResponse.isEmpty()) {
plugin.getChatHistoryManager().saveChatHistory(playerUuid, aiName, conversationId, prompt, finalResponse);
suggestedResponseHandler.sendSuggestedResponses(player, aiName, prompt, finalResponse);
}
plugin.getProgressManager().complete(player);
} catch (Exception e) {
plugin.getLogger().severe("Error processing " + aiName + " request: " + e.getMessage());
if (player.isOnline()) {
sendErrorMessage(player, configManager.getMessage("error-prefix", null) + "Failed to get response from " + aiName);
}
plugin.getProgressManager().error(player);
}
});
}
private void sendFormattedResponse(Player player, String response) {
if (response.length() > configManager.getMaxResponseLength()) {
response = response.substring(0, configManager.getMaxResponseLength()) + "...";
}
//player.sendMessage(configManager.getMessage("response-prefix", null) + response);
Bukkit.broadcastMessage(configManager.getMessage("response-prefix", null) + response);
}
private void sendErrorMessage(Player player, String errorMessage) {
player.sendMessage(errorMessage);
}
}

View File

@ -0,0 +1,181 @@
package com.ollamachat.chat;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.ollamachat.AIService;
import com.ollamachat.core.ConfigManager;
import com.ollamachat.core.Ollamachat;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.*;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class SuggestedResponseHandler {
private final Ollamachat plugin;
private final ConfigManager configManager;
private final AIService aiService;
private final Gson gson;
private final Map<UUID, Long> lastSuggestionTimes;
public SuggestedResponseHandler(Ollamachat plugin) {
this.plugin = plugin;
this.configManager = plugin.getConfigManager();
this.aiService = new AIService();
this.gson = new Gson();
this.lastSuggestionTimes = new HashMap<>();
}
public boolean isSuggestionsEnabledForPlayer(Player player) {
Map<UUID, Boolean> playerSuggestionToggles = plugin.getPlayerSuggestionToggles();
return playerSuggestionToggles.computeIfAbsent(player.getUniqueId(), k -> true);
}
public void toggleSuggestionsForPlayer(Player player, boolean enabled) {
plugin.getPlayerSuggestionToggles().put(player.getUniqueId(), enabled);
}
public boolean canGenerateSuggestions(Player player) {
int cooldown = configManager.getSuggestedResponseCooldown();
if (cooldown <= 0) return true;
long currentTime = System.currentTimeMillis();
Long lastTime = lastSuggestionTimes.get(player.getUniqueId());
if (lastTime == null || (currentTime - lastTime) / 1000 >= cooldown) {
lastSuggestionTimes.put(player.getUniqueId(), currentTime);
return true;
}
return false;
}
public void sendSuggestedResponses(Player player, String originalAIName, String originalPrompt, String originalResponse) {
if (!configManager.isSuggestedResponsesEnabled() || !isSuggestionsEnabledForPlayer(player)) return;
if (!canGenerateSuggestions(player)) {
int cooldown = configManager.getSuggestedResponseCooldown();
long lastTime = lastSuggestionTimes.get(player.getUniqueId());
long secondsRemaining = cooldown - (System.currentTimeMillis() - lastTime) / 1000;
sendMessage(player, ChatColor.RED + configManager.getMessage("suggests-rate-limit",
Map.of("seconds", String.valueOf(secondsRemaining))));
return;
}
List<String> suggestedResponses = new ArrayList<>();
if (configManager.isSuggestedResponsePresetsEnabled()) {
suggestedResponses.addAll(configManager.getSuggestedResponsePresets());
}
List<String> suggestedModels = configManager.getSuggestedResponseModels();
if (!suggestedModels.isEmpty()) {
CompletableFuture.runAsync(() -> {
for (String model : suggestedModels) {
if (model.equalsIgnoreCase(originalAIName)) continue;
if (!configManager.getSuggestedResponseModelToggles().getOrDefault(model, true)) continue;
try {
String apiUrl;
String apiKey;
boolean isMessagesFormat;
ConfigManager.AIConfig aiConfig = configManager.getOtherAIConfigs().get(model);
if (aiConfig != null && configManager.getOtherAIEnabled().getOrDefault(model, false)) {
apiUrl = aiConfig.getApiUrl();
apiKey = aiConfig.getApiKey();
isMessagesFormat = aiConfig.isMessagesFormat();
} else {
apiUrl = configManager.getOllamaApiUrl();
apiKey = null;
isMessagesFormat = false;
}
String promptTemplate = configManager.getSuggestedResponsePrompt();
String context = promptTemplate
.replace("{prompt}", originalPrompt)
.replace("{response}", originalResponse)
.replace("{count}", String.valueOf(configManager.getSuggestedResponseCount()));
String responseBody = aiService.sendRequest(apiUrl, apiKey, model, context, isMessagesFormat).join();
String suggestedText;
if (isMessagesFormat) {
JsonObject json = gson.fromJson(responseBody, JsonObject.class);
suggestedText = json.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.get("message").getAsJsonObject()
.get("content").getAsString();
} else {
JsonObject json = gson.fromJson(responseBody, JsonObject.class);
suggestedText = json.get("response").getAsString();
}
String[] suggestions = suggestedText.split("\n");
for (String suggestion : suggestions) {
String cleanedSuggestion = suggestion.replaceAll("^\\d+\\.\\s*", "").trim();
if (!cleanedSuggestion.isEmpty()) {
suggestedResponses.add("[" + model + "] " + cleanedSuggestion);
}
}
} catch (Exception e) {
plugin.getLogger().warning("Failed to get suggested response from " + model + ": " + e.getMessage());
}
}
if (player.isOnline() && !suggestedResponses.isEmpty()) {
sendMessage(player, ChatColor.GREEN + configManager.getMessage("suggested-responses-header", null));
for (String response : suggestedResponses) {
if (response.length() > configManager.getMaxResponseLength()) {
response = response.substring(0, configManager.getMaxResponseLength()) + "...";
sendMessage(player, ChatColor.YELLOW + configManager.getMessage("response-truncated", null));
}
sendClickableMessage(player,
configManager.getMessage("suggested-response-prefix", null) + response,
"/aichat " + originalAIName + " " + response.replace("\"", "\\\""),
configManager.getMessage("suggested-response-hover", null));
}
}
});
} else if (player.isOnline() && !suggestedResponses.isEmpty()) {
sendMessage(player, ChatColor.GREEN + configManager.getMessage("suggested-responses-header", null));
for (String response : suggestedResponses) {
if (response.length() > configManager.getMaxResponseLength()) {
response = response.substring(0, configManager.getMaxResponseLength()) + "...";
sendMessage(player, ChatColor.YELLOW + configManager.getMessage("response-truncated", null));
}
sendClickableMessage(player,
configManager.getMessage("suggested-response-prefix", null) + response,
"/aichat " + originalAIName + " " + response.replace("\"", "\\\""),
configManager.getMessage("suggested-response-hover", null));
}
}
}
private void sendMessage(Player player, String message) {
if (player.isOnline()) {
player.sendMessage(message);
}
}
private void sendClickableMessage(Player player, String text, String command, String hoverText) {
TextComponent message = new TextComponent(text);
message.setColor(ChatColor.WHITE);
message.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, command));
message.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
new ComponentBuilder(hoverText).color(ChatColor.YELLOW).create()));
player.spigot().sendMessage(message);
}
}

View File

@ -0,0 +1,48 @@
package com.ollamachat.command;
import com.ollamachat.core.Ollamachat;
import com.ollamachat.core.ConfigManager;
import com.ollamachat.chat.ChatTriggerHandler;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.Map;
public class AIChatCommand implements CommandExecutor {
private final Ollamachat plugin;
private final ConfigManager configManager;
private final ChatTriggerHandler chatTriggerHandler;
public AIChatCommand(Ollamachat plugin) {
this.plugin = plugin;
this.configManager = plugin.getConfigManager();
this.chatTriggerHandler = new ChatTriggerHandler(plugin);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length < 2) {
sender.sendMessage(configManager.getMessage("usage-aichat", null));
return true;
}
String aiName = args[0];
String prompt = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
if (aiName.equalsIgnoreCase("ollama") || configManager.getOtherAIConfigs().containsKey(aiName)) {
if (sender instanceof Player) {
chatTriggerHandler.processAIQuery((Player) sender, aiName, prompt);
} else {
sender.sendMessage(configManager.getMessage("player-only", null));
}
} else {
sender.sendMessage(configManager.getMessage("invalid-ai-name", Map.of("ai-list", String.join(", ", configManager.getOtherAIConfigs().keySet()))));
}
return true;
}
}

View File

@ -0,0 +1,265 @@
package com.ollamachat.command;
import com.ollamachat.core.Ollamachat;
import com.ollamachat.core.ConfigManager;
import com.ollamachat.chat.ChatTriggerHandler;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class OllamaChatCommand implements CommandExecutor {
private final Ollamachat plugin;
private final ConfigManager configManager;
private final ChatTriggerHandler chatTriggerHandler;
public OllamaChatCommand(Ollamachat plugin) {
this.plugin = plugin;
this.configManager = plugin.getConfigManager();
this.chatTriggerHandler = new ChatTriggerHandler(plugin);
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("usage-ollamachat", null));
return true;
}
if (args[0].equalsIgnoreCase("reload")) {
if (!sender.hasPermission("ollamachat.reload")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
configManager.reloadConfigValues();
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("reload-success", null));
return true;
} else if (args[0].equalsIgnoreCase("toggle") && args.length > 1) {
if (!sender.hasPermission("ollamachat.toggle")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
String aiName = args[1];
if (aiName.equalsIgnoreCase("ollama")) {
configManager.setOllamaEnabled(!configManager.isOllamaEnabled());
sender.sendMessage(ChatColor.GREEN + configManager.getMessage(configManager.isOllamaEnabled() ? "ollama-enabled" : "ollama-disabled", null));
} else if (configManager.getOtherAIConfigs().containsKey(aiName)) {
boolean newState = !configManager.getOtherAIEnabled().getOrDefault(aiName, false);
configManager.getOtherAIEnabled().put(aiName, newState);
sender.sendMessage(ChatColor.GREEN + configManager.getMessage(newState ? "toggle-enabled" : "toggle-disabled", Map.of("ai-name", aiName)));
} else {
sender.sendMessage(ChatColor.RED + configManager.getMessage("invalid-ai-name", Map.of("ai-list", String.join(", ", configManager.getOtherAIConfigs().keySet()))));
}
return true;
} else if (args[0].equalsIgnoreCase("prompt") && args.length > 1) {
String subCommand = args[1].toLowerCase();
if (subCommand.equals("set") && args.length > 3) {
if (!sender.hasPermission("ollamachat.prompt.set")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
String promptName = args[2];
String promptContent = String.join(" ", Arrays.copyOfRange(args, 3, args.length));
plugin.getConfig().set("prompts." + promptName, promptContent);
plugin.saveConfig();
configManager.getPrompts().put(promptName, promptContent);
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-set", Map.of("name", promptName)));
return true;
} else if (subCommand.equals("delete") && args.length == 3) {
if (!sender.hasPermission("ollamachat.prompt.delete")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
String promptName = args[2];
if (configManager.getPrompts().containsKey(promptName)) {
plugin.getConfig().set("prompts." + promptName, null);
if (configManager.getDefaultPrompt().equals(promptName)) {
plugin.getConfig().set("default-prompt", "");
configManager.setDefaultPrompt("");
}
plugin.saveConfig();
configManager.getPrompts().remove(promptName);
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-deleted", Map.of("name", promptName)));
} else {
sender.sendMessage(ChatColor.RED + configManager.getMessage("prompt-not-found", Map.of("name", promptName)));
}
return true;
} else if (subCommand.equals("list")) {
if (!sender.hasPermission("ollamachat.prompt.list")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
if (configManager.getPrompts().isEmpty()) {
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-list-empty", null));
} else {
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-list", Map.of("prompts", String.join(", ", configManager.getPrompts().keySet()))));
}
if (!configManager.getDefaultPrompt().isEmpty() && configManager.getPrompts().containsKey(configManager.getDefaultPrompt())) {
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-default", Map.of("name", configManager.getDefaultPrompt())));
} else if (!configManager.getDefaultPrompt().isEmpty()) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("prompt-default-invalid", Map.of("name", configManager.getDefaultPrompt())));
}
return true;
} else if (subCommand.equals("select") && args.length == 3) {
if (!sender.hasPermission("ollamachat.prompt.select")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
String promptName = args[2];
if (configManager.getPrompts().containsKey(promptName)) {
plugin.getConfig().set("default-prompt", promptName);
plugin.saveConfig();
configManager.setDefaultPrompt(promptName);
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-selected", Map.of("name", promptName)));
} else {
sender.sendMessage(ChatColor.RED + configManager.getMessage("prompt-not-found", Map.of("name", promptName)));
}
return true;
} else if (subCommand.equals("clear")) {
if (!sender.hasPermission("ollamachat.prompt.select")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
plugin.getConfig().set("default-prompt", "");
plugin.saveConfig();
configManager.setDefaultPrompt("");
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("prompt-cleared", null));
return true;
} else {
sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("prompt-usage", null));
return true;
}
} else if (args[0].equalsIgnoreCase("conversation") && args.length > 1) {
if (!(sender instanceof Player)) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("player-only", null));
return true;
}
Player player = (Player) sender;
String subCommand = args[1].toLowerCase();
String aiName = args.length > 2 ? args[2] : "ollama";
if (!aiName.equals("ollama") && !configManager.getOtherAIConfigs().containsKey(aiName)) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("invalid-ai-name", Map.of("ai-list", String.join(", ", configManager.getOtherAIConfigs().keySet()))));
return true;
}
if (subCommand.equals("new") && args.length == 4) {
if (!sender.hasPermission("ollamachat.conversation.new")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
String convName = args[3];
String convId = plugin.getChatHistoryManager().createConversation(player.getUniqueId(), aiName, convName);
configManager.getSelectedConversations().computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()).put(aiName, convName);
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-created", Map.of("name", convName, "ai-name", aiName)));
return true;
} else if (subCommand.equals("select") && args.length == 4) {
if (!sender.hasPermission("ollamachat.conversation.select")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
String convName = args[3];
if (plugin.getChatHistoryManager().conversationExistsByName(player.getUniqueId(), aiName, convName)) {
configManager.getSelectedConversations().computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()).put(aiName, convName);
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-selected", Map.of("name", convName, "ai-name", aiName)));
} else {
sender.sendMessage(ChatColor.RED + configManager.getMessage("conversation-not-found", Map.of("name", convName)));
}
return true;
} else if (subCommand.equals("delete") && args.length == 4) {
if (!sender.hasPermission("ollamachat.conversation.delete")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
String convName = args[3];
String convId = plugin.getChatHistoryManager().getConversationId(player.getUniqueId(), aiName, convName);
if (convId != null && plugin.getChatHistoryManager().deleteConversation(player.getUniqueId(), aiName, convId)) {
Map<String, String> convMap = configManager.getSelectedConversations().get(player.getUniqueId());
if (convMap != null && convName.equals(convMap.get(aiName))) {
convMap.remove(aiName);
}
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-deleted", Map.of("name", convName, "ai-name", aiName)));
} else {
sender.sendMessage(ChatColor.RED + configManager.getMessage("conversation-not-found", Map.of("name", convName)));
}
return true;
} else if (subCommand.equals("list") && args.length == 3) {
if (!sender.hasPermission("ollamachat.conversation.list")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
Map<String, String> conversations = plugin.getChatHistoryManager().listConversations(player.getUniqueId(), aiName);
if (conversations.isEmpty()) {
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-list-empty", Map.of("ai-name", aiName)));
} else {
String convList = String.join(", ", conversations.values());
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-list", Map.of("conversations", convList, "ai-name", aiName)));
}
String selectedConv = configManager.getSelectedConversations().computeIfAbsent(player.getUniqueId(), k -> new HashMap<>()).get(aiName);
if (selectedConv != null && conversations.containsValue(selectedConv)) {
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("conversation-default", Map.of("name", selectedConv, "ai-name", aiName)));
} else if (selectedConv != null) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("conversation-default-invalid", Map.of("name", selectedConv, "ai-name", aiName)));
}
return true;
} else {
sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("conversation-usage", null));
return true;
}
} else if (args[0].equalsIgnoreCase("suggests") && args.length == 2) {
if (!(sender instanceof Player)) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("player-only", null));
return true;
}
if (!sender.hasPermission("ollamachat.suggests.toggle")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
Player player = (Player) sender;
String subCommand = args[1].toLowerCase();
if (subCommand.equals("on")) {
plugin.getSuggestedResponseHandler().toggleSuggestionsForPlayer(player, true);
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("suggests-enabled", Map.of("player", player.getName())));
return true;
} else if (subCommand.equals("off")) {
plugin.getSuggestedResponseHandler().toggleSuggestionsForPlayer(player, false);
sender.sendMessage(ChatColor.RED + configManager.getMessage("suggests-disabled", Map.of("player", player.getName())));
return true;
} else {
sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("usage-ollamachat", null));
return true;
}
} else if (args[0].equalsIgnoreCase("suggests-presets") && args.length == 2) {
if (!sender.hasPermission("ollamachat.suggests-presets.toggle")) {
sender.sendMessage(ChatColor.RED + configManager.getMessage("no-permission", null));
return true;
}
String subCommand = args[1].toLowerCase();
if (subCommand.equals("on")) {
configManager.setSuggestedResponsePresetsEnabled(true);
sender.sendMessage(ChatColor.GREEN + configManager.getMessage("suggests-presets-enabled", null));
return true;
} else if (subCommand.equals("off")) {
configManager.setSuggestedResponsePresetsEnabled(false);
sender.sendMessage(ChatColor.RED + configManager.getMessage("suggests-presets-disabled", null));
return true;
} else {
sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("usage-ollamachat", null));
return true;
}
}
sender.sendMessage(ChatColor.YELLOW + configManager.getMessage("usage-ollamachat", null));
return true;
}
}

View File

@ -0,0 +1,143 @@
package com.ollamachat.command;
import com.ollamachat.core.Ollamachat;
import com.ollamachat.core.ConfigManager;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class OllamaChatTabCompleter implements TabCompleter {
private final Ollamachat plugin;
private final ConfigManager configManager;
public OllamaChatTabCompleter(Ollamachat plugin) {
this.plugin = plugin;
this.configManager = plugin.getConfigManager();
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
List<String> completions = new ArrayList<>();
if (command.getName().equalsIgnoreCase("ollamachat")) {
if (args.length == 1) {
List<String> subCommands = new ArrayList<>();
if (sender.hasPermission("ollamachat.reload")) {
subCommands.add("reload");
}
if (sender.hasPermission("ollamachat.toggle")) {
subCommands.add("toggle");
}
if (sender.hasPermission("ollamachat.prompt.set") ||
sender.hasPermission("ollamachat.prompt.delete") ||
sender.hasPermission("ollamachat.prompt.list") ||
sender.hasPermission("ollamachat.prompt.select")) {
subCommands.add("prompt");
}
if (sender instanceof Player && (
sender.hasPermission("ollamachat.conversation.new") ||
sender.hasPermission("ollamachat.conversation.select") ||
sender.hasPermission("ollamachat.conversation.delete") ||
sender.hasPermission("ollamachat.conversation.list"))) {
subCommands.add("conversation");
}
if (sender instanceof Player && sender.hasPermission("ollamachat.suggests.toggle")) {
subCommands.add("suggests");
}
if (sender.hasPermission("ollamachat.suggests-presets.toggle")) {
subCommands.add("suggests-presets");
}
return filterCompletions(subCommands, args[0]);
} else if (args.length == 2 && args[0].equalsIgnoreCase("toggle") && sender.hasPermission("ollamachat.toggle")) {
List<String> aiNames = new ArrayList<>();
aiNames.add("ollama");
aiNames.addAll(configManager.getOtherAIConfigs().keySet());
return filterCompletions(aiNames, args[1]);
} else if (args.length == 2 && args[0].equalsIgnoreCase("prompt") && (
sender.hasPermission("ollamachat.prompt.set") ||
sender.hasPermission("ollamachat.prompt.delete") ||
sender.hasPermission("ollamachat.prompt.list") ||
sender.hasPermission("ollamachat.prompt.select"))) {
List<String> promptSubCommands = new ArrayList<>();
if (sender.hasPermission("ollamachat.prompt.set")) {
promptSubCommands.add("set");
}
if (sender.hasPermission("ollamachat.prompt.delete")) {
promptSubCommands.add("delete");
}
if (sender.hasPermission("ollamachat.prompt.list")) {
promptSubCommands.add("list");
}
if (sender.hasPermission("ollamachat.prompt.select")) {
promptSubCommands.add("select");
promptSubCommands.add("clear");
}
return filterCompletions(promptSubCommands, args[1]);
} else if (args.length == 3 && args[0].equalsIgnoreCase("prompt") && (
args[1].equalsIgnoreCase("delete") || args[1].equalsIgnoreCase("select")) &&
(sender.hasPermission("ollamachat.prompt.delete") || sender.hasPermission("ollamachat.prompt.select"))) {
return filterCompletions(new ArrayList<>(configManager.getPrompts().keySet()), args[2]);
} else if (args.length == 2 && args[0].equalsIgnoreCase("conversation") && sender instanceof Player &&
(sender.hasPermission("ollamachat.conversation.new") ||
sender.hasPermission("ollamachat.conversation.select") ||
sender.hasPermission("ollamachat.conversation.delete") ||
sender.hasPermission("ollamachat.conversation.list"))) {
List<String> convSubCommands = new ArrayList<>();
if (sender.hasPermission("ollamachat.conversation.new")) {
convSubCommands.add("new");
}
if (sender.hasPermission("ollamachat.conversation.select")) {
convSubCommands.add("select");
}
if (sender.hasPermission("ollamachat.conversation.delete")) {
convSubCommands.add("delete");
}
if (sender.hasPermission("ollamachat.conversation.list")) {
convSubCommands.add("list");
}
return filterCompletions(convSubCommands, args[1]);
} else if (args.length == 3 && args[0].equalsIgnoreCase("conversation") && sender instanceof Player) {
List<String> aiNames = new ArrayList<>();
aiNames.add("ollama");
aiNames.addAll(configManager.getOtherAIConfigs().keySet());
return filterCompletions(aiNames, args[2]);
} else if (args.length == 4 && args[0].equalsIgnoreCase("conversation") &&
(args[1].equalsIgnoreCase("select") || args[1].equalsIgnoreCase("delete")) && sender instanceof Player &&
(sender.hasPermission("ollamachat.conversation.select") || sender.hasPermission("ollamachat.conversation.delete"))) {
String aiName = args[2];
Map<String, String> conversations = plugin.getChatHistoryManager().listConversations(((Player) sender).getUniqueId(), aiName);
return filterCompletions(new ArrayList<>(conversations.values()), args[3]);
} else if (args.length == 2 && (args[0].equalsIgnoreCase("suggests") || args[0].equalsIgnoreCase("suggests-presets")) && sender instanceof Player &&
(sender.hasPermission("ollamachat.suggests.toggle") || sender.hasPermission("ollamachat.suggests-presets.toggle"))) {
return filterCompletions(Arrays.asList("on", "off"), args[1]);
}
} else if (command.getName().equalsIgnoreCase("aichat") && sender.hasPermission("ollamachat.use")) {
if (args.length == 1) {
List<String> aiNames = new ArrayList<>();
aiNames.add("ollama");
aiNames.addAll(configManager.getOtherAIConfigs().keySet());
return filterCompletions(aiNames, args[0]);
}
}
return completions;
}
private List<String> filterCompletions(List<String> options, String input) {
return options.stream()
.filter(option -> option.toLowerCase().startsWith(input.toLowerCase()))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,325 @@
package com.ollamachat.core;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;
public class ConfigManager {
private final Ollamachat plugin;
private final Gson gson;
private JsonObject langConfig;
private String ollamaApiUrl;
private String ollamaModel;
private List<String> triggerPrefixes;
private int maxResponseLength;
private Map<String, AIConfig> otherAIConfigs;
private boolean ollamaEnabled;
private Map<String, Boolean> otherAIEnabled;
private boolean streamingEnabled;
private String defaultPrompt;
private Map<String, String> prompts;
private Map<UUID, Map<String, String>> selectedConversations;
private int maxHistory;
private List<String> suggestedResponseModels;
private boolean suggestedResponsesEnabled;
private int suggestedResponseCount;
private String suggestedResponsePrompt;
private List<String> suggestedResponsePresets;
private Map<String, Boolean> suggestedResponseModelToggles;
private int suggestedResponseCooldown;
private boolean suggestedResponsePresetsEnabled;
public ConfigManager(Ollamachat plugin) {
this.plugin = plugin;
this.gson = new Gson();
this.selectedConversations = new HashMap<>();
this.suggestedResponseModelToggles = new HashMap<>();
}
public void initialize() {
plugin.saveDefaultConfig();
reloadConfigValues();
loadLanguageFile(plugin.getConfig().getString("language", "en_us"));
}
private void updateConfig() {
FileConfiguration config = plugin.getConfig();
if (!config.contains("ollama-enabled")) config.set("ollama-enabled", true);
if (!config.contains("language")) config.set("language", "en_us");
if (!config.contains("other-ai-configs")) config.createSection("other-ai-configs");
if (!config.contains("max-history")) config.set("max-history", 5);
if (!config.contains("stream-settings")) config.set("stream-settings.enabled", true);
if (!config.contains("prompts")) config.createSection("prompts");
if (!config.contains("default-prompt")) config.set("default-prompt", "");
if (!config.contains("trigger-prefixes")) {
config.set("trigger-prefixes", Arrays.asList("@bot", "@ai"));
}
if (!config.contains("suggested-response-models")) {
config.set("suggested-response-models", Arrays.asList("llama3"));
}
if (!config.contains("suggested-responses-enabled")) config.set("suggested-responses-enabled", true);
if (!config.contains("suggested-response-count")) config.set("suggested-response-count", 3);
if (!config.contains("suggested-response-prompt")) {
config.set("suggested-response-prompt", "Conversation:\nUser: {prompt}\nAI: {response}\n\nBased on the above conversation, suggest {count} natural follow-up responses the user might want to say. They should be conversational in tone rather than questions. List them as:\n1. Response 1\n2. Response 2\n3. Response 3");
}
if (!config.contains("suggested-response-presets")) {
config.set("suggested-response-presets", Arrays.asList("I see what you mean.", "That's interesting!", "Tell me more about that."));
}
if (!config.contains("suggested-response-model-toggles")) {
config.createSection("suggested-response-model-toggles");
for (String model : config.getStringList("suggested-response-models")) {
if (!config.contains("suggested-response-model-toggles." + model)) {
config.set("suggested-response-model-toggles." + model, true);
}
}
}
if (!config.contains("suggested-response-cooldown")) config.set("suggested-response-cooldown", 10);
if (!config.contains("suggested-response-presets-enabled")) config.set("suggested-response-presets-enabled", true);
if (!config.contains("database")) {
config.set("database.type", "sqlite");
config.set("database.mysql.host", "localhost");
config.set("database.mysql.port", 3306);
config.set("database.mysql.database", "ollamachat");
config.set("database.mysql.username", "root");
config.set("database.mysql.password", "");
}
plugin.saveConfig();
}
public void reloadConfigValues() {
File configFile = new File(plugin.getDataFolder(), "config.yml");
if (!configFile.exists()) {
plugin.saveDefaultConfig();
} else {
try {
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
if (!config.contains("ollama-api-url") || !config.contains("model")) {
plugin.getLogger().warning(getMessage("config-invalid", null));
configFile.delete();
plugin.saveDefaultConfig();
}
} catch (Exception e) {
plugin.getLogger().severe(getMessage("config-load-failed", Map.of("error", e.getMessage())));
configFile.delete();
plugin.saveDefaultConfig();
}
}
plugin.reloadConfig();
updateConfig();
FileConfiguration config = plugin.getConfig();
ollamaApiUrl = config.getString("ollama-api-url", "http://localhost:11434/api/generate");
ollamaModel = config.getString("model", "llama3");
triggerPrefixes = config.getStringList("trigger-prefixes");
maxResponseLength = config.getInt("max-response-length", 500);
ollamaEnabled = config.getBoolean("ollama-enabled", true);
maxHistory = config.getInt("max-history", 5);
streamingEnabled = config.getBoolean("stream-settings.enabled", true);
defaultPrompt = config.getString("default-prompt", "");
suggestedResponseModels = config.getStringList("suggested-response-models");
suggestedResponsesEnabled = config.getBoolean("suggested-responses-enabled", true);
suggestedResponseCount = config.getInt("suggested-response-count", 3);
suggestedResponsePrompt = config.getString("suggested-response-prompt", "Conversation:\nUser: {prompt}\nAI: {response}\n\nBased on the above conversation, suggest {count} natural follow-up responses the user might want to say. They should be conversational in tone rather than questions. List them as:\n1. Response 1\n2. Response 2\n3. Response 3");
suggestedResponsePresets = config.getStringList("suggested-response-presets");
suggestedResponseCooldown = config.getInt("suggested-response-cooldown", 10);
suggestedResponsePresetsEnabled = config.getBoolean("suggested-response-presets-enabled", true);
prompts = new HashMap<>();
if (config.contains("prompts") && config.getConfigurationSection("prompts") != null) {
for (String promptName : config.getConfigurationSection("prompts").getKeys(false)) {
String promptContent = config.getString("prompts." + promptName);
prompts.put(promptName, promptContent);
}
}
otherAIConfigs = new HashMap<>();
otherAIEnabled = new HashMap<>();
if (config.contains("other-ai-configs") && config.getConfigurationSection("other-ai-configs") != null) {
for (String aiName : config.getConfigurationSection("other-ai-configs").getKeys(false)) {
String apiUrl = config.getString("other-ai-configs." + aiName + ".api-url");
String apiKey = config.getString("other-ai-configs." + aiName + ".api-key");
String model = config.getString("other-ai-configs." + aiName + ".model");
boolean enabled = config.getBoolean("other-ai-configs." + aiName + ".enabled", true);
boolean isMessagesFormat = config.getBoolean("other-ai-configs." + aiName + ".messages-format", false);
otherAIConfigs.put(aiName, new AIConfig(apiUrl, apiKey, model, isMessagesFormat));
otherAIEnabled.put(aiName, enabled);
}
}
suggestedResponseModelToggles = new HashMap<>();
if (config.contains("suggested-response-model-toggles") && config.getConfigurationSection("suggested-response-model-toggles") != null) {
for (String model : config.getConfigurationSection("suggested-response-model-toggles").getKeys(false)) {
suggestedResponseModelToggles.put(model, config.getBoolean("suggested-response-model-toggles." + model, true));
}
}
}
private void loadLanguageFile(String language) {
File langFolder = new File(plugin.getDataFolder(), "lang");
if (!langFolder.exists()) {
langFolder.mkdirs();
}
File langFile = new File(langFolder, language + ".json");
if (!langFile.exists()) {
plugin.saveResource("lang/" + language + ".json", false);
}
try (FileReader reader = new FileReader(langFile)) {
langConfig = gson.fromJson(reader, JsonObject.class);
if (langConfig == null) {
langConfig = new JsonObject();
plugin.getLogger().warning("Language file is empty or invalid: " + langFile.getName());
}
} catch (IOException e) {
plugin.getLogger().severe("Failed to load language file: " + langFile.getName() + " - " + e.getMessage());
langConfig = new JsonObject();
}
}
public String getMessage(String key, Map<String, String> placeholders) {
String message = langConfig != null && langConfig.has(key) ? langConfig.get(key).getAsString() : "§c[OllamaChat] Missing language key: " + key;
if (placeholders != null) {
for (Map.Entry<String, String> entry : placeholders.entrySet()) {
message = message.replace("{" + entry.getKey() + "}", entry.getValue());
}
}
return message;
}
public boolean isSuggestedResponsePresetsEnabled() {
return suggestedResponsePresetsEnabled;
}
public void setSuggestedResponsePresetsEnabled(boolean enabled) {
this.suggestedResponsePresetsEnabled = enabled;
plugin.getConfig().set("suggested-response-presets-enabled", enabled);
plugin.saveConfig();
}
// Getters
public String getOllamaApiUrl() {
return ollamaApiUrl;
}
public String getOllamaModel() {
return ollamaModel;
}
public List<String> getTriggerPrefixes() {
return triggerPrefixes;
}
public int getMaxResponseLength() {
return maxResponseLength;
}
public Map<String, AIConfig> getOtherAIConfigs() {
return otherAIConfigs;
}
public boolean isOllamaEnabled() {
return ollamaEnabled;
}
public void setOllamaEnabled(boolean enabled) {
this.ollamaEnabled = enabled;
}
public Map<String, Boolean> getOtherAIEnabled() {
return otherAIEnabled;
}
public boolean isStreamingEnabled() {
return streamingEnabled;
}
public String getDefaultPrompt() {
return defaultPrompt;
}
public void setDefaultPrompt(String prompt) {
this.defaultPrompt = prompt;
}
public Map<String, String> getPrompts() {
return prompts;
}
public Map<UUID, Map<String, String>> getSelectedConversations() {
return selectedConversations;
}
public int getMaxHistory() {
return maxHistory;
}
public List<String> getSuggestedResponseModels() {
return suggestedResponseModels;
}
public boolean isSuggestedResponsesEnabled() {
return suggestedResponsesEnabled;
}
public int getSuggestedResponseCount() {
return suggestedResponseCount;
}
public String getSuggestedResponsePrompt() {
return suggestedResponsePrompt;
}
public List<String> getSuggestedResponsePresets() {
return suggestedResponsePresets;
}
public Map<String, Boolean> getSuggestedResponseModelToggles() {
return suggestedResponseModelToggles;
}
public int getSuggestedResponseCooldown() {
return suggestedResponseCooldown;
}
public static class AIConfig {
private final String apiUrl;
private final String apiKey;
private final String model;
private final boolean isMessagesFormat;
public AIConfig(String apiUrl, String apiKey, String model, boolean isMessagesFormat) {
this.apiUrl = apiUrl;
this.apiKey = apiKey;
this.model = model;
this.isMessagesFormat = isMessagesFormat;
}
public String getApiUrl() {
return apiUrl;
}
public String getApiKey() {
return apiKey;
}
public String getModel() {
return model;
}
public boolean isMessagesFormat() {
return isMessagesFormat;
}
}
}

View File

@ -0,0 +1,112 @@
package com.ollamachat.core;
import com.ollamachat.ChatHistoryManager;
import com.ollamachat.DatabaseManager;
import com.ollamachat.DependencyLoader;
import com.ollamachat.ProgressManager;
import com.ollamachat.chat.ChatTriggerHandler;
import com.ollamachat.chat.SuggestedResponseHandler;
import com.ollamachat.command.AIChatCommand;
import com.ollamachat.command.OllamaChatCommand;
import com.ollamachat.command.OllamaChatTabCompleter;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class Ollamachat extends JavaPlugin {
private ConfigManager configManager;
private DatabaseManager databaseManager;
private ChatHistoryManager chatHistoryManager;
private ProgressManager progressManager;
private SuggestedResponseHandler suggestedResponseHandler;
private Map<UUID, Boolean> playerSuggestionToggles;
@Override
public void onEnable() {
// Load dependencies first
DependencyLoader dependencyLoader = new DependencyLoader(this);
ClassLoader dependencyClassLoader;
try {
dependencyLoader.loadDependencies();
dependencyClassLoader = dependencyLoader.getClass().getClassLoader();
} catch (Exception e) {
getLogger().severe("Failed to load dependencies: " + e.getMessage());
getServer().getPluginManager().disablePlugin(this);
return;
}
configManager = new ConfigManager(this);
configManager.initialize();
try {
databaseManager = new DatabaseManager(this, dependencyClassLoader);
} catch (Exception e) {
getLogger().severe("Failed to initialize DatabaseManager: " + e.getMessage());
getServer().getPluginManager().disablePlugin(this);
return;
}
int maxHistory = configManager.getMaxHistory();
chatHistoryManager = new ChatHistoryManager(databaseManager, maxHistory);
progressManager = new ProgressManager(this);
suggestedResponseHandler = new SuggestedResponseHandler(this);
playerSuggestionToggles = new HashMap<>();
// Register events
getServer().getPluginManager().registerEvents(new ChatTriggerHandler(this), this);
// Register commands
getCommand("ollamachat").setExecutor(new OllamaChatCommand(this));
getCommand("ollamachat").setTabCompleter(new OllamaChatTabCompleter(this));
getCommand("aichat").setExecutor(new AIChatCommand(this));
getCommand("aichat").setTabCompleter(new OllamaChatTabCompleter(this));
}
@Override
public void onDisable() {
if (databaseManager != null) {
databaseManager.close();
} else {
getLogger().warning("DatabaseManager was null, skipping close.");
}
if (progressManager != null) {
getServer().getOnlinePlayers().forEach(progressManager::cleanup);
}
}
// Getter methods
public ConfigManager getConfigManager() {
return configManager;
}
public DatabaseManager getDatabaseManager() {
return databaseManager;
}
public ChatHistoryManager getChatHistoryManager() {
return chatHistoryManager;
}
public ProgressManager getProgressManager() {
return progressManager;
}
public SuggestedResponseHandler getSuggestedResponseHandler() {
return suggestedResponseHandler;
}
public Map<UUID, Boolean> getPlayerSuggestionToggles() {
return playerSuggestionToggles;
}
}

View File

@ -3,9 +3,14 @@ ollama-api-url: "http://localhost:11434/api/generate"
model: "llama3"
ollama-enabled: true
# Streaming settings
stream-settings:
enabled: true # Whether to enable streaming
# Chat
trigger-prefix: "@bot "
response-prefix: "§b[AI] §r"
trigger-prefixes:
- "@bot"
- "@ai"
# Length
max-response-length: 500
@ -14,7 +19,50 @@ max-response-length: 500
max-history: 5
# Language Settings
language: "en"
language: "en_us"
# Progress Display Settings
progress-display:
enabled: true # Whether to enable progress display
type: "bossbar" # Display type (bossbar or actionbar)
color: "BLUE" # BossBar color (BLUE, GREEN, RED, etc.)
style: "SOLID" # BossBar style (SOLID, SEGMENTED_6, etc.)
update-interval: 1 # Progress update frequency (in seconds)
# Suggested Response
suggested-responses-enabled: false
suggested-response-models:
- "llama3"
suggested-response-count: 3
suggested-response-prompt: "Conversation:\nUser: {prompt}\nAI: {response}\n\nBased on the above conversation, suggest {count} natural follow-up responses the user might want to say. They should be conversational in tone rather than questions. List them as:\n1. Response 1\n2. Response 2\n3. Response 3"
suggested-response-presets:
- "I see what you mean."
- "That's interesting!"
- "Tell me more about that."
suggested-response-model-toggles:
- llama3
suggested-response-cooldown: 10
suggested-response-presets-enabled: false
# Database (for mysql, set database.type: mysql.)
database:
type: sqlite
mysql:
host: localhost
port: 3306
database: ollamachat
username: root
password: ""
# Default prompt to prepend to user inputs (empty for none)
default-prompt: ""
# Custom prompts
prompts:
# Example:
# friendly: "You are a friendly assistant who responds in a cheerful tone."
# formal: "You are a professional assistant who responds formally."
# Other AI Configurations
other-ai-configs:
@ -22,5 +70,6 @@ other-ai-configs:
api-url: "https://api.openai.com/v1/chat/completions"
api-key: "your-openai-api-key"
model: "gpt-4"
enabled: true
enabled: false
messages-format: true

View File

@ -1,18 +0,0 @@
# General messages
reload-success: "§aConfiguration reloaded."
toggle-enabled: "§a{ai-name} is now enabled."
toggle-disabled: "§a{ai-name} is now disabled."
invalid-ai-name: "§cInvalid AI name. Available AIs: {ai-list}."
usage-aichat: "§cUsage: /aichat <ai-name> <prompt>"
usage-ollamachat: "§cUsage: /ollamachat <reload|toggle <ai-name>>"
player-only: "§cThis command can only be used by players."
ollama-enabled: "§aOllama is now enabled."
ollama-disabled: "§aOllama is now disabled."
# Chat interaction
response-prefix: "§b[AI] §r"
error-prefix: "§c[Error] §r"
# Log messages
config-invalid: "§cConfig file is invalid or incomplete. Regenerating default config..."
config-load-failed: "§cFailed to load config file: {error}. Regenerating default config..."

View File

@ -0,0 +1,51 @@
{
"usage-ollamachat": "§e[OllamaChat] Usage:\n- /ollamachat reload\n- /ollamachat toggle <ai-name>\n- /ollamachat prompt <set|delete|list|select|clear> [args]\n- /ollamachat conversation <new|select|delete|list> [args]",
"usage-aichat": "§e[OllamaChat] Usage:\n- /aichat <ai-name> <prompt>",
"reload-success": "§a[OllamaChat] Configuration reloaded successfully!",
"ollama-enabled": "§a[OllamaChat] Ollama integration enabled!",
"ollama-disabled": "§c[OllamaChat] Ollama integration disabled!",
"toggle-enabled": "§a[OllamaChat] {ai-name} integration enabled!",
"toggle-disabled": "§c[OllamaChat] {ai-name} integration disabled!",
"invalid-ai-name": "§c[OllamaChat] Invalid AI name! Available AIs: {ai-list}",
"player-only": "§c[OllamaChat] This command can only be used by players!",
"config-invalid": "§c[OllamaChat] Configuration file is invalid, resetting to default.",
"config-load-failed": "§c[OllamaChat] Failed to load configuration: {error}",
"response-prefix": "§7[OllamaChat] §f",
"error-prefix": "§c[OllamaChat] §f",
"generating-status": "§a[OllamaChat] Generating... ({progress}%)",
"complete-status": "§a[OllamaChat] Completed!",
"error-status": "§c[OllamaChat] Error!",
"no-permission": "§c[OllamaChat] You do not have permission to use this command!",
"prompt-usage": "§e[OllamaChat] Prompt Usage:\n- /ollamachat prompt set <name> <content>\n- /ollamachat prompt delete <name>\n- /ollamachat prompt list\n- /ollamachat prompt select <name>\n- /ollamachat prompt clear",
"prompt-set": "§a[OllamaChat] Prompt '{name}' set successfully!",
"prompt-deleted": "§a[OllamaChat] Prompt '{name}' deleted successfully!",
"prompt-not-found": "§c[OllamaChat] Prompt '{name}' not found!",
"prompt-list": "§a[OllamaChat] Available prompts:\n- {prompts}",
"prompt-list-empty": "§a[OllamaChat] No prompts available.",
"prompt-selected": "§a[OllamaChat] Default prompt set to '{name}'.",
"prompt-cleared": "§a[OllamaChat] Default prompt cleared.",
"prompt-default": "§a[OllamaChat] Current default prompt: '{name}'",
"prompt-default-invalid": "§c[OllamaChat] Current default prompt '{name}' is invalid (not found).",
"conversation-usage": "§e[OllamaChat] Conversation Usage:\n- /ollamachat conversation new <ai-name> <name>\n- /ollamachat conversation select <ai-name> <id>\n- /ollamachat conversation delete <ai-name> <id>\n- /ollamachat conversation list <ai-name>",
"conversation-created": "§a[OllamaChat] Conversation '{name}' created for {ai-name}!",
"conversation-selected": "§a[OllamaChat] Selected conversation '{name}' for {ai-name}.",
"conversation-deleted": "§a[OllamaChat] Conversation '{name}' deleted for {ai-name}.",
"conversation-not-found": "§c[OllamaChat] Conversation '{name}' not found!",
"conversation-list": "§a[OllamaChat] Available conversations for {ai-name}:\n- {conversations}",
"conversation-list-empty": "§a[OllamaChat] No conversations available for {ai-name}.",
"conversation-default": "§a[OllamaChat] Current conversation for {ai-name}: '{name}'",
"conversation-default-invalid": "§c[OllamaChat] Current conversation '{name}' for {ai-name} is invalid (not found).",
"suggested-responses-header": "§a[OllamaChat] Suggested Responses:",
"suggested-response-prefix": "§7[Click to Reply] §f",
"general-error": "§c[OllamaChat] An unexpected error occurred: {error}",
"streaming-interrupted": "§c[OllamaChat] Streaming response was interrupted.",
"database-error": "§c[OllamaChat] Database error occurred: {error}",
"response-truncated": "§e[OllamaChat] Response was truncated due to length limit.",
"json-render-error": "§c[OllamaChat] Failed to render suggested response: {error}",
"suggests-enabled": "§a[OllamaChat] Suggested responses enabled for {player}!",
"suggests-disabled": "§c[OllamaChat] Suggested responses disabled for {player}!",
"suggests-rate-limit": "§c[OllamaChat] Please wait {seconds} seconds before generating more suggestions.",
"suggested-response-hover": "§eClick to send this suggestion as a prompt!",
"suggests-presets-enabled": "§a[OllamaChat] Suggested response presets have been enabled.",
"suggests-presets-disabled": "§c[OllamaChat] Suggested response presets have been disabled."
}

View File

@ -0,0 +1,52 @@
{
"usage-ollamachat": "§e[OllamaChat] 用法说明:\n- /ollamachat reload\n- /ollamachat toggle <AI名称>\n- /ollamachat prompt <set|delete|list|select|clear> [参数]\n- /ollamachat conversation <new|select|delete|list> [参数]",
"usage-aichat": "§e[OllamaChat] 用法说明:\n- /aichat <AI名称> <提示内容>",
"reload-success": "§a[OllamaChat] 配置已成功重新加载!",
"ollama-enabled": "§a[OllamaChat] Ollama 集成功能已启用!",
"ollama-disabled": "§c[OllamaChat] Ollama 集成功能已停用!",
"toggle-enabled": "§a[OllamaChat] {ai-name} 集成功能已启用!",
"toggle-disabled": "§c[OllamaChat] {ai-name} 集成功能已停用!",
"invalid-ai-name": "§c[OllamaChat] 无效的 AI 名称!可用 AI 列表:{ai-list}",
"player-only": "§c[OllamaChat] 此命令仅限玩家使用!",
"config-invalid": "§c[OllamaChat] 配置文件无效,已重置为默认设置。",
"config-load-failed": "§c[OllamaChat] 加载配置失败:{error}",
"response-prefix": "§7[OllamaChat] §f",
"error-prefix": "§c[OllamaChat] §f",
"generating-status": "§a[OllamaChat] 生成中... ({progress}%)",
"complete-status": "§a[OllamaChat] 已完成!",
"error-status": "§c[OllamaChat] 出错!",
"no-permission": "§c[OllamaChat] 您没有权限使用此命令!",
"prompt-usage": "§e[OllamaChat] 提示词用法说明:\n- /ollamachat prompt set <名称> <内容>\n- /ollamachat prompt delete <名称>\n- /ollamachat prompt list\n- /ollamachat prompt select <名称>\n- /ollamachat prompt clear",
"prompt-set": "§a[OllamaChat] 提示词 '{name}' 已设置成功!",
"prompt-deleted": "§a[OllamaChat] 提示词 '{name}' 已删除成功!",
"prompt-not-found": "§c[OllamaChat] 未找到提示词 '{name}'",
"prompt-list": "§a[OllamaChat] 可用提示词列表:\n- {prompts}",
"prompt-list-empty": "§a[OllamaChat] 暂无可用提示词。",
"prompt-selected": "§a[OllamaChat] 已将 '{name}' 设为默认提示词。",
"prompt-cleared": "§a[OllamaChat] 默认提示词已清除。",
"prompt-default": "§a[OllamaChat] 当前默认提示词:'{name}'",
"prompt-default-invalid": "§c[OllamaChat] 当前默认提示词 '{name}' 无效(未找到)。",
"conversation-usage": "§e[OllamaChat] 对话管理用法说明:\n- /ollamachat conversation new <AI名称> <名称>\n- /ollamachat conversation select <AI名称> <ID>\n- /ollamachat conversation delete <AI名称> <ID>\n- /ollamachat conversation list <AI名称>",
"conversation-created": "§a[OllamaChat] 已为 {ai-name} 创建对话 '{name}'",
"conversation-selected": "§a[OllamaChat] 已为 {ai-name} 选择对话 '{name}'。",
"conversation-deleted": "§a[OllamaChat] 已为 {ai-name} 删除对话 '{name}'。",
"conversation-not-found": "§c[OllamaChat] 未找到对话 '{name}'",
"conversation-list": "§a[OllamaChat] {ai-name} 的可用对话列表:\n- {conversations}",
"conversation-list-empty": "§a[OllamaChat] {ai-name} 暂无可用对话。",
"conversation-default": "§a[OllamaChat] {ai-name} 的当前对话:'{name}'",
"conversation-default-invalid": "§c[OllamaChat] {ai-name} 的当前对话 '{name}' 无效(未找到)。",
"suggested-responses-header": "§a[OllamaChat] 推荐回复:",
"suggested-response-prefix": "§7[点击回复] §f",
"general-error": "§c[OllamaChat] 发生意外错误:{error}",
"streaming-interrupted": "§c[OllamaChat] 流式响应已中断。",
"database-error": "§c[OllamaChat] 数据库错误:{error}",
"response-truncated": "§e[OllamaChat] 响应因长度限制已被截断。",
"json-render-error": "§c[OllamaChat] 未能渲染成功推荐回复: {error}",
"suggests-enabled": "§a[OllamaChat] 已为玩家{player}启用推荐回复!",
"suggests-disabled": "§c[OllamaChat] 已为玩家{player}禁用推荐回复!",
"suggests-rate-limit": "§c[OllamaChat] 请等待{seconds}秒后再生成推荐回复",
"suggested-response-hover": "§e点击发送此推荐内容",
"suggests-presets-enabled": "§a[OllamaChat] 已启用预设推荐回复",
"suggests-presets-disabled": "§c[OllamaChat] 已启用预设推荐回复"
}

View File

@ -1,18 +0,0 @@
# 通用消息
reload-success: "§a配置已重新加载。"
toggle-enabled: "§a{ai-name} 已启用。"
toggle-disabled: "§a{ai-name} 已禁用。"
invalid-ai-name: "§c无效的 AI 名称。可用的 AI: {ai-list}。"
usage-aichat: "§c用法: /aichat <ai名称> <提示>"
usage-ollamachat: "§c用法: /ollamachat <reload|toggle <ai名称>>"
player-only: "§c该命令只能由玩家使用。"
ollama-enabled: "§aOllama 已启用。"
ollama-disabled: "§aOllama 已禁用。"
# 聊天交互
response-prefix: "§b[AI] §r"
error-prefix: "§c[错误] §r"
# 日志消息
config-invalid: "§c配置文件无效或不完整正在重新生成默认配置..."
config-load-failed: "§c加载配置文件失败: {error},正在重新生成默认配置..."

View File

@ -1,25 +1,58 @@
name: OllamaChat
version: '1.0.2'
main: com.ollamachat.Ollamachat
version: '1.1.4'
main: com.ollamachat.core.Ollamachat
api-version: '1.21'
authors: [xwwsdd]
description: A plugin used to connect Ollama with Minecraft.
description: A plugin used to connect Ollama and OtherAI with Minecraft.
website: https://chat.sarskin.cn/invite/iHgI6LTX
commands:
ollamachat:
description: Manage OllamaChat plugin (reload configuration or toggle AI)
usage: "{usage-ollamachat}"
permission: ollamachat.admin
permission: ollamachat.reload
aichat:
description: Interact with other AI services
usage: "{usage-aichat}"
permission: ollamachat.use
permissions:
ollamachat.admin:
description: Allows managing the plugin (reload and toggle AI)
ollamachat.reload:
description: Allows managing the plugin (reload)
default: op
ollamachat.toggle:
description: Allows managing the plugin (toggle AI)
default: op
ollamachat.use:
description: Allows using the /aichat command
default: true
default: true
ollamachat.prompt.set:
description: Allows setting a new prompt
default: op
ollamachat.prompt.delete:
description: Allows deleting a prompt
default: op
ollamachat.prompt.list:
description: Allows listing all prompts
default: op
ollamachat.prompt.select:
description: Allows selecting or clearing a prompt
default: op
ollamachat.conversation.new:
description: Allows creating a new conversation
default: true
ollamachat.conversation.select:
description: Allows selecting a conversation
default: true
ollamachat.conversation.list:
description: Allows listing conversations
default: true
ollamachat.conversation.delete:
description: Allows deleting a conversation
default: true
ollamachat.suggests.toggle:
description: Allows toggling suggested responses on or off
default: true
ollamachat.suggests-presets.toggle:
description: Allows toggling of suggested response presets
default: op