diff --git a/pom.xml b/pom.xml index e372d82..50de5cc 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot @@ -16,7 +16,30 @@ Demo project for Spring Boot 17 + 1.7.0-beta2 + 0.33.0 + + + + + org.bsc.langgraph4j + langgraph4j-bom + ${langgraph4j.version} + pom + import + + + + + dev.langchain4j + langchain4j-bom + ${langchain4j.version} + pom + import + + + org.projectlombok @@ -145,6 +168,25 @@ runtime true + + + org.bsc.langgraph4j + langgraph4j-core + + + + + dev.langchain4j + langchain4j-core + + + dev.langchain4j + langchain4j-open-ai + + + dev.langchain4j + langchain4j + diff --git a/src/main/java/com/webapp/bankingportal/config/LLMConfig.java b/src/main/java/com/webapp/bankingportal/config/LLMConfig.java new file mode 100644 index 0000000..77ddd09 --- /dev/null +++ b/src/main/java/com/webapp/bankingportal/config/LLMConfig.java @@ -0,0 +1,28 @@ +package com.webapp.bankingportal.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import dev.langchain4j.model.openai.OpenAiChatModel; + +@Configuration +public class LLMConfig { + + @Value("${groq.api.key}") + private String apiKey; + + @Value("${groq.api.model}") + private String modelName; + + @Value("${groq.api.endpoint}") + private String baseUrl; + + @Bean + public OpenAiChatModel openAiChatModel() { + return OpenAiChatModel.builder() + .apiKey(apiKey) + .baseUrl(baseUrl) + .modelName(modelName) + .build(); + } +} diff --git a/src/main/java/com/webapp/bankingportal/controller/WorkFlowController.java b/src/main/java/com/webapp/bankingportal/controller/WorkFlowController.java new file mode 100644 index 0000000..d7bcd9d --- /dev/null +++ b/src/main/java/com/webapp/bankingportal/controller/WorkFlowController.java @@ -0,0 +1,35 @@ +package com.webapp.bankingportal.controller; + +import com.webapp.bankingportal.workflow.graph.GraphBuilder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/workflow") +public class WorkFlowController { + + private final GraphBuilder graphBuilder; + + public WorkFlowController(GraphBuilder graphBuilder) { + this.graphBuilder = graphBuilder; + } + + /** + * Run the reconciliation workflow end-to-end. + * Example: GET /api/workflow/run + */ + @GetMapping("/run") + public ResponseEntity runWorkflow() { + try { + graphBuilder.runGraph(); + return ResponseEntity.ok("Workflow executed successfully"); + } catch (Exception e) { + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Workflow execution failed: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/webapp/bankingportal/service/EmailService.java b/src/main/java/com/webapp/bankingportal/service/EmailService.java index b7de907..3c480ca 100644 --- a/src/main/java/com/webapp/bankingportal/service/EmailService.java +++ b/src/main/java/com/webapp/bankingportal/service/EmailService.java @@ -2,6 +2,7 @@ import java.util.concurrent.CompletableFuture; +import com.webapp.bankingportal.dto.UserResponse; import org.springframework.scheduling.annotation.Async; public interface EmailService { @@ -14,4 +15,8 @@ public interface EmailService { public String getOtpLoginEmailTemplate(String name, String accountNumber, String otp); public String getBankStatementEmailTemplate(String name, String statementText); + + String getReconciliationReportTemplate(String user, + String introSentence, + String reportContent); } diff --git a/src/main/java/com/webapp/bankingportal/service/EmailServiceImpl.java b/src/main/java/com/webapp/bankingportal/service/EmailServiceImpl.java index 93c0eb3..f7cc2b5 100644 --- a/src/main/java/com/webapp/bankingportal/service/EmailServiceImpl.java +++ b/src/main/java/com/webapp/bankingportal/service/EmailServiceImpl.java @@ -3,6 +3,7 @@ import java.io.File; import java.util.concurrent.CompletableFuture; +import com.webapp.bankingportal.dto.UserResponse; import org.springframework.mail.MailException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; @@ -102,7 +103,7 @@ public String getOtpLoginEmailTemplate(String name, String accountNumber, String @Override public String getBankStatementEmailTemplate(String name, String statementText) { - return "
" + + return "
" + "

Bank Statement

" + "

Dear " + name + ",

" + "

Here is your latest bank statement:

