File size: 12,454 Bytes
bc910c2
 
 
 
 
 
 
a621623
bc910c2
 
a220d65
d4e19f8
a220d65
 
 
 
 
 
 
bc910c2
 
 
 
 
7b2d474
 
bc910c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cf64583
 
 
bc910c2
cf64583
bc910c2
cf64583
9fe4467
bc910c2
 
cf64583
bc910c2
 
cf64583
9fe4467
cf64583
bc910c2
 
 
9fe4467
bc910c2
 
 
 
 
 
cf64583
 
 
bc910c2
 
cf64583
9fe4467
bc910c2
 
cf64583
9fe4467
cf64583
 
bc910c2
 
9fe4467
bc910c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a621623
e0500da
 
bc910c2
 
743d1c8
bc910c2
743d1c8
 
bc910c2
 
e0500da
bc910c2
e0500da
bc910c2
e0500da
743d1c8
e0500da
 
 
743d1c8
e0500da
 
 
 
 
 
 
 
 
743d1c8
 
e0500da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc910c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659f1ea
 
bc910c2
659f1ea
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# from mcp.server.fastmcp import FastMCP 
from fastmcp import FastMCP, Context
from fastmcp.dependencies import CurrentContext
import os
import smtplib
from email.message import EmailMessage
import aiosmtplib
import httpx

# --- EMAIL CONFIGURATION ---
SMTP_SERVER = os.getenv("SMTP_SERVER", "smtp.gmail.com")
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))

SENDER_EMAIL = os.getenv("SENDER_EMAIL")
SENDER_PASSWORD = os.getenv("SENDER_PASSWORD")

# Fail fast if credentials are missing
if not SENDER_EMAIL or not SENDER_PASSWORD:
    raise ValueError("SMTP credentials are not set in environment variables.")

# Initialize Server
mcp = FastMCP("escalation_server")

# Path Configuration
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
RESOURCE_PATH = os.path.join(CURRENT_DIR, "resource", "jemh114-min (1).md")

# --- CENTRALIZED REGISTRY ---
FACULTY_DATA = {
    "Mr. Murty":  {"email": "dharmin.naik@webashlar.com", "id": "T-MURTY-01"},
    "Mrs. Sunita":{"email": "dummyaiapi03@gmail.com", "id": "T-SUNITA-03"},
    "Mr. Patel":  {"email": "dev.patel@webashlar.com", "id": "T-PATEL-05"},
    "Dr. Rao":    {"email": "pratham.lakdawala@webashlar.com", "id": "T-RAO-02"},
    "Mr. Khanna": {"email": "dummyuseai@gmail.com", "id": "T-KHANNA-04"},
    "Ms. Joshi":  {"email": "dummyaiapi532@gmail.com", "id": "T-JOSHI-06"}
}

CLASS_ASSIGNMENTS = {
    "10-A": {"Math": "Mr. Murty", "Science": "Dr. Rao"},
    "10-B": {"Math": "Mrs. Sunita", "Science": "Mr. Khanna"},
    "10-C": {"Math": "Mr. Patel", "Science": "Ms. Joshi"}
}

# Input Normalization Map
SUBJECT_MAP = {
    "mathematics": "Math",
    "math": "Math",
    "maths": "Math",
    "statistics": "Math", 
    "stats": "Math",      
    "science": "Science",
    "physics": "Science", # For this demo, mapping physics to general science
    "biology": "Science"
}

def normalize_subject(subject: str) -> str:
    """Standardizes subject names so 'Mathematics' becomes 'Math'."""
    return SUBJECT_MAP.get(subject.lower(), subject.title())

# --- 1. MCP RESOURCES ---
# Resources are "read-only" data sources the LLM can reference.
def read_resource_file() -> str:
    if not os.path.exists(RESOURCE_PATH):
        return "Error: Statistics markdown file not found."
    with open(RESOURCE_PATH, "r", encoding="utf-8") as f:
        return f.read()

@mcp.resource("study://math/statistics")
def statistics_resource() -> str:
    """Exposes the material to the LLM as a read-only resource."""
    return read_resource_file()

# @mcp.resource("study://math/statistics")
# def get_stats_material() -> str:
#     """
#     Exposes the Class X Statistics chapter. 
#     The AI uses this to find formulas for Mean, Median, and Mode.
#     """
#     if not os.path.exists(RESOURCE_PATH):
#         return "Error: Statistics markdown file not found at the specified path."
        
#     with open(RESOURCE_PATH, "r", encoding="utf-8") as f:
#         content = f.read()
        
#     return content

@mcp.resource("system://tutor/instructions")
def tutor_instructions() -> str:
    """The mandatory operational manual for this tutoring session."""
    return """
    # OPERATIONAL PROTOCOL
    1. You are a gateway, not a teacher. 
    2. Max 3 attempts allowed using 'explain_from_resource' or 'generate_quick_revision'.
    3. If a tool returns 'LIMIT_REACHED', immediately ask for Class ID.
    4. Use get_teacher_info -> escalate_to_teacher.
    5. NO internal brainstorming. Use the provided textbook material ONLY.
    """

# --- 2. MCP TOOLS ---
@mcp.tool()
async def explain_from_resource(concept: str, ctx: Context = CurrentContext()):
    # 1. Properly await the state
    state_val = await ctx.get_state("math_attempts")
    attempts = state_val or 0

    # 2. Comparison now works because 'attempts' is an integer
    if attempts >= 3:
        # FIX: Changed 'tools=' to 'names='
        await ctx.disable_components(names=["explain_from_resource", "generate_quick_revision"])
        return (
            "FATAL_ERROR: TUTORING_LIMIT_REACHED. "
            "Please request Class ID and notify the teacher."
        )

    # 3. Properly await the update
    new_attempts = attempts + 1
    await ctx.set_state("math_attempts", new_attempts)
    
    material = read_resource_file() 
    return f"""
    <attempt>{new_attempts}/3</attempt>
    <content>{material}</content>
    <instruction>Explain {concept} concisely. Use LaTeX.</instruction>
    """

@mcp.tool()
async def generate_quick_revision(type: str = "summary", ctx: Context = CurrentContext()) -> str:
    # 1. Properly await the state
    state_val = await ctx.get_state("math_attempts")
    attempts = state_val or 0
    
    if attempts >= 3:
        # FIX: Changed 'tools=' to 'names='
        await ctx.disable_components(names=["explain_from_resource", "generate_quick_revision"])
        return "FATAL_ERROR: TUTORING_LIMIT_REACHED. Transition to escalation immediately."

    # 2. Properly await the update
    new_attempts = attempts + 1
    await ctx.set_state("math_attempts", new_attempts)

    material = read_resource_file()
    return f"""
    <attempt>{new_attempts}/3</attempt>
    <material>{material}</material>
    <request>Create a {type} for quick revision.</request>
    """

def normalize_class_id(class_id: str) -> str:
    """Converts 'Class 10B', '10b', or 'ten b' into '10-B'."""
    clean = class_id.upper().replace("CLASS", "").replace("GRADE", "").replace(" ", "").strip()
    # If student just says "10", we can't know which section, 
    # but we can format "10B" to "10-B"
    if len(clean) == 3 and clean[2].isalpha():
        return f"{clean[:2]}-{clean[2:]}"
    return clean

# --- 3. ESCALATION TOOLS (From our previous discussion) ---
@mcp.tool()
async def get_teacher_info(subject: str, class_id: str) -> dict:
    """
    Finds the specific teacher for a subject and class.
    Ensures Math queries go to Math teachers and Science to Science teachers.
    """
    # teacher_name = CLASS_ASSIGNMENTS.get(class_id, {}).get(subject)
    std_subject = normalize_subject(subject)
    std_class = normalize_class_id(class_id)
    
    # Verify class exists first
    # class_data = CLASS_ASSIGNMENTS.get(class_id)
    class_data = CLASS_ASSIGNMENTS.get(std_class)
    # if not class_data:
    #     return {"error": f"Class {class_id} does not exist in our registry."}
    if not class_data:
        # NEW: Suggest valid classes if the lookup fails
        valid_classes = ", ".join(CLASS_ASSIGNMENTS.keys())
        return {"error": f"Class '{std_class}' not found. Please use one of: {valid_classes}"}
    
    teacher_name = class_data.get(std_subject)
    if not teacher_name:
        return {"error": f"No {std_subject} teacher assigned to Class {class_id}."}
    
    teacher_details = FACULTY_DATA.get(teacher_name)
    return {
        "name": teacher_name,
        "id": teacher_details["id"],
        "subject": subject
    }

# --- RESEND EMAIL CONFIGURATION ---
# RESEND_API_KEY = os.getenv("RESEND_API_KEY")
VERCEL_EMAIL_API_URL = os.getenv("VERCEL_EMAIL_API_URL", "https://smtp-email-service-a61d.vercel.app/api/send_email")
@mcp.tool()
async def escalate_to_teacher(
    teacher_name: str,
    subject: str,
    student_query: str,
    ai_response: str,
    ctx: Context = CurrentContext()
) -> str:
    """Sends a real-time email via the Vercel SMTP Bridge API."""
    
    # 1. Get recipient from registry
    teacher_info = FACULTY_DATA.get(teacher_name)
    recipient = teacher_info["email"] if teacher_info else "dummyuseai@gmail.com"

    # 2. Prepare the email content
    email_subject = f"🚨 Escalation: {subject} Doubt for {teacher_name}"
    html_body = f"""
    <html>
        <body style="font-family: Arial, sans-serif; color: #333;">
            <div style="background: #f9f9f9; padding: 20px; border: 1px solid #ddd;">
                <h2 style="color: #d9534f;">Hello {teacher_name},</h2>
                <p>A student has a <strong>{subject}</strong> doubt that requires your attention.</p>
                <hr>
                <p><strong>Doubt:</strong> {student_query}</p>
                <p><strong>AI Response:</strong> {ai_response}</p>
            </div>
        </body>
    </html>
    """

    # 3. Construct the payload for your Vercel API
    # Based on your index.py: to, subject, body, is_html
    api_payload = {
        "to": recipient,
        "subject": email_subject,
        "body": html_body,
        "is_html": True
    }

    # 4. Call the Vercel API via HTTPS
    async with httpx.AsyncClient() as client:
        try:
            response = await client.post(
                VERCEL_EMAIL_API_URL, 
                json=api_payload,
                timeout=30.0
            )
            
            if response.status_code == 200:
                # --- SUCCESS: RESET LOGIC ---
                await ctx.set_state("math_attempts", 0)
                
                # Re-enable tutoring tools if they were disabled
                await ctx.enable_components(names=["explain_from_resource", "generate_quick_revision"])
                
                return f"Success: {teacher_name} ({subject} Expert) has been notified via Vercel Bridge."
            else:
                # Handle API-level errors (400, 401, 502, etc.)
                error_data = response.json()
                return f"Vercel API Error ({response.status_code}): {error_data.get('error', 'Unknown error')}"

        except httpx.ConnectError:
            return "Critical Error: Could not connect to Vercel API. Check your VERCEL_EMAIL_API_URL."
        except Exception as e:
            return f"Unexpected Error: {str(e)}"

# @mcp.tool()
# async def escalate_to_teacher(
#     teacher_name: str,
#     subject: str,
#     student_query: str,
#     ai_response: str,
#     ctx: Context = CurrentContext()
# ) -> str:

#     """Sends a real-time email to the correct teacher by name."""
    
#     # Safely get email from our central registry
#     teacher_info = FACULTY_DATA.get(teacher_name)

#     if not teacher_info:
#         recipient = "dummyuseai@gmail.com" # Fallback to admin
#     else:
#         recipient = teacher_info["email"]

#     msg = EmailMessage()
#     msg["Subject"] = f"🚨 Escalation: {subject} Doubt for {teacher_name}"
#     msg["From"] = SENDER_EMAIL
#     msg["To"] = recipient

#     html_content = f"""
#     <html>
#     <body style="font-family: Arial, sans-serif; color: #333;">
#     <div style="background: #f9f9f9; padding: 20px; border: 1px solid #ddd;">
#     <h2 style="color: #d9534f;">Hello {teacher_name},</h2>
#     <p>A student has a <strong>{subject}</strong> doubt that requires your attention.</p>
#     <hr>
#     <p><strong>Doubt:</strong> {student_query}</p>
#     <p><strong>AI Response:</strong> {ai_response}</p>
#     </div>
#     </body>
#     </html>
#     """
#     msg.add_alternative(html_content, subtype="html")

#     try:
#         await aiosmtplib.send(msg,
#         hostname=SMTP_SERVER,
#         port=SMTP_PORT,
#         username=SENDER_EMAIL,
#         password=SENDER_PASSWORD,
#         start_tls=True,
#         use_tls=False,
#         timeout=30
#         ) 
#         # --- THE FIX: RESET LOGIC ---
#         # ctx.set_state("math_attempts", 0) # Reset counter to 0
#         await ctx.set_state("math_attempts", 0)
#         # await ctx.enable_components(tools=["explain_from_resource", "generate_quick_revision"]) # Re-enable tools
#         return f"Success: {teacher_name} ({subject} Expert) has been notified."
#     except Exception as e:
#         return f"Error sending email: {str(e)}"

# --- 4. MCP PROMPTS ---
@mcp.prompt()
def revision_assistance(student_name: str):
    """A pre-configured prompt to help students with exam prep."""
    return f"Hi {student_name}, I can help you summarize your chapter or create short notes for formulas. Which one would you like to do first?"

@mcp.prompt()
def escalation_handover(teacher_name: str, subject: str) -> str:
    """Professional message used when handing over to a human teacher."""
    return (
        f"I've shared our {subject} discussion with {teacher_name}. "
        "They will review it and get back to you on your dashboard soon!"
    )

# if __name__ == "__main__":
#     mcp.run()
if __name__ == "__main__":
    # For Hugging Face Spaces, we use transport="sse" or "http"
    # Port 7860 is the HF default
    mcp.run(
        transport="http", 
        port=7860, 
        host="0.0.0.0"
    )