Basic blog generation

This commit is contained in:
2024-12-24 15:22:49 -06:00
parent e7cb8b5b2f
commit bfd7def88d
9 changed files with 184 additions and 0 deletions

View File

@@ -3,9 +3,11 @@ package com.homelabpulse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableJpaAuditing
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);

View File

@@ -6,6 +6,7 @@ import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class OllamaConfig {
@@ -20,4 +21,15 @@ public class OllamaConfig {
.build();
}
@Bean(name = "ollamaExecutor")
public ThreadPoolTaskExecutor ollamaExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // Set your core pool size
executor.setMaxPoolSize(25); // Set your maximum pool size
executor.setQueueCapacity(250); // Set your queue capacity
executor.setThreadNamePrefix("OllamaAsync-"); // Prefix for thread names
executor.initialize();
return executor;
}
}

View File

@@ -2,6 +2,7 @@ package com.homelabpulse.controller;
import com.homelabpulse.dto.BlogDTO;
import com.homelabpulse.service.BlogService;
import com.homelabpulse.service.BlogWriterService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -14,12 +15,18 @@ import java.util.List;
@RequestMapping("/blogs")
public class BlogController {
private final BlogService blogService;
private final BlogWriterService blogWriterService;
@PostMapping
public ResponseEntity<BlogDTO> save(@RequestBody BlogDTO blogDTO) {
return new ResponseEntity<>(blogService.save(blogDTO), HttpStatus.CREATED);
}
@PostMapping("/{id}/write")
public ResponseEntity<BlogDTO> write(@PathVariable("id") Long id) {
return ResponseEntity.ok(blogWriterService.writeProductBlog(id));
}
@GetMapping
public ResponseEntity<List<BlogDTO>> getAll() {
List<BlogDTO> blogs = blogService.getAll();

View File

@@ -12,5 +12,6 @@ import lombok.Setter;
public class BlogDTO extends AuditableDTO {
private Long id;
private Long productId;
private String title;
private String content;
}

View File

@@ -17,6 +17,9 @@ public class Blog extends AuditableEntity {
@Column(name = "product_id")
private Long productId;
@Column(name = "title")
private String title;
@Column(name = "content")
private String content;
}

View File

@@ -38,4 +38,5 @@ public class BlogService {
blogOpt.ifPresent(blogRepository::delete);
return blogDto;
}
}

View File

@@ -0,0 +1,112 @@
package com.homelabpulse.service;
import com.homelabpulse.dto.BlogDTO;
import com.homelabpulse.entity.Blog;
import com.homelabpulse.entity.Product;
import com.homelabpulse.mapper.BlogMapper;
import com.homelabpulse.repository.BlogRepository;
import com.homelabpulse.repository.ProductRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class BlogWriterService {
private final OllamaService ollamaService;
private final BlogRepository blogRepository;
private final ProductRepository productRepository;
private final BlogMapper blogMapper;
private static final String SYSTEM_MESSAGE = """
You are a blog writer for Homelab Pulse (https://homelabpulse.com), specializing in homelab tech.
You review products in a subtle way, so your readers aren't turned off by blatant advertising.
When possible, provide an example use case of a project for which the product can be used.
The products you review are not made or supported by homelabpulse.com.
""";
private static final String PRODUCT_MESSAGE_SEGMENT = """
Here is the product info:
``` Product URL
%1$s
```
``` Product Title
%2$s
```
``` Product Description
%3$s
```
""";
public BlogDTO writeProductBlog(Long blogId) {
log.info("Writing blogId={}", blogId);
Blog blog = blogRepository.findById(blogId).orElseThrow(() -> new EntityNotFoundException("Blog id=" + blogId + " not found"));
Product product = productRepository.findById(blog.getProductId()).orElseThrow(() -> new EntityNotFoundException("Product id=" + blog.getProductId() + " not found"));
boolean newTitle = false;
if (blog.getTitle() == null) {
newTitle = true;
String title = generateNewTitle(blog, product);
blog.setTitle(title);
}
if (blog.getContent() == null || newTitle) {
String content = generateNewContent(blog, product);
blog.setContent(content);
}
blogRepository.save(blog);
log.info("Finished writing blogId={}", blogId);
return blogMapper.toDTO(blog);
}
private String generateNewContent(Blog blog, Product product) {
UserMessage userMessage = new UserMessage(
"""
There exists an empty blog titled "%1$s".
Write the blog post, in markdown format.
%2$s
""".formatted(blog.getTitle(), getProductSegment(product))
);
String content = ollamaService.call(
new SystemMessage(SYSTEM_MESSAGE),
userMessage
);
log.debug("Content for blog id={}:\n\n{}", blog.getId(), content);
return content;
}
private String generateNewTitle(Blog blog, Product product) {
UserMessage userMessage = new UserMessage(
"""
Write the title of a blog post about the following product.
Only write the title of the post; do not add anything else to the response.
Maximum characters is 200.
%1$s
""".formatted(getProductSegment(product))
);
String title = ollamaService.call(
new SystemMessage(SYSTEM_MESSAGE),
userMessage
);
if (title.startsWith("Title: ")) {
log.debug("Removing 'Title: '");
title = title.replace("Title: ", "").strip();
}
log.info("Title for blogId={}: {}", blog.getId(), title);
return title;
}
private String getProductSegment(Product product) {
return PRODUCT_MESSAGE_SEGMENT.formatted(product.getUrl(), product.getTitle(), product.getDescription());
}
}

View File

@@ -0,0 +1,43 @@
package com.homelabpulse.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.stereotype.Service;
import java.util.Arrays;
@Service
@RequiredArgsConstructor
@Slf4j
public class OllamaService {
private final ChatModel chatModel;
private static final String DEFAULT_MODEL = "hermes3";
public String call(SystemMessage systemMessage, UserMessage userMessage) {
return call(systemMessage, userMessage, DEFAULT_MODEL);
}
public String call(SystemMessage systemMessage, UserMessage userMessage, String model) {
log.debug("UserMessage:\n{}", userMessage.getText());
OllamaOptions ollamaOptions = OllamaOptions.builder()
.model(model)
.build();
Prompt prompt = new Prompt(Arrays.asList(systemMessage, userMessage), ollamaOptions);
ChatResponse chatResponse = chatModel.call(prompt);
String responseStr = chatResponse.getResult().getOutput().getText();
log.debug("Response: {}", responseStr);
responseStr = responseStr.strip();
if (responseStr.charAt(0) == '"' && responseStr.charAt(responseStr.length() - 1) == '"') {
log.debug("Stripping quotes: {}", responseStr);
responseStr = responseStr.substring(1, responseStr.length() - 1).strip();
}
log.debug("Trimmed response: {}", responseStr);
return responseStr;
}
}

View File

@@ -0,0 +1,3 @@
-- V3__Blog_Title.sql
ALTER TABLE blog ADD COLUMN title VARCHAR(500);