Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
Expand All @@ -16,7 +16,30 @@
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
<langgraph4j.version>1.7.0-beta2</langgraph4j.version>
<langchain4j.version>0.33.0</langchain4j.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- LangGraph4j BOM -->
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-bom</artifactId>
<version>${langgraph4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- LangChain4j BOM -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
Expand Down Expand Up @@ -145,6 +168,25 @@
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- LangGraph4j -->
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-core</artifactId>
</dependency>

<!-- LangChain4j -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/com/webapp/bankingportal/config/LLMConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String> runWorkflow() {
try {
graphBuilder.runGraph();
return ResponseEntity.ok("Workflow executed successfully");
} catch (Exception e) {
e.printStackTrace();
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using printStackTrace() is not recommended for production code. Replace with proper logging: inject a Logger and use log.error(\"Workflow execution failed\", e); to ensure exceptions are logged through the application's logging framework.

Copilot uses AI. Check for mistakes.
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Workflow execution failed: " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.concurrent.CompletableFuture;

import com.webapp.bankingportal.dto.UserResponse;
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imported UserResponse class is not used in this interface. Remove this unused import.

Suggested change
import com.webapp.bankingportal.dto.UserResponse;

Copilot uses AI. Check for mistakes.
import org.springframework.scheduling.annotation.Async;

public interface EmailService {
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.File;
import java.util.concurrent.CompletableFuture;

import com.webapp.bankingportal.dto.UserResponse;
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imported UserResponse class is not used in this file. Remove this unused import.

Suggested change
import com.webapp.bankingportal.dto.UserResponse;

Copilot uses AI. Check for mistakes.
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
Expand Down Expand Up @@ -102,7 +103,7 @@ public String getOtpLoginEmailTemplate(String name, String accountNumber, String

@Override
public String getBankStatementEmailTemplate(String name, String statementText) {
return "<div style=\"font-family: Arial, sans-serif; padding: 20px;\">" +
return "<div style=\"font-family: Arial, sans-serif; padding: 20px;\">" +
"<h2>Bank Statement</h2>" +
"<p>Dear " + name + ",</p>" +
"<p>Here is your latest bank statement:</p>" +
Expand All @@ -113,6 +114,25 @@ public String getBankStatementEmailTemplate(String name, String statementText) {
"</div>";
}

public String getReconciliationReportTemplate(String user,
String introSentence,
String reportContent) {
return """
<html>
<body style="font-family: Arial, sans-serif;">
<h2>Dear %s,</h2>
<p>%s</p>
<h3>Your Reconciliation Report</h3>
<pre style="background-color:#f4f4f4;padding:10px;border:1px solid #ddd;">
%s
</pre>
<p>Thank you for banking with OneStopBank.</p>
<p>Regards,<br/>OneStopBank Team</p>
</body>
</html>
""".formatted(user, introSentence, reportContent);
}

public void sendEmailWithAttachment(String to, String subject, String text, String attachmentFilePath) {
try {
val message = mailSender.createMimeMessage();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ReconState> 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);
}
Comment on lines +65 to +67
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console output statements should be replaced with proper logging using SLF4J or similar framework. Add a logger instance: private static final Logger log = LoggerFactory.getLogger(GraphBuilder.class); and use log.info() or log.debug() instead.

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
@@ -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<ReconState> {

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<String, Object> 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<Map<String, String>> userList = (List<Map<String, String>>) state.value(ReconState.USER_KEY)
.orElseThrow(() -> new IllegalStateException("User info not found in state"));

Map<String, String> userMap = userList.get(userList.size() - 1);

String email = userMap.getOrDefault("email", "[email protected]");
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();
}
}
Original file line number Diff line number Diff line change
@@ -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<ReconState> {
private final OpenAiChatModel model;

public ReportGenerationNode(OpenAiChatModel model) {
this.model = model;
}

@Override
@SuppressWarnings("unchecked")
public Map<String, Object> apply(ReconState state) {
// Transactions are stored as List<String>
List<String> txns = (List<String>) 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 = """
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'Reconcilation' to 'Reconciliation' in the PR title (note: appears in title, not in this code file)

Copilot uses AI. Check for mistakes.
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);
}
}
Loading