Files changed (1) hide show
  1. app_main.py +0 -385
app_main.py DELETED
@@ -1,385 +0,0 @@
1
- from fastapi import FastAPI, HTTPException, Request
2
- from fastapi.middleware.cors import CORSMiddleware
3
- from fastapi.responses import JSONResponse
4
- from pydantic import BaseModel
5
- import openai
6
- import os
7
- import json
8
- import re
9
- from typing import Dict, List, Optional, Tuple, Any
10
-
11
- app = FastAPI(title="TestCreationAgent",
12
- description="An API for collecting test creation parameters through conversation")
13
-
14
- # Add CORS middleware to allow requests from frontend
15
- app.add_middleware(
16
- CORSMiddleware,
17
- allow_origins=["*"], # Allows all origins
18
- allow_credentials=True,
19
- allow_methods=["*"], # Allows all methods
20
- allow_headers=["*"], # Allows all headers
21
- )
22
-
23
- # Define subject chapters mapping
24
- SUBJECT_CHAPTERS = {
25
- "Mathematics": [
26
- "Number Systems", "Polynomials", "Coordinate Geometry", "Linear Equations in Two Variables",
27
- "Introduction to Euclid's Geometry", "Lines and Angles", "Triangles", "Quadrilaterals",
28
- "Areas of Parallelograms and Triangles", "Circles", "Constructions", "Heron's Formula",
29
- "Surface Areas and Volumes", "Statistics", "Probability", "Real Numbers",
30
- "Pair of Linear Equations in Two Variables", "Quadratic Equations", "Arithmetic Progressions",
31
- "Introduction to Trigonometry", "Some Applications of Trigonometry", "Areas Related to Circles",
32
- "Sets", "Relations and Functions", "Trigonometric Functions", "Principle of Mathematical Induction",
33
- "Complex Numbers and Quadratic Equations", "Linear Inequalities", "Permutations and Combinations",
34
- "Binomial Theorem", "Sequences and Series", "Straight Lines", "Conic Sections",
35
- "Introduction to Three Dimensional Geometry", "Limits and Derivatives",
36
- "Inverse Trigonometric Functions", "Matrices", "Determinants",
37
- "Continuity and Differentiability", "Application of Derivatives", "Integrals",
38
- "Application of Integrals", "Differential Equations", "Vector Algebra",
39
- "Three Dimensional Geometry", "Linear Programming"
40
- ],
41
- "Physics": [
42
- "Motion", "Force and Laws of Motion", "Gravitation", "Work and Energy", "Sound",
43
- "Light: Reflection and Refraction", "Human Eye and Colourful World", "Electricity",
44
- "Magnetic Effects of Electric Current", "Physical World and Measurement", "Kinematics",
45
- "Laws of Motion", "Work, Energy and Power", "Motion of System of Particles and Rigid Body",
46
- "Properties of Bulk Matter", "Thermodynamics", "Behaviour of Perfect Gases and Kinetic Theory",
47
- "Oscillations and Waves", "Electrostatics", "Current Electricity",
48
- "Magnetic Effects of Current and Magnetism", "Electromagnetic Induction and Alternating Currents",
49
- "Electromagnetic Waves", "Optics", "Dual Nature of Radiation and Matter", "Atoms", "Nuclei",
50
- "Semiconductor Electronics: Materials, Devices and Simple Circuits", "Vectors"
51
- ],
52
- "Chemistry": [
53
- "Matter in Our Surroundings", "Is Matter Around Us Pure?", "Atoms and Molecules",
54
- "Structure of the Atom", "Chemical Reactions and Equations", "Acids, Bases and Salts",
55
- "Metals and Non-metals", "Carbon and Its Compounds", "Periodic Classification of Elements",
56
- "Some Basic Concepts of Chemistry", "Structure of Atom",
57
- "Classification of Elements and Periodicity in Properties",
58
- "Chemical Bonding and Molecular Structure", "States of Matter: Gases and Liquids",
59
- "Thermodynamics", "Equilibrium", "Redox Reactions",
60
- "Organic Chemistry: Some Basic Principles and Techniques", "Hydrocarbons",
61
- "Environmental Chemistry", "Solid State", "Solutions", "Electrochemistry",
62
- "Chemical Kinetics", "Surface Chemistry", "General Principles and Processes of Isolation of Elements",
63
- "p-Block Elements", "d- and f-Block Elements", "Coordination Compounds",
64
- "Haloalkanes and Haloarenes", "Alcohols, Phenols and Ethers",
65
- "Aldehydes, Ketones and Carboxylic Acids", "Amines", "Biomolecules", "Polymers",
66
- "Chemistry in Everyday Life"
67
- ],
68
- "Organic Chemistry": [
69
- "Organic Chemistry: Some Basic Principles and Techniques", "Hydrocarbons",
70
- "Haloalkanes and Haloarenes", "Alcohols, Phenols and Ethers",
71
- "Aldehydes, Ketones and Carboxylic Acids", "Amines", "Biomolecules",
72
- "Polymers", "Chemistry in Everyday Life"
73
- ],
74
- "Inorganic Chemistry": [
75
- "Classification of Elements and Periodicity in Properties",
76
- "Chemical Bonding and Molecular Structure", "Redox Reactions",
77
- "p-Block Elements", "d- and f-Block Elements", "Coordination Compounds"
78
- ]
79
- }
80
-
81
- # Create a flat mapping of misspelled/approximate chapter names to correct ones
82
- CHAPTER_MAPPING = {}
83
- for subject, chapters in SUBJECT_CHAPTERS.items():
84
- for chapter in chapters:
85
- # Add the correct chapter name
86
- CHAPTER_MAPPING[chapter.lower()] = (subject, chapter)
87
-
88
- # Add common misspellings/variations
89
- if chapter.lower() == "thermodynamics":
90
- CHAPTER_MAPPING["termodyanamics"] = (subject, chapter)
91
- CHAPTER_MAPPING["termodyn"] = (subject, chapter)
92
- CHAPTER_MAPPING["thermo"] = (subject, chapter)
93
- CHAPTER_MAPPING["thermodynamic"] = (subject, chapter)
94
-
95
-
96
- class UserInput(BaseModel):
97
- message: str
98
- session_id: str
99
-
100
-
101
- class SessionState(BaseModel):
102
- params: Dict[str, str] = {
103
- "chapters_of_the_test": "",
104
- "questions_per_chapter": "",
105
- "difficulty_distribution": "",
106
- "test_duration": "",
107
- "test_date": "",
108
- "test_time": ""
109
- }
110
- completed: bool = False
111
- attempt_count: int = 0
112
-
113
-
114
- # In-memory session storage
115
- sessions = {}
116
-
117
-
118
- def normalize_chapter_name(chapter_input: str) -> Optional[Tuple[str, str]]:
119
- """
120
- Maps user input to standardized chapter names from the curriculum.
121
- Returns tuple of (subject, correct_chapter_name) or None if no match.
122
- """
123
- if not chapter_input:
124
- return None
125
-
126
- # Direct mapping for exact matches or known misspellings
127
- norm_input = chapter_input.lower().strip()
128
- if norm_input in CHAPTER_MAPPING:
129
- return CHAPTER_MAPPING[norm_input]
130
-
131
- # Try fuzzy matching if no direct match
132
- # Look for partial matches
133
- for chapter_key, (subject, correct_name) in CHAPTER_MAPPING.items():
134
- if norm_input in chapter_key or chapter_key in norm_input:
135
- return (subject, correct_name)
136
-
137
- # No match found
138
- return None
139
-
140
-
141
- async def llm_extractParams(user_input: str, current_params: Dict[str, str]) -> Dict[str, str]:
142
- """
143
- Extracts structured test parameters from natural language input
144
- and updates the provided params dictionary.
145
- """
146
- system_prompt = """
147
- You are an expert educational test creation assistant that extracts test setup parameters from user input.
148
- Extract ONLY the parameters explicitly mentioned in the user's message.
149
-
150
- Return a JSON object with all the following keys:
151
- - chapters_of_the_test (string: list of chapters or topics)
152
- - questions_per_chapter (string or number: how many questions per chapter)
153
- - difficulty_distribution (string: e.g., "easy:40%, medium:40%, hard:20%" or any format specified)
154
- - test_duration (string or number: time in minutes)
155
- - test_date (string: in any reasonable date format)
156
- - test_time (string: time of day)
157
-
158
- Important rules:
159
- - Do NOT make assumptions - if information isn't provided, leave as empty string ("")
160
- - Only fill in values explicitly mentioned by the user
161
- - For difficulty_distribution:
162
- * Convert numeric sequences like "30 40 30" to "easy:30%, medium:40%, hard:30%" if they appear to be distributions
163
- * Convert descriptions like "mostly hard" to approximate percentages (e.g., "easy:20%, medium:20%, hard:60%")
164
- * Accept formats like "60 easy, 20 medium, 20 hard" and convert to percentages
165
- - Return valid JSON with all keys, even if empty
166
- """
167
- messages = [
168
- {"role": "system", "content": system_prompt},
169
- {"role": "user", "content": user_input}
170
- ]
171
-
172
- try:
173
- response = openai.chat.completions.create(
174
- model="gpt-4o-mini",
175
- messages=messages,
176
- temperature=0.2
177
- )
178
-
179
- extracted_json = response.choices[0].message.content.strip()
180
-
181
- # Handle potential JSON formatting issues by extracting JSON from response
182
- if not extracted_json.startswith('{'):
183
- # Find JSON object in text if it's not a clean JSON response
184
- start_idx = extracted_json.find('{')
185
- end_idx = extracted_json.rfind('}') + 1
186
- if start_idx >= 0 and end_idx > start_idx:
187
- extracted_json = extracted_json[start_idx:end_idx]
188
- else:
189
- raise ValueError("Unable to extract valid JSON from response")
190
-
191
- # Parse and update the current_params safely
192
- extracted_dict = json.loads(extracted_json)
193
- updated_params = current_params.copy()
194
-
195
- for key in updated_params:
196
- if key.lower() in extracted_dict and extracted_dict[key.lower()]:
197
- updated_params[key] = extracted_dict[key.lower()]
198
- elif key in extracted_dict and extracted_dict[key]:
199
- updated_params[key] = extracted_dict[key]
200
-
201
- # Apply chapter mapping if chapters were specified
202
- if updated_params["chapters_of_the_test"] and updated_params["chapters_of_the_test"] != current_params["chapters_of_the_test"]:
203
- chapters_input = updated_params["chapters_of_the_test"]
204
- # Split multiple chapters if comma-separated
205
- chapter_list = [ch.strip() for ch in re.split(r',|;', chapters_input)]
206
-
207
- mapped_chapters = []
208
- for chapter in chapter_list:
209
- result = normalize_chapter_name(chapter)
210
- if result:
211
- subject, correct_name = result
212
- mapped_chapters.append(f"{correct_name} ({subject})")
213
- else:
214
- mapped_chapters.append(chapter) # Keep as-is if no mapping found
215
-
216
- updated_params["chapters_of_the_test"] = ", ".join(mapped_chapters)
217
-
218
- return updated_params
219
-
220
- except json.JSONDecodeError as e:
221
- print(f"Error: Could not parse response as JSON: {e}")
222
- return current_params
223
- except Exception as e:
224
- print(f"Error during parameter extraction: {e}")
225
- return current_params
226
-
227
-
228
- def gate(params: Dict[str, str]) -> List[str]:
229
- """
230
- Checks which fields are still empty in the params.
231
- Returns a list of missing parameter keys.
232
- """
233
- return [key for key, val in params.items() if not val]
234
-
235
-
236
- async def llm_getMissingParams(missing_keys: List[str]) -> str:
237
- """
238
- Generates a human-readable prompt to ask user for missing fields.
239
- """
240
- # Create context-aware prompts for specific missing fields
241
- context_details = {
242
- "chapters_of_the_test": "such as Math, Science, History, etc.",
243
- "questions_per_chapter": "the number of questions for each chapter",
244
- "difficulty_distribution": "as percentages or numbers (easy, medium, hard)",
245
- "test_duration": "in minutes",
246
- "test_date": "when the test will be given",
247
- "test_time": "the time of day for the test"
248
- }
249
-
250
- # Create a more specific prompt based on what's missing
251
- if len(missing_keys) == 1:
252
- key = missing_keys[0]
253
- prompt = f"Please provide the {key.replace('_', ' ')} {context_details.get(key, '')}."
254
- else:
255
- formatted_missing = [f"{key.replace('_', ' ')} ({context_details.get(key, '')})" for key in missing_keys]
256
- prompt = f"The following test details are still needed: {', '.join(formatted_missing)}."
257
-
258
- messages = [
259
- {"role": "system", "content": "You are a helpful assistant who creates clear, concise questions to collect missing test setup information. Keep your response under 2 sentences and focus only on what's missing."},
260
- {"role": "user", "content": prompt}
261
- ]
262
-
263
- try:
264
- response = openai.chat.completions.create(
265
- model="gpt-4o-mini",
266
- messages=messages,
267
- temperature=0.3
268
- )
269
- return response.choices[0].message.content.strip()
270
- except Exception as e:
271
- print(f"Error generating prompt for missing values: {e}")
272
- return f"Please provide the following missing information: {', '.join(missing_keys)}."
273
-
274
-
275
- @app.on_event("startup")
276
- async def startup_event():
277
- # Set up OpenAI API key from environment variable
278
- openai.api_key = os.getenv("OPENAI_API_KEY")
279
- if not openai.api_key:
280
- print("⚠️ WARNING: OPENAI_API_KEY environment variable not set.")
281
-
282
-
283
- @app.get("/")
284
- async def root():
285
- return {"message": "Test Creation Agent API is running"}
286
-
287
-
288
- @app.post("/chat")
289
- async def chat(user_input: UserInput):
290
- session_id = user_input.session_id
291
-
292
- # Initialize session if it doesn't exist
293
- if session_id not in sessions:
294
- sessions[session_id] = SessionState()
295
-
296
- session = sessions[session_id]
297
-
298
- # If this is the first message, send a welcome message
299
- if session.attempt_count == 0:
300
- session.attempt_count += 1
301
- return {
302
- "response": "👋 Welcome! Please provide the test setup details. I need: chapters, questions per chapter, difficulty distribution, test duration, date, and time.",
303
- "session_state": {
304
- "params": session.params,
305
- "completed": False
306
- }
307
- }
308
-
309
- # Process user input to extract parameters
310
- session.params = await llm_extractParams(user_input.message, session.params)
311
- session.attempt_count += 1
312
-
313
- # Check if we have all required parameters
314
- missing = gate(session.params)
315
-
316
- # If we have all parameters or exceeded max attempts, return completion
317
- max_attempts = 10
318
- if not missing or session.attempt_count > max_attempts:
319
- session.completed = True
320
- if not missing:
321
- result = "✅ All test parameters are now complete:"
322
- else:
323
- result = "⚠️ Some parameters could not be filled after multiple attempts:"
324
-
325
- # Format the parameters as a readable string
326
- for k, v in session.params.items():
327
- result += f"\n- {k.replace('_', ' ').title()}: {v or 'Not provided'}"
328
-
329
- return {
330
- "response": result,
331
- "session_state": {
332
- "params": session.params,
333
- "completed": True
334
- }
335
- }
336
-
337
- # Otherwise, ask for missing parameters
338
- follow_up_prompt = await llm_getMissingParams(missing)
339
-
340
- return {
341
- "response": follow_up_prompt,
342
- "session_state": {
343
- "params": session.params,
344
- "completed": False
345
- }
346
- }
347
-
348
-
349
- @app.get("/session/{session_id}")
350
- async def get_session(session_id: str):
351
- if session_id not in sessions:
352
- raise HTTPException(status_code=404, detail="Session not found")
353
-
354
- session = sessions[session_id]
355
- return {
356
- "params": session.params,
357
- "completed": session.completed,
358
- "attempt_count": session.attempt_count
359
- }
360
-
361
-
362
- @app.delete("/session/{session_id}")
363
- async def delete_session(session_id: str):
364
- if session_id in sessions:
365
- del sessions[session_id]
366
- return {"message": "Session deleted successfully"}
367
-
368
-
369
- @app.post("/reset")
370
- async def reset_session(user_input: UserInput):
371
- session_id = user_input.session_id
372
- sessions[session_id] = SessionState()
373
-
374
- return {
375
- "response": "Session reset. 👋 Welcome! Please provide the test setup details. I need: chapters, questions per chapter, difficulty distribution, test duration, date, and time.",
376
- "session_state": {
377
- "params": sessions[session_id].params,
378
- "completed": False
379
- }
380
- }
381
-
382
-
383
- if __name__ == "__main__":
384
- import uvicorn
385
- uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 8000)), reload=True)