" + @@ -113,6 +114,25 @@ public String getBankStatementEmailTemplate(String name, String statementText) { "
"; } + public String getReconciliationReportTemplate(String user, + String introSentence, + String reportContent) { + return """ + + +

Dear %s,

+

%s

+

Your Reconciliation Report

+
+                    %s
+                    
+

Thank you for banking with OneStopBank.

+

Regards,
OneStopBank Team

+ + + """.formatted(user, introSentence, reportContent); + } + public void sendEmailWithAttachment(String to, String subject, String text, String attachmentFilePath) { try { val message = mailSender.createMimeMessage(); diff --git a/src/main/java/com/webapp/bankingportal/workflow/graph/GraphBuilder.java b/src/main/java/com/webapp/bankingportal/workflow/graph/GraphBuilder.java new file mode 100644 index 0000000..96c3c51 --- /dev/null +++ b/src/main/java/com/webapp/bankingportal/workflow/graph/GraphBuilder.java @@ -0,0 +1,69 @@ +package com.webapp.bankingportal.workflow.graph; + +import com.webapp.bankingportal.workflow.nodes.MailReportNode; +import com.webapp.bankingportal.workflow.nodes.ReportGenerationNode; +import com.webapp.bankingportal.workflow.nodes.TransactionFetchNode; +import com.webapp.bankingportal.workflow.state.ReconState; +import org.bsc.langgraph4j.GraphStateException; +import org.bsc.langgraph4j.StateGraph; +import org.bsc.langgraph4j.CompiledGraph; + +import static org.bsc.langgraph4j.StateGraph.START; +import static org.bsc.langgraph4j.StateGraph.END; +import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async; + +import java.util.Map; + +import com.webapp.bankingportal.service.DashboardService; +import com.webapp.bankingportal.service.EmailService; +import com.webapp.bankingportal.service.TransactionService; +import dev.langchain4j.model.openai.OpenAiChatModel; +import org.springframework.stereotype.Component; + +@Component +public class GraphBuilder { + + private final TransactionService transactionService; + private final DashboardService dashboardService; + private final EmailService emailService; + private final OpenAiChatModel chatModel; + + public GraphBuilder(TransactionService transactionService, + DashboardService dashboardService, + EmailService emailService, + OpenAiChatModel chatModel) { + this.transactionService = transactionService; + this.dashboardService = dashboardService; + this.emailService = emailService; + this.chatModel = chatModel; + } + + /** Build and return the compiled graph */ + public CompiledGraph buildGraph() throws GraphStateException { + var fetchNode = new TransactionFetchNode(transactionService, dashboardService); + var reportNode = new ReportGenerationNode(chatModel); + var mailNode = new MailReportNode(emailService); + + var stateGraph = new StateGraph<>( + ReconState.SCHEMA, + ReconState::new + ) + .addNode("fetchTransactions", node_async(fetchNode)) + .addNode("generateReport", node_async(reportNode)) + .addNode("mailReport", node_async(mailNode)) + .addEdge(START, "fetchTransactions") + .addEdge("fetchTransactions", "generateReport") + .addEdge("generateReport", "mailReport") + .addEdge("mailReport", END); + + return stateGraph.compile(); + } + + /** Run the compiled graph */ + public void runGraph() throws GraphStateException { + var compiled = buildGraph(); + for (var item : compiled.stream(Map.of())) { + System.out.println("State after step: " + item); + } + } +} diff --git a/src/main/java/com/webapp/bankingportal/workflow/nodes/MailReportNode.java b/src/main/java/com/webapp/bankingportal/workflow/nodes/MailReportNode.java new file mode 100644 index 0000000..64f61de --- /dev/null +++ b/src/main/java/com/webapp/bankingportal/workflow/nodes/MailReportNode.java @@ -0,0 +1,48 @@ +package com.webapp.bankingportal.workflow.nodes; + +import com.webapp.bankingportal.service.EmailService; +import com.webapp.bankingportal.workflow.state.ReconState; +import org.bsc.langgraph4j.action.NodeAction; + +import java.util.*; + +// --- Node 3: Mail Report --- +public class MailReportNode implements NodeAction { + + private final EmailService emailService; + + // Constructor injection (instead of @Autowired for clarity in workflow wiring) + public MailReportNode(EmailService emailService) { + this.emailService = emailService; + } + + @Override + @SuppressWarnings("unchecked") + public Map apply(ReconState state) { + String report = state.report().orElse("No report generated"); + String account = state.accountNumber().orElse("UNKNOWN"); + + // Because USER_KEY uses an appender channel, unwrap the list and take the latest map + List> userList = (List>) state.value(ReconState.USER_KEY) + .orElseThrow(() -> new IllegalStateException("User info not found in state")); + + Map userMap = userList.get(userList.size() - 1); + + String email = userMap.getOrDefault("email", "unknown@example.com"); + String name = userMap.getOrDefault("name", "Customer"); + + // Build email body using template + String emailBody = emailService.getReconciliationReportTemplate( + name, + "Here is your latest reconciliation report for account " + account + ".", + report + ); + + // Send email + emailService.sendEmail(email, "Your Reconciliation Report - OneStopBank", emailBody); + + System.out.println("Mailing report for account " + account + " to " + email); + + return Map.of(); + } +} diff --git a/src/main/java/com/webapp/bankingportal/workflow/nodes/ReportGenerationNode.java b/src/main/java/com/webapp/bankingportal/workflow/nodes/ReportGenerationNode.java new file mode 100644 index 0000000..974f8ae --- /dev/null +++ b/src/main/java/com/webapp/bankingportal/workflow/nodes/ReportGenerationNode.java @@ -0,0 +1,65 @@ +package com.webapp.bankingportal.workflow.nodes; + +import com.webapp.bankingportal.workflow.state.ReconState; +import dev.langchain4j.model.openai.OpenAiChatModel; +import org.bsc.langgraph4j.action.NodeAction; + +import java.util.*; + +// --- Node 2: Report Generation with LLM --- +public class ReportGenerationNode implements NodeAction { + private final OpenAiChatModel model; + + public ReportGenerationNode(OpenAiChatModel model) { + this.model = model; + } + + @Override + @SuppressWarnings("unchecked") + public Map apply(ReconState state) { + // Transactions are stored as List + List txns = (List) state.value(ReconState.TXNS_KEY) + .orElse(List.of()); + String account = state.accountNumber().orElse("UNKNOWN"); + + // Join the stringified transactions into one block + String txnSummary = String.join("\n", txns); + + String prompt = """ +You are a financial reconciliation assistant. +Generate a clear, easy-to-read reconciliation report for account %s based on the following transactions: + +%s + +Rules for formatting and tone: +1. Do NOT use markdown symbols like **, ##, or bullet markers (*, -). + Use plain text headings such as "Balance Check", "Narrative", "Alerts", "Trends", "Summary". +2. Do NOT include technical details like transaction IDs, codes, or field names. + Just describe deposits and withdrawals in plain, everyday language. +3. Balance Check: + - Show opening balance (if available), total deposits, total withdrawals, and closing balance. + - If the math doesn’t add up, clearly flag it. +4. Narrative: + - Tell the story of the day in natural language, like a diary. + Example: "You added ₹10,000 in the morning, withdrew ₹2,000 at lunch, and sent ₹5,000 in the evening. That left you with ₹53,700." +5. Alerts: + - If any withdrawal is larger than ₹10,000, add: "Heads up, you withdrew a large amount today." + - If balance drops below ₹1,000, add: "Low balance warning." +6. Trends: + - Provide weekly or monthly summaries in plain language. + Example: "This week you deposited ₹25,000 and withdrew ₹18,000. Net savings: ₹7,000." +7. Overall Summary: + - End with a simple, encouraging statement about the account’s health and spending habits. +8. Make the tone friendly, professional, and easy for a non-technical person to understand. +""".formatted(account, txnSummary); + + + + // Call LLM + String report = model.generate(prompt); + + System.out.println("Generated report:\n" + report); + + return Map.of(ReconState.REPORT_KEY, report); + } +} diff --git a/src/main/java/com/webapp/bankingportal/workflow/nodes/TransactionFetchNode.java b/src/main/java/com/webapp/bankingportal/workflow/nodes/TransactionFetchNode.java new file mode 100644 index 0000000..e89e318 --- /dev/null +++ b/src/main/java/com/webapp/bankingportal/workflow/nodes/TransactionFetchNode.java @@ -0,0 +1,56 @@ +package com.webapp.bankingportal.workflow.nodes; + +import com.webapp.bankingportal.dto.TransactionDTO; +import com.webapp.bankingportal.dto.UserResponse; +import com.webapp.bankingportal.service.DashboardService; +import com.webapp.bankingportal.service.TransactionService; +import com.webapp.bankingportal.util.LoggedinUser; + +import org.bsc.langgraph4j.action.NodeAction; + +import java.util.*; +import java.util.stream.Collectors; +import com.webapp.bankingportal.workflow.state.ReconState; + +// --- Node 1: Transaction Fetch --- +public class TransactionFetchNode implements NodeAction { + private final TransactionService transactionService; + private final DashboardService dashboardService; + + public TransactionFetchNode(TransactionService transactionService, DashboardService dashboardService) { + this.transactionService = transactionService; + this.dashboardService = dashboardService; + } + + @Override + public Map apply(ReconState state) { + String accountNumber = LoggedinUser.getAccountNumber(); + UserResponse userResponse = dashboardService.getUserDetails(accountNumber); + List transactions = transactionService + .getAllTransactionsByAccountNumber(accountNumber); + + // Convert DTOs into safe string representations + List txnSummaries = transactions.stream() + .map(txn -> String.format( + "ID:%d | Amount:%.2f | Type:%s | Date:%s | From:%s | To:%s", + txn.getId(), + txn.getAmount(), + txn.getTransactionType(), + txn.getTransactionDate(), + txn.getSourceAccountNumber(), + txn.getTargetAccountNumber() + )) + .collect(Collectors.toList()); + + System.out.println("Fetched transactions for account: " + accountNumber); + + return Map.of( + ReconState.ACCOUNT_KEY, accountNumber, + ReconState.TXNS_KEY, txnSummaries, + ReconState.USER_KEY, Map.of( + "name", userResponse.getName(), + "email", userResponse.getEmail() + ) + ); + } +} diff --git a/src/main/java/com/webapp/bankingportal/workflow/state/ReconState.java b/src/main/java/com/webapp/bankingportal/workflow/state/ReconState.java new file mode 100644 index 0000000..5edd1f5 --- /dev/null +++ b/src/main/java/com/webapp/bankingportal/workflow/state/ReconState.java @@ -0,0 +1,53 @@ +package com.webapp.bankingportal.workflow.state; + +import com.webapp.bankingportal.dto.UserResponse; + +import org.bsc.langgraph4j.state.AgentState; +import org.bsc.langgraph4j.state.Channel; +import org.bsc.langgraph4j.state.Channels; + +import java.util.*; + +// --- State --- +public class ReconState extends AgentState { + public static final String ACCOUNT_KEY = "accountNumber"; + public static final String TXNS_KEY = "transactions"; + public static final String REPORT_KEY = "report"; + public static final String USER_KEY = "userResponse"; + + // Using appender for all keys, then we pick the last value in getters + public static final Map> SCHEMA = Map.of( + ACCOUNT_KEY, Channels.appender(ArrayList::new), + TXNS_KEY, Channels.appender(ArrayList::new), + REPORT_KEY, Channels.appender(ArrayList::new), + USER_KEY, Channels.appender(ArrayList::new) + ); + + public ReconState(Map initData) { + super(initData); + } + + public Optional accountNumber() { + return this.>value(ACCOUNT_KEY) + .flatMap(list -> list.isEmpty() ? Optional.empty() : Optional.of(list.get(list.size() - 1))); + } + + @SuppressWarnings("unchecked") + public Optional> transactions() { + return this.value(TXNS_KEY); + } + + public Optional report() { + return this.>value(REPORT_KEY) + .flatMap(list -> list.isEmpty() ? Optional.empty() : Optional.of(list.get(list.size() - 1))); + } + + public Optional userResponse() { + return this.>value(USER_KEY) + .flatMap(list -> list.isEmpty() ? Optional.empty() : Optional.of(list.get(list.size() - 1))); + } +} + + + + diff --git a/src/main/resources/application.properties.sample b/src/main/resources/application.properties.sample index ed32c48..65a7048 100644 --- a/src/main/resources/application.properties.sample +++ b/src/main/resources/application.properties.sample @@ -30,3 +30,8 @@ spring.mail.properties.mail.smtp.starttls.enable=true # Geolocation API geo.api.url=https://api.findip.net/ geo.api.key=your-api-key + +# LLM API +groq.api.key = your-api-key +groq.api.model = your-model +groq.api.endpoint = your-model-endpoint