RuneHive-Game
Loading...
Searching...
No Matches
OpenAIService.java
Go to the documentation of this file.
1package com.runehive.content.ai;
2
3import com.openai.client.OpenAIClient;
4import com.openai.client.okhttp.OpenAIOkHttpClient;
5import com.openai.models.ChatModel;
6import com.openai.models.chat.completions.ChatCompletion;
7import com.openai.models.chat.completions.ChatCompletionCreateParams;
8import com.openai.models.chat.completions.ChatCompletionMessage;
9import org.slf4j.LoggerFactory;
10import org.slf4j.Logger;
11
12import java.io.IOException;
13import java.nio.file.Files;
14import java.nio.file.Path;
15import java.nio.file.Paths;
16import java.time.Duration;
17import java.util.ArrayList;
18import java.util.List;
19import java.util.concurrent.CompletableFuture;
20import java.util.concurrent.ConcurrentHashMap;
21import java.util.concurrent.ExecutorService;
22import java.util.concurrent.Executors;
23import java.util.concurrent.atomic.AtomicBoolean;
24
25public final class OpenAIService {
26
27 private static final Logger logger = LoggerFactory.getLogger(OpenAIService.class);
28 private static final String API_KEY_ENV = "OPENAI_API_KEY";
29 private static final String API_KEY_FILE = "openai-java-key.txt";
30 private static final int MAX_TOKENS = 500;
31 private static final double TEMPERATURE = 0.8;
32 private static final int MAX_CONVERSATION_HISTORY = 10;
33
34 private static final OpenAIService INSTANCE = new OpenAIService();
35
36 private final ExecutorService executorService;
37 private final ConcurrentHashMap<String, List<ChatMessage>> conversationHistory;
38 private final AtomicBoolean initialized = new AtomicBoolean(false);
39 private OpenAIClient openAIClient;
40 private String apiKey;
41
42 private OpenAIService() {
43 this.executorService = Executors.newFixedThreadPool(4);
44 this.conversationHistory = new ConcurrentHashMap<>();
45 }
46
47 public static OpenAIService getInstance() {
48 return INSTANCE;
49 }
50
51 public void initialize() {
52 if (initialized.compareAndSet(false, true)) {
53 logger.info("Initializing Official OpenAI Service with GPT-4o-mini model");
54
55 try {
56 loadApiKey();
57
58 openAIClient = OpenAIOkHttpClient.builder()
59 .apiKey(apiKey)
60 .timeout(Duration.ofSeconds(30))
61 .maxRetries(3)
62 .build();
63
64 logger.info("Official OpenAI Service initialized and ready for GPT-4o-mini requests");
65 } catch (Exception e) {
66 logger.error("Failed to initialize Official OpenAI Service", e);
67 initialized.set(false);
68 throw new RuntimeException("OpenAI Service initialization failed", e);
69 }
70 }
71 }
72
73 private void loadApiKey() throws IOException {
74 apiKey = System.getenv(API_KEY_ENV);
75
76 if (apiKey != null && !apiKey.trim().isEmpty()) {
77 apiKey = apiKey.trim();
78 if (apiKey.matches("^sk-[a-zA-Z0-9\\-_]{20,}$")) {
79 logger.info("OpenAI API key loaded from environment variable");
80 return;
81 } else {
82 logger.warn("Invalid API key format in environment variable - must match pattern: sk-[alphanumeric]{20+}");
83 apiKey = null;
84 }
85 }
86
87 try {
88 Path keyFile = Paths.get(API_KEY_FILE);
89 if (Files.exists(keyFile)) {
90 byte[] bytes = Files.readAllBytes(keyFile);
91 apiKey = new String(bytes, "UTF-8").trim();
92 if (apiKey.isEmpty() || !apiKey.matches("^sk-[a-zA-Z0-9\\-_]{20,}$")) {
93 throw new IllegalArgumentException("Invalid API key format in file - must match pattern: sk-[alphanumeric]{20+}");
94 }
95 logger.warn("OpenAI API key loaded from file - consider using environment variable {} for better security", API_KEY_ENV);
96 return;
97 }
98 } catch (IOException e) {
99 logger.error("Could not read API key file: {}", API_KEY_FILE);
100 }
101
102 throw new IOException("OpenAI API key not found. Set environment variable " + API_KEY_ENV +
103 " or create file " + API_KEY_FILE + " with a valid API key starting with 'sk-'");
104 }
105
106 public CompletableFuture<String> processPlayerMessage(String username, String message, int npcId) {
107 return CompletableFuture.supplyAsync(() -> {
108 try {
109 if (!initialized.get()) {
110 logger.warn("OpenAI Service not initialized");
111 return "The mystical networks are not yet connected. Please try again.";
112 }
113
114 List<ChatMessage> history = conversationHistory.computeIfAbsent(username,
115 k -> new ArrayList<>());
116
117 history.add(new ChatMessage("user", message));
118
119 if (history.size() > MAX_CONVERSATION_HISTORY) {
120 history = new ArrayList<>(history.subList(
121 history.size() - MAX_CONVERSATION_HISTORY, history.size()));
122 conversationHistory.put(username, history);
123 }
124
125 ChatCompletionCreateParams.Builder paramsBuilder = ChatCompletionCreateParams.builder()
126 .model(ChatModel.GPT_4O_MINI)
127 .maxCompletionTokens(MAX_TOKENS)
128 .temperature(TEMPERATURE)
129 .addDeveloperMessage(
130 "You are a helpful AI assistant. Provide accurate, concise, and direct answers to questions. " +
131 "Keep responses under 150 tokens while ensuring they fully answer the question."
132 );
133
134 for (ChatMessage msg : history) {
135 if ("user".equals(msg.getRole())) {
136 paramsBuilder.addUserMessage(msg.getContent());
137 } else if ("assistant".equals(msg.getRole())) {
138 paramsBuilder.addAssistantMessage(msg.getContent());
139 }
140 }
141
142 ChatCompletionCreateParams params = paramsBuilder.build();
143
144 logger.debug("Making OpenAI API request for user: {}", hashUsername(username));
145 ChatCompletion completion = openAIClient.chat().completions().create(params);
146
147 if (completion.choices().isEmpty()) {
148 logger.warn("No choices returned from OpenAI API");
149 return "The mystical spirits did not respond. Please try again.";
150 }
151
152 String response = completion.choices().get(0).message().content().orElse("");
153 if (response.isEmpty()) {
154 logger.warn("Empty response from OpenAI API");
155 return "The AI spirits whispered too quietly. Please try again.";
156 }
157
158 history.add(new ChatMessage("assistant", response));
159
160 completion.usage().ifPresent(usage -> {
161 logger.info("OpenAI API usage for {}: prompt_tokens={}, completion_tokens={}, total_tokens={}",
162 hashUsername(username), usage.promptTokens(), usage.completionTokens(), usage.totalTokens());
163 });
164
165 logger.debug("Successfully processed OpenAI request for user: {}", hashUsername(username));
166 return response;
167
168 } catch (Exception e) {
169 logger.error("Error processing OpenAI request for user: " + username, e);
170 return "The mystical connection encountered turbulence. Please try again later.";
171 }
172 }, executorService);
173 }
174
175 public void clearConversationHistory(String username) {
176 if (conversationHistory.remove(username) != null) {
177 logger.debug("Cleared conversation history for user: {}", hashUsername(username));
178 }
179 }
180
181 private String hashUsername(String username) {
182 if (username == null) return "null";
183 int hash = username.hashCode();
184 return "user_" + Integer.toHexString(hash);
185 }
186
187 public boolean isInitialized() {
188 return initialized.get();
189 }
190
191 public int getMinWordCount() {
192 return 0;
193 }
194
196 return conversationHistory.size();
197 }
198
199 public void shutdown() {
200 logger.info("Shutting down Official OpenAI Service...");
201
202 conversationHistory.clear();
203
204 if (executorService != null && !executorService.isShutdown()) {
205 executorService.shutdown();
206 }
207
208 openAIClient = null;
209
210 initialized.set(false);
211 logger.info("Official OpenAI Service shutdown complete");
212 }
213
214 private static class ChatMessage {
215 private final String role;
216 private final String content;
217
218 public ChatMessage(String role, String content) {
219 this.role = role;
220 this.content = content;
221 }
222
223 public String getRole() {
224 return role;
225 }
226
227 public String getContent() {
228 return content;
229 }
230 }
231}
CompletableFuture< String > processPlayerMessage(String username, String message, int npcId)
final ConcurrentHashMap< String, List< ChatMessage > > conversationHistory
static final OpenAIService INSTANCE
void clearConversationHistory(String username)