Skip to content

Commit 29494e6

Browse files
committed
Add endpoint to extract transaction information from a text.
1 parent 7b2be7f commit 29494e6

File tree

6 files changed

+98
-8
lines changed

6 files changed

+98
-8
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.jongsoft.finance.rest.transaction;
2+
3+
import io.micronaut.serde.annotation.Serdeable;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
6+
@Serdeable
7+
record TransactionExtractRequest(
8+
@Schema(description = "The text to extract the transaction information from.")
9+
String fromText) {
10+
}

fintrack-api/src/main/java/com/jongsoft/finance/rest/transaction/TransactionSuggestionResource.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package com.jongsoft.finance.rest.transaction;
22

33
import com.jongsoft.finance.core.RuleColumn;
4+
import com.jongsoft.finance.core.exception.StatusException;
45
import com.jongsoft.finance.learning.SuggestionEngine;
56
import com.jongsoft.finance.learning.SuggestionInput;
7+
import com.jongsoft.finance.learning.TransactionResult;
68
import com.jongsoft.finance.rest.model.TagResponse;
79
import com.jongsoft.finance.security.AuthenticationRoles;
8-
import io.micronaut.http.annotation.Body;
9-
import io.micronaut.http.annotation.Controller;
10-
import io.micronaut.http.annotation.Post;
10+
import io.micronaut.http.annotation.*;
1111
import io.micronaut.security.annotation.Secured;
1212
import io.swagger.v3.oas.annotations.Operation;
1313
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -18,7 +18,7 @@
1818
import java.util.Optional;
1919

2020
@Tag(name = "Transactions")
21-
@Controller("/api/transactions/suggestions")
21+
@Controller("/api/transactions")
2222
@Secured(AuthenticationRoles.IS_AUTHENTICATED)
2323
class TransactionSuggestionResource {
2424

@@ -28,13 +28,12 @@ class TransactionSuggestionResource {
2828
this.suggestionEngine = suggestionEngine;
2929
}
3030

31-
@Post
31+
@Post("suggestions")
3232
@Operation(
3333
operationId = "suggestTransaction",
3434
summary = "Suggest changes",
3535
description = "Suggest changes to a transaction based upon the rules in the system.")
3636
Map<String, ?> suggest(@Body TransactionForSuggestionRequest request) {
37-
3837
var transactionInput = new SuggestionInput(
3938
LocalDate.now(),
4039
request.description(),
@@ -60,4 +59,14 @@ class TransactionSuggestionResource {
6059

6160
return output;
6261
}
62+
63+
@Post("/generate-transaction")
64+
@Operation(
65+
operationId = "extractTransaction",
66+
summary = "Extract transaction info",
67+
description = "Extract transaction information from the presented text.")
68+
public TransactionResult extractTransaction(TransactionExtractRequest request) {
69+
return suggestionEngine.extractTransaction(request.fromText())
70+
.orElseThrow(() -> StatusException.badRequest("No extractor configured.", "llm.not.configured"));
71+
}
6372
}

learning/learning-module-llm/src/main/java/com/jongsoft/finance/llm/AiSuggestionEngine.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import com.jongsoft.finance.learning.SuggestionEngine;
44
import com.jongsoft.finance.learning.SuggestionInput;
55
import com.jongsoft.finance.learning.SuggestionResult;
6+
import com.jongsoft.finance.learning.TransactionResult;
67
import com.jongsoft.finance.llm.agent.ClassificationAgent;
8+
import com.jongsoft.finance.llm.agent.TransactionExtractorAgent;
79
import com.jongsoft.finance.llm.stores.ClassificationEmbeddingStore;
810
import io.micronaut.context.annotation.Primary;
911
import jakarta.inject.Singleton;
@@ -24,10 +26,15 @@ class AiSuggestionEngine implements SuggestionEngine {
2426

2527
private final ClassificationEmbeddingStore embeddingStore;
2628
private final ClassificationAgent classificationAgent;
29+
private final TransactionExtractorAgent transactionExtractorAgent;
2730

28-
public AiSuggestionEngine(ClassificationEmbeddingStore embeddingStore, ClassificationAgent classificationAgent) {
31+
public AiSuggestionEngine(
32+
ClassificationEmbeddingStore embeddingStore,
33+
ClassificationAgent classificationAgent,
34+
TransactionExtractorAgent transactionExtractorAgent) {
2935
this.embeddingStore = embeddingStore;
3036
this.classificationAgent = classificationAgent;
37+
this.transactionExtractorAgent = transactionExtractorAgent;
3138
}
3239

3340
@Override
@@ -47,6 +54,19 @@ public SuggestionResult makeSuggestions(SuggestionInput transactionInput) {
4754
return suggestions;
4855
}
4956

57+
@Override
58+
public Optional<TransactionResult> extractTransaction(String transactionInput) {
59+
var extracted = transactionExtractorAgent.extractTransaction(transactionInput);
60+
return Optional.of(new TransactionResult(
61+
extracted.type(),
62+
extracted.date(),
63+
extracted.fromAccount().name().isBlank() ? null : extracted.fromAccount().name(),
64+
extracted.toAccount().name().isBlank() ? null : extracted.toAccount().name(),
65+
extracted.description(),
66+
extracted.amount()
67+
));
68+
}
69+
5070
private SuggestionResult fallbackToLLM(SuggestionInput transactionInput) {
5171
log.debug("No embedding found for the input, falling back to LLM.");
5272
var suggestion = classificationAgent.classifyTransaction(UUID.randomUUID(),

learning/learning-module-llm/src/test/java/com/jongsoft/finance/llm/AiSuggestionEngineTest.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package com.jongsoft.finance.llm;
22

3+
import com.jongsoft.finance.core.TransactionType;
34
import com.jongsoft.finance.learning.SuggestionInput;
5+
import com.jongsoft.finance.learning.TransactionResult;
46
import com.jongsoft.finance.llm.agent.ClassificationAgent;
7+
import com.jongsoft.finance.llm.agent.TransactionExtractorAgent;
8+
import com.jongsoft.finance.llm.dto.AccountDTO;
59
import com.jongsoft.finance.llm.dto.ClassificationDTO;
10+
import com.jongsoft.finance.llm.dto.TransactionDTO;
611
import com.jongsoft.finance.llm.stores.ClassificationEmbeddingStore;
712
import org.junit.jupiter.api.Test;
813

@@ -20,7 +25,7 @@ class AiSuggestionEngineTest {
2025
void makeSuggestions() {
2126
// given
2227
var mockAiAgent = mock(ClassificationAgent.class);
23-
var subject = new AiSuggestionEngine(mock(ClassificationEmbeddingStore.class), mockAiAgent);
28+
var subject = new AiSuggestionEngine(mock(ClassificationEmbeddingStore.class), mockAiAgent, mock(TransactionExtractorAgent.class));
2429
var suggestion = new SuggestionInput(
2530
LocalDate.of(2022, 1, 1),
2631
"My transaction",
@@ -38,4 +43,31 @@ void makeSuggestions() {
3843
.extracting("budget", "category", "tags")
3944
.containsExactly("Food", "Groceries", List.of("shopping", "groceries"));
4045
}
46+
47+
@Test
48+
void extractTransaction() {
49+
var mockExtractionAgent = mock(TransactionExtractorAgent.class);
50+
var subject = new AiSuggestionEngine(mock(ClassificationEmbeddingStore.class), mock(ClassificationAgent.class), mockExtractionAgent);
51+
52+
when(mockExtractionAgent.extractTransaction(anyString()))
53+
.thenReturn(new TransactionDTO(
54+
new AccountDTO("Checking account", "checking"),
55+
new AccountDTO("Savings account", "savings"),
56+
"My transaction",
57+
LocalDate.of(2010, 1, 1),
58+
20.2D,
59+
TransactionType.DEBIT));
60+
61+
var answer = subject.extractTransaction("My transaction");
62+
63+
assertThat(answer)
64+
.isPresent()
65+
.contains(new TransactionResult(
66+
TransactionType.DEBIT,
67+
LocalDate.of(2010, 1, 1),
68+
"Checking account",
69+
"Savings account",
70+
"My transaction",
71+
20.2D));
72+
}
4173
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
package com.jongsoft.finance.learning;
22

3+
import java.util.Optional;
4+
35
public interface SuggestionEngine {
46

57
SuggestionResult makeSuggestions(SuggestionInput transactionInput);
68

9+
/**
10+
* Extracts a transaction based on the given input string and returns a {@code TransactionResult}.
11+
*
12+
* @param transactionInput the raw transaction input string to be processed
13+
* @return a {@code TransactionResult} containing the extracted transaction data
14+
*/
15+
default Optional<TransactionResult> extractTransaction(String transactionInput) {
16+
return Optional.empty();
17+
}
718
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.jongsoft.finance.learning;
2+
3+
import com.jongsoft.finance.core.TransactionType;
4+
5+
import java.time.LocalDate;
6+
7+
public record TransactionResult(TransactionType type, LocalDate date, String from, String to, String description, double amount) {
8+
}

0 commit comments

Comments
 (0)