-
Notifications
You must be signed in to change notification settings - Fork 38
AI-207: Add Smart Suggestion Engine for MCP Tools #297
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
Open
KAishwarya2429
wants to merge
10
commits into
openMF:main
Choose a base branch
from
KAishwarya2429:feature/suggestion-engine
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
fd1e835
Add Smart Suggestion Engine for MCP tools
aishwryakawade 1a3e7c8
Integrate suggestion engine with registry and add documentation
aishwryakawade 858fbd0
Refactor: Removed suggestion logic from MCP tools and kept it externa…
aishwryakawade 4fc4277
Refactor: Removed suggestion logic from MCP tools and kept it externa…
aishwryakawade 27ec324
Fix: Addressed review comments and cleaned unused suggestion references
aishwryakawade df8a745
Merge branch 'main' of https://github.com/openMF/mcp-mifosx
aishwryakawade ff7b32c
WIP: fixing suggestion engine and MCP response structure
aishwryakawade aeb2442
Merge branch 'add-validation-utils' into feature/suggestion-engine
aishwryakawade 1bda2db
Fix response structure and resolve reviewer comments
aishwryakawade e5ee988
Fix MCP overdue loans tool: remove early return, add proper response …
aishwryakawade File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| def generate_suggestions(intent, data): | ||
| """ | ||
| Generate context-aware suggestions based on MCP tool responses. | ||
|
|
||
| This function analyzes the intent (which tool was called) | ||
| and the returned data to provide meaningful next-step actions | ||
| for users inside the Mifos AI Assistant. | ||
|
|
||
| Args: | ||
| intent (str): Name of the MCP tool / action performed | ||
| data (dict or list): Response data from the tool | ||
|
|
||
| Returns: | ||
| list: A list of suggested next actions (strings) | ||
| """ | ||
|
|
||
| # Initialize empty suggestion list | ||
| suggestions = [] | ||
|
|
||
| # 🔹 Case 1: Overdue loans → suggest recovery actions | ||
| if intent == "get_overdue_loans": | ||
| for loan in data: | ||
| loan_id = loan.get("id") | ||
|
|
||
| # Suggest applying penalty | ||
| suggestions.append(f"Apply a late fee to loan {loan_id}") | ||
|
|
||
| # Suggest checking repayment plan | ||
| suggestions.append(f"View repayment schedule for loan {loan_id}") | ||
|
|
||
| # Suggest notifying the client | ||
| suggestions.append(f"Send a repayment reminder for loan {loan_id}") | ||
|
|
||
| # 🔹 Case 2: Loan details → suggest actions based on loan status | ||
| elif intent == "get_loan_details": | ||
| loan_id = data.get("loanId") | ||
|
|
||
| # Normalize status for safe comparison | ||
| status = data.get("status", "").lower() | ||
|
|
||
| # If loan is active → allow repayment actions | ||
| if "active" in status: | ||
| suggestions.append(f"Make a repayment for loan {loan_id}") | ||
| suggestions.append(f"View repayment schedule for loan {loan_id}") | ||
|
|
||
| # If loan is pending → allow approval/rejection | ||
| if "pending" in status or "submitted" in status: | ||
| suggestions.append(f"Approve loan {loan_id}") | ||
| suggestions.append(f"Reject loan {loan_id}") | ||
|
|
||
| # 🔹 Return final suggestions list | ||
| return suggestions |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,6 @@ | ||
| # Copyright since 2025 Mifos Initiative | ||
| # This Source Code Form is subject to the terms of the Mozilla Public | ||
| # License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| # License, v. 2.0. | ||
|
|
||
| from tools.domains.accounting import create_journal_entry, get_journal_entries, list_gl_accounts | ||
| from tools.domains.charges import create_charge, get_charge, list_charges, update_charge | ||
|
|
@@ -56,31 +55,24 @@ | |
| ) | ||
| from tools.domains.staff import get_office_details, get_staff_details, list_offices, list_staff | ||
|
|
||
| # ✅ AI Suggestion Engine (New Contribution) | ||
| from core.suggestion_engine import generate_suggestions | ||
|
|
||
|
|
||
| class DomainRegistry: | ||
| """ | ||
| Domain router that maps user intent to a filtered subset of MCP tools. | ||
|
|
||
| This is used by MCP clients that want to limit the tool context window | ||
| based on the user's query. Instead of sending all 49 tools to the LLM, | ||
| the router returns only the tools relevant to the detected domain(s). | ||
|
|
||
| Usage: | ||
| from tools.registry import router | ||
| tools = router.route_intent("Show me the repayment schedule for loan 5") | ||
| # returns only the Loans domain tools | ||
|
|
||
| Full domain map: | ||
| router.domain_map["clients"] - 15 client & KYC tools | ||
| router.domain_map["groups"] - 6 group & center tools | ||
| router.domain_map["loans"] - 14 loan tools | ||
| router.domain_map["savings"] - 9 savings tools | ||
| router.domain_map["staff"] - 4 staff & office tools | ||
| router.domain_map["accounting"] - 3 accounting tools | ||
| router.domain_map["reports"] - 5 report definition & execution tools | ||
| router.domain_map["products"] - 4 loan & savings product tools | ||
| router.domain_map["charges"] - 4 charge/fee tools | ||
| router.domain_map["codetables"] - 3 code table & datatable tools | ||
| Domain router that maps user intent to MCP tools. | ||
|
|
||
| NOTE: | ||
| ----- | ||
| The Suggestion Engine is NOT registered as a tool because: | ||
| - It does not call Fineract APIs directly | ||
| - It enhances responses AFTER tool execution | ||
| - It is used as a helper layer inside MCP server | ||
|
|
||
| This maintains clean separation of concerns: | ||
| - Tools → Data operations | ||
| - Suggestion Engine → UX enhancement layer | ||
| """ | ||
|
|
||
| def __init__(self): | ||
|
|
@@ -130,14 +122,9 @@ def __init__(self): | |
| } | ||
|
|
||
| def get_domain(self, domain: str) -> list: | ||
| """ | ||
| Return all tools for a specific domain by name. | ||
| Valid names: 'clients', 'groups', 'loans', 'savings', 'staff', 'accounting', 'charges', 'codetables' | ||
| """ | ||
| return self.domain_map.get(domain, []) | ||
|
|
||
| def get_all_tools(self) -> list: | ||
| """Return the full flat list of all 61 tools across all domains.""" | ||
| all_tools = [] | ||
| seen = set() | ||
| for tools in self.domain_map.values(): | ||
|
|
@@ -148,71 +135,61 @@ def get_all_tools(self) -> list: | |
| return all_tools | ||
|
|
||
| def route_intent(self, user_query: str) -> list: | ||
| """ | ||
| Analyzes a user prompt and returns only the tool objects needed. | ||
| Used to keep the LLM context window lean for single-domain queries. | ||
| """ | ||
| query = user_query.lower() | ||
| active_domains = set() | ||
|
|
||
| # Loans | ||
| loan_keywords = [ | ||
| "loan", "repayment", "disburse", "overdue", "arrear", | ||
| "reject", "waive", "installment", "undo", "reschedule", "template", | ||
| ] | ||
| if any(w in query for w in loan_keywords): | ||
| if any(w in query for w in ["loan", "repayment", "overdue", "disburse", "reschedule"]): | ||
| active_domains.add("loans") | ||
|
|
||
| # Clients | ||
| if any(w in query for w in ["client", "person", "search", "activate", "mobile", "kyc", "identifier", "address", "charge", "fee", "document"]): | ||
| if any(w in query for w in ["client", "search", "kyc", "mobile", "document"]): | ||
| active_domains.add("clients") | ||
|
|
||
| # Groups & Centers | ||
| if any(w in query for w in ["group", "center", "centre", "member", "lending group"]): | ||
| if any(w in query for w in ["group", "center", "member"]): | ||
| active_domains.add("groups") | ||
|
|
||
| # Savings | ||
| if any(w in query for w in ["saving", "deposit", "withdraw", "balance", "wallet", "interest"]): | ||
| if any(w in query for w in ["saving", "deposit", "withdraw", "interest"]): | ||
| active_domains.add("savings") | ||
|
|
||
| # Staff & Offices | ||
| if any(w in query for w in ["staff", "officer", "employee", "office", "branch"]): | ||
| if any(w in query for w in ["staff", "office", "branch"]): | ||
| active_domains.add("staff") | ||
|
|
||
| # Accounting | ||
| if any(w in query for w in ["journal", "ledger", "account", "gl account", "debit", "credit", "accounting"]): | ||
| if any(w in query for w in ["journal", "ledger", "accounting"]): | ||
| active_domains.add("accounting") | ||
|
Comment on lines
+156
to
157
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Route GL account queries to accounting tools. After narrowing the keywords, queries like “list GL accounts” or “chart of accounts” do not match accounting and fall back to clients. Proposed fix- if any(w in query for w in ["journal", "ledger", "accounting"]):
+ if any(w in query for w in ["journal", "ledger", "accounting", "gl account", "general ledger", "chart of accounts"]):
active_domains.add("accounting")🤖 Prompt for AI Agents |
||
|
|
||
| # Reports | ||
| if any(w in query for w in ["report", "run report", "report template", "portfolio report", "generate report", "sql report"]): | ||
| if any(w in query for w in ["report", "generate report"]): | ||
| active_domains.add("reports") | ||
|
|
||
| # Products | ||
| if any(w in query for w in ["product", "loan product", "savings product", "product type", "available products"]): | ||
| if any(w in query for w in ["product"]): | ||
| active_domains.add("products") | ||
|
|
||
| # Charges | ||
| if any(w in query for w in ["charge", "fee", "penalty", "late fee", "disbursement fee"]): | ||
| if any(w in query for w in ["charge", "fee", "penalty"]): | ||
| active_domains.add("charges") | ||
|
|
||
| # Code Tables | ||
| if any(w in query for w in ["code", "dropdown", "code value", "gender", "id type", "datatable", "custom field"]): | ||
| if any(w in query for w in ["code", "datatable"]): | ||
| active_domains.add("codetables") | ||
|
|
||
| # Default: return clients for basic lookup if nothing matched | ||
| if not active_domains: | ||
| active_domains.add("clients") | ||
|
|
||
| # Flatten and deduplicate across matched domains | ||
| seen = set() | ||
| result = [] | ||
| for domain in active_domains: | ||
| for tool in self.domain_map[domain]: | ||
| if tool.name not in seen: | ||
| result.append(tool) | ||
| seen.add(tool.name) | ||
|
|
||
| return result | ||
|
|
||
| # ✅ NEW HELPER METHOD | ||
| def get_suggestions(self, intent: str, data): | ||
| """ | ||
| Generate intelligent suggestions based on tool output. | ||
|
|
||
| This enhances MCP responses by guiding users with next actions. | ||
| """ | ||
| return generate_suggestions(intent, data) | ||
|
|
||
|
|
||
| # Global router instance — import this in your MCP client | ||
| router = DomainRegistry() | ||
| # Global router instance | ||
| router = DomainRegistry() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Do not wrap loan responses with server-side suggestions.
generate_suggestionsis not imported in this file, soget_loanraisesNameError. This also changes the tool contract from top-level loan fields to{data, suggestions}despite the documented MCP-only execution scope.Proposed fix
As per coding guidelines, “Ensure
mcp_server.py,mcp_adapter.py, andregistry.pyproperly decouple the transport protocol from the business schemas.”📝 Committable suggestion
🤖 Prompt for AI Agents