View Javadoc
1   package fr.tiogars.chat.domains.prompt.services;
2   
3   import fr.tiogars.chat.domains.lmstudio.models.LmStudioSettings;
4   import fr.tiogars.chat.domains.lmstudio.services.LmStudioSettingsService;
5   import org.springframework.stereotype.Component;
6   
7   import java.io.IOException;
8   import java.net.URI;
9   import java.net.http.HttpClient;
10  import java.net.http.HttpRequest;
11  import java.net.http.HttpResponse;
12  import java.time.Duration;
13  
14  /**
15   * Implementation of ChatClientAdapter for LMStudio.
16   * This uses direct HTTP calls to LMStudio's API until Spring AI is available.
17   * Connection settings are persisted by the dedicated LM Studio domain.
18   */
19  @Component
20  public class LmStudioChatClientImpl implements ChatClientAdapter {
21  
22      private final LmStudioSettingsService settingsService;
23  
24      public LmStudioChatClientImpl(LmStudioSettingsService settingsService) {
25          this.settingsService = settingsService;
26      }
27  
28      @Override
29      public String call(String prompt) {
30          return call(null, prompt);
31      }
32  
33      @Override
34      public String call(String systemPrompt, String userPrompt) {
35          LmStudioSettings settings = settingsService.requireSettings();
36          Duration timeout = Duration.ofSeconds(settings.timeoutSeconds());
37          try (HttpClient httpClient = HttpClient.newBuilder().connectTimeout(timeout).build()) {
38              String requestBody = buildRequestBody(settings.model(), systemPrompt, userPrompt);
39              
40              HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
41                      .uri(URI.create(settings.baseUrl().replaceAll("/+$", "") + "/v1/chat/completions"))
42                      .timeout(timeout)
43                      .header("Content-Type", "application/json")
44                      .POST(HttpRequest.BodyPublishers.ofString(requestBody));
45              if (settings.isTokenConfigured()) {
46                  requestBuilder.header("Authorization", "Bearer " + settings.token());
47              }
48              HttpRequest request = requestBuilder.build();
49  
50              HttpResponse<String> response = httpClient.send(
51                      request, 
52                      HttpResponse.BodyHandlers.ofString()
53              );
54  
55              if (response.statusCode() != 200) {
56                  throw new RuntimeException("LMStudio API error: " + response.statusCode());
57              }
58  
59              return parseResponse(response.body());
60          } catch (InterruptedException e) {
61              Thread.currentThread().interrupt();
62              throw new RuntimeException("LMStudio API call interrupted", e);
63          } catch (IOException e) {
64              throw new RuntimeException("Error calling LMStudio API", e);
65          }
66      }
67  
68      private String buildRequestBody(String model, String systemPrompt, String userPrompt) {
69          if (systemPrompt != null && !systemPrompt.isBlank()) {
70              return String.format("""
71                  {
72                      "model": "%s",
73                      "messages": [
74                          {"role": "system", "content": "%s"},
75                          {"role": "user", "content": "%s"}
76                      ],
77                      "stream": false,
78                      "temperature": 0.7
79                  }
80                  """, escapeJson(model), escapeJson(systemPrompt), escapeJson(userPrompt));
81          } else {
82              return String.format("""
83                  {
84                      "model": "%s",
85                      "messages": [
86                          {"role": "user", "content": "%s"}
87                      ],
88                      "stream": false,
89                      "temperature": 0.7
90                  }
91                  """, escapeJson(model), escapeJson(userPrompt));
92          }
93      }
94  
95      private String parseResponse(String responseBody) {
96          // Simple parsing - in a real implementation, use JSON parsing
97          // This is a simplified version that extracts the content from the response
98          // Expected format: {"choices":[{"message":{"content":"..."}}]}
99          int contentIndex = responseBody.indexOf("\"content\":");
100         if (contentIndex == -1) {
101             return responseBody;
102         }
103         
104         int start = responseBody.indexOf('"', contentIndex + 10) + 1;
105         int end = responseBody.lastIndexOf('"');
106         
107         if (start < 0 || end < start) {
108             return responseBody;
109         }
110         
111         return responseBody.substring(start, end);
112     }
113 
114     private String escapeJson(String input) {
115         if (input == null) {
116             return "";
117         }
118         return input.replace("\\", "\\\\")
119                 .replace("\"", "\\\"")
120                 .replace("\n", "\\n")
121                 .replace("\r", "\\r")
122                 .replace("\t", "\\t");
123     }
124 }