Basic blog generation
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12,5 +12,6 @@ import lombok.Setter;
|
||||
public class BlogDTO extends AuditableDTO {
|
||||
private Long id;
|
||||
private Long productId;
|
||||
private String title;
|
||||
private String content;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -38,4 +38,5 @@ public class BlogService {
|
||||
blogOpt.ifPresent(blogRepository::delete);
|
||||
return blogDto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
112
src/main/java/com/homelabpulse/service/BlogWriterService.java
Normal file
112
src/main/java/com/homelabpulse/service/BlogWriterService.java
Normal 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());
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/homelabpulse/service/OllamaService.java
Normal file
43
src/main/java/com/homelabpulse/service/OllamaService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
3
src/main/resources/db/migration/V3__Blog_Title.sql
Normal file
3
src/main/resources/db/migration/V3__Blog_Title.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- V3__Blog_Title.sql
|
||||
|
||||
ALTER TABLE blog ADD COLUMN title VARCHAR(500);
|
||||
Reference in New Issue
Block a user