Spaces:
Sleeping
Sleeping
Commit
Β·
cffeaa1
0
Parent(s):
Initial deploy
Browse files- .dockerignore +7 -0
- .gitattributes +2 -0
- .gitignore +57 -0
- Dockerfile +37 -0
- GenAI_Loan_Advisor +1 -0
- README.md +77 -0
- app.py +74 -0
- crew/__init__.py +0 -0
- crew/agents/__init__.py +0 -0
- crew/agents/context_agent.py +47 -0
- crew/agents/data_access_agent.py +123 -0
- crew/agents/loan_officer_agent.py +219 -0
- crew/agents/rag_agent.py +217 -0
- crew/agents/resolution_agent.py +43 -0
- crew/agents/underwriter_agent.py +49 -0
- crew/tasks/context_tasks.py +47 -0
- crew/tasks/data_access_tasks.py +43 -0
- crew/tasks/rag_tasks.py +61 -0
- crew/tasks/resolution_tasks.py +50 -0
- crew/tasks/underwriting_tasks.py +58 -0
- crew/tools/__init__.py +0 -0
- crew/tools/db_utils.py +32 -0
- crew/tools/government_db_tools.py +58 -0
- crew/tools/internal_db_tools.py +85 -0
- database/create_government_db.py +36 -0
- database/create_internal_db.py +82 -0
- rag/ingest_policies.py +100 -0
- rag/policies/Bank Loan Interest Rate Policy.pdf +0 -0
- rag/policies/Bank Loan Overall Risk Policy.pdf +0 -0
- requirements.txt +17 -0
- templates/index.html +197 -0
.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>
|