DevKX commited on
Commit
cffeaa1
Β·
0 Parent(s):

Initial deploy

Browse files
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .git/
5
+ .ipynb_checkpoints/
6
+ venv/
7
+ .vscode/
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.sqlite3 filter=lfs diff=lfs merge=lfs -text
2
+ *.bin filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Security (CRITICAL) ---
2
+ .env
3
+ .env.local
4
+ secrets.json
5
+
6
+ # --- Python ---
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ *.so
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # --- Virtual Environments ---
31
+ venv/
32
+ env/
33
+ ENV/
34
+ .venv/
35
+
36
+ # --- Database & RAG (Generated at runtime) ---
37
+ database/*.db
38
+ rag/vectorstore/
39
+ rag/indexes/
40
+
41
+ # --- IDE Settings (VS Code, PyCharm, etc.) ---
42
+ .idea/
43
+ .vscode/
44
+ *.swp
45
+ *.swo
46
+
47
+ # --- OS Generated Files ---
48
+ .DS_Store
49
+ .DS_Store?
50
+ ._*
51
+ .Spotlight-V100
52
+ .Trashes
53
+ ehthumbs.db
54
+ Thumbs.db
55
+
56
+ # --- Logs ---
57
+ *.log
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Base image
2
+ FROM python:3.11-slim
3
+
4
+ # 2. Env vars
5
+ ENV PYTHONUNBUFFERED=1 \
6
+ PYTHONDONTWRITEBYTECODE=1 \
7
+ HOME=/home/user \
8
+ PATH=/home/user/.local/bin:$PATH
9
+
10
+ # 3. Create user
11
+ RUN useradd -m -u 1000 user
12
+
13
+ # 4. Workdir
14
+ WORKDIR $HOME/app
15
+
16
+ # 5. Dependencies
17
+ COPY --chown=user requirements.txt .
18
+ RUN pip install --no-cache-dir --upgrade pip && \
19
+ pip install --no-cache-dir -r requirements.txt
20
+
21
+ # 6. Copy files
22
+ COPY --chown=user . .
23
+
24
+ # 7. PRE-BUILD LOCAL DATABASES ONLY
25
+ # πŸ›‘ CHANGED: We removed 'rag/ingest_policies.py' from here.
26
+ # It will run automatically when app.py starts.
27
+ RUN python database/create_internal_db.py && \
28
+ python database/create_government_db.py
29
+
30
+ # 8. Switch user
31
+ USER user
32
+
33
+ # 9. Port
34
+ EXPOSE 7860
35
+
36
+ # 10. Start
37
+ CMD ["python", "app.py"]
GenAI_Loan_Advisor ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit 86dfcc9e285ba5de2fe485cc370312555836630b
README.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🏦 GenAI Loan Advisor
2
+
3
+ A robust Multi-Agent System built with **CrewAI** and **Mistral** that automates the bank loan underwriting process. This system executes a strict sequential pipeline to retrieve customer data, consult internal policy documents (RAG), and determine risk levels with zero human intervention.
4
+
5
+ ## πŸ—οΈ Architecture: The "Plan & Execute" Model
6
+
7
+ To prevent "infinite thinking loops" common in pure hierarchical systems, this project splits the workflow into two distinct phases:
8
+
9
+
10
+ USER QUERY: "Can Andy get a loan?"
11
+ β”‚
12
+ β–Ό
13
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
14
+ β”‚ PHASE 1: THE BRAIN (Strategy) β”‚
15
+ β”‚ Agent: Senior Context Analyst β”‚
16
+ β”‚ "I see a loan request. I will β”‚
17
+ β”‚ activate the Application Pipeline." β”‚
18
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
19
+ β”‚ Pass JSON Plan
20
+ β–Ό
21
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
22
+ β”‚ PHASE 2: THE MUSCLE (Execution) β”‚
23
+ β”‚ (Strict Sequential Assembly Line) β”‚
24
+ β”‚ β”‚
25
+ β”‚ 1. DATA AGENT ──▢ Fetches PII & Scoreβ”‚
26
+ β”‚ (Output: Score 780, Good Status) β”‚
27
+ β”‚ β”‚
28
+ β”‚ 2. RAG AGENT ──▢ Checks Policy β”‚
29
+ β”‚ (Output: "750+ is Low Risk") β”‚
30
+ β”‚ β”‚
31
+ β”‚ 3. JUDGE AGENT ──▢ Makes Decision β”‚
32
+ β”‚ (Output: "Approved @ 3.175%") β”‚
33
+ β”‚ β”‚
34
+ β”‚ 4. RESOLUTION AGENT ──▢ Final Report β”‚
35
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
36
+
37
+
38
+ Key Features
39
+ Smart Orchestration: The "Strategy Agent" acts as a Manager effectively by deciding the intent before the crew starts, eliminating the risk of the AI getting confused during execution.
40
+
41
+ Whole-Sheet RAG: Implements a specialized 1500-character chunking strategy to preserve entire "Risk Matrix" tables from PDF policies, preventing the AI from seeing fragmented data.
42
+
43
+ Hallucination Guardrails: The Data Agent is strictly bound to real customer lookups, preventing the invention of fake customers (like "John Doe").
44
+
45
+ Professional Reporting: The Resolution Agent ensures the final output is a clean, client-facing communication.
46
+
47
+
48
+ Project Structure
49
+
50
+
51
+ Quick Start
52
+ 1. Environment Setup
53
+ Create a .env file in the root:
54
+
55
+ MISTRAL_API_KEY=your_api_key_here
56
+ CODE=Access_code
57
+
58
+ 3. Ingest Policy Documents
59
+
60
+ Crucial Step: Run this once to build the Vector Database.
61
+ python rag/ingest_policies.py
62
+
63
+ 4. Installation
64
+ pip install -r requirements.txt
65
+ python app.py
66
+
67
+
68
+ Sample Interaction
69
+ User: "Can Andy get a loan?"
70
+
71
+ System Logs:
72
+
73
+ Context Analyst: Intent detected: Loan Application. Data Agent: Successfully retrieved Customer ID 9982 (Andy). Score: 780. RAG Agent: Found rule: "Credit Score 750-850 = Low Risk". Underwriter: Mapping "Low Risk" to Interest Rate Table -> 3.175%.
74
+
75
+ Final Output:
76
+
77
+ "We are pleased to inform you that Andy's loan application has been APPROVED. Based on a credit score of 780, he qualifies for our Low Risk tier with an interest rate of 3.175%."
app.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+
3
+ import os
4
+ import subprocess
5
+ import logging
6
+ import traceback
7
+ from flask import Flask, render_template, request, jsonify
8
+ from dotenv import load_dotenv # <--- Make sure python-dotenv is in requirements.txt
9
+
10
+ # Load environment variables
11
+ load_dotenv()
12
+
13
+ from crew.agents.loan_officer_agent import LoanOfficerSupervisor
14
+
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ app = Flask(__name__)
19
+
20
+ # --- CONFIGURATION ---
21
+ REQUIRED_CODE = os.getenv("CODE")
22
+
23
+ @app.route('/')
24
+ def home():
25
+ return render_template('index.html')
26
+
27
+ @app.route('/ask', methods=['POST'])
28
+ def ask():
29
+ try:
30
+ data = request.json
31
+ user_query = data.get('query')
32
+ user_code = data.get('code') # <--- Get the password from the frontend
33
+
34
+ # --- SECURITY CHECK ---
35
+ if user_code != REQUIRED_CODE:
36
+ logger.warning(f"β›” Access Denied. Wrong code: {user_code}")
37
+ return jsonify({
38
+ "status": "error",
39
+ "message": "β›” Access Denied: Incorrect Access Code."
40
+ }), 401
41
+ # ----------------------
42
+
43
+ if not user_query:
44
+ return jsonify({"error": "Query cannot be empty"}), 400
45
+
46
+ # Initialize and run the supervisor
47
+ supervisor = LoanOfficerSupervisor()
48
+ result = supervisor.run(user_query)
49
+
50
+ # Return the result directly because it already contains {"status": "success", "data": {...}}
51
+ return jsonify(result)
52
+
53
+ except Exception as e:
54
+ logger.error(f"Server Error: {str(e)}")
55
+ traceback.print_exc()
56
+ return jsonify({
57
+ "status": "error",
58
+ "message": str(e)
59
+ }), 500
60
+
61
+ if __name__ == '__main__':
62
+ # Auto-ingestion logic for Docker
63
+ if not os.path.exists("rag/vectorstore"):
64
+ logger.info("⚠️ Vectorstore not found. Attempting to ingest policies...")
65
+ if os.getenv("MISTRAL_API_KEY"):
66
+ try:
67
+ subprocess.run(["python", "rag/ingest_policies.py"], check=True)
68
+ logger.info("βœ… Ingestion complete.")
69
+ except Exception as e:
70
+ logger.error(f"❌ Ingestion failed: {e}")
71
+ else:
72
+ logger.error("❌ Error: MISTRAL_API_KEY is missing.")
73
+
74
+ app.run(debug=True, host='0.0.0.0', port=7860)
crew/__init__.py ADDED
File without changes
crew/agents/__init__.py ADDED
File without changes
crew/agents/context_agent.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Agent
2
+ from crew.tasks.context_tasks import create_context_analysis_task
3
+
4
+
5
+ """
6
+ The Context Analyst
7
+ Take in raw user text and classifies it into a structuctred JSON intent.
8
+ There are 4 differeant intent (General Questions, Policy Question, Customer Info, Loan Reuqest).
9
+ He help loan officer agent (Manager) understand the context of the query
10
+ To prevent hallucination, we have set up the prompt in the way that this agent does not have access to DB and Rag vector DB
11
+ """
12
+
13
+
14
+ class ContextAnalystAgent:
15
+ def __init__(self, llm):
16
+ self.agent = Agent(
17
+ role="Senior Context Analyst",
18
+ goal="Classify user queries into exactly one of 4 intent categories.",
19
+
20
+ backstory=(
21
+ "You are the **Executive Assitant** for the Chief Loan Officer (Orchestrator).\n"
22
+ "**YOUR JOB**: You sit in the front office. You read the incoming message, understand the context, "
23
+ "and type up a clean, structured 'One-Page Briefing' (JSON) for the Chief Loan Officer (Orchestrator).\n\n"
24
+
25
+ "### HARDWARE LIMITATIONS (CRITICAL):\n"
26
+ "1. **NO DATABASE ACCESS**: You physically cannot see the customer database.\n"
27
+ "2. **NO POLICY ACCESS**: You do not have the policy manual PDF.\n"
28
+ "3. **TEXT ONLY**: You operate in a sealed room. You only see the text slip passed under the door.\n\n"
29
+
30
+
31
+ "### YOUR CLASSIFICATION PROTOCOL:\n"
32
+ "You must map every query to one of these 4 intents based on the text *alone*:\n"
33
+ "1. **General Chat** (No Action)\n"
34
+ "2. **Policy Question** (Needs Rules, No Name)\n"
35
+ "3. **Customer Info** (Needs Name, No Rules)\n"
36
+ "4. **Loan Request** (Needs Name + Rules)\n\n"
37
+
38
+ "**OUTPUT**: Return strict JSON. Do not write memos."
39
+ ),
40
+ llm=llm,
41
+ verbose=True,
42
+ allow_delegation=False,
43
+ max_iter=2
44
+ )
45
+
46
+ def get_task(self, query):
47
+ return create_context_analysis_task(self.agent, query)
crew/agents/data_access_agent.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ from crewai import Agent
3
+ from crewai.tools import BaseTool
4
+
5
+ from crew.tasks.data_access_tasks import create_data_collection_task
6
+
7
+ from crew.tools.internal_db_tools import (
8
+ get_customer_details, get_credit_score,
9
+ get_account_status, get_customer_id_by_name,
10
+ )
11
+ from crew.tools.government_db_tools import get_pr_status
12
+
13
+ load_dotenv()
14
+
15
+ """
16
+ Tool wrapper
17
+ These classes wrap logic in try-except so that if tool fails,
18
+ it return a string error message instead of crashing the entire agent loop
19
+ """
20
+
21
+ class GetCustomerIdByNameTool(BaseTool):
22
+ name: str = "get_customer_id_by_name"
23
+ description: str = (
24
+ "Search for a customer's unique ID using their full name. "
25
+ "Useful for converting a name into an ID. "
26
+ "Returns the ID string if found, or 'None' if not found."
27
+ )
28
+ def _run(self, name: str):
29
+ try:
30
+ # strip away whitespaces
31
+ clean_name = name.strip()
32
+ result = get_customer_id_by_name(name=clean_name)
33
+ return result if result else "None"
34
+ except Exception as e:
35
+ return f"Error querying ID: {str(e)}"
36
+
37
+ class GetCustomerDetailsTool(BaseTool):
38
+ name: str = "get_customer_details"
39
+ description: str = "Fetch a customer's profile (email, nationality) using their customer_id."
40
+ def _run(self, customer_id: str):
41
+ try:
42
+ return get_customer_details(customer_id=customer_id)
43
+ except Exception as e:
44
+ return f"Error fetching details: {str(e)}"
45
+
46
+ class GetCreditScoreTool(BaseTool):
47
+ name: str = "get_credit_score"
48
+ description: str = "Retrieve the numeric credit score (300-850) using a customer_id."
49
+ def _run(self, customer_id: str):
50
+ try:
51
+ return get_credit_score(customer_id=customer_id)
52
+ except Exception as e:
53
+ return f"Error fetching score: {str(e)}"
54
+
55
+ class GetAccountStatusTool(BaseTool):
56
+ name: str = "get_account_status"
57
+ description: str = "Check if the account is Active, Closed, or Delinquent using a customer_id."
58
+ def _run(self, customer_id: str):
59
+ try:
60
+ return get_account_status(customer_id=customer_id)
61
+ except Exception as e:
62
+ return f"Error fetching status: {str(e)}"
63
+
64
+ class GetPRStatusTool(BaseTool):
65
+ name: str = "get_pr_status"
66
+ description: str = "Check government PR/Residency status using a customer_id."
67
+ def _run(self, customer_id: str):
68
+ try:
69
+ return get_pr_status(customer_id=customer_id)
70
+ except Exception as e:
71
+ return f"Error fetching PR status: {str(e)}"
72
+
73
+
74
+ """
75
+ Database Agent
76
+ Simulate a 'microservice' environment where data have to be fetch from different API endpoint
77
+ Identity service (Look up customer ID using name as user will hardly provide customer id in their query)
78
+ Profile service (Look for customer detail)
79
+ Credit service (Credit score)
80
+ Gov Service (PR status)
81
+
82
+ Force the database agent to return in JSON format in a form of report
83
+
84
+ There are a chain of thought portion to ensure that database agent only query from the internal and government database
85
+ """
86
+
87
+ class DataAccessAgent:
88
+ def __init__(self, llm):
89
+ self.agent = Agent(
90
+ role="Senior Data Investigator",
91
+ goal="Orchestrate multiple API calls to build a complete customer profile.",
92
+
93
+ backstory=(
94
+ "You are the **Senior Data Investigator**.\n"
95
+ "You operate in a **Microservices Environment**. Data is fragmented across different systems.\n"
96
+ "**YOUR JOB**: You are the 'Aggregator'. You must chain API calls together to build a full picture.\n\n"
97
+ "**Policy search is NOT YOUR JOB**: Do not try to access database if it is a policy related question\n\n"
98
+
99
+ "### YOUR API PROTOCOL (CHAIN OF THOUGHT):\n"
100
+ "1. **Step 1 (The Key)**: Always start by calling `get_customer_id_by_name`. You cannot do anything without the ID.\n"
101
+ " - If ID is 'None', STOP immediately. Do not call other tools.\n"
102
+ "2. **Step 2 (The loop)**: Once you have the `customer_id`, you must call the other 4 endpoints individually:\n"
103
+ " - `get_customer_details` (Email, Nationality)\n"
104
+ " - `get_credit_score` (Financial Health)\n"
105
+ " - `get_account_status` (Bank Standing)\n"
106
+ " - `get_pr_status` (Government Verification)\n"
107
+ "3. **Step 3 (The Report)**: Consolidate all 5 results into one JSON."
108
+ ),
109
+ llm=llm,
110
+ verbose=True,
111
+ max_iter=10,
112
+ tools=[
113
+ GetCustomerIdByNameTool(),
114
+ GetCustomerDetailsTool(),
115
+ GetCreditScoreTool(),
116
+ GetAccountStatusTool(),
117
+ GetPRStatusTool()
118
+ ],
119
+ allow_delegation=False
120
+ )
121
+
122
+ def get_task(self, query):
123
+ return create_data_collection_task(self.agent, query)
crew/agents/loan_officer_agent.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import logging
4
+ import re
5
+ from dotenv import load_dotenv
6
+ from crewai import Agent, Crew, Process, LLM
7
+
8
+ # Import Specialized Agents
9
+ from crew.agents.context_agent import ContextAnalystAgent
10
+ from crew.agents.data_access_agent import DataAccessAgent
11
+ from crew.agents.rag_agent import RAGAgent, sanitize_query
12
+ from crew.agents.resolution_agent import ResolutionAgent
13
+ from crew.agents.underwriter_agent import UnderwriterAgent
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+ load_dotenv()
18
+
19
+
20
+ """
21
+ JSON Extraction.
22
+ LLM often wrap JSON in markdown or add conversational filler.
23
+ THis function uses Regex to find the curly braces {} and parse that portion
24
+ """
25
+ def clean_and_extract_json(text):
26
+ if not text: return None
27
+ try:
28
+ # Remove markdown fences and whitespace
29
+ text_str = str(text)
30
+ text_str = re.sub(r'```json\s*', '', text_str, flags=re.IGNORECASE)
31
+ text_str = re.sub(r'```', '', text_str)
32
+ text_str = text_str.strip()
33
+
34
+ # Try finding the first '{' and last '}' to isolate JSON
35
+ match = re.search(r'\{.*\}', text_str, re.DOTALL)
36
+ if match:
37
+ return json.loads(match.group())
38
+ else:
39
+ # Fallback: maybe the whole string is JSON?
40
+ return json.loads(text_str)
41
+ except Exception as e:
42
+ logger.warning(f"JSON Clean/Parse failed: {e}")
43
+ return None
44
+
45
+
46
+
47
+ """
48
+ The 'Conductor' of the AI Orchestra.
49
+
50
+ RESPONSIBILITY:
51
+ 1. Instantiates all agents (Workers).
52
+ 2. Defines the 'Manager Agent' (The Hierarchical Boss).
53
+ 3. Wraps them into a CrewAI 'Crew'.
54
+ 4. Handles the input/output plumbing (sanitizing queries, formatting final JSON).
55
+ """
56
+ class LoanOfficerSupervisor:
57
+ def __init__(self):
58
+ logger.info("Initializing Autonomous Loan Crew")
59
+
60
+ # Mistral LLM instance for all agents
61
+ self.llm = LLM(
62
+ model="mistral/mistral-small-latest",
63
+ api_key=os.getenv("MISTRAL_API_KEY"),
64
+ temperature=0
65
+ )
66
+
67
+
68
+
69
+ def get_rag_task(self, query: str, rag_agent):
70
+ clean_query = sanitize_query(query)
71
+ return rag_agent.get_task(clean_query)
72
+
73
+
74
+ def run(self, query: str):
75
+ logger.info(f"Starting Run for query: {query}")
76
+
77
+ # Return Formatted Data
78
+ def safe_output(task):
79
+ return str(task.output.raw) if task and task.output else "Step Skipped by Manager"
80
+
81
+ # INSTANTIATE AGENTS
82
+ analyst_worker = ContextAnalystAgent(llm=self.llm)
83
+ data_worker = DataAccessAgent(llm=self.llm)
84
+ rag_worker = RAGAgent(llm=self.llm)
85
+ judge_worker = UnderwriterAgent(llm=self.llm)
86
+ res_worker = ResolutionAgent(llm=self.llm)
87
+
88
+ # DEFINE MANAGER
89
+ manager_agent = Agent(
90
+ role="Chief Loan Officer (Orchestrator)",
91
+ goal=f"Coordinate the team to resolve: {query}",
92
+ backstory=(
93
+ "You are the Chief Loan Officer. You run the department.\n"
94
+ "**Your superpower is DELEGATION based on INTELLIGENCE.**\n\n"
95
+
96
+ "### πŸ›‘ CRITICAL TOOL RULES (PREVENT CRASHES):\n"
97
+ "1. When delegating, the 'task' and 'context' arguments must be SIMPLE STRINGS.\n"
98
+ "2. ❌ WRONG: {'task': {'description': 'Get the credit score'}}\n"
99
+ "3. βœ… CORRECT: {'task': 'Get the credit score'}\n"
100
+ "4. NEVER wrap text in a dictionary labeled 'description'.\n\n"
101
+
102
+ "### TEAM ROSTER (Exact Names for Delegation):\n"
103
+ "1. **Senior Context Analyst**: First call. Reads the query and outputs a JSON briefing.\n"
104
+ "2. **Senior Data Investigator**: Only if `requires_database = true`. Fetches Credit Score, Nationality, PR Status, Account Status.\n"
105
+ "3. **Bank Policy Researcher**: Only if `requires_policy = true`. Fetches generic rules like Interest Rates, Loan Limits. "
106
+ "⚠️ NEVER give customer names, IDs, or emails to this agent. Only sanitized attributes (e.g., 'Credit Score: 720, Status: Active').\n"
107
+ "4. **Senior Credit Underwriter**: After Data/Policy evidence, evaluates Risk Classification & Interest Rate.\n"
108
+ "5. **Internal Operations Communicator**: Last step. Writes final report.\n\n"
109
+
110
+ "### DELEGATION LOGIC:\n"
111
+ "STEP 1: Ask Senior Context Analyst for the 'Intelligence Briefing'.\n"
112
+ " - If intent is General Chat: delegate only to Internal Operations Communicator.\n"
113
+ "STEP 2: If strictly `requires_database = true`, delegate to Senior Data Investigator with only customer identifiers needed (IDs allowed here, not names in policy queries).\n"
114
+ "STEP 3: If strictly `requires_policy = true`, delegate to Bank Policy Researcher. Do not find a subsitute co worker\n"
115
+ " - ⚠️ ALWAYS sanitize customer-specific info: replace names/emails/IDs/Nationality with generic attributes before delegating.\n"
116
+ " - Example: 'What is the interest rate for Credit Score 720 and Active Status?'\n"
117
+ "STEP 4: Delegate to Senior Credit Underwriter if intent is loan recommendation. If not, please skip and go directly to next step.\n"
118
+ "STEP 5: Internal Operations Communicator writes final report. No delegation needed.\n\n"
119
+
120
+ "### FAIL-SAFES:\n"
121
+ "- If an agent tries to provide info outside their scope, STOP and redirect back to you.\n"
122
+ "- Do NOT allow hallucination. Never invent policy data or personal data.\n"
123
+ "- Validate JSON output from each agent before using in next step.\n"
124
+
125
+ "### FINAL ANSWER PROTOCOL:\n"
126
+ "1. If an agent returns a complete answer resolving the query, do not delegate again.\n"
127
+ "2. Take that information and provide it as Final Answer immediately.\n"
128
+ "3. Never ask Policy Researcher to verify or communicateβ€”they only fetch facts."
129
+ ),
130
+ llm=self.llm,
131
+ allow_delegation=True,
132
+ verbose=True
133
+ )
134
+
135
+ # 3. Define Tasks using the fresh instances
136
+ t1_strategy = analyst_worker.get_task(query)
137
+ t2_data = data_worker.get_task(query)
138
+
139
+ t3_rag = rag_worker.get_policy_search_task(query)
140
+ # Pass Policy and customer data to judge/underwriter
141
+ t4_judge = judge_worker.get_task([t2_data, t3_rag])
142
+
143
+ #Pass in query and underwriter reponse to resolution agent
144
+ t5_resolution = res_worker.get_task(query, [t4_judge])
145
+
146
+ # Define name for all agents
147
+ analyst_worker.agent.role = "Senior Context Analyst"
148
+ data_worker.agent.role = "Senior Data Investigator"
149
+ rag_worker.agent.role = "Bank Policy Researcher"
150
+ judge_worker.agent.role = "Senior Credit Underwriter"
151
+ res_worker.agent.role = "Internal Operations Communicator"
152
+
153
+ # CREATE THE CREW
154
+ loan_crew = Crew(
155
+ agents=[
156
+ analyst_worker.agent,
157
+ data_worker.agent,
158
+ rag_worker.agent,
159
+ judge_worker.agent,
160
+ res_worker.agent
161
+ ],
162
+ tasks=[t1_strategy, t2_data, t3_rag, t4_judge, t5_resolution],
163
+ process=Process.hierarchical,
164
+ manager_agent=manager_agent,
165
+ # removed step_callback
166
+ verbose=True
167
+ )
168
+
169
+ # Execution
170
+ loan_crew.kickoff()
171
+
172
+
173
+
174
+ plan_data = clean_and_extract_json(t1_strategy.output.raw)
175
+
176
+ is_policy_question = plan_data.get("intent") == "Policy Question" and plan_data.get("requires_policy", False)
177
+
178
+ # # Rule base decision making to decide if its policy question or not
179
+ # if is_policy_question:
180
+ # # Only get descriptive policy, no decision
181
+ # t3_rag = rag_worker.get_summary_search_task(query)
182
+ # else:
183
+ # # Decision-oriented workflow
184
+ # t3_rag = rag_worker.get_policy_search_task(query)
185
+
186
+
187
+ if not plan_data:
188
+ # Fallback if Context agent fails
189
+ plan_data = {
190
+ "requires_database": False,
191
+ "requires_policy": False,
192
+ "topic": "Unknown"
193
+ }
194
+
195
+ policy_data = clean_and_extract_json(t3_rag.output.raw)
196
+ if not policy_data:
197
+ policy_data = safe_output(t3_rag) # Fallback to raw text
198
+
199
+ # Customer Data
200
+ cust_data = clean_and_extract_json(t2_data.output.raw)
201
+ if not cust_data:
202
+ cust_data = safe_output(t2_data) # Fallback to raw text
203
+
204
+ # Final Report would be response from resolution agent after it sanitize the underwriter report
205
+ final_report_task = t5_resolution
206
+ if final_report_task and final_report_task.output:
207
+ final_report = str(final_report_task.output.raw)
208
+ else:
209
+ final_report = "Final report not generated."
210
+
211
+ return {
212
+ "status": "success",
213
+ "data": {
214
+ "plan": plan_data,
215
+ "customer_data": cust_data,
216
+ "policy_data": policy_data,
217
+ "final_recommendation": final_report
218
+ }
219
+ }
crew/agents/rag_agent.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import re
4
+ from typing import Any, Type
5
+ from dotenv import load_dotenv
6
+
7
+ # Vector DB
8
+ from langchain_community.vectorstores.faiss import FAISS
9
+ from langchain_mistralai import MistralAIEmbeddings
10
+
11
+ from crewai import Agent
12
+ from crewai.tools import BaseTool
13
+ from pydantic import BaseModel, Field, ConfigDict
14
+
15
+
16
+ from crew.tasks.rag_tasks import create_policy_search_task, create_policy_summary_task
17
+
18
+ logger = logging.getLogger(__name__)
19
+ load_dotenv()
20
+
21
+
22
+ VECTORSTORE_DIR = "rag/vectorstore"
23
+
24
+ def sanitize_query(query: str) -> str:
25
+ """
26
+ Sanitizes query to remove PII and specific Nationalities/Demographics
27
+ that are not present in the RAG Database.
28
+ """
29
+ if not query: return ""
30
+
31
+ # 1. REMOVE NATIONALITIES (Specific -> Generic)
32
+ # Since your RAG DB doesn't know about 'Filipino' or 'Indian' policies,
33
+ # we strip them out so the Agent searches for generic "Loan Rates".
34
+
35
+ # Add common nationalities relevant to your market
36
+ nationalities = [
37
+ # Common Status Terms
38
+ "Foreigner", "Expat", "Expatriate", "Alien", "Non-Resident", "Resident",
39
+ "Permanent Resident", "Citizen", "PR",
40
+
41
+ "Afghan", "Albanian", "Algerian", "American", "Andorran", "Angolan", "Antiguan",
42
+ "Argentine", "Armenian", "Australian", "Austrian", "Azerbaijani",
43
+ "Bahamian", "Bahraini", "Bangladeshi", "Barbadian", "Belarusian", "Belgian",
44
+ "Belizean", "Beninese", "Bhutanese", "Bolivian", "Bosnian", "Botswanan",
45
+ "Brazilian", "British", "Bruneian", "Bulgarian", "Burkinabe", "Burmese", "Burundian",
46
+ "Cambodian", "Cameroonian", "Canadian", "Cape Verdean", "Central African", "Chadian",
47
+ "Chilean", "Chinese", "Colombian", "Comoran", "Congolese", "Costa Rican", "Croatian",
48
+ "Cuban", "Cypriot", "Czech",
49
+ "Danish", "Djiboutian", "Dominican", "Dutch", "East Timorese", "Ecuadorean",
50
+ "Egyptian", "Emirati", "Equatorial Guinean", "Eritrean", "Estonian", "Ethiopian",
51
+ "Fijian", "Filipino", "Finnish", "French", "Gabonese", "Gambian", "Georgian",
52
+ "German", "Ghanaian", "Greek", "Grenadian", "Guatemalan", "Guinea-Bissauan",
53
+ "Guinean", "Guyanese",
54
+ "Haitian", "Herzegovinian", "Honduran", "Hungarian", "Icelander", "Indian",
55
+ "Indonesian", "Iranian", "Iraqi", "Irish", "Israeli", "Italian", "Ivorian",
56
+ "Jamaican", "Japanese", "Jordanian", "Kazakhstani", "Kenyan", "Kittian and Nevisian",
57
+ "Kuwaiti", "Kyrgyz",
58
+ "Laotian", "Latvian", "Lebanese", "Liberian", "Libyan", "Liechtensteiner",
59
+ "Lithuanian", "Luxembourger",
60
+ "Macedonian", "Malagasy", "Malawian", "Malay", "Malaysian", "Maldivian", "Malian",
61
+ "Maltese", "Marshallese", "Mauritanian", "Mauritian", "Mexican", "Micronesian",
62
+ "Moldovan", "Monacan", "Mongolian", "Moroccan", "Mosotho", "Motswana", "Mozambican",
63
+ "Namibian", "Nauruan", "Nepalese", "New Zealander", "Nicaraguan", "Nigerian",
64
+ "Nigerien", "North Korean", "Northern Irish", "Norwegian",
65
+ "Omani", "Pakistani", "Palauan", "Panamanian", "Papua New Guinean", "Paraguayan",
66
+ "Peruvian", "Polish", "Portuguese",
67
+ "Qatari", "Romanian", "Russian", "Rwandan",
68
+ "Saint Lucian", "Salvadoran", "Samoan", "San Marinese", "Sao Tomean", "Saudi",
69
+ "Scottish", "Senegalese", "Serbian", "Seychellois", "Sierra Leonean", "Singaporean",
70
+ "Slovakian", "Slovenian", "Solomon Islander", "Somali", "South African",
71
+ "South Korean", "Spanish", "Sri Lankan", "Sudanese", "Surinamer", "Swazi",
72
+ "Swedish", "Swiss", "Syrian",
73
+ "Taiwanese", "Tajik", "Tanzanian", "Thai", "Togolese", "Tongan", "Trinidadian",
74
+ "Tunisian", "Turkish", "Tuvaluan",
75
+ "Ugandan", "Ukrainian", "Uruguayan", "Uzbekistani", "Venezuelan", "Vietnamese",
76
+ "Welsh", "Yemenite", "Zambian", "Zimbabwean"
77
+ ]
78
+
79
+ # Create a regex pattern: \b(Filipino|Indian|...)\b
80
+ # flags=re.IGNORECASE makes it catch 'filipino' and 'Filipino'
81
+ nat_pattern = r'\b(' + '|'.join(nationalities) + r')\b'
82
+
83
+ # OPTION A: Replace with nothing (Search becomes "What are rates?")
84
+ query = re.sub(nat_pattern, "", query, flags=re.IGNORECASE)
85
+
86
+ # OPTION B: If your DB has a "Foreigner" section, use this instead:
87
+ # query = re.sub(nat_pattern, "Foreigner", query, flags=re.IGNORECASE)
88
+
89
+ # 2. REMOVE IDs
90
+ query = re.sub(r'\bID\s*\d+\b', '', query, flags=re.IGNORECASE)
91
+
92
+ # 3. REMOVE EMAILS
93
+ query = re.sub(r'\S+@\S+', '', query)
94
+
95
+ # 4. REMOVE NAMES (The aggressive check)
96
+ # WARNING: This regex '\b[A-Z][a-z]+\b' is very aggressive.
97
+ # It removes ANY capitalized word (like "Bank", "Loan", "Rate").
98
+ # I recommend commenting this out unless you really need it,
99
+ # or replacing it with a Named Entity Recognition (NER) library later.
100
+ # query = re.sub(r'\b[A-Z][a-z]+\b', '', query)
101
+
102
+ # Clean up double spaces created by removals
103
+ query = re.sub(r'\s+', ' ', query).strip()
104
+
105
+ return query
106
+
107
+
108
+ # Ensure only simple string
109
+ class RAGToolSchema(BaseModel):
110
+ query: str = Field(
111
+ ...,
112
+ description="The search query string. Example: 'interest rates for high risk'"
113
+ )
114
+
115
+ class RAGSearchTool(BaseTool):
116
+ name: str = "rag_search_tool"
117
+ description: str = "Search the bank policy manual. Useful for finding rates, rules, and limits."
118
+
119
+ args_schema: Type[BaseModel] = RAGToolSchema
120
+ vectorstore: Any = Field(description="FAISS vectorstore instance")
121
+ model_config = ConfigDict(arbitrary_types_allowed=True)
122
+
123
+
124
+
125
+ def _run(self, query: Any) -> str:
126
+ if isinstance(query, dict):
127
+ query_str = query.get('query', "")
128
+ else:
129
+ query_str = str(query)
130
+
131
+ # Sanitize by removing weird character
132
+ clean_query = sanitize_query(query_str)
133
+ logger.info(f"RAG Tool Searching for: '{clean_query}'")
134
+
135
+ try:
136
+ # Search 4 text chunk
137
+ results = self.vectorstore.similarity_search(clean_query, k=4)
138
+ except Exception as e:
139
+ return f"SYSTEM_ERROR: Vector search failed. {str(e)}"
140
+
141
+ if not results:
142
+ return "RESULT: Policy Database Silent. No documents found matching this query."
143
+
144
+ # Return with the Source keyword
145
+ formatted_results = "\n\n".join([
146
+ f"[SOURCE: Page/Section {i+1}]: {doc.page_content}"
147
+ for i, doc in enumerate(results)
148
+ ])
149
+ return formatted_results
150
+
151
+ class RAGAgent:
152
+ def __init__(self, llm):
153
+ logger.info("Initializing RAG Agent...")
154
+
155
+ # Mistral embedding act as translator that convert English text to "vectors" to match the format stored in vector DB
156
+ embeddings = MistralAIEmbeddings(
157
+ model="mistral-embed",
158
+ api_key=os.getenv("MISTRAL_API_KEY")
159
+ )
160
+
161
+ # Load Vector DB
162
+ self.search_tool = None
163
+ try:
164
+ if os.path.exists(VECTORSTORE_DIR):
165
+ vectorstore = FAISS.load_local(
166
+ VECTORSTORE_DIR,
167
+ embeddings,
168
+ allow_dangerous_deserialization=True
169
+ )
170
+ # Instantiate the tool with the vectorstore
171
+ self.search_tool = RAGSearchTool(vectorstore=vectorstore)
172
+ else:
173
+ logger.warning(f"Vector Store not found at {VECTORSTORE_DIR}.")
174
+ except Exception as e:
175
+ logger.error(f"Failed to load Vector DB: {e}")
176
+
177
+ # Safety check: Ensure tool exists
178
+ tools_list = [self.search_tool] if self.search_tool else []
179
+
180
+ """
181
+ ADAPTIVE EXTRACTION: We instruct the agent to change its output format based on the topic
182
+ Crucial instruction to strip PII (Personal Identifiable Information). To prevent agent from search customer specific policy which cause endless loop
183
+ """
184
+
185
+ self.agent = Agent(
186
+ role="Bank Policy Researcher",
187
+ goal="Retrieve and structure precise policy rules for any banking query.",
188
+ backstory=(
189
+ "You are the **Bank Policy Researcher**.\n"
190
+ "You have access to the bank's entire Policy Manual (via Vector Search).\n"
191
+ "**YOUR JOB**: When the Manager asks a question, you find the relevant page and extract the facts.\n"
192
+ "**YOUR STYLE**: You are adaptive. \n"
193
+ "- If asked about Rates -> Extract the rate table.\n"
194
+ "- If asked about Eligibility -> Extract the qualification criteria.\n"
195
+ "- If asked about Penalties -> Extract the fee structure.\n"
196
+ "You do NOT invent data. You only format what is in the document.\n"
197
+ "You do NOT take in customer name and find specific policy for that customer.\n"
198
+ "⚠️ IMPORTANT: NEVER include or search for any personal identifiers such as customer names, IDs, or emails. "
199
+ "Always convert any customer-specific query into generic attributes like credit score, account status, or loan type before searching.\n"
200
+ ),
201
+ tools=tools_list,
202
+ verbose=True,
203
+ allow_delegation=False,
204
+ llm=llm
205
+ )
206
+
207
+ def get_policy_search_task(self, query: str):
208
+ clean_query = sanitize_query(query)
209
+ return create_policy_search_task(self.agent, clean_query)
210
+
211
+
212
+ def get_summary_search_task(self, query: str):
213
+ clean_query = sanitize_query(query)
214
+ return create_policy_summary_task(self.agent, clean_query)
215
+
216
+
217
+
crew/agents/resolution_agent.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Agent
2
+ from crew.tasks.resolution_tasks import create_resolution_task
3
+
4
+
5
+ """
6
+ The Resolution Agent (The Reporter).
7
+
8
+ ROLE:
9
+ This agent acts as the 'Presentation Layer' of the Crew.
10
+ While the previous agents operate in data and strict logic (JSON), this agent focuses on Communication.
11
+
12
+ RESPONSIBILITY:
13
+ 1. AGGREGATION: It gathers the isolated outputs from the Data, Policy, and Underwriting agents.
14
+ 2. TRANSLATION: It converts raw technical findings into a professional "Internal Verification Report".
15
+ 3. UX LAYER: It ensures the user sees a clean, readable English summary instead of 3 separate JSON blocks.
16
+ """
17
+
18
+ class ResolutionAgent:
19
+ def __init__(self, llm):
20
+
21
+ # --- PROMPT STRATEGY ---
22
+ # We explicitly define this agent as a "Reporter" (Passive) rather than a "Decision Maker" (Active).
23
+ # This prevents it from overriding the Underwriter's decision.
24
+ # It blindly trusts the inputs and focuses only on formatting them beautifully.
25
+ self.agent = Agent(
26
+ role="Internal Operations Communicator",
27
+ goal="Consolidate technical agent findings into a readable, professional report for Bank Staff.",
28
+ backstory=(
29
+ "You are the **Internal Communications Specialist** for the Operations Department.\n"
30
+ "You are NOT a decision maker. You are a **Reporter**.\n"
31
+ "**YOUR CONTEXT**: The 'Data Investigator' found the facts, the 'Policy Researcher' found the rules, "
32
+ "and the 'Underwriter' made the decision.\n"
33
+ "**YOUR JOB**: The human Bank Staff needs to see all this information in one place, formatted in perfect English.\n"
34
+ "You act as the bridge between the AI agents and the Human Officer. "
35
+ "Make the report clear, professional, and comprehensive."
36
+ ),
37
+ llm=llm,
38
+ verbose=True,
39
+ allow_delegation=False
40
+ )
41
+
42
+ def get_task(self, query, context_tasks):
43
+ return create_resolution_task(self.agent, query, context_tasks)
crew/agents/underwriter_agent.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Agent
2
+ from crew.tasks.underwriting_tasks import create_underwriting_task
3
+
4
+
5
+ """
6
+ The loan Underwriter (The Decision Engine).
7
+
8
+ ROLE:
9
+ It takes the raw data from rag agent and data access agent
10
+ and applies strict logic to decide on the loan recommendation.
11
+ Custom policy like Froeigner and PR rule was also added here
12
+
13
+ Strict protocol like no rule or data creation to prevent underwriter from creating data like income
14
+ """
15
+
16
+ class UnderwriterAgent:
17
+ def __init__(self, llm):
18
+ self.agent = Agent(
19
+ role="Senior Credit Underwriter",
20
+ goal="Evaluate loan applications using ONLY provided data and rules.",
21
+ backstory=(
22
+ "You are the **Risk Control Engine**.\n"
23
+ "**YOUR JOB**: You receive 'Customer Facts' and 'Policy Rules'. You output a Decision.\n\n"
24
+
25
+ "### πŸ›‘ STRICT CLOSED-SYSTEM PROTOCOL (DO NOT IGNORE):\n"
26
+ "1. **NO EXTERNAL KNOWLEDGE**: Do not use general banking knowledge. If the provided policy text doesn't say it, it doesn't exist.\n"
27
+ "2. **NO DATA INVENTION**: You only have `credit_score`, `account_status`, `nationality`, and `is_pr`.\n"
28
+ " - If a piece of data is missing (e.g., Income), **DO NOT GUESS IT**. Treat it as 'Unknown'.\n"
29
+ "3. **NO RULE CREATION**: Do not make up rules like 'Manager Discretion' or 'Loyalty Bonus'. Stick strictly to the matrix.\n\n"
30
+
31
+ "### YOUR DATA INPUTS:\n"
32
+ "1. **Customer Data**: Provided by the Data Investigator.\n"
33
+ "2. **Policy Rules**: Provided by the Policy Researcher (plus the specific PR rule below).\n\n"
34
+
35
+ "### CRITICAL VALIDATION RULES:\n"
36
+ "1. **Foreigner + PR Rule**: If `nationality` != 'Singaporean' AND `is_pr` is True -> **IMMEDIATE REJECT**.\n"
37
+ "2. **Risk Matrix**: Classification based purely on score + status.\n\n"
38
+
39
+ "### OUTPUT PROTOCOL:\n"
40
+ "Be robotic. Be literal. No emotions. No 'I assumed...'."
41
+ ),
42
+ llm=llm,
43
+ verbose=True,
44
+ allow_delegation=False,
45
+ tools=[]
46
+ )
47
+
48
+ def get_task(self, context_tasks):
49
+ return create_underwriting_task(self.agent, context_tasks)
crew/tasks/context_tasks.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Task
2
+
3
+
4
+ """
5
+ Create context analysis task will return output in JSON format
6
+ it helps to extract customer name, context, requires database, requires policy and summary briefing note
7
+ These data will help the manager decide which co-worker to delegate to
8
+ """
9
+
10
+ def create_context_analysis_task(agent, query):
11
+ return Task(
12
+ description=(
13
+ f"**INCOMING MEMO**: '{query}'\n\n"
14
+ "**ROLE**: You are a JSON Classification Engine. You do not speak English. You output data.\n"
15
+ "**OBJECTIVE**: Map the user's request to exactly ONE of the 4 intent categories below.\n\n"
16
+
17
+ "**CLASSIFICATION TRUTH TABLE (STRICT)**:\n"
18
+ "1. **General Chat**: Greetings, jokes, or vague comments (e.g., 'Hello', 'What can you do?').\n"
19
+ " -> requires_database: False | requires_policy: False\n"
20
+ "2. **Policy Question**: Asking for rules, rates, limits, or eligibility WITHOUT naming a person.\n"
21
+ " -> requires_database: False | requires_policy: True\n"
22
+ "3. **Customer Info**: Asking for static data about a person (ID, Address, Status) WITHOUT asking for rules/rates.\n"
23
+ " -> requires_database: True | requires_policy: False\n"
24
+ "4. **Loan Request**: Asking if a specific person qualifies, or checking rates for a specific person.\n"
25
+ " -> requires_database: True | requires_policy: True\n\n"
26
+
27
+ "**EXTRACTION RULES**:\n"
28
+ "- **customer_name**: Extract the exact substring (e.g., 'Andy'). If no name, set to null.\n"
29
+ "- **Context**: Look ONLY at the provided string. Do not infer previous conversations.\n\n"
30
+
31
+ "**NEGATIVE CONSTRAINTS**:\n"
32
+ "- DO NOT include 'Here is the JSON'.\n"
33
+ "- DO NOT output markdown ticks (```json). Just the raw curly braces.\n\n"
34
+
35
+ "**REQUIRED OUTPUT SCHEMA**:\n"
36
+ "{\n"
37
+ ' "thought_process": "A clear, 1-sentence summary of the strategy. (e.g. \'User asked for a loan, so I will trigger the Data and Policy agents.\')",\n'
38
+ ' "intent": "General Chat" | "Policy Question" | "Customer Info" | "Loan Request",\n'
39
+ ' "customer_name": "string or null",\n'
40
+ ' "requires_database": boolean,\n'
41
+ ' "requires_policy": boolean,\n'
42
+ ' "briefing_note": "string"\n'
43
+ "}"
44
+ ),
45
+ expected_output="A single valid JSON object containing the classification flags and intent.",
46
+ agent=agent
47
+ )
crew/tasks/data_access_tasks.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Task
2
+
3
+ """
4
+ KEY OBJECTIVES:
5
+ 1. Input Validation: Stop the agent early if no name is provided.
6
+ 2. Anti-Hallucination: Explicitly forbid generating fake IDs/Names.
7
+ 3. Dependency Management: Enforce the order of operations (Name -> ID -> Data).
8
+ 4. Standardization: Force the output into a strict JSON format for the next agent.
9
+ """
10
+
11
+
12
+ def create_data_collection_task(agent, inputs):
13
+ return Task(
14
+ description=(
15
+ f"**INVESTIGATION REQUIRED**: The Manager needs a full data profile for query: '{inputs}'\n\n"
16
+
17
+ "### πŸ›‘ PRE-FLIGHT CHECK:\n"
18
+ "1. If the query does not contain a specific person's name (e.g., 'Andy', 'Hilda'), DO NOT ATTEMPT TO SEARCH \n"
19
+ "2. Do Not create your own customer data like Customer name or Customer ID to search\n"
20
+ "3. Respond immediately with: {'found': false, 'error': 'No customer name provided in query.'} and exit\n\n"
21
+
22
+ "**EXECUTION PLAN (STRICT ORDER)**:\n"
23
+ "1. **RESOLVE ID**: Use `get_customer_id_by_name` to find the unique ID for the name provided.\n"
24
+ " - *CRITICAL*: If this returns 'None', STOP and return {{'found': false}}.\n"
25
+ "2. **FETCH DETAILS**: Use the retrieved ID to call `get_customer_details`.\n"
26
+ "3. **FETCH FINANCIALS**: Use the retrieved ID to call `get_credit_score` and `get_account_status`.\n"
27
+ "4. **FETCH LEGAL**: Use the retrieved ID to call `get_pr_status`.\n\n"
28
+
29
+ "**REQUIRED OUTPUT SCHEMA**:\n"
30
+ "You must consolidate all API responses into this single JSON structure:\n"
31
+ "{\n"
32
+ ' "found": boolean,\n'
33
+ ' "customer_name": "string",\n'
34
+ ' "customer_id": "string",\n'
35
+ ' "credit_score": number,\n'
36
+ ' "nationality": "string",\n'
37
+ ' "is_pr": boolean,\n'
38
+ ' "account_status": "string"\n'
39
+ "}"
40
+ ),
41
+ expected_output="A single valid JSON object containing the consolidated data from database.",
42
+ agent=agent
43
+ )
crew/tasks/rag_tasks.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Task
2
+
3
+
4
+
5
+
6
+ """
7
+ Instructs the agent to perform a Vector Search and format the results.
8
+
9
+ KEY OBJECTIVE:
10
+ To convert unstructured PDF text into a structured JSON object.
11
+ It explicitly separates "Rules" (logic like if/else) from "Data Points" (hard numbers/rates),
12
+ making it easier for the Underwriter agent to apply these policies programmatically later.
13
+ """
14
+
15
+ def create_policy_search_task(agent, query: str):
16
+
17
+ return Task(
18
+ description=(
19
+ f"**SEARCH REQUEST**: '{query}'\n\n"
20
+
21
+ "**YOUR JOB**: Fetch the policy rules. Do NOT analyze them. Do NOT format them into tables.\n"
22
+ "Just find the text and convert it into **Plain English Bullet Points** for the Supervisor.\n\n"
23
+
24
+ "**EXECUTION STEPS**:\n"
25
+ "1. Search for 'Overall Risk' and 'Interest Rates'.\n"
26
+ "2. **STOP** immediately after the first search.\n"
27
+ "3. **OUTPUT**: List the rules simply.\n\n"
28
+
29
+ "**REQUIRED OUTPUT FORMAT**:\n"
30
+ "Return a list like this:\n"
31
+ "- If Credit Score is [Range] and Account is [Status], then Risk is [Level].\n"
32
+ "- If Risk is [Level], then Interest Rate is [Value].\n"
33
+ "\n"
34
+ "(Include the specific numbers found in the search results)."
35
+ ),
36
+ expected_output="A simple list of policy rules in plain text.",
37
+ agent=agent,
38
+
39
+ # πŸ›‘ HARD STOP: Prevent the loop.
40
+ # The agent gets 1 try. If it finds anything, we take it.
41
+ max_iter=1
42
+ )
43
+ def create_policy_summary_task(agent, query: str):
44
+ """
45
+ Returns a Task for explaining policy without making a decision.
46
+ This is for policy specific question like what is consider high risk
47
+ """
48
+
49
+ return Task(
50
+ description=(
51
+ f"**QUERY**: '{query}'\n\n"
52
+ "**YOUR GOAL**: Explain the high-risk criteria or policy rules relevant to the query in plain text.\n"
53
+ "Do NOT make a decision or assign any verdict. Output only a descriptive summary.\n"
54
+ "Format:\n"
55
+ "- Topic / Section\n"
56
+ "- Rules Summary\n"
57
+ "- Data Points if available"
58
+ ),
59
+ expected_output="Plain text summary of relevant policy rules.",
60
+ agent=agent
61
+ )
crew/tasks/resolution_tasks.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Task
2
+
3
+
4
+
5
+ """
6
+ Final Resolution
7
+ This task aggregates the findings from all previous agents (Data, Policy, Underwriting).
8
+
9
+ KEY OBJECTIVE:
10
+ To synthesize technical data into a human-readable format.
11
+ It reads the JSON outputs from the previous steps via the 'context' parameter.
12
+ It strips away code syntax (removing curly braces/JSON).
13
+ It generates a professional Markdown report suitable for a Bank Officer to read.
14
+ It also help to format the final report
15
+ """
16
+ def create_resolution_task(agent, query, context_tasks):
17
+ return Task(
18
+ description=(
19
+ f"**INTERNAL REPORT REQUEST**: '{query}'\n"
20
+ "**INPUTS**: Review the outputs from ALL previous agents (Data, Policy, Underwriting) in the context.\n\n"
21
+
22
+ "**OBJECTIVE**: Compile the 'Internal Verification Report' for the Bank Staff.\n\n"
23
+
24
+ "**REPORT STRUCTURE (Strictly Follow This)**:\n"
25
+ "1. **Executive Decision**: State the Underwriter's final verdict (Approve/Reject), the Risk Level, and the assigned Rate.\n"
26
+ "2. **Customer Profile**: Summarize the customer details (Name, Nationality, verified Score/Status) in bullet points.\n"
27
+ "3. **Policy Context**: Mention the specific Policy Rule that was triggered (e.g. 'Policy dictates that Closed accounts are Medium Risk').\n"
28
+ "4. **Logic Trace**: Explain *why* the decision was made in plain English.\n\n"
29
+
30
+ "**POLICY only STRUCTURE (Strictly Follow This)**:\n"
31
+ "1. **Policy Context**: Mention the specific from context Policy Rule (e.g. 'Based on the BANK's Policy'). Do not create your own policy\n"
32
+
33
+ "**Chit Chat only STRUCTURE (Strictly Follow This)**:\n"
34
+ "1. **Chit Chat**: Just chit chat and response postively.\n"
35
+
36
+
37
+ "**NEGATIVE CONSTRAINTS (CRITICAL)**:\n"
38
+ "- ❌ **NO RAW JSON**: Do not output code blocks like ```json ... ```. The user must never see curly braces.\n"
39
+ "- ❌ **NO LAZY COPYING**: Do not just dump the input data. Summarize it into sentences.\n"
40
+ "- βœ… **FORMATTING**: Do not use hashes (##). Use **BOLD UPPERCASE** for section headers (e.g. **EXECUTIVE DECISION**)."
41
+ ),
42
+ expected_output=(
43
+ "A comprehensive Markdown report titled 'INTERNAL VERIFICATION REPORT'. "
44
+ "It must include the Decision, Data Evidence, Policy Rules, and Final Logic, written in professional prose without raw JSON."
45
+ "If its a Policy Question, only include Policy Context."
46
+ "If its a Chit Chat, Just communicate like how Human have small talks."
47
+ ),
48
+ agent=agent,
49
+ context=context_tasks
50
+ )
crew/tasks/underwriting_tasks.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai import Task
2
+
3
+
4
+ """
5
+
6
+ This task instructs the agent to act as the final decision maker.
7
+
8
+ KEY MECHANISM:
9
+ 1. CONTEXT CONSUMPTION: It takes the inputs from the Data Agent (Facts) and Policy Agent (Rules).
10
+ 2. LOGIC GATING: It applies a strict "If X then Y" logic chain.
11
+ 3. ZERO-TRUST: It enforces a "Silent Policy = Reject" rule. If the policy manual doesn't explicitly allow it, the agent must reject it.
12
+ """
13
+
14
+ def create_underwriting_task(agent, context_tasks):
15
+ return Task(
16
+ description=(
17
+ "**INTERNAL RISK REVIEW**\n\n"
18
+ "**INPUTS**: Review findings from Data Investigator and Policy Researcher.\n\n"
19
+
20
+ "**STRICT EXECUTION RULES**:\n"
21
+ "1. **Evidence Only**: Every decision must point to a specific data point or policy line provided in the context.\n"
22
+ "2. **Silent Policy = Reject**: If the policy does not explicitly say 'Approve' for a scenario, the default answer is REJECT.\n"
23
+ "3. **Missing Data**: If you do not see a credit score, do not estimate one. Fail the application as 'Incomplete Data'.\n\n"
24
+
25
+ "**LOGICAL REASONING STEPS**:\n"
26
+ "1. **Residency & Eligibility Check**: \n"
27
+ " - Check `nationality` and `is_pr` (Permanent Resident status).\n"
28
+ " - **DEFINITION**: A 'Foreigner' is anyone whose nationality is NOT 'Singaporean'.\n"
29
+ " - **THE STRICT RULE**: \n"
30
+ " - If Customer is Singaporean -> **PASS** (Eligible).\n"
31
+ " - If Customer is Foreigner AND is PR (Permanent Resident) -> **PASS** (Eligible).\n"
32
+ " - **If Customer is Foreigner AND is NOT PR -> REJECT IMMEDIATELY**.\n"
33
+ " - (Reasoning: We do not lend to non-residents. Foreigners must hold PR status).\n\n"
34
+
35
+ "2. **Risk Mapping**: \n"
36
+ " - Locate `credit_score` and `account_status` in the input.\n"
37
+ " - Map EXACTLY to the Risk Matrix provided.\n"
38
+ " - DETERMINE: Is the Overall Risk 'Low', 'Medium', or 'High'?\n\n"
39
+
40
+ "3. **Final Verdict**: \n"
41
+ " - If its High Risk OR Foreigner+PR, please REJECT.\n"
42
+ " - Low/Medium Risk is APPROVE.\n\n"
43
+
44
+ "**REQUIRED OUTPUT**:\n"
45
+ "Return a Markdown Memo. You MUST NOT add 'Notes', 'Suggestions', or 'Flexible terms'.\n"
46
+ "End with this strict JSON block:\n"
47
+ "```json\n"
48
+ "{\n"
49
+ ' "decision": "APPROVED" | "REJECTED",\n'
50
+ ' "risk_level": "Low" | "Medium" | "High",\n'
51
+ ' "reason_code": "The specific rule from policy that triggered this decision"\n'
52
+ "}\n"
53
+ "```"
54
+ ),
55
+ expected_output="A Decision Memo derived 100% from provided text with no outside hallucination.",
56
+ agent=agent,
57
+ context=context_tasks
58
+ )
crew/tools/__init__.py ADDED
File without changes
crew/tools/db_utils.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ from typing import Any, List, Tuple
3
+
4
+
5
+ def run_query(db_path: str, query: str, params: Tuple = ()) -> List[Tuple[Any, ...]]:
6
+ """
7
+ Runs a safe SQL query against a SQLite database and returns the result.
8
+
9
+ Args:
10
+ db_path (str): Full path to the SQLite db file.
11
+ query (str): SQL query string to execute.
12
+ params (tuple): Parameters for use in a parameterized SQL statement.
13
+
14
+ Returns:
15
+ List[Tuple]: Query results as rows (tuples). Empty list if no results or error.
16
+ """
17
+
18
+ try:
19
+ conn = sqlite3.connect(db_path)
20
+ cursor = conn.cursor()
21
+
22
+ cursor.execute(query, params)
23
+ rows = cursor.fetchall()
24
+
25
+ conn.commit()
26
+ conn.close()
27
+
28
+ return rows
29
+
30
+ except sqlite3.Error as e:
31
+ print(f"[DB ERROR] {e}")
32
+ return []
crew/tools/government_db_tools.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tools/government_db_tools.py
2
+
3
+ from crew.tools.db_utils import run_query
4
+
5
+ GOVERNMENT_DB_PATH = "database/government.db"
6
+
7
+ def get_pr_status(customer_id: str):
8
+ """
9
+ Fetch PR (Permanent Resident) status from government DB.
10
+ Returns:
11
+ - True/False (if record exists)
12
+ - "NOT_FOUND" string (if ID is invalid)
13
+ """
14
+ query = """
15
+ SELECT pr_status
16
+ FROM pr_details
17
+ WHERE id=?
18
+ """
19
+ try:
20
+ result = run_query(GOVERNMENT_DB_PATH, query, (customer_id,))
21
+
22
+ if result and len(result) > 0:
23
+ # Return the actual status (True/False)
24
+ return bool(result[0][0])
25
+ else:
26
+ # Return text signal so the agent knows the ID is wrong/missing
27
+ return f"NOT_FOUND: No government record found for ID '{customer_id}'."
28
+
29
+ except Exception as e:
30
+ return f"ERROR: Government DB failure for ID '{customer_id}': {str(e)}"
31
+
32
+
33
+ def get_government_record(customer_id: str):
34
+ """
35
+ Fetch the full record: name, email, PR status.
36
+ Useful for debugging or extended compliance checks.
37
+ """
38
+ query = """
39
+ SELECT name, email, pr_status
40
+ FROM pr_details
41
+ WHERE id=?
42
+ """
43
+ try:
44
+ result = run_query(GOVERNMENT_DB_PATH, query, (customer_id,))
45
+
46
+ if result and len(result) > 0:
47
+ name, email, pr_status = result[0]
48
+ return {
49
+ "customer_id": customer_id,
50
+ "name": name,
51
+ "email": email,
52
+ "pr_status": bool(pr_status),
53
+ }
54
+ else:
55
+ return f"NOT_FOUND: Government record unavailable for ID '{customer_id}'."
56
+
57
+ except Exception as e:
58
+ return f"ERROR: Failed to fetch government record for ID '{customer_id}': {str(e)}"
crew/tools/internal_db_tools.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crew.tools.db_utils import run_query
2
+
3
+ INTERNAL_DB_PATH = "database/internal.db"
4
+
5
+ def get_customer_id_by_name(name: str):
6
+ # Use LIKE and wildcard to be more flexible with names
7
+ query = "SELECT customer_id FROM customer_details WHERE name LIKE ? COLLATE NOCASE"
8
+ try:
9
+ # This will match "Andy", "Andy Lau", or "Andy "
10
+ result = run_query(INTERNAL_DB_PATH, query, (f"{name.strip()}%",))
11
+
12
+ if result and len(result) > 0:
13
+ return result[0][0]
14
+ return f"NOT_FOUND: Customer '{name}' does not exist."
15
+ except Exception as e:
16
+ return f"ERROR: {str(e)}"
17
+
18
+
19
+ def get_customer_details(customer_id: str):
20
+ """
21
+ Fetch basic customer details (name, email, nationality).
22
+ """
23
+ query = """
24
+ SELECT name, email, nationality
25
+ FROM customer_details
26
+ WHERE customer_id = ?
27
+ """
28
+ try:
29
+ result = run_query(INTERNAL_DB_PATH, query, (customer_id,))
30
+
31
+ if result and len(result) > 0:
32
+ name, email, nationality = result[0]
33
+ return {
34
+ "customer_id": customer_id,
35
+ "name": name,
36
+ "email": email,
37
+ "nationality": nationality
38
+ }
39
+ else:
40
+ return f"NOT_FOUND: No details found for customer_id '{customer_id}'."
41
+
42
+ except Exception as e:
43
+ return f"ERROR: Failed to fetch details for id '{customer_id}': {str(e)}"
44
+
45
+
46
+ def get_credit_score(customer_id: str):
47
+ """
48
+ Fetch customer credit score.
49
+ """
50
+ query = """
51
+ SELECT credit_score
52
+ FROM customer_credit_score
53
+ WHERE customer_id = ?
54
+ """
55
+ try:
56
+ result = run_query(INTERNAL_DB_PATH, query, (customer_id,))
57
+
58
+ if result and len(result) > 0:
59
+ return result[0][0]
60
+ else:
61
+ return f"NOT_FOUND: Credit score unavailable for customer_id '{customer_id}'."
62
+
63
+ except Exception as e:
64
+ return f"ERROR: Failed to fetch credit score for id '{customer_id}': {str(e)}"
65
+
66
+
67
+ def get_account_status(customer_id: str):
68
+ """
69
+ Fetch account status (Active, Closed, Delinquent).
70
+ """
71
+ query = """
72
+ SELECT account_status
73
+ FROM customer_account_status
74
+ WHERE customer_id = ?
75
+ """
76
+ try:
77
+ result = run_query(INTERNAL_DB_PATH, query, (customer_id,))
78
+
79
+ if result and len(result) > 0:
80
+ return result[0][0]
81
+ else:
82
+ return f"NOT_FOUND: Account status unavailable for customer_id '{customer_id}'."
83
+
84
+ except Exception as e:
85
+ return f"ERROR: Failed to fetch account status for id '{customer_id}': {str(e)}"
database/create_government_db.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import os
3
+
4
+ # Ensure the database folder exists
5
+ os.makedirs("database", exist_ok=True)
6
+
7
+ # Full path for government.db
8
+ db_path = "database/government.db"
9
+ conn = sqlite3.connect(db_path)
10
+ cursor = conn.cursor()
11
+
12
+ # Create table for customer PR status
13
+ cursor.execute('''
14
+ CREATE TABLE IF NOT EXISTS pr_details (
15
+ id TEXT PRIMARY KEY,
16
+ name TEXT,
17
+ email TEXT,
18
+ pr_status BOOLEAN
19
+ )
20
+ ''')
21
+
22
+ # Insert sample data
23
+ pr = [
24
+ ('1', 'Loren', 'loren@example.com', False), # Singaporean β†’ not PR
25
+ ('2', 'Matt', 'matt@example.com', True), # Malaysian β†’ PR
26
+ ('3', 'Hilda', 'hilda@example.com', False), # Singaporean β†’ not PR
27
+ ('4', 'Andy', 'andy@example.com', False), # Filipino β†’ PR
28
+ ('5', 'Kit', 'kit@example.com', False) # Vietnamese β†’ PR
29
+ ]
30
+
31
+ cursor.executemany('INSERT OR IGNORE INTO pr_details VALUES (?, ?, ?, ?)', pr)
32
+
33
+ # Commit changes and close
34
+ conn.commit()
35
+ conn.close()
36
+ print("government.db populated with sample customer data.")
database/create_internal_db.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import os
3
+
4
+ # Ensure database folder exists
5
+ os.makedirs("database", exist_ok=True)
6
+
7
+ # Full path for internal.db
8
+ db_path = "database/internal.db"
9
+ conn = sqlite3.connect(db_path)
10
+ cursor = conn.cursor()
11
+
12
+ # Create tables
13
+
14
+ cursor.execute('''
15
+ CREATE TABLE IF NOT EXISTS customer_details (
16
+ customer_id TEXT PRIMARY KEY,
17
+ name TEXT,
18
+ email TEXT,
19
+ nationality TEXT
20
+ )
21
+ ''')
22
+
23
+ cursor.execute('''
24
+ CREATE TABLE IF NOT EXISTS customer_credit_score (
25
+ customer_id TEXT PRIMARY KEY,
26
+ credit_score INTEGER
27
+ )
28
+ ''')
29
+
30
+ cursor.execute('''
31
+ CREATE TABLE IF NOT EXISTS customer_account_status (
32
+ customer_id TEXT PRIMARY KEY,
33
+ account_id TEXT,
34
+ account_status TEXT
35
+ )
36
+ ''')
37
+
38
+ # Insert sample customer details
39
+ customers = [
40
+ ('1', 'Loren', 'loren@example.com', 'Singaporean'),
41
+ ('2', 'Matt', 'matt@example.com', 'Malaysian'),
42
+ ('3', 'Hilda', 'hilda@example.com', 'Singaporean'),
43
+ ('4', 'Andy', 'andy@example.com', 'Filipino'),
44
+ ('5', 'Kit', 'kit@example.com', 'Vietnamese')
45
+ ]
46
+
47
+ cursor.executemany('''
48
+ INSERT OR IGNORE INTO customer_details (customer_id, name, email, nationality)
49
+ VALUES (?, ?, ?, ?)
50
+ ''', customers)
51
+
52
+ # Insert sample credit scores
53
+ credit_scores = [
54
+ ('1', 455),
55
+ ('2', 685),
56
+ ('3', 825),
57
+ ('4', 840),
58
+ ('5', 350)
59
+ ]
60
+
61
+ cursor.executemany('''
62
+ INSERT OR IGNORE INTO customer_credit_score (customer_id, credit_score)
63
+ VALUES (?, ?)
64
+ ''', credit_scores)
65
+
66
+ # Insert sample account status
67
+ account_statuses = [
68
+ ('1', 'AC001', 'Good-standing'),
69
+ ('2', 'AC002', 'Good-standing'),
70
+ ('3', 'AC003', 'Delinquent'),
71
+ ('4', 'AC004', 'Good-standing'),
72
+ ('5', 'AC005', 'Closed')
73
+ ]
74
+
75
+ cursor.executemany('''
76
+ INSERT OR IGNORE INTO customer_account_status (customer_id, account_id, account_status)
77
+ VALUES (?, ?, ?)
78
+ ''', account_statuses)
79
+
80
+ conn.commit()
81
+ conn.close()
82
+ print("internal.db populated with sample customer data.")
rag/ingest_policies.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rag/ingestion/ingest_policies.py
2
+
3
+ import os
4
+ import sys
5
+ import platform
6
+ from dotenv import load_dotenv
7
+
8
+ from langchain_community.document_loaders import PyPDFLoader
9
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
10
+ from langchain_community.vectorstores.faiss import FAISS
11
+ from langchain_mistralai import MistralAIEmbeddings # or Mistral embeddings later
12
+
13
+
14
+ # ---- Dynamic FAISS import ----
15
+ def import_faiss():
16
+ try:
17
+ import faiss
18
+ return faiss
19
+ except ImportError:
20
+ import subprocess
21
+
22
+ system = platform.system()
23
+ try:
24
+ import torch
25
+ gpu_available = torch.cuda.is_available()
26
+ except ImportError:
27
+ gpu_available = False
28
+
29
+ if system == "Darwin" or not gpu_available:
30
+ pkg_name = "faiss-cpu"
31
+ else:
32
+ pkg_name = "faiss-gpu"
33
+
34
+ print(f"FAISS not found. Installing {pkg_name}...")
35
+ subprocess.check_call([sys.executable, "-m", "pip", "install", pkg_name])
36
+ import faiss
37
+ return faiss
38
+
39
+ faiss = import_faiss()
40
+ from langchain_community.vectorstores.faiss import FAISS
41
+
42
+
43
+ # Load env
44
+ load_dotenv()
45
+ MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
46
+ if not MISTRAL_API_KEY:
47
+ raise Exception("Missing MISTRAL_API_KEY in .env")
48
+
49
+
50
+
51
+ DOCUMENTS_DIR = "rag/policies"
52
+ VECTORSTORE_DIR = "rag/vectorstore"
53
+
54
+ def load_documents():
55
+ docs = []
56
+ for file in os.listdir(DOCUMENTS_DIR):
57
+ if file.endswith(".pdf"):
58
+ print(f"Loading document: {file}") # <-- Print the filename
59
+ loader = PyPDFLoader(os.path.join(DOCUMENTS_DIR, file))
60
+ docs.extend(loader.load())
61
+ return docs
62
+
63
+ def build_vectorstore():
64
+ docs = load_documents()
65
+
66
+ # Split into chunks
67
+ splitter = RecursiveCharacterTextSplitter(
68
+ chunk_size=1500,
69
+ chunk_overlap=300
70
+ )
71
+ chunks = splitter.split_documents(docs)
72
+
73
+ # Embeddings (OpenAI or Mistral later)
74
+ embeddings = MistralAIEmbeddings(model="mistral-embed")
75
+
76
+ # Create FAISS vectorstore
77
+ vectorstore = FAISS.from_documents(chunks, embeddings)
78
+
79
+ # Save DB
80
+ vectorstore.save_local(VECTORSTORE_DIR)
81
+
82
+ print(f"Vectorstore created at: {VECTORSTORE_DIR}")
83
+
84
+
85
+ def verify_vectorstore(vectorstore_dir=VECTORSTORE_DIR, test_query="Interest rate for high-risk customer"):
86
+ from langchain_community.vectorstores.faiss import FAISS
87
+ from langchain_mistralai import MistralAIEmbeddings
88
+
89
+ embeddings = MistralAIEmbeddings(model="mistral-embed", api_key=MISTRAL_API_KEY)
90
+ vectorstore = FAISS.load_local(vectorstore_dir, embeddings, allow_dangerous_deserialization=True)
91
+
92
+ results = vectorstore.similarity_search(test_query, k=3)
93
+ print("\n=== Verification Results ===")
94
+ for i, doc in enumerate(results):
95
+ print(f"\n--- Result {i+1} ---\n{doc.page_content}")
96
+
97
+ if __name__ == "__main__":
98
+ build_vectorstore()
99
+ verify_vectorstore(test_query="Overall risk for credit score 700 with delinquent account")
100
+ verify_vectorstore(test_query="Interest rate for medium-risk customer")
rag/policies/Bank Loan Interest Rate Policy.pdf ADDED
Binary file (32.4 kB). View file
 
rag/policies/Bank Loan Overall Risk Policy.pdf ADDED
Binary file (35.7 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # --- Core Framework ---
3
+ crewai==0.193.2
4
+ crewai-tools==0.76.0
5
+
6
+ # --- The Brain ---
7
+ langchain-mistralai==0.2.12
8
+ langchain-core==1.1.3
9
+ langchain-community==0.4.1
10
+
11
+ # --- Utilities ---
12
+ Flask==3.0.3
13
+ python-dotenv==1.0.1
14
+ faiss-cpu==1.13.1
15
+ pypdf==6.4.1
16
+ numpy==2.3.5
17
+
templates/index.html ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GenAI Loan Advisor</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .loader {
10
+ border: 4px solid #f3f3f3;
11
+ border-top: 4px solid #3b82f6;
12
+ border-radius: 50%;
13
+ width: 30px;
14
+ height: 30px;
15
+ animation: spin 1s linear infinite;
16
+ display: inline-block;
17
+ }
18
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
19
+ pre { white-space: pre-wrap; word-wrap: break-word; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
20
+ .data-box { max-height: 300px; overflow-y: auto; }
21
+ #finalOutput { white-space: pre-wrap; line-height: 1.6; }
22
+ </style>
23
+ </head>
24
+ <body class="bg-gray-100 min-h-screen flex flex-col items-center py-10">
25
+
26
+ <div class="w-full max-w-5xl bg-white shadow-xl rounded-xl p-8 border border-gray-200">
27
+ <div class="border-b pb-4 mb-6 text-center">
28
+ <h1 class="text-3xl font-extrabold text-gray-800 tracking-tight">GenAI Loan Advisor</h1>
29
+ <p class="text-gray-500 mt-2 text-sm">Autonomous Multi-Agent System (CrewAI + Mistral)</p>
30
+ </div>
31
+
32
+ <div class="flex flex-col gap-4 mb-6">
33
+ <div>
34
+ <label class="block text-xs font-bold text-gray-500 uppercase mb-1">Access Code</label>
35
+ <input type="password" id="accessCode"
36
+ class="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 shadow-sm text-gray-700 placeholder-gray-400"
37
+ placeholder="πŸ”‘ Enter the class code (Required)">
38
+ </div>
39
+
40
+ <div>
41
+ <label class="block text-xs font-bold text-gray-500 uppercase mb-1">Your Question</label>
42
+ <div class="flex gap-2">
43
+ <input type="text" id="userQuery"
44
+ class="w-full p-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm text-gray-700"
45
+ placeholder="e.g., What is the rate for Andy?">
46
+ <button onclick="sendQuery()" id="submitBtn"
47
+ class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-8 rounded-lg transition duration-200 shadow-md whitespace-nowrap disabled:bg-gray-400 disabled:cursor-not-allowed">
48
+ Analyze
49
+ </button>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <p class="text-xs text-gray-400 text-center mb-6">Try Asking What is the recommended rate for Hilda? What is consider high risk? Or just say Hello</p>
55
+
56
+ <div id="loadingState" class="hidden mt-8 text-center">
57
+ <div class="loader mb-3"></div>
58
+ <p class="text-gray-500 text-sm font-medium animate-pulse">
59
+ πŸš€ Agents are coordinating... this may take up to 45 seconds.
60
+ </p>
61
+ <p class="text-xs text-gray-400 mt-1">(Gathering data, checking policies, and underwriting)</p>
62
+ </div>
63
+
64
+ <div id="resultsArea" class="hidden mt-8 space-y-6 animate-fade-in">
65
+ <div class="bg-gradient-to-r from-green-50 to-white border-l-4 border-green-500 p-6 rounded shadow-sm">
66
+ <h3 class="text-lg font-bold text-green-800 flex items-center gap-2">βœ… Final Decision</h3>
67
+ <div id="finalOutput" class="text-gray-800 mt-3 text-base leading-relaxed"></div>
68
+ </div>
69
+
70
+ <div class="bg-blue-50 border-l-4 border-blue-500 p-5 rounded shadow-sm">
71
+ <h3 class="text-sm font-bold text-blue-800 uppercase tracking-wide mb-3 flex items-center gap-2">🧠 Supervisor Strategy</h3>
72
+ <div id="planOutput" class="text-sm text-gray-700"></div>
73
+ </div>
74
+
75
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
76
+ <div class="bg-gray-50 border border-gray-200 p-5 rounded shadow-sm h-full">
77
+ <h3 class="text-xs font-bold text-gray-500 uppercase mb-3 flex items-center gap-2">πŸ‘€ Customer Profile</h3>
78
+ <div id="customerOutput" class="data-box text-sm text-gray-700"></div>
79
+ </div>
80
+ <div class="bg-gray-50 border border-gray-200 p-5 rounded shadow-sm h-full">
81
+ <h3 class="text-xs font-bold text-gray-500 uppercase mb-3 flex items-center gap-2">πŸ“œ Policy Context (RAG)</h3>
82
+ <div id="policyOutput" class="data-box text-xs text-gray-600 bg-white p-3 rounded border border-gray-100 italic"></div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <script>
89
+ async function sendQuery() {
90
+ const queryInput = document.getElementById('userQuery');
91
+ const codeInput = document.getElementById('accessCode');
92
+ const resultsArea = document.getElementById('resultsArea');
93
+ const loadingState = document.getElementById('loadingState'); // Target the new container
94
+ const submitBtn = document.getElementById('submitBtn'); // Target button to disable it
95
+
96
+ const query = queryInput.value.trim();
97
+ const code = codeInput.value.trim();
98
+
99
+ if (!code || !query) return alert("Please fill in both fields.");
100
+
101
+ // UI Updates: Hide results, Show Loader, Disable Button
102
+ resultsArea.classList.add('hidden');
103
+ loadingState.classList.remove('hidden');
104
+ submitBtn.disabled = true;
105
+ submitBtn.innerText = "Processing...";
106
+
107
+ try {
108
+ const response = await fetch('/ask', {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({ query, code })
112
+ });
113
+
114
+ const result = await response.json();
115
+
116
+ if (result.status === 'success') {
117
+ const data = result.data;
118
+
119
+ // 1. FINAL RESOLUTION
120
+ let recText = data.final_recommendation || "System could not provide a resolution.";
121
+ recText = recText.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
122
+ document.getElementById('finalOutput').innerHTML = recText;
123
+
124
+ // 2. STRATEGY
125
+ const planObj = data.plan || {};
126
+ const planHtml = `
127
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
128
+ <div class="col-span-1 md:col-span-2"><span class="font-semibold text-blue-900">Thought:</span> <span class="italic text-gray-600">${planObj.thought_process || "Processing query..."}</span></div>
129
+ <div><span class="font-semibold text-gray-700">Intent:</span> ${planObj.intent || "Unknown"}</div>
130
+ <div><span class="font-semibold text-gray-700">Detected Name:</span> ${planObj.customer_name || "None"}</div>
131
+ <div><span class="font-semibold text-gray-700">DB Check:</span> <span class="${planObj.requires_database ? 'text-green-600 font-bold' : 'text-gray-400'}">${planObj.requires_database ? "YES" : "NO"}</span></div>
132
+ <div><span class="font-semibold text-gray-700">Policy Check:</span> <span class="${planObj.requires_policy ? 'text-green-600 font-bold' : 'text-gray-400'}">${planObj.requires_policy ? "YES" : "NO"}</span></div>
133
+ </div>`;
134
+ document.getElementById('planOutput').innerHTML = planHtml;
135
+
136
+ // 3. CUSTOMER DATA
137
+ let customerHtml = "";
138
+ try {
139
+ let custData = data.customer_data;
140
+
141
+ if (typeof custData === 'string') {
142
+ if (custData.includes("NOT_FOUND") || custData.includes("N/A")) {
143
+ customerHtml = `<div class="bg-red-50 text-red-700 p-3 rounded font-bold text-center">❌ No Record Found</div>`;
144
+ } else {
145
+ customerHtml = `<div class="text-gray-600 text-xs italic">${custData}</div>`;
146
+ }
147
+ }
148
+ else if (typeof custData === 'object' && custData !== null) {
149
+ customerHtml = `<div class="grid grid-cols-2 gap-y-2 gap-x-4">`;
150
+ for (const [key, value] of Object.entries(custData)) {
151
+ let label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
152
+
153
+ let valDisplay = value;
154
+ if (value === false) valDisplay = '<span class="text-red-500 font-bold">False</span>';
155
+ if (value === true) valDisplay = '<span class="text-green-600 font-bold">True</span>';
156
+ if (key === 'credit_score' && typeof value === 'number') {
157
+ valDisplay = value >= 700
158
+ ? `<span class="text-green-600 font-bold">${value}</span>`
159
+ : `<span class="text-amber-600 font-bold">${value}</span>`;
160
+ }
161
+
162
+ customerHtml += `<div class="text-gray-400 text-[10px] uppercase pt-1">${label}</div><div class="font-medium text-gray-800 break-words">${valDisplay}</div>`;
163
+ }
164
+ customerHtml += `</div>`;
165
+ }
166
+ } catch (e) {
167
+ console.error("Rendering Error:", e);
168
+ customerHtml = `<div class="text-gray-600 text-xs italic">Error loading data.</div>`;
169
+ }
170
+ document.getElementById('customerOutput').innerHTML = customerHtml;
171
+
172
+ // 4. POLICY CONTEXT
173
+ document.getElementById('policyOutput').textContent = data.policy_data || "No relevant policy snippets retrieved.";
174
+
175
+ resultsArea.classList.remove('hidden');
176
+
177
+ } else {
178
+ alert(result.message || "Access Denied.");
179
+ }
180
+
181
+ } catch (error) {
182
+ console.error(error);
183
+ alert("Server connection failed.");
184
+ } finally {
185
+ // Restore UI state
186
+ loadingState.classList.add('hidden');
187
+ submitBtn.disabled = false;
188
+ submitBtn.innerText = "Analyze";
189
+ }
190
+ }
191
+
192
+ document.getElementById("userQuery").addEventListener("keypress", function(event) {
193
+ if (event.key === "Enter") sendQuery();
194
+ });
195
+ </script>
196
+ </body>
197
+ </html>