-
Notifications
You must be signed in to change notification settings - Fork 169
Added Reconcilation Report Generation Fetaure #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) | ||
| .body("Workflow execution failed: " + e.getMessage()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |||
|
|
||||
| import java.util.concurrent.CompletableFuture; | ||||
|
|
||||
| import com.webapp.bankingportal.dto.UserResponse; | ||||
|
||||
| import com.webapp.bankingportal.dto.UserResponse; |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |||
| import java.io.File; | ||||
| import java.util.concurrent.CompletableFuture; | ||||
|
|
||||
| import com.webapp.bankingportal.dto.UserResponse; | ||||
|
||||
| import com.webapp.bankingportal.dto.UserResponse; |
| 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
|
||
| } | ||
| } | ||
| 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 = """ | ||
|
||
| 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); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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 uselog.error(\"Workflow execution failed\", e);to ensure exceptions are logged through the application's logging framework.