286 lines
13 KiB
Java
286 lines
13 KiB
Java
package com.krrishg.service;
|
|
|
|
import com.krrishg.config.EncryptionService;
|
|
import com.krrishg.dto.LLMOptions;
|
|
import com.krrishg.dto.LLMResult;
|
|
import com.krrishg.dto.Reference;
|
|
import com.krrishg.dto.SiteStructure;
|
|
import org.springframework.web.client.HttpClientErrorException;
|
|
import org.springframework.web.client.ResourceAccessException;
|
|
import com.krrishg.model.GenerationVersion;
|
|
import com.krrishg.model.User;
|
|
import com.krrishg.repository.UserRepository;
|
|
import com.krrishg.service.llm.LLMProvider;
|
|
import com.krrishg.service.llm.ProviderFactory;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.core.io.Resource;
|
|
import org.springframework.core.io.ResourceLoader;
|
|
import org.springframework.data.domain.Sort;
|
|
import org.springframework.data.mongodb.core.MongoTemplate;
|
|
import org.springframework.data.mongodb.core.query.Criteria;
|
|
import org.springframework.data.mongodb.core.query.Query;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.time.LocalDateTime;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
import java.util.stream.Collectors;
|
|
|
|
@Service
|
|
public class LLMService {
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(LLMService.class);
|
|
private static final int MAX_VERSIONS_PER_SITE = 100;
|
|
|
|
private final ProviderFactory providerFactory;
|
|
private final LLMResponseParser responseParser;
|
|
private final ReferenceService referenceService;
|
|
private final MongoTemplate mongoTemplate;
|
|
private final UserRepository userRepository;
|
|
private final EncryptionService encryptionService;
|
|
private final String systemPrompt;
|
|
private final String refinerPrompt;
|
|
|
|
public LLMService(ProviderFactory providerFactory,
|
|
LLMResponseParser responseParser,
|
|
ReferenceService referenceService,
|
|
MongoTemplate mongoTemplate,
|
|
UserRepository userRepository,
|
|
EncryptionService encryptionService,
|
|
ResourceLoader resourceLoader,
|
|
@Value("${indie.llm.system-prompt-path:classpath:prompts/site-generator.txt}") String promptPath,
|
|
@Value("${indie.llm.refiner-prompt-path:classpath:prompts/site-refiner.txt}") String refinerPath) {
|
|
this.providerFactory = providerFactory;
|
|
this.responseParser = responseParser;
|
|
this.referenceService = referenceService;
|
|
this.mongoTemplate = mongoTemplate;
|
|
this.userRepository = userRepository;
|
|
this.encryptionService = encryptionService;
|
|
this.systemPrompt = loadSystemPrompt(resourceLoader, promptPath);
|
|
this.refinerPrompt = loadSystemPrompt(resourceLoader, refinerPath);
|
|
}
|
|
|
|
public SiteStructure generateSite(UUID userId, UUID siteId, String prompt,
|
|
List<Reference> references, String provider, String apiKey) {
|
|
return generateSite(userId, siteId, prompt, references, provider, apiKey, null, null);
|
|
}
|
|
|
|
public SiteStructure generateSite(UUID userId, UUID siteId, String prompt,
|
|
List<Reference> references, String provider, String apiKey, String baseUrl) {
|
|
return generateSite(userId, siteId, prompt, references, provider, apiKey, baseUrl, null);
|
|
}
|
|
|
|
public SiteStructure generateSite(UUID userId, UUID siteId, String prompt,
|
|
List<Reference> references, String provider, String apiKey, String baseUrl, String model) {
|
|
String resolvedKey = resolveApiKey(userId, provider, apiKey);
|
|
String resolvedBaseUrl = resolveBaseUrl(userId, provider, baseUrl);
|
|
String resolvedModel = resolveModel(userId, provider, model);
|
|
String referenceContext = referenceService.buildReferenceContext(references);
|
|
String fullPrompt = prompt + referenceContext;
|
|
|
|
LLMOptions options = LLMOptions.builder().build();
|
|
LLMResult result = callLLM(provider, resolvedKey, resolvedBaseUrl, resolvedModel, systemPrompt, fullPrompt, references, options);
|
|
log.info("LLM generation completed: model={}, latency={}ms, tokens={}+{}",
|
|
result.getModel(), result.getLatencyMs(),
|
|
result.getPromptTokens(), result.getCompletionTokens());
|
|
|
|
SiteStructure structure = responseParser.parse(result.getContent());
|
|
saveVersion(userId, siteId, prompt, references, result.getContent());
|
|
|
|
return structure;
|
|
}
|
|
|
|
public SiteStructure refineSite(UUID userId, UUID siteId, SiteStructure currentStructure,
|
|
String refinementPrompt, List<Reference> references,
|
|
String provider, String apiKey) {
|
|
return refineSite(userId, siteId, currentStructure, refinementPrompt, references, provider, apiKey, null, null);
|
|
}
|
|
|
|
public SiteStructure refineSite(UUID userId, UUID siteId, SiteStructure currentStructure,
|
|
String refinementPrompt, List<Reference> references,
|
|
String provider, String apiKey, String baseUrl) {
|
|
return refineSite(userId, siteId, currentStructure, refinementPrompt, references, provider, apiKey, baseUrl, null);
|
|
}
|
|
|
|
public SiteStructure refineSite(UUID userId, UUID siteId, SiteStructure currentStructure,
|
|
String refinementPrompt, List<Reference> references,
|
|
String provider, String apiKey, String baseUrl, String model) {
|
|
String resolvedKey = resolveApiKey(userId, provider, apiKey);
|
|
String resolvedBaseUrl = resolveBaseUrl(userId, provider, baseUrl);
|
|
String resolvedModel = resolveModel(userId, provider, model);
|
|
String currentJson = serializeStructure(currentStructure);
|
|
String referenceContext = referenceService.buildReferenceContext(references);
|
|
String combinedPrompt = "Current site structure:\n" + currentJson
|
|
+ "\n\nRefinement request: " + refinementPrompt
|
|
+ referenceContext
|
|
+ "\n\nOutput the complete updated JSON structure with the requested changes applied.";
|
|
|
|
LLMOptions options = LLMOptions.builder()
|
|
.temperature(0.5)
|
|
.build();
|
|
LLMResult result = callLLM(provider, resolvedKey, resolvedBaseUrl, resolvedModel, refinerPrompt, combinedPrompt, references, options);
|
|
log.info("LLM refinement completed: model={}, latency={}ms, tokens={}+{}",
|
|
result.getModel(), result.getLatencyMs(),
|
|
result.getPromptTokens(), result.getCompletionTokens());
|
|
|
|
SiteStructure structure = responseParser.parse(result.getContent());
|
|
saveVersion(userId, siteId, refinementPrompt, references, result.getContent());
|
|
|
|
return structure;
|
|
}
|
|
|
|
public List<GenerationVersion> getVersions(UUID siteId) {
|
|
Query query = new Query(Criteria.where("siteId").is(siteId))
|
|
.with(Sort.by(Sort.Direction.DESC, "versionNumber"))
|
|
.limit(MAX_VERSIONS_PER_SITE);
|
|
return mongoTemplate.find(query, GenerationVersion.class);
|
|
}
|
|
|
|
public SiteStructure restoreVersion(String versionId) {
|
|
GenerationVersion version = mongoTemplate.findById(versionId, GenerationVersion.class);
|
|
if (version == null) {
|
|
throw new IllegalArgumentException("Version not found: " + versionId);
|
|
}
|
|
return responseParser.parse(version.getFullStructure());
|
|
}
|
|
|
|
public void deleteVersion(String versionId) {
|
|
GenerationVersion version = mongoTemplate.findById(versionId, GenerationVersion.class);
|
|
if (version == null) {
|
|
throw new IllegalArgumentException("Version not found: " + versionId);
|
|
}
|
|
mongoTemplate.remove(version);
|
|
}
|
|
|
|
private String resolveApiKey(UUID userId, String provider, String apiKey) {
|
|
if (apiKey != null && !apiKey.isBlank()) {
|
|
return apiKey;
|
|
}
|
|
User user = userRepository.findById(userId)
|
|
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
|
Map<String, String> keys = user.getLlmApiKeys();
|
|
if (keys != null && keys.containsKey(provider)) {
|
|
return encryptionService.decrypt(keys.get(provider));
|
|
}
|
|
throw new IllegalStateException("No API key configured for " + provider
|
|
+ ". Add one in Settings.");
|
|
}
|
|
|
|
private String resolveModel(UUID userId, String provider, String model) {
|
|
if (model != null && !model.isBlank()) {
|
|
return model;
|
|
}
|
|
User user = userRepository.findById(userId)
|
|
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
|
Map<String, String> models = user.getLlmModels();
|
|
if (models != null && models.containsKey(provider)) {
|
|
return models.get(provider);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private String resolveBaseUrl(UUID userId, String provider, String baseUrl) {
|
|
if (baseUrl != null && !baseUrl.isBlank()) {
|
|
return baseUrl;
|
|
}
|
|
User user = userRepository.findById(userId)
|
|
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
|
Map<String, String> urls = user.getLlmBaseUrls();
|
|
if (urls != null && urls.containsKey(provider)) {
|
|
return urls.get(provider);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private LLMResult callLLM(String provider, String apiKey, String baseUrl, String model, String systemPrompt,
|
|
String userPrompt, List<Reference> references, LLMOptions options) {
|
|
LLMProvider p = providerFactory.createProvider(provider, apiKey, baseUrl, model);
|
|
try {
|
|
return p.generate(systemPrompt, userPrompt, references, options);
|
|
} catch (ResourceAccessException e) {
|
|
throw new IllegalStateException(
|
|
provider + " timed out. Please try again or use a simpler prompt.");
|
|
} catch (HttpClientErrorException e) {
|
|
String detail = e.getResponseBodyAsString();
|
|
String msg = switch (e.getStatusCode().value()) {
|
|
case 401 -> provider + " rejected the API key (401 Unauthorized). Verify the key is valid in Settings.";
|
|
case 429 -> provider + " rate limit exceeded (429). Try again later.";
|
|
default -> {
|
|
String body = (detail != null && !detail.isBlank()) ? ": " + detail : "";
|
|
yield provider + " API error: " + e.getStatusCode() + " " + e.getStatusText() + body;
|
|
}
|
|
};
|
|
throw new IllegalStateException(msg);
|
|
}
|
|
}
|
|
|
|
private void saveVersion(UUID userId, UUID siteId, String prompt,
|
|
List<Reference> references, String generatedJson) {
|
|
int nextVersion = getNextVersionNumber(siteId);
|
|
|
|
List<GenerationVersion.ReferenceEntry> refEntries = null;
|
|
if (references != null) {
|
|
refEntries = references.stream()
|
|
.map(r -> GenerationVersion.ReferenceEntry.builder()
|
|
.type(r.getType().name())
|
|
.url(r.getUrl())
|
|
.altText(r.getAltText())
|
|
.build())
|
|
.toList();
|
|
}
|
|
|
|
GenerationVersion version = GenerationVersion.builder()
|
|
.siteId(siteId)
|
|
.userId(userId)
|
|
.versionNumber(nextVersion)
|
|
.prompt(prompt)
|
|
.references(refEntries)
|
|
.fullStructure(generatedJson)
|
|
.createdAt(LocalDateTime.now())
|
|
.build();
|
|
mongoTemplate.save(version);
|
|
}
|
|
|
|
private int getNextVersionNumber(UUID siteId) {
|
|
Query query = new Query(Criteria.where("siteId").is(siteId))
|
|
.with(Sort.by(Sort.Direction.DESC, "versionNumber"))
|
|
.limit(1);
|
|
GenerationVersion latest = mongoTemplate.findOne(query, GenerationVersion.class);
|
|
return latest != null ? latest.getVersionNumber() + 1 : 1;
|
|
}
|
|
|
|
private String loadSystemPrompt(ResourceLoader resourceLoader, String path) {
|
|
try {
|
|
Resource resource = resourceLoader.getResource(path);
|
|
if (!resource.exists()) {
|
|
log.warn("System prompt file not found at {}, using default", path);
|
|
return "You are a website generator. Given a user's description, output a valid JSON site structure.";
|
|
}
|
|
try (BufferedReader reader = new BufferedReader(
|
|
new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
|
|
return reader.lines().collect(Collectors.joining("\n"));
|
|
}
|
|
} catch (IOException e) {
|
|
log.warn("Failed to load system prompt from {}: {}", path, e.getMessage());
|
|
return "You are a website generator. Given a user's description, output a valid JSON site structure.";
|
|
}
|
|
}
|
|
|
|
private String serializeStructure(SiteStructure structure) {
|
|
try {
|
|
ObjectMapper mapper = new ObjectMapper();
|
|
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(structure);
|
|
} catch (Exception e) {
|
|
throw new RuntimeException("Failed to serialize site structure", e);
|
|
}
|
|
}
|
|
}
|