generate site using llm prompt
This commit is contained in:
40
backend/src/main/java/com/indie/config/RateLimitConfig.java
Normal file
40
backend/src/main/java/com/indie/config/RateLimitConfig.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.indie.config;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket;
|
||||
import io.github.bucket4j.Refill;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class RateLimitConfig {
|
||||
|
||||
private final Map<UUID, Bucket> cache = new ConcurrentHashMap<>();
|
||||
private final int maxRequestsPerHour;
|
||||
|
||||
public RateLimitConfig(
|
||||
@Value("${indie.llm.rate-limit.max-requests-per-hour:10}") int maxRequestsPerHour) {
|
||||
this.maxRequestsPerHour = maxRequestsPerHour;
|
||||
}
|
||||
|
||||
public boolean tryConsume(UUID userId) {
|
||||
Bucket bucket = cache.computeIfAbsent(userId, this::createBucket);
|
||||
return bucket.tryConsume(1);
|
||||
}
|
||||
|
||||
public int getAvailableTokens(UUID userId) {
|
||||
Bucket bucket = cache.computeIfAbsent(userId, this::createBucket);
|
||||
return (int) bucket.getAvailableTokens();
|
||||
}
|
||||
|
||||
private Bucket createBucket(UUID userId) {
|
||||
Bandwidth limit = Bandwidth.classic(maxRequestsPerHour,
|
||||
Refill.greedy(maxRequestsPerHour, Duration.ofHours(1)));
|
||||
return Bucket.builder().addLimit(limit).build();
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package com.indie.controller;
|
||||
|
||||
import com.indie.config.UserPrincipal;
|
||||
import com.indie.dto.ComponentResponse;
|
||||
import com.indie.dto.PageWithComponentsResponse;
|
||||
import com.indie.dto.SaveComponentsRequest;
|
||||
import com.indie.model.Component;
|
||||
import com.indie.model.Page;
|
||||
import com.indie.model.Site;
|
||||
import com.indie.repository.ComponentRepository;
|
||||
import com.indie.repository.PageRepository;
|
||||
import com.indie.repository.SiteRepository;
|
||||
import com.indie.service.BuilderService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/builder/sites/{siteId}")
|
||||
public class BuilderController {
|
||||
|
||||
private final SiteRepository siteRepository;
|
||||
private final PageRepository pageRepository;
|
||||
private final ComponentRepository componentRepository;
|
||||
private final BuilderService builderService;
|
||||
|
||||
public BuilderController(SiteRepository siteRepository, PageRepository pageRepository,
|
||||
ComponentRepository componentRepository, BuilderService builderService) {
|
||||
this.siteRepository = siteRepository;
|
||||
this.pageRepository = pageRepository;
|
||||
this.componentRepository = componentRepository;
|
||||
this.builderService = builderService;
|
||||
}
|
||||
|
||||
@GetMapping("/pages")
|
||||
public ResponseEntity<List<PageWithComponentsResponse>> listPagesWithComponents(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID siteId) {
|
||||
Site site = findSiteForUser(siteId, principal.id());
|
||||
List<Page> pages = pageRepository.findBySiteIdOrderByOrderIndexAsc(site.getId());
|
||||
|
||||
List<PageWithComponentsResponse> response = pages.stream()
|
||||
.map(page -> {
|
||||
List<ComponentResponse> components = componentRepository
|
||||
.findByPageIdOrderByOrderIndexAsc(page.getId())
|
||||
.stream()
|
||||
.map(ComponentResponse::from)
|
||||
.toList();
|
||||
return PageWithComponentsResponse.from(page, components);
|
||||
})
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PutMapping("/pages/{pageId}/components")
|
||||
public ResponseEntity<List<ComponentResponse>> saveComponents(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID siteId,
|
||||
@PathVariable UUID pageId,
|
||||
@Valid @RequestBody SaveComponentsRequest request) {
|
||||
Site site = findSiteForUser(siteId, principal.id());
|
||||
Page page = findPageForSite(pageId, site.getId());
|
||||
|
||||
List<Component> components = builderService.saveComponents(page.getId(), request);
|
||||
|
||||
List<ComponentResponse> response = components.stream()
|
||||
.map(ComponentResponse::from)
|
||||
.toList();
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/theme")
|
||||
public ResponseEntity<Map<String, Object>> getTheme(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID siteId) {
|
||||
Site site = findSiteForUser(siteId, principal.id());
|
||||
return ResponseEntity.ok(site.getThemeConfig());
|
||||
}
|
||||
|
||||
@PutMapping("/theme")
|
||||
public ResponseEntity<Map<String, Object>> updateTheme(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID siteId,
|
||||
@RequestBody Map<String, Object> themeConfig) {
|
||||
Site site = findSiteForUser(siteId, principal.id());
|
||||
site.setThemeConfig(themeConfig);
|
||||
siteRepository.save(site);
|
||||
return ResponseEntity.ok(site.getThemeConfig());
|
||||
}
|
||||
|
||||
private Site findSiteForUser(UUID siteId, UUID userId) {
|
||||
Site site = siteRepository.findById(siteId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Site not found"));
|
||||
if (!site.getUserId().equals(userId)) {
|
||||
throw new IllegalArgumentException("Site not found");
|
||||
}
|
||||
return site;
|
||||
}
|
||||
|
||||
private Page findPageForSite(UUID pageId, UUID siteId) {
|
||||
Page page = pageRepository.findById(pageId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Page not found"));
|
||||
if (!page.getSiteId().equals(siteId)) {
|
||||
throw new IllegalArgumentException("Page not found");
|
||||
}
|
||||
return page;
|
||||
}
|
||||
}
|
||||
138
backend/src/main/java/com/indie/controller/LLMController.java
Normal file
138
backend/src/main/java/com/indie/controller/LLMController.java
Normal file
@@ -0,0 +1,138 @@
|
||||
package com.indie.controller;
|
||||
|
||||
import com.indie.config.RateLimitConfig;
|
||||
import com.indie.config.UserPrincipal;
|
||||
import com.indie.dto.Reference;
|
||||
import com.indie.dto.SiteStructure;
|
||||
import com.indie.model.GenerationVersion;
|
||||
import com.indie.service.LLMService;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/llm")
|
||||
public class LLMController {
|
||||
|
||||
private final LLMService llmService;
|
||||
private final RateLimitConfig rateLimitConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public LLMController(LLMService llmService, RateLimitConfig rateLimitConfig, ObjectMapper objectMapper) {
|
||||
this.llmService = llmService;
|
||||
this.rateLimitConfig = rateLimitConfig;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@PostMapping("/generate")
|
||||
public ResponseEntity<?> generate(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@RequestBody Map<String, Object> request) {
|
||||
if (!rateLimitConfig.tryConsume(principal.id())) {
|
||||
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
|
||||
.body(Map.of("error", "Rate limit exceeded. Max " +
|
||||
rateLimitConfig.getAvailableTokens(principal.id()) +
|
||||
" requests per hour."));
|
||||
}
|
||||
|
||||
String prompt = (String) request.get("prompt");
|
||||
if (prompt == null || prompt.isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Prompt is required"));
|
||||
}
|
||||
|
||||
String siteIdStr = (String) request.get("siteId");
|
||||
if (siteIdStr == null || siteIdStr.isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "siteId is required"));
|
||||
}
|
||||
UUID siteId = UUID.fromString(siteIdStr);
|
||||
|
||||
List<Reference> references = null;
|
||||
if (request.containsKey("references")) {
|
||||
references = objectMapper.convertValue(
|
||||
request.get("references"), new TypeReference<List<Reference>>() {});
|
||||
}
|
||||
|
||||
SiteStructure structure = llmService.generateSite(principal.id(), siteId, prompt, references);
|
||||
return ResponseEntity.ok(structure);
|
||||
}
|
||||
|
||||
@PostMapping("/refine")
|
||||
public ResponseEntity<?> refine(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@RequestBody Map<String, Object> request) {
|
||||
if (!rateLimitConfig.tryConsume(principal.id())) {
|
||||
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
|
||||
.body(Map.of("error", "Rate limit exceeded. Max " +
|
||||
rateLimitConfig.getAvailableTokens(principal.id()) +
|
||||
" requests per hour."));
|
||||
}
|
||||
|
||||
String prompt = (String) request.get("prompt");
|
||||
if (prompt == null || prompt.isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Prompt is required"));
|
||||
}
|
||||
|
||||
String siteIdStr = (String) request.get("siteId");
|
||||
if (siteIdStr == null || siteIdStr.isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "siteId is required"));
|
||||
}
|
||||
UUID siteId = UUID.fromString(siteIdStr);
|
||||
|
||||
SiteStructure currentStructure;
|
||||
try {
|
||||
currentStructure = objectMapper.convertValue(
|
||||
request.get("currentStructure"), SiteStructure.class);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid currentStructure"));
|
||||
}
|
||||
|
||||
if (currentStructure == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "currentStructure is required"));
|
||||
}
|
||||
|
||||
List<Reference> references = null;
|
||||
if (request.containsKey("references")) {
|
||||
references = objectMapper.convertValue(
|
||||
request.get("references"), new TypeReference<List<Reference>>() {});
|
||||
}
|
||||
|
||||
SiteStructure structure = llmService.refineSite(principal.id(), siteId,
|
||||
currentStructure, prompt, references);
|
||||
return ResponseEntity.ok(structure);
|
||||
}
|
||||
|
||||
@GetMapping("/versions/{siteId}")
|
||||
public ResponseEntity<List<GenerationVersion>> getVersions(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID siteId) {
|
||||
List<GenerationVersion> versions = llmService.getVersions(siteId);
|
||||
return ResponseEntity.ok(versions);
|
||||
}
|
||||
|
||||
@PostMapping("/versions/{id}/restore")
|
||||
public ResponseEntity<?> restoreVersion(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable String id) {
|
||||
SiteStructure structure = llmService.restoreVersion(id);
|
||||
return ResponseEntity.ok(structure);
|
||||
}
|
||||
|
||||
@DeleteMapping("/versions/{id}")
|
||||
public ResponseEntity<?> deleteVersion(
|
||||
@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable String id) {
|
||||
try {
|
||||
llmService.deleteVersion(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package com.indie.controller;
|
||||
|
||||
import com.indie.config.UserPrincipal;
|
||||
import com.indie.dto.PageRequest;
|
||||
import com.indie.dto.PageResponse;
|
||||
import com.indie.model.Page;
|
||||
import com.indie.model.Site;
|
||||
import com.indie.repository.PageRepository;
|
||||
import com.indie.repository.SiteRepository;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/sites/{siteId}/pages")
|
||||
public class PageController {
|
||||
|
||||
private final PageRepository pageRepository;
|
||||
private final SiteRepository siteRepository;
|
||||
|
||||
public PageController(PageRepository pageRepository, SiteRepository siteRepository) {
|
||||
this.pageRepository = pageRepository;
|
||||
this.siteRepository = siteRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<PageResponse>> listPages(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID siteId) {
|
||||
Site site = findSiteForUser(siteId, principal.id());
|
||||
List<Page> pages = pageRepository.findBySiteIdOrderByOrderIndexAsc(site.getId());
|
||||
List<PageResponse> response = pages.stream()
|
||||
.map(PageResponse::from)
|
||||
.toList();
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<PageResponse> createPage(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID siteId,
|
||||
@Valid @RequestBody PageRequest request) {
|
||||
Site site = findSiteForUser(siteId, principal.id());
|
||||
|
||||
if (pageRepository.existsBySiteIdAndSlug(site.getId(), request.getSlug())) {
|
||||
throw new IllegalArgumentException("A page with this slug already exists");
|
||||
}
|
||||
|
||||
Page page = Page.builder()
|
||||
.siteId(site.getId())
|
||||
.title(request.getTitle())
|
||||
.slug(request.getSlug())
|
||||
.seoTitle(request.getSeoTitle())
|
||||
.seoDescription(request.getSeoDescription())
|
||||
.orderIndex(request.getOrderIndex())
|
||||
.build();
|
||||
|
||||
page = pageRepository.save(page);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(PageResponse.from(page));
|
||||
}
|
||||
|
||||
@GetMapping("/{pageId}")
|
||||
public ResponseEntity<PageResponse> getPage(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID siteId,
|
||||
@PathVariable UUID pageId) {
|
||||
Site site = findSiteForUser(siteId, principal.id());
|
||||
Page page = findPageForSite(pageId, site.getId());
|
||||
return ResponseEntity.ok(PageResponse.from(page));
|
||||
}
|
||||
|
||||
@PutMapping("/{pageId}")
|
||||
public ResponseEntity<PageResponse> updatePage(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID siteId,
|
||||
@PathVariable UUID pageId,
|
||||
@Valid @RequestBody PageRequest request) {
|
||||
Site site = findSiteForUser(siteId, principal.id());
|
||||
Page page = findPageForSite(pageId, site.getId());
|
||||
|
||||
if (!page.getSlug().equals(request.getSlug())
|
||||
&& pageRepository.existsBySiteIdAndSlug(site.getId(), request.getSlug())) {
|
||||
throw new IllegalArgumentException("A page with this slug already exists");
|
||||
}
|
||||
|
||||
page.setTitle(request.getTitle());
|
||||
page.setSlug(request.getSlug());
|
||||
page.setSeoTitle(request.getSeoTitle());
|
||||
page.setSeoDescription(request.getSeoDescription());
|
||||
page.setOrderIndex(request.getOrderIndex());
|
||||
|
||||
page = pageRepository.save(page);
|
||||
return ResponseEntity.ok(PageResponse.from(page));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{pageId}")
|
||||
public ResponseEntity<Void> deletePage(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable UUID siteId,
|
||||
@PathVariable UUID pageId) {
|
||||
Site site = findSiteForUser(siteId, principal.id());
|
||||
Page page = findPageForSite(pageId, site.getId());
|
||||
pageRepository.delete(page);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private Site findSiteForUser(UUID siteId, UUID userId) {
|
||||
Site site = siteRepository.findById(siteId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Site not found"));
|
||||
if (!site.getUserId().equals(userId)) {
|
||||
throw new IllegalArgumentException("Site not found");
|
||||
}
|
||||
return site;
|
||||
}
|
||||
|
||||
private Page findPageForSite(UUID pageId, UUID siteId) {
|
||||
Page page = pageRepository.findById(pageId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Page not found"));
|
||||
if (!page.getSiteId().equals(siteId)) {
|
||||
throw new IllegalArgumentException("Page not found");
|
||||
}
|
||||
return page;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,6 @@ public class SiteController {
|
||||
.name(request.getName())
|
||||
.description(request.getDescription())
|
||||
.subdomain(subdomain)
|
||||
.themeConfig(request.getThemeConfig())
|
||||
.build();
|
||||
|
||||
site = siteRepository.save(site);
|
||||
@@ -76,9 +75,6 @@ public class SiteController {
|
||||
site.setName(request.getName());
|
||||
site.setDescription(request.getDescription());
|
||||
site.setSubdomain(newSubdomain);
|
||||
if (request.getThemeConfig() != null) {
|
||||
site.setThemeConfig(request.getThemeConfig());
|
||||
}
|
||||
|
||||
site = siteRepository.save(site);
|
||||
return ResponseEntity.ok(SiteResponse.from(site));
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.indie.dto;
|
||||
|
||||
import com.indie.model.Component;
|
||||
import com.indie.model.ComponentType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ComponentResponse {
|
||||
|
||||
private UUID id;
|
||||
private UUID pageId;
|
||||
private ComponentType type;
|
||||
private Map<String, Object> config;
|
||||
private Map<String, Object> styles;
|
||||
private int orderIndex;
|
||||
|
||||
public static ComponentResponse from(Component component) {
|
||||
return ComponentResponse.builder()
|
||||
.id(component.getId())
|
||||
.pageId(component.getPageId())
|
||||
.type(component.getType())
|
||||
.config(component.getConfig())
|
||||
.styles(component.getStyles())
|
||||
.orderIndex(component.getOrderIndex())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
20
backend/src/main/java/com/indie/dto/LLMOptions.java
Normal file
20
backend/src/main/java/com/indie/dto/LLMOptions.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.indie.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class LLMOptions {
|
||||
|
||||
@Builder.Default
|
||||
private double temperature = 0.7;
|
||||
|
||||
@Builder.Default
|
||||
private int maxTokens = 4096;
|
||||
|
||||
@Builder.Default
|
||||
private int timeoutSeconds = 60;
|
||||
}
|
||||
17
backend/src/main/java/com/indie/dto/LLMResult.java
Normal file
17
backend/src/main/java/com/indie/dto/LLMResult.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.indie.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class LLMResult {
|
||||
|
||||
private String content;
|
||||
private int promptTokens;
|
||||
private int completionTokens;
|
||||
private String model;
|
||||
private long latencyMs;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.indie.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PageRequest {
|
||||
|
||||
@NotBlank(message = "Page title is required")
|
||||
private String title;
|
||||
|
||||
@NotBlank(message = "Page slug is required")
|
||||
@Pattern(regexp = "^[a-z0-9-]+$", message = "Slug must be lowercase alphanumeric with hyphens")
|
||||
private String slug;
|
||||
|
||||
private String seoTitle;
|
||||
|
||||
private String seoDescription;
|
||||
|
||||
private int orderIndex;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.indie.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class PageResponse {
|
||||
|
||||
private UUID id;
|
||||
private UUID siteId;
|
||||
private String title;
|
||||
private String slug;
|
||||
private String seoTitle;
|
||||
private String seoDescription;
|
||||
private int orderIndex;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public static PageResponse from(com.indie.model.Page page) {
|
||||
return PageResponse.builder()
|
||||
.id(page.getId())
|
||||
.siteId(page.getSiteId())
|
||||
.title(page.getTitle())
|
||||
.slug(page.getSlug())
|
||||
.seoTitle(page.getSeoTitle())
|
||||
.seoDescription(page.getSeoDescription())
|
||||
.orderIndex(page.getOrderIndex())
|
||||
.createdAt(page.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.indie.dto;
|
||||
|
||||
import com.indie.model.Page;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class PageWithComponentsResponse {
|
||||
|
||||
private UUID id;
|
||||
private UUID siteId;
|
||||
private String title;
|
||||
private String slug;
|
||||
private String seoTitle;
|
||||
private String seoDescription;
|
||||
private int orderIndex;
|
||||
private LocalDateTime createdAt;
|
||||
private List<ComponentResponse> components;
|
||||
|
||||
public static PageWithComponentsResponse from(Page page, List<ComponentResponse> components) {
|
||||
return PageWithComponentsResponse.builder()
|
||||
.id(page.getId())
|
||||
.siteId(page.getSiteId())
|
||||
.title(page.getTitle())
|
||||
.slug(page.getSlug())
|
||||
.seoTitle(page.getSeoTitle())
|
||||
.seoDescription(page.getSeoDescription())
|
||||
.orderIndex(page.getOrderIndex())
|
||||
.createdAt(page.getCreatedAt())
|
||||
.components(components)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
19
backend/src/main/java/com/indie/dto/Reference.java
Normal file
19
backend/src/main/java/com/indie/dto/Reference.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.indie.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Reference {
|
||||
|
||||
private ReferenceType type;
|
||||
private String url;
|
||||
private String altText;
|
||||
|
||||
public enum ReferenceType {
|
||||
URL, IMAGE
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.indie.dto;
|
||||
|
||||
import com.indie.model.ComponentType;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class SaveComponentsRequest {
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
private List<ComponentDTO> components;
|
||||
|
||||
@Data
|
||||
public static class ComponentDTO {
|
||||
private UUID id;
|
||||
@NotNull
|
||||
private ComponentType type;
|
||||
private Map<String, Object> config;
|
||||
private Map<String, Object> styles;
|
||||
private int orderIndex;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ package com.indie.dto;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class SiteRequest {
|
||||
|
||||
@@ -14,6 +12,4 @@ public class SiteRequest {
|
||||
private String description;
|
||||
|
||||
private String subdomain;
|
||||
|
||||
private Map<String, Object> themeConfig;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.indie.dto;
|
||||
|
||||
import com.indie.model.Site;
|
||||
import com.indie.model.SiteStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@@ -19,20 +19,18 @@ public class SiteResponse {
|
||||
private String name;
|
||||
private String description;
|
||||
private String subdomain;
|
||||
private Map<String, Object> themeConfig;
|
||||
private SiteStatus status;
|
||||
private LocalDateTime publishedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public static SiteResponse from(com.indie.model.Site site) {
|
||||
public static SiteResponse from(Site site) {
|
||||
return SiteResponse.builder()
|
||||
.id(site.getId())
|
||||
.userId(site.getUserId())
|
||||
.name(site.getName())
|
||||
.description(site.getDescription())
|
||||
.subdomain(site.getSubdomain())
|
||||
.themeConfig(site.getThemeConfig())
|
||||
.status(site.getStatus())
|
||||
.publishedAt(site.getPublishedAt())
|
||||
.createdAt(site.getCreatedAt())
|
||||
|
||||
50
backend/src/main/java/com/indie/dto/SiteStructure.java
Normal file
50
backend/src/main/java/com/indie/dto/SiteStructure.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.indie.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class SiteStructure {
|
||||
|
||||
private ThemeConfig theme;
|
||||
private List<PageStructure> pages;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class ThemeConfig {
|
||||
private Map<String, String> colors;
|
||||
private Map<String, String> fonts;
|
||||
private Map<String, String> spacing;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class PageStructure {
|
||||
private String id;
|
||||
private String title;
|
||||
private String slug;
|
||||
private String seoTitle;
|
||||
private String seoDescription;
|
||||
private int orderIndex;
|
||||
private List<ComponentStructure> components;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class ComponentStructure {
|
||||
private String id;
|
||||
private String type;
|
||||
private int orderIndex;
|
||||
private Map<String, Object> config;
|
||||
private Map<String, Object> styles;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.indie.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "components")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Component {
|
||||
|
||||
@Id
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID pageId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private ComponentType type;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private Map<String, Object> config;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private Map<String, Object> styles;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int orderIndex;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (id == null) {
|
||||
id = UUID.randomUUID();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.indie.model;
|
||||
|
||||
public enum ComponentType {
|
||||
HEADING, TEXT, IMAGE, BUTTON, DIVIDER, GALLERY, VIDEO, CONTACT_FORM, MAP, CUSTOM_HTML
|
||||
}
|
||||
41
backend/src/main/java/com/indie/model/GenerationVersion.java
Normal file
41
backend/src/main/java/com/indie/model/GenerationVersion.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.indie.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Document(collection = "generation_versions")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class GenerationVersion {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
private UUID siteId;
|
||||
private UUID userId;
|
||||
private int versionNumber;
|
||||
private String prompt;
|
||||
private List<ReferenceEntry> references;
|
||||
private String fullStructure;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class ReferenceEntry {
|
||||
private String type;
|
||||
private String url;
|
||||
private String altText;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package com.indie.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "pages")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Page {
|
||||
|
||||
@Id
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private UUID siteId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String slug;
|
||||
|
||||
private String seoTitle;
|
||||
|
||||
@Column(length = 500)
|
||||
private String seoDescription;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int orderIndex;
|
||||
|
||||
@Column(nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
if (id == null) {
|
||||
id = UUID.randomUUID();
|
||||
}
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,8 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@@ -35,9 +32,6 @@ public class Site {
|
||||
@Column(unique = true)
|
||||
private String subdomain;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
private Map<String, Object> themeConfig;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.indie.repository;
|
||||
|
||||
import com.indie.model.Component;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface ComponentRepository extends JpaRepository<Component, UUID> {
|
||||
|
||||
List<Component> findByPageIdOrderByOrderIndexAsc(UUID pageId);
|
||||
|
||||
void deleteByPageId(UUID pageId);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.indie.repository;
|
||||
|
||||
import com.indie.model.Page;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PageRepository extends JpaRepository<Page, UUID> {
|
||||
|
||||
List<Page> findBySiteIdOrderByOrderIndexAsc(UUID siteId);
|
||||
|
||||
Optional<Page> findBySiteIdAndSlug(UUID siteId, String slug);
|
||||
|
||||
boolean existsBySiteIdAndSlug(UUID siteId, String slug);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.indie.service;
|
||||
|
||||
import com.indie.dto.SaveComponentsRequest;
|
||||
import com.indie.model.Component;
|
||||
import com.indie.repository.ComponentRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BuilderService {
|
||||
|
||||
private final ComponentRepository componentRepository;
|
||||
|
||||
static final int MAX_COMPONENTS_PER_PAGE = 50;
|
||||
|
||||
public void validateComponents(SaveComponentsRequest request) {
|
||||
if (request.getComponents() == null || request.getComponents().isEmpty()) {
|
||||
throw new IllegalArgumentException("Component list must not be empty");
|
||||
}
|
||||
if (request.getComponents().size() > MAX_COMPONENTS_PER_PAGE) {
|
||||
throw new IllegalArgumentException("Maximum " + MAX_COMPONENTS_PER_PAGE + " components allowed per page");
|
||||
}
|
||||
|
||||
for (SaveComponentsRequest.ComponentDTO dto : request.getComponents()) {
|
||||
if (dto.getType() == null) {
|
||||
throw new IllegalArgumentException("Component type is required");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<Component> saveComponents(UUID pageId, SaveComponentsRequest request) {
|
||||
validateComponents(request);
|
||||
|
||||
componentRepository.deleteByPageId(pageId);
|
||||
|
||||
List<Component> components = request.getComponents().stream()
|
||||
.map(dto -> Component.builder()
|
||||
.id(dto.getId() != null ? dto.getId() : UUID.randomUUID())
|
||||
.pageId(pageId)
|
||||
.type(dto.getType())
|
||||
.config(dto.getConfig())
|
||||
.styles(dto.getStyles())
|
||||
.orderIndex(dto.getOrderIndex())
|
||||
.build())
|
||||
.toList();
|
||||
|
||||
return componentRepository.saveAll(components);
|
||||
}
|
||||
}
|
||||
191
backend/src/main/java/com/indie/service/LLMResponseParser.java
Normal file
191
backend/src/main/java/com/indie/service/LLMResponseParser.java
Normal file
@@ -0,0 +1,191 @@
|
||||
package com.indie.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.indie.dto.SiteStructure;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class LLMResponseParser {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LLMResponseParser.class);
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public LLMResponseParser(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper.copy()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
public SiteStructure parse(String llmOutput) {
|
||||
String json = extractJson(llmOutput);
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(json);
|
||||
return parseSiteStructure(root);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Failed to parse LLM output as JSON: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
String extractJson(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
throw new IllegalArgumentException("LLM output is empty");
|
||||
}
|
||||
|
||||
String trimmed = raw.trim();
|
||||
|
||||
if (trimmed.startsWith("```")) {
|
||||
int start = trimmed.indexOf('\n');
|
||||
int end = trimmed.lastIndexOf("```");
|
||||
if (start > 0 && end > start) {
|
||||
trimmed = trimmed.substring(start, end).trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!trimmed.startsWith("{")) {
|
||||
int brace = trimmed.indexOf('{');
|
||||
if (brace >= 0) {
|
||||
trimmed = trimmed.substring(brace);
|
||||
}
|
||||
}
|
||||
|
||||
if (!trimmed.endsWith("}")) {
|
||||
int brace = trimmed.lastIndexOf('}');
|
||||
if (brace >= 0) {
|
||||
trimmed = trimmed.substring(0, brace + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private SiteStructure parseSiteStructure(JsonNode root) {
|
||||
SiteStructure.SiteStructureBuilder builder = SiteStructure.builder();
|
||||
|
||||
if (root.has("theme")) {
|
||||
builder.theme(parseTheme(root.get("theme")));
|
||||
}
|
||||
|
||||
List<SiteStructure.PageStructure> pages = new ArrayList<>();
|
||||
if (root.has("pages") && root.get("pages").isArray()) {
|
||||
for (JsonNode pageNode : root.get("pages")) {
|
||||
pages.add(parsePage(pageNode));
|
||||
}
|
||||
}
|
||||
builder.pages(pages);
|
||||
|
||||
SiteStructure structure = builder.build();
|
||||
if (structure.getPages() == null || structure.getPages().isEmpty()) {
|
||||
throw new IllegalArgumentException("LLM output must contain at least one page");
|
||||
}
|
||||
|
||||
return structure;
|
||||
}
|
||||
|
||||
private SiteStructure.ThemeConfig parseTheme(JsonNode themeNode) {
|
||||
SiteStructure.ThemeConfig.ThemeConfigBuilder builder = SiteStructure.ThemeConfig.builder();
|
||||
|
||||
if (themeNode.has("colors")) {
|
||||
builder.colors(parseStringMap(themeNode.get("colors")));
|
||||
}
|
||||
if (themeNode.has("fonts")) {
|
||||
builder.fonts(parseStringMap(themeNode.get("fonts")));
|
||||
}
|
||||
if (themeNode.has("spacing")) {
|
||||
builder.spacing(parseStringMap(themeNode.get("spacing")));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private Map<String, String> parseStringMap(JsonNode node) {
|
||||
Map<String, String> map = new java.util.LinkedHashMap<>();
|
||||
Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
|
||||
while (fields.hasNext()) {
|
||||
Map.Entry<String, JsonNode> field = fields.next();
|
||||
map.put(field.getKey(), field.getValue().asText());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private SiteStructure.PageStructure parsePage(JsonNode pageNode) {
|
||||
SiteStructure.PageStructure.PageStructureBuilder builder = SiteStructure.PageStructure.builder();
|
||||
|
||||
if (pageNode.has("id")) {
|
||||
builder.id(pageNode.get("id").asText());
|
||||
} else {
|
||||
builder.id(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
if (pageNode.has("title")) {
|
||||
builder.title(pageNode.get("title").asText());
|
||||
}
|
||||
if (pageNode.has("slug")) {
|
||||
builder.slug(pageNode.get("slug").asText());
|
||||
}
|
||||
if (pageNode.has("seoTitle")) {
|
||||
builder.seoTitle(pageNode.get("seoTitle").asText());
|
||||
}
|
||||
if (pageNode.has("seoDescription")) {
|
||||
builder.seoDescription(pageNode.get("seoDescription").asText());
|
||||
}
|
||||
if (pageNode.has("orderIndex")) {
|
||||
builder.orderIndex(pageNode.get("orderIndex").asInt());
|
||||
}
|
||||
|
||||
List<SiteStructure.ComponentStructure> components = new ArrayList<>();
|
||||
if (pageNode.has("components") && pageNode.get("components").isArray()) {
|
||||
for (JsonNode compNode : pageNode.get("components")) {
|
||||
components.add(parseComponent(compNode));
|
||||
}
|
||||
}
|
||||
builder.components(components);
|
||||
|
||||
SiteStructure.PageStructure page = builder.build();
|
||||
if (page.getTitle() == null || page.getSlug() == null) {
|
||||
throw new IllegalArgumentException("Page must have title and slug");
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
private SiteStructure.ComponentStructure parseComponent(JsonNode compNode) {
|
||||
SiteStructure.ComponentStructure.ComponentStructureBuilder builder = SiteStructure.ComponentStructure.builder();
|
||||
|
||||
if (compNode.has("id")) {
|
||||
builder.id(compNode.get("id").asText());
|
||||
} else {
|
||||
builder.id(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
if (compNode.has("type")) {
|
||||
builder.type(compNode.get("type").asText());
|
||||
}
|
||||
if (compNode.has("orderIndex")) {
|
||||
builder.orderIndex(compNode.get("orderIndex").asInt());
|
||||
}
|
||||
if (compNode.has("config")) {
|
||||
builder.config(objectMapper.convertValue(compNode.get("config"), Map.class));
|
||||
}
|
||||
if (compNode.has("styles")) {
|
||||
builder.styles(objectMapper.convertValue(compNode.get("styles"), Map.class));
|
||||
}
|
||||
|
||||
SiteStructure.ComponentStructure component = builder.build();
|
||||
if (component.getType() == null) {
|
||||
throw new IllegalArgumentException("Component must have a type");
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
}
|
||||
191
backend/src/main/java/com/indie/service/LLMService.java
Normal file
191
backend/src/main/java/com/indie/service/LLMService.java
Normal file
@@ -0,0 +1,191 @@
|
||||
package com.indie.service;
|
||||
|
||||
import com.indie.dto.LLMOptions;
|
||||
import com.indie.dto.LLMResult;
|
||||
import com.indie.dto.Reference;
|
||||
import com.indie.dto.SiteStructure;
|
||||
import com.indie.model.GenerationVersion;
|
||||
import com.indie.service.llm.LLMFactory;
|
||||
import com.indie.service.llm.LLMProvider;
|
||||
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.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 LLMFactory llmFactory;
|
||||
private final LLMResponseParser responseParser;
|
||||
private final ReferenceService referenceService;
|
||||
private final MongoTemplate mongoTemplate;
|
||||
private final String systemPrompt;
|
||||
|
||||
public LLMService(LLMFactory llmFactory,
|
||||
LLMResponseParser responseParser,
|
||||
ReferenceService referenceService,
|
||||
MongoTemplate mongoTemplate,
|
||||
ResourceLoader resourceLoader,
|
||||
@Value("${indie.llm.system-prompt-path:classpath:prompts/site-generator.txt}") String promptPath) {
|
||||
this.llmFactory = llmFactory;
|
||||
this.responseParser = responseParser;
|
||||
this.referenceService = referenceService;
|
||||
this.mongoTemplate = mongoTemplate;
|
||||
this.systemPrompt = loadSystemPrompt(resourceLoader, promptPath);
|
||||
}
|
||||
|
||||
public SiteStructure generateSite(UUID userId, UUID siteId, String prompt, List<Reference> references) {
|
||||
String referenceContext = referenceService.buildReferenceContext(references);
|
||||
String fullPrompt = prompt + referenceContext;
|
||||
|
||||
LLMOptions options = LLMOptions.builder().build();
|
||||
LLMResult result = callLLM(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 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(systemPrompt, 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 LLMResult callLLM(String systemPrompt, String userPrompt, List<Reference> references, LLMOptions options) {
|
||||
LLMProvider provider = llmFactory.getActiveProvider();
|
||||
try {
|
||||
return provider.generate(systemPrompt, userPrompt, references, options);
|
||||
} catch (Exception e) {
|
||||
log.warn("Active provider {} failed: {}. Trying fallback...",
|
||||
provider.getName(), e.getMessage());
|
||||
return llmFactory.getFallbackProvider()
|
||||
.orElseThrow(() -> new RuntimeException("All LLM providers failed", e))
|
||||
.generate(systemPrompt, userPrompt, references, options);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.indie.service;
|
||||
|
||||
import com.indie.dto.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class ReferenceService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ReferenceService.class);
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public ReferenceService() {
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
public String buildReferenceContext(List<Reference> references) {
|
||||
if (references == null || references.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder("\n\nReference materials provided by the user:\n");
|
||||
for (int i = 0; i < references.size(); i++) {
|
||||
Reference ref = references.get(i);
|
||||
sb.append("[").append(i + 1).append("] ");
|
||||
sb.append(ref.getType()).append(": ").append(ref.getUrl());
|
||||
if (ref.getAltText() != null && !ref.getAltText().isBlank()) {
|
||||
sb.append(" (").append(ref.getAltText()).append(")");
|
||||
}
|
||||
sb.append("\n");
|
||||
|
||||
if (ref.getType() == Reference.ReferenceType.URL) {
|
||||
try {
|
||||
String content = fetchUrlContent(ref.getUrl());
|
||||
sb.append(" Content: ").append(truncate(content, 2000)).append("\n");
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to fetch reference URL {}: {}", ref.getUrl(), e.getMessage());
|
||||
sb.append(" Content: [unable to fetch]\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
String fetchUrlContent(String url) {
|
||||
String html = restTemplate.getForObject(url, String.class);
|
||||
if (html == null) return "";
|
||||
return html.replaceAll("<[^>]+>", " ")
|
||||
.replaceAll("\\s+", " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
private String truncate(String text, int maxLen) {
|
||||
if (text.length() <= maxLen) return text;
|
||||
return text.substring(0, maxLen) + "...";
|
||||
}
|
||||
}
|
||||
133
backend/src/main/java/com/indie/service/llm/GeminiProvider.java
Normal file
133
backend/src/main/java/com/indie/service/llm/GeminiProvider.java
Normal file
@@ -0,0 +1,133 @@
|
||||
package com.indie.service.llm;
|
||||
|
||||
import com.indie.dto.LLMResult;
|
||||
import com.indie.dto.LLMOptions;
|
||||
import com.indie.dto.Reference;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class GeminiProvider implements LLMProvider {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final String apiKey;
|
||||
private final String model;
|
||||
private final String baseUrl;
|
||||
|
||||
public GeminiProvider(
|
||||
@Value("${indie.llm.providers.gemini.api-key:}") String apiKey,
|
||||
@Value("${indie.llm.providers.gemini.model:gemini-2.0-flash}") String model,
|
||||
@Value("${indie.llm.providers.gemini.url:https://generativelanguage.googleapis.com/v1beta/models}") String baseUrl,
|
||||
RestTemplateBuilder builder) {
|
||||
this.apiKey = apiKey;
|
||||
this.model = model;
|
||||
this.baseUrl = baseUrl;
|
||||
this.restTemplate = builder
|
||||
.setConnectTimeout(Duration.ofSeconds(30))
|
||||
.setReadTimeout(Duration.ofSeconds(60))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LLMResult generate(String systemPrompt, String userPrompt, List<Reference> references, LLMOptions options) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
List<Map<String, Object>> parts = new ArrayList<>();
|
||||
parts.add(Map.of("text", userPrompt));
|
||||
|
||||
if (references != null) {
|
||||
for (Reference ref : references) {
|
||||
if (ref.getType() == Reference.ReferenceType.IMAGE && ref.getUrl() != null) {
|
||||
parts.add(Map.of(
|
||||
"inlineData", Map.of(
|
||||
"mimeType", "image/jpeg",
|
||||
"data", ref.getUrl()
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> systemInstruction = Map.of(
|
||||
"parts", List.of(Map.of("text", systemPrompt))
|
||||
);
|
||||
|
||||
Map<String, Object> contents = Map.of(
|
||||
"parts", parts
|
||||
);
|
||||
|
||||
Map<String, Object> generationConfig = Map.of(
|
||||
"temperature", options.getTemperature(),
|
||||
"maxOutputTokens", options.getMaxTokens()
|
||||
);
|
||||
|
||||
Map<String, Object> requestBody = Map.of(
|
||||
"system_instruction", systemInstruction,
|
||||
"contents", List.of(contents),
|
||||
"generationConfig", generationConfig
|
||||
);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
String url = baseUrl + "/" + model + ":generateContent?key=" + apiKey;
|
||||
|
||||
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, request, Map.class);
|
||||
|
||||
long latencyMs = System.currentTimeMillis() - start;
|
||||
|
||||
Map<String, Object> responseBody = response.getBody();
|
||||
String content = extractContent(responseBody);
|
||||
|
||||
int promptTokens = 0;
|
||||
int completionTokens = 0;
|
||||
if (responseBody != null && responseBody.containsKey("usageMetadata")) {
|
||||
Map<String, Object> usage = (Map<String, Object>) responseBody.get("usageMetadata");
|
||||
promptTokens = ((Number) usage.getOrDefault("promptTokenCount", 0)).intValue();
|
||||
completionTokens = ((Number) usage.getOrDefault("candidatesTokenCount", 0)).intValue();
|
||||
}
|
||||
|
||||
return LLMResult.builder()
|
||||
.content(content)
|
||||
.promptTokens(promptTokens)
|
||||
.completionTokens(completionTokens)
|
||||
.model(model)
|
||||
.latencyMs(latencyMs)
|
||||
.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String extractContent(Map<String, Object> responseBody) {
|
||||
try {
|
||||
List<Map<String, Object>> candidates = (List<Map<String, Object>>) responseBody.get("candidates");
|
||||
if (candidates != null && !candidates.isEmpty()) {
|
||||
Map<String, Object> first = candidates.get(0);
|
||||
Map<String, Object> content = (Map<String, Object>) first.get("content");
|
||||
if (content != null) {
|
||||
List<Map<String, Object>> parts = (List<Map<String, Object>>) content.get("parts");
|
||||
if (parts != null && !parts.isEmpty()) {
|
||||
return (String) parts.get(0).get("text");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse Gemini response: " + e.getMessage());
|
||||
}
|
||||
throw new RuntimeException("Empty response from Gemini API");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "gemini";
|
||||
}
|
||||
}
|
||||
55
backend/src/main/java/com/indie/service/llm/LLMFactory.java
Normal file
55
backend/src/main/java/com/indie/service/llm/LLMFactory.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package com.indie.service.llm;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class LLMFactory {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LLMFactory.class);
|
||||
|
||||
private final List<LLMProvider> providers;
|
||||
private final String activeProvider;
|
||||
private final String fallbackProvider;
|
||||
|
||||
private Map<String, LLMProvider> providerMap;
|
||||
|
||||
public LLMFactory(List<LLMProvider> providers,
|
||||
@Value("${indie.llm.active-provider:gemini}") String activeProvider,
|
||||
@Value("${indie.llm.fallback-provider:openai}") String fallbackProvider) {
|
||||
this.providers = providers;
|
||||
this.activeProvider = activeProvider;
|
||||
this.fallbackProvider = fallbackProvider;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
providerMap = providers.stream()
|
||||
.collect(Collectors.toMap(LLMProvider::getName, Function.identity()));
|
||||
log.info("Available LLM providers: {}", providerMap.keySet());
|
||||
}
|
||||
|
||||
public LLMProvider getActiveProvider() {
|
||||
return getProvider(activeProvider)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"Active LLM provider '" + activeProvider + "' not found. Available: " + providerMap.keySet()));
|
||||
}
|
||||
|
||||
public Optional<LLMProvider> getFallbackProvider() {
|
||||
return getProvider(fallbackProvider);
|
||||
}
|
||||
|
||||
private Optional<LLMProvider> getProvider(String name) {
|
||||
return Optional.ofNullable(providerMap.get(name));
|
||||
}
|
||||
}
|
||||
14
backend/src/main/java/com/indie/service/llm/LLMProvider.java
Normal file
14
backend/src/main/java/com/indie/service/llm/LLMProvider.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.indie.service.llm;
|
||||
|
||||
import com.indie.dto.LLMResult;
|
||||
import com.indie.dto.LLMOptions;
|
||||
import com.indie.dto.Reference;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface LLMProvider {
|
||||
|
||||
LLMResult generate(String systemPrompt, String userPrompt, List<Reference> references, LLMOptions options);
|
||||
|
||||
String getName();
|
||||
}
|
||||
103
backend/src/main/java/com/indie/service/llm/OpenAIProvider.java
Normal file
103
backend/src/main/java/com/indie/service/llm/OpenAIProvider.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package com.indie.service.llm;
|
||||
|
||||
import com.indie.dto.LLMResult;
|
||||
import com.indie.dto.LLMOptions;
|
||||
import com.indie.dto.Reference;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class OpenAIProvider implements LLMProvider {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final String apiKey;
|
||||
private final String model;
|
||||
private final String url;
|
||||
|
||||
public OpenAIProvider(
|
||||
@Value("${indie.llm.providers.openai.api-key:}") String apiKey,
|
||||
@Value("${indie.llm.providers.openai.model:gpt-4o-mini}") String model,
|
||||
@Value("${indie.llm.providers.openai.url:https://api.openai.com/v1/chat/completions}") String url,
|
||||
RestTemplateBuilder builder) {
|
||||
this.apiKey = apiKey;
|
||||
this.model = model;
|
||||
this.url = url;
|
||||
this.restTemplate = builder
|
||||
.setConnectTimeout(Duration.ofSeconds(30))
|
||||
.setReadTimeout(Duration.ofSeconds(60))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LLMResult generate(String systemPrompt, String userPrompt, List<Reference> references, LLMOptions options) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
Map<String, Object> requestBody = Map.of(
|
||||
"model", model,
|
||||
"messages", List.of(
|
||||
Map.of("role", "system", "content", systemPrompt),
|
||||
Map.of("role", "user", "content", userPrompt)
|
||||
),
|
||||
"temperature", options.getTemperature(),
|
||||
"max_tokens", options.getMaxTokens()
|
||||
);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, request, Map.class);
|
||||
|
||||
long latencyMs = System.currentTimeMillis() - start;
|
||||
|
||||
Map<String, Object> responseBody = response.getBody();
|
||||
String content = extractContent(responseBody);
|
||||
|
||||
int promptTokens = 0;
|
||||
int completionTokens = 0;
|
||||
if (responseBody != null && responseBody.containsKey("usage")) {
|
||||
Map<String, Object> usage = (Map<String, Object>) responseBody.get("usage");
|
||||
promptTokens = ((Number) usage.getOrDefault("prompt_tokens", 0)).intValue();
|
||||
completionTokens = ((Number) usage.getOrDefault("completion_tokens", 0)).intValue();
|
||||
}
|
||||
|
||||
return LLMResult.builder()
|
||||
.content(content)
|
||||
.promptTokens(promptTokens)
|
||||
.completionTokens(completionTokens)
|
||||
.model(model)
|
||||
.latencyMs(latencyMs)
|
||||
.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String extractContent(Map<String, Object> responseBody) {
|
||||
try {
|
||||
List<Map<String, Object>> choices = (List<Map<String, Object>>) responseBody.get("choices");
|
||||
if (choices != null && !choices.isEmpty()) {
|
||||
Map<String, Object> first = choices.get(0);
|
||||
Map<String, Object> message = (Map<String, Object>) first.get("message");
|
||||
if (message != null) {
|
||||
return (String) message.get("content");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse OpenAI response: " + e.getMessage());
|
||||
}
|
||||
throw new RuntimeException("Empty response from OpenAI API");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "openai";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user