Spaces:
Sleeping
Sleeping
| import json | |
| import os | |
| import copy | |
| import sqlite3 | |
| import operator | |
| import streamlit as st | |
| from openai import OpenAI | |
| from langchain_openai import ChatOpenAI | |
| from langchain_core.messages import HumanMessage, SystemMessage | |
| from typing import Annotated, List | |
| from pydantic import BaseModel, Field | |
| from typing_extensions import TypedDict, Literal | |
| from langgraph.graph import StateGraph, START, END | |
| from langgraph.constants import Send | |
| # Page configuration | |
| st.set_page_config(layout="wide", page_title="JEE Roadmap Planner") | |
| # Initialize session state variables | |
| if "data" not in st.session_state: | |
| st.session_state.data = None | |
| if "data_old" not in st.session_state: | |
| st.session_state.data_old = None | |
| if "report_data" not in st.session_state: | |
| st.session_state.report_data = None | |
| if "incomplete_tasks" not in st.session_state: | |
| st.session_state.incomplete_tasks = None | |
| if "incomplete_task_list" not in st.session_state: | |
| st.session_state.incomplete_task_list = None | |
| if "final_report" not in st.session_state: | |
| st.session_state.final_report = None | |
| if "shifted_roadmap" not in st.session_state: | |
| st.session_state.shifted_roadmap = None | |
| if "available_dates" not in st.session_state: | |
| st.session_state.available_dates = [] | |
| if "updated_roadmap" not in st.session_state: | |
| st.session_state.updated_roadmap = None | |
| if "max_optimizer_iterations" not in st.session_state: | |
| st.session_state.max_optimizer_iterations = 3 # Limit optimizer to 3 iterations | |
| # Navigation sidebar setup | |
| st.sidebar.title("JEE Roadmap Planner") | |
| page = st.sidebar.radio("Navigation", ["Home", "Roadmap Manager", "Task Analysis","Roadmap Chatbot"]) | |
| # For roadmap chatbot | |
| def get_chapters_and_subtopics(): | |
| with open("full_roadmap.json", "r") as f: | |
| data = json.load(f) | |
| ch_subt = { | |
| "Physics": {}, | |
| "Chemistry": {}, | |
| "Maths": {} | |
| } | |
| for day in data["schedule"]: | |
| for subject in day['subjects']: | |
| sub = ch_subt[subject['name']] | |
| for task in subject['tasks']: | |
| sub[task['ChapterName']] = [] | |
| for day in data["schedule"]: | |
| for subject in day['subjects']: | |
| sub = ch_subt[subject['name']] | |
| for task in subject['tasks']: | |
| if task['subtopic'] not in sub[task['ChapterName']]: | |
| sub[task['ChapterName']].append(task['subtopic']) | |
| return ch_subt | |
| # Function to convert NL query to SQL | |
| def generate_sql_from_nl(prompt): | |
| client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
| table_struct = """CREATE TABLE IF NOT EXISTS roadmap ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| day_num INTEGER, | |
| date TEXT, -- [yyyy-mm-dd] | |
| subject TEXT, -- [Physics, Chemistry or Maths] | |
| chapter_name TEXT, | |
| task_type TEXT, -- (Concept Understanding, Question Practice, Revision, Test) | |
| time TEXT, -- formatted like '0.5 hour', '1 hour', '2 Hours', and so on | |
| subtopic TEXT, | |
| )""" | |
| ch_subt = get_chapters_and_subtopics() | |
| response = client.chat.completions.create( | |
| model="gpt-4o-mini", | |
| messages=[ | |
| {"role": "system", "content": f""""You are an helper who runs in the background of an AI agent, | |
| which helps students for their JEE Preparation. Now your Job is to analyze the users prompt and | |
| create an SQL query to extract the related Information from an sqlite3 database with the table | |
| structure: {table_struct}. | |
| Note: | |
| - For the time column, the data is formatted like '0.5 hour', '1 hour', '2 hours' and | |
| so on. So make sure to create queries that compare just the numbers within the text. | |
| - If the student mention about any chapters or subtopics, browse through this json file {ch_subt}, | |
| find the one with the closest match to the users query and use only those exact names of Chapers | |
| and Subtopics present in this file to create SQL the query. | |
| You will also make sure multiple times that you give an SQL | |
| Query that adheres to the given table structure, and you Output just the SQL query. | |
| Do not include anyting else like new line statements, ```sql or any other text. Your output | |
| is going to be directly fed into a Python script to extract the required information. So, | |
| please follow all the given Instructions. | |
| """}, | |
| {"role": "user", "content": f"""Keeping the table structure in mind: {table_struct}, | |
| Convert this prompt to an SQL query for the given table: {prompt}. Make sure your | |
| output is just the SQL query, which can directly be used to extract required content"""} | |
| ] | |
| ) | |
| return response.choices[0].message.content.strip() | |
| # Function to convert SQL output to natural language | |
| def generate_nl_from_sql_output(prompt, query, data): | |
| client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
| response = client.chat.completions.create( | |
| model="gpt-4o-mini", | |
| messages=[ | |
| {"role": "system", "content": f"""You are an helpful AI chatbot working under the roadmap | |
| section of an AI Agent, whose role is to aid students in their preparation for the JEE examination. | |
| You are going to play a very crucial role of a Roadmap Assistant, who helps the student out with whatever query | |
| they have related to their roadmap, the data required to answer the users query is already extracted | |
| from the Roadmap table of a SQLite3 database and given to you here {data}. Analyse the users query deeply and | |
| reply to it with the relevant information from the given data in a supportive manner. If you get empty data | |
| as an input, deeply analyze the user's prompt and the sql query and give a suitable reply."""}, | |
| {"role": "user", "content": f"""Answer to this users query using the data given to you, while keeping | |
| your role in mind: {prompt}"""} | |
| ] | |
| ) | |
| return response.choices[0].message.content.strip() | |
| # Function to fetch data from SQLite | |
| def fetch_data_from_sql(sql_query): | |
| conn = sqlite3.connect("jee_roadmap.db") | |
| cursor = conn.cursor() | |
| cursor.execute(sql_query) | |
| rows = cursor.fetchall() | |
| conn.close() | |
| return rows | |
| # Main function for chatbot | |
| def answer_user_query(prompt): | |
| initialize_roadmap_db() | |
| query = generate_sql_from_nl(prompt) | |
| st.write(query) | |
| rows = fetch_data_from_sql(query) | |
| st.write(rows) | |
| return generate_nl_from_sql_output(prompt, query, rows) | |
| def initialize_roadmap_db(): | |
| if not os.path.exists("jee_roadmap.db"): | |
| try: | |
| with open("full_roadmap.json") as f: | |
| roadmap_data = json.load(f) | |
| conn = sqlite3.connect("jee_roadmap.db") | |
| cursor = conn.cursor() | |
| cursor.execute(""" | |
| CREATE TABLE IF NOT EXISTS roadmap ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| day_num INTEGER, | |
| date TEXT, | |
| subject TEXT, | |
| chapter_name TEXT, | |
| task_type TEXT, | |
| time TEXT, | |
| subtopic TEXT | |
| ) | |
| """) | |
| for day in roadmap_data["schedule"]: | |
| date = day["date"] | |
| day_num = day["dayNumber"] | |
| for subj in day["subjects"]: | |
| subject = subj["name"] | |
| for task in subj["tasks"]: | |
| cursor.execute(""" | |
| INSERT INTO roadmap (day_num, date, subject, chapter_name, task_type, time, subtopic) | |
| VALUES (?, ?, ?, ?, ?, ?, ?) | |
| """, ( | |
| day_num, | |
| date, | |
| subject, | |
| task["ChapterName"], | |
| task["type"], | |
| task["time"], | |
| task["subtopic"] | |
| )) | |
| conn.commit() | |
| conn.close() | |
| print("✅ Database created and data inserted successfully.") | |
| except Exception as e: | |
| print(f"⚠️ Error initializing database: {e}") | |
| # Function to load initial data | |
| def load_initial_data(): | |
| with st.spinner("Loading roadmap data..."): | |
| try: | |
| with open('fourdayRoadmap.json', 'r') as file: | |
| data = json.load(file) | |
| st.session_state.data = data | |
| st.session_state.data_old = copy.deepcopy(data) | |
| st.success("Data loaded successfully!") | |
| return True | |
| except Exception as e: | |
| st.error(f"Error loading data: {e}") | |
| return False | |
| # Function to mark tasks as incomplete | |
| def process_task_completion_data(): | |
| with st.spinner("Processing task completion data..."): | |
| data = st.session_state.data | |
| for day in data["schedule"]: | |
| for subject in day["subjects"]: | |
| for task in subject["tasks"]: | |
| task["task_completed"] = False | |
| task["completion_timestamp"] = None | |
| st.session_state.data = data | |
| st.success("Task completion data processed!") | |
| # Function to extract incomplete tasks | |
| def extract_incomplete_tasks(): | |
| with st.spinner("Extracting incomplete tasks..."): | |
| data = st.session_state.data | |
| previous_day = data["schedule"][0] | |
| incomplete_tasks = { | |
| "dayNumber": previous_day["dayNumber"], | |
| "date": previous_day["date"], | |
| "subjects": [] | |
| } | |
| for subject in previous_day["subjects"]: | |
| incomplete_subject_tasks = [ | |
| { | |
| "ChapterName": task["ChapterName"], | |
| "type": task["type"], | |
| "subtopic": task["subtopic"], | |
| "time": task["time"], | |
| "task_completed": False, | |
| "completion_timestamp": None | |
| } | |
| for task in subject["tasks"] if not task["task_completed"] | |
| ] | |
| if incomplete_subject_tasks: | |
| incomplete_tasks["subjects"].append({ | |
| "name": subject["name"], | |
| "tasks": incomplete_subject_tasks | |
| }) | |
| # Convert to JSON format | |
| incomplete_tasks_json = json.dumps(incomplete_tasks, indent=4) | |
| st.session_state.incomplete_tasks = incomplete_tasks | |
| # Generate a list of incomplete tasks for the agent | |
| incomplete_task_list = [] | |
| for subject in incomplete_tasks["subjects"]: | |
| for task in subject["tasks"]: | |
| if not task["task_completed"]: | |
| incomplete_task = { | |
| "subject": subject["name"], | |
| "ChapterName": task["ChapterName"], | |
| "type": task["type"], | |
| "subtopic": task["subtopic"], | |
| "time": task["time"], | |
| "task_completed": task["task_completed"], | |
| "completion_timestamp": task["completion_timestamp"] | |
| } | |
| incomplete_task_list.append(incomplete_task) | |
| st.session_state.incomplete_task_list = incomplete_task_list | |
| st.success("Incomplete tasks extracted!") | |
| def generate_sql_for_report(llm, prompt): | |
| table_struct = """ | |
| CREATE TABLE IF NOT EXISTS roadmap ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| day_num INTEGER, | |
| date TEXT, | |
| subject TEXT, | |
| chapter_name TEXT, | |
| task_type TEXT, | |
| time TEXT, | |
| subtopic TEXT, | |
| task_completed BOOLEAN, | |
| completion_timestamp TEXT | |
| ) | |
| """ | |
| response = llm.invoke( | |
| [ | |
| SystemMessage(content=f"""You are a helper who runs in the background of an AI agent, | |
| which helps students for their JEE Preparation. Now your job is to analyze the user's prompt and | |
| create an SQL query to extract the related Information from an sqlite3 database with the table | |
| structure: {table_struct}. | |
| Note: For the time column, the data is formatted like '0.5 hour', '1 hour', '2 hours' and | |
| so on, it tells the amount of time required to complete that specific task. So make sure | |
| to create queries that compare just the numbers within the text. For the task_type column, | |
| the data is either of these (Concept Understanding, Question Practice, Revision or Test) | |
| You will also make sure multiple times that you give an SQL | |
| Query that adheres to the given table structure, and you output just the SQL query. | |
| Do not include anything else like new line statements, ```sql or any other text. Your output | |
| is going to be directly fed into a Python script to extract the required information. So, | |
| please follow all the given instructions. | |
| Verify multiple times that the SQL query is error free for the SQLite3 format."""), | |
| HumanMessage(content=f"""Keeping the table structure in mind: {table_struct}, | |
| Convert this prompt to an SQL query for the given table: {prompt}. Make sure your | |
| output is just the SQL query, which can directly be used to extract required content.""") | |
| ] | |
| ) | |
| return response.content.strip() | |
| def get_sql_data_for_report(sql_query): | |
| conn = sqlite3.connect("jee_full_roadmap.db") | |
| cursor = conn.cursor() | |
| results = [] | |
| queries = [q.strip() for q in sql_query.strip().split(';') if q.strip()] | |
| for query in queries: | |
| cursor.execute(query) | |
| columns = [desc[0] for desc in cursor.description] | |
| rows = cursor.fetchall() | |
| results.append({ | |
| "query": query, | |
| "columns": columns, | |
| "rows": rows | |
| }) | |
| conn.close() | |
| return results | |
| def create_db_for_report(roadmap_data): | |
| try: | |
| conn = sqlite3.connect("jee_full_roadmap.db") | |
| cursor = conn.cursor() | |
| cursor.execute("DROP TABLE IF EXISTS roadmap") | |
| cursor.execute(""" | |
| CREATE TABLE roadmap ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| day_num INTEGER, | |
| date TEXT, | |
| subject TEXT, | |
| chapter_name TEXT, | |
| task_type TEXT, | |
| time TEXT, | |
| subtopic TEXT, | |
| task_completed BOOLEAN, | |
| completion_timestamp TEXT | |
| ) | |
| """) | |
| for day in roadmap_data["schedule"]: | |
| date = day["date"] | |
| day_num = day["dayNumber"] | |
| for subj in day["subjects"]: | |
| subject = subj["name"] | |
| for task in subj["tasks"]: | |
| cursor.execute(""" | |
| INSERT INTO roadmap (day_num, date, subject, chapter_name, task_type, time, subtopic, task_completed, completion_timestamp) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| """, ( | |
| day_num, | |
| date, | |
| subject, | |
| task["ChapterName"], | |
| task["type"], | |
| task["time"], | |
| task["subtopic"], | |
| task["task_completed"], | |
| task["completion_timestamp"] | |
| )) | |
| conn.commit() | |
| conn.close() | |
| print("✅ Database created and data inserted successfully.") | |
| except Exception as e: | |
| print(f"⚠️ Error initializing database: {e}") | |
| # Function to generate report | |
| llm = ChatOpenAI(model="gpt-4o-mini") | |
| class Section(BaseModel): | |
| name: str = Field( | |
| description="Name for this section of the report.", | |
| ) | |
| description: str = Field( | |
| description="Brief overview of the main topics and concepts to be covered in this section.", | |
| ) | |
| data_requirements: str = Field( | |
| description="Description of the data needed from the roadmap database to write this section.", | |
| ) | |
| class Sections(BaseModel): | |
| sections: List[Section] = Field( | |
| description="Sections of the report.", | |
| ) | |
| planner = llm.with_structured_output(Sections) | |
| class State(TypedDict): | |
| sections: list[Section] # List of report sections | |
| completed_sections: Annotated[list, operator.add] # All workers write to this key in parallel | |
| final_report: str # Final report | |
| # Combined helper-worker state | |
| class ProcessorState(TypedDict): | |
| section: Section | |
| completed_sections: Annotated[list, operator.add] | |
| def orchestrator(state: State): | |
| """Orchestrator that generates a plan for the report with data requirements""" | |
| schema = """CREATE TABLE IF NOT EXISTS roadmap ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| day_num INTEGER, | |
| date TEXT, -- [yyyy-mm-dd] | |
| subject TEXT, -- (Physics, Chemistry or Maths) | |
| chapter_name TEXT, | |
| task_type TEXT, -- (Concept Understanding, Question Practice, Revision, Test) | |
| time TEXT, -- formatted like '0.5 hour', '1 hour', '2 Hours', and so on -- Tells the amount of time required to finish the task | |
| subtopic TEXT, | |
| task_completed BOOLEAN, -- 0/1 indicates task completion status | |
| completion_timestamp TEXT | |
| )""" | |
| # Generate queries | |
| report_sections = planner.invoke( | |
| [ | |
| SystemMessage(content=f"""You are responsible for creating a structured plan for a JEE preparation analysis report. | |
| Audience: The report is intended primarily for students, but must also be insightful to mentors and parents. | |
| Keep the language motivational and supportive, with actionable insights backed by data. | |
| Report Format: The report will be composed of exactly 4 concise sections. Your job is to define these sections. Each section must include: | |
| - **Name**: A short, descriptive title | |
| - **Description**: What the section analyzes and how it helps the student | |
| - **Data Requirements**: A plain-English description of what fields and metrics are needed from the roadmap | |
| database whose schema is given here: {schema} | |
| DO NOT invent new sections or formats. Use exactly the following four section templates and fill in the | |
| descriptions and data requirements precisely. | |
| --- | |
| ### Study Time Analysis | |
| **Description**: Analyze how much total time the student planned to spend vs how much they actually completed, | |
| across different subjects and task types. This will help the student understand where their time is really going. | |
| **Data Requirements**: | |
| - Fields: `subject`, `task_type`, `time`, `task_completed` | |
| - Metrics: | |
| - Total planned time → SUM of all `time` | |
| - Total actual time → SUM of `time` where `task_completed = 1` | |
| - Grouped by both `subject` and `task_type` | |
| --- | |
| ### Task Completion Metrics | |
| **Description**: Measure the student’s consistency and follow-through by looking at completion rates across | |
| subjects and task types. | |
| **Data Requirements**: | |
| - Fields: `subject`, `task_type`, `task_completed` | |
| - Metrics: | |
| - Total tasks → COUNT of all tasks | |
| - Completed tasks → COUNT of tasks where `task_completed = 1` | |
| - Completion percentage per subject and task type | |
| --- | |
| ### Study Balance Analysis | |
| **Description**: Evaluate how the student's study time is distributed across task types (e.g., Practice, Revision, Test) | |
| within each subject. This highlights over- or under-emphasis on any category. | |
| **Data Requirements**: | |
| - Fields: `subject`, `task_type`, `time` | |
| - Metrics: | |
| - SUM of `time` for each (subject, task_type) pair where task_completed = 1 | |
| - Relative distribution of time per subject to detect imbalance | |
| --- | |
| ### Strengths and Areas for Improvement | |
| **Description**: | |
| This section analyzes how the student's effort is distributed — not by estimating how long they spent, | |
| but by combining how many tasks they completed and how much time those completed tasks represent. | |
| This helps identify: | |
| - Subjects and task types where the student is showing strong commitment | |
| - Areas that may be neglected or inconsistently approached | |
| **Data Requirements**: | |
| - Fields: subject, task_type, task_completed, time | |
| - Metrics (filtered where task_completed = 1): | |
| - Total Number of completed tasks | |
| - Total amount of time spent | |
| - Grouped by subject and task_type | |
| --- | |
| Important Constraints: | |
| - You must include **all the mentioned fields** in the `data_requirements` — no assumptions | |
| - Use only **aggregate metrics** — no need for per-task or per-day analysis | |
| - Keep descriptions student-focused, clear, and motivational | |
| - Do not alter section names or invent new ones | |
| - Do not output anything outside the strict format above | |
| Your output will be passed into a structured data pipeline. Return only the filled-out section definitions as described above. | |
| """), | |
| HumanMessage(content="""Use the given table structure of the roadmap and decide all the sections of | |
| the report along with what should be in it and the clearly mention all the data thats required for it | |
| from the roadmap table"""), | |
| ] | |
| ) | |
| return {"sections": report_sections.sections} | |
| def processor(state: ProcessorState): | |
| """Combined helper and worker - gets data and writes section in one step""" | |
| section = state['section'] | |
| # HELPER PART: Get data for this section | |
| sql_query = generate_sql_for_report(llm, section.data_requirements) | |
| rows = get_sql_data_for_report(sql_query) | |
| # WORKER PART: Write the section using the data | |
| section_result = llm.invoke( | |
| [ | |
| SystemMessage( | |
| content=f"""Create a concise, data-driven JEE preparation report section that provides actionable insights for students, | |
| parents, and mentors. | |
| Requirements: | |
| 1. Begin directly with key metrics and insights - no introductory preamble | |
| 2. Use specific numbers, percentages, and ratios to quantify performance | |
| 3. Include concise tables or bullet points for clarity where appropriate | |
| 4. Highlight patterns related to: | |
| - Task completion rates | |
| - Time allocation efficiency | |
| - Subject/topic focus distribution | |
| - Study consistency patterns | |
| 5. For each observation, provide a brief actionable recommendation focused on student improvement. | |
| 6. Use professional but motivational tone appropriate for academic context | |
| 7. Strictly use Markdown for formatting all the tables and the numbers | |
| 8. Strictly keep each section very focused and write it under 0 to 50 words | |
| 9. Verify the formatting of all the tables multiple times to ensure the markdown is correct. | |
| 10. Check all the numbers and calculations made by you multiple times to ensure accuracy | |
| Base all analysis strictly on the provided data - avoid assumptions beyond what's explicitly given to you. | |
| Don't assume anything else, even a little bit. | |
| *Important* | |
| If you receive an empty data input, understand that the student hasn't done tasks matching the given data description. Also, | |
| know that this report is for the student to improve themselves, and they have no part in making sure the data is logged for | |
| this analysis. Deeply analyze the SQL query ->{sql_query} and the data description ->{section.data_requirements} used to | |
| extract the data and figure out why there was no data available in the roadmap, which the student went through and write | |
| the section accordingly. | |
| """ | |
| ), | |
| HumanMessage( | |
| content=f"""Here is the section name: {section.name} and description: {section.description} | |
| Data for writing this section: {rows}""" | |
| ), | |
| ] | |
| ) | |
| # Return completed section | |
| return {"completed_sections": [section_result.content]} | |
| def synthesizer(state: State): | |
| """Synthesize full report from sections""" | |
| # List of completed sections | |
| completed_sections = state["completed_sections"] | |
| # Format completed section to str to use as context for final sections | |
| completed_report_sections = "\n\n---\n\n".join(completed_sections) | |
| return {"final_report": completed_report_sections} | |
| # Assign processors function | |
| def assign_processors(state: State): | |
| """Assign a processor to each section in the plan""" | |
| return [Send("processor", {"section": s}) for s in state["sections"]] | |
| def generate_report(full_roadmap): | |
| with st.spinner("Generating performance report using AI..."): | |
| # Build workflow | |
| workflow_builder = StateGraph(State) | |
| # Add the nodes | |
| workflow_builder.add_node("orchestrator", orchestrator) | |
| workflow_builder.add_node("processor", processor) | |
| workflow_builder.add_node("synthesizer", synthesizer) | |
| # Add edges to connect nodes | |
| workflow_builder.add_edge(START, "orchestrator") | |
| workflow_builder.add_conditional_edges("orchestrator", assign_processors, ["processor"]) | |
| workflow_builder.add_edge("processor", "synthesizer") | |
| workflow_builder.add_edge("synthesizer", END) | |
| # Compile the workflow | |
| workflow = workflow_builder.compile() | |
| # Initialize database | |
| create_db_for_report(full_roadmap) | |
| # Invoke | |
| state = workflow.invoke({}) | |
| st.session_state.final_report = state["final_report"] | |
| # Function to extract available dates | |
| def extract_available_dates(): | |
| with st.spinner("Extracting available dates for rescheduling..."): | |
| data = st.session_state.data | |
| def remove_the_first_day(roadmap): | |
| new_roadmap = { | |
| "schedule": [] | |
| } | |
| for day in roadmap['schedule']: | |
| if day['dayNumber'] != 1: | |
| new_roadmap['schedule'].append(day) | |
| return new_roadmap | |
| roadmap = remove_the_first_day(data) | |
| available_dates = [] | |
| for day in roadmap['schedule']: | |
| available_dates.append(day['date']) | |
| st.session_state.available_dates = available_dates | |
| st.success(f"Found {len(available_dates)} available dates for rescheduling!") | |
| # Function to shift incomplete tasks using the evaluator-optimizer approach | |
| def shift_incomplete_tasks(): | |
| with st.spinner("Optimizing task distribution using evaluator-optimizer approach..."): | |
| try: | |
| # Initialize needed components | |
| llm = ChatOpenAI(model="gpt-4o-mini", api_key = os.getenv("OPENAI_API_KEY")) | |
| # Schema for structured output to use in evaluation | |
| class Feedback(BaseModel): | |
| grade: Literal["added", "not added"] = Field( | |
| description="Check if all the incomplete tasks are added to the roadmap or not", | |
| ) | |
| feedback: str = Field( | |
| description="If some tasks are not added, give feedback to add those tasks also", | |
| ) | |
| # Augment the LLM with schema for structured output | |
| evaluator = llm.with_structured_output(Feedback) | |
| # Graph state | |
| class State(TypedDict): | |
| roadmap: dict | |
| available_dates: list | |
| incomplete_task_list: list | |
| feedback: str | |
| added_or_not: str | |
| iteration_count: int | |
| # Initialize state | |
| current_state = { | |
| "roadmap": {}, | |
| "available_dates": st.session_state.available_dates, | |
| "incomplete_task_list": st.session_state.incomplete_task_list, | |
| "feedback": "", | |
| "added_or_not": "", | |
| "iteration_count": 0 | |
| } | |
| # Progress bar for iterations | |
| progress_bar = st.progress(0) | |
| iteration_status = st.empty() | |
| # First call to generator | |
| iteration_status.write("Iteration 1: Generating initial task distribution...") | |
| if current_state.get("feedback"): | |
| msg = llm.invoke( | |
| f"""Add the following incomplete_tasks {current_state['incomplete_task_list']} to the roadmap key and take into account the feedback {current_state['feedback']} | |
| and make sure that we dynamically add the tasks not increasing the load on just one day, also we have to add the tasks on | |
| following dates only {current_state['available_dates']} Make sure you only give roadmap json as output and nothing else, strictly follow the output structure: | |
| ```json | |
| {{ | |
| "roadmap": [ | |
| {{ | |
| "date": "YYYY-MM-DD", | |
| "tasks": [ | |
| {{ | |
| "subject": "Subject Name", | |
| "ChapterName": "Chapter Name", | |
| "type": "Type of Task", | |
| "subtopic": "Subtopic Name", | |
| "time": "estimated time" | |
| }} | |
| ] | |
| }} | |
| ] | |
| }} | |
| ``` | |
| """ | |
| ) | |
| else: | |
| msg = llm.invoke( | |
| f"""Add the following incomplete_tasks {current_state['incomplete_task_list']} to the roadmap key | |
| and make sure that we dynamically add the tasks not increasing the load on just one day, also we have to add the tasks on | |
| following dates only {current_state['available_dates']} Make sure you only give roadmap json as output and nothing else, strictly follow the output structure: | |
| ```json | |
| {{ | |
| "roadmap": [ | |
| {{ | |
| "date": "YYYY-MM-DD", | |
| "tasks": [ | |
| {{ | |
| "subject": "Subject Name", | |
| "ChapterName": "Chapter Name", | |
| "type": "Type of Task", | |
| "subtopic": "Subtopic Name", | |
| "time": "estimated time" | |
| }} | |
| ] | |
| }} | |
| ] | |
| }} | |
| ``` | |
| """ | |
| ) | |
| current_state["roadmap"] = msg.content | |
| progress_bar.progress(1/6) | |
| # Enter optimization loop | |
| max_iterations = st.session_state.max_optimizer_iterations | |
| current_iteration = 1 | |
| while current_iteration <= max_iterations: | |
| # Evaluate the current roadmap | |
| iteration_status.write(f"Iteration {current_iteration}: Evaluating task distribution...") | |
| grade = evaluator.invoke( | |
| f"Grade the roadmap {current_state['roadmap']} by checking if {current_state['incomplete_task_list']} are all added or not and make sure that we dynamically add the tasks not increasing the load on just one day" | |
| ) | |
| current_state["added_or_not"] = grade.grade | |
| current_state["feedback"] = grade.feedback | |
| current_state["iteration_count"] += 1 | |
| progress_bar.progress((current_iteration * 2 - 1)/6) | |
| # Check if we're done or need another iteration | |
| if current_state["added_or_not"] == "added": | |
| iteration_status.write(f"✅ Success! All tasks distributed after {current_iteration} iterations.") | |
| break | |
| if current_iteration == max_iterations: | |
| iteration_status.write(f"⚠️ Reached maximum iterations ({max_iterations}). Using best result so far.") | |
| break | |
| # Generate an improved roadmap based on feedback | |
| iteration_status.write(f"Iteration {current_iteration + 1}: Improving task distribution based on feedback...") | |
| msg = llm.invoke( | |
| f"""Add the following incomplete_tasks {current_state['incomplete_task_list']} to the roadmap key | |
| and take into account the feedback: {current_state['feedback']} | |
| Make sure that we dynamically add the tasks not increasing the load on just one day. | |
| Only add tasks on these available dates: {current_state['available_dates']} | |
| Make sure you only give roadmap json as output and nothing else, strictly follow the output structure: | |
| ```json | |
| {{ | |
| "roadmap": [ | |
| {{ | |
| "date": "YYYY-MM-DD", | |
| "tasks": [ | |
| {{ | |
| "subject": "Subject Name", | |
| "ChapterName": "Chapter Name", | |
| "type": "Type of Task", | |
| "subtopic": "Subtopic Name", | |
| "time": "estimated time" | |
| }} | |
| ] | |
| }} | |
| ] | |
| }} | |
| ``` | |
| """ | |
| ) | |
| current_state["roadmap"] = msg.content | |
| current_iteration += 1 | |
| progress_bar.progress((current_iteration * 2 - 2)/6) | |
| # Process the final roadmap content | |
| shifted_tasks_roadmap = current_state["roadmap"] | |
| # Extract JSON part from the response | |
| if "```json" in shifted_tasks_roadmap: | |
| shifted_tasks_roadmap = shifted_tasks_roadmap.split("```json")[1].split("```")[0] | |
| elif "```" in shifted_tasks_roadmap: | |
| shifted_tasks_roadmap = shifted_tasks_roadmap.split("```")[1].split("```")[0] | |
| st.session_state.shifted_roadmap = shifted_tasks_roadmap | |
| st.success(f"Tasks rescheduled successfully after {current_iteration} iterations!") | |
| progress_bar.progress(1.0) | |
| except Exception as e: | |
| st.error(f"Error in optimization process: {e}") | |
| # Function to merge shifted tasks into main roadmap | |
| def merge_shifted_tasks(): | |
| with st.spinner("Merging rescheduled tasks into main roadmap..."): | |
| try: | |
| data = st.session_state.data | |
| shifted_roadmap = json.loads(st.session_state.shifted_roadmap) | |
| def add_task(roadmap, task, date_task_to_be_added): | |
| subject_name = task["subject"] | |
| chapter_name = task["ChapterName"] | |
| topic_name = task["subtopic"] | |
| type_name = task["type"] | |
| # Check if the date exists | |
| for day in roadmap['schedule']: | |
| if day['date'] == date_task_to_be_added: | |
| # Find or create subject | |
| subject_exists = False | |
| for subject in day['subjects']: | |
| if subject['name'] == subject_name: | |
| subject_exists = True | |
| # Check if task already exists | |
| task_exists = False | |
| for existing_task in subject['tasks']: | |
| if (existing_task.get('ChapterName') == chapter_name and | |
| existing_task.get('type') == type_name and | |
| (existing_task.get('subtopic', '') == topic_name or | |
| existing_task.get('topic', '') == topic_name)): | |
| task_exists = True | |
| break | |
| if not task_exists: | |
| # Add task | |
| temp_task = { | |
| "ChapterName": chapter_name, | |
| "type": type_name, | |
| "subtopic": topic_name, | |
| "time": task["time"], | |
| "task_completed": False, | |
| "completion_timestamp": None | |
| } | |
| subject['tasks'].append(temp_task) | |
| break | |
| # If subject doesn't exist, create it | |
| if not subject_exists: | |
| new_subject = { | |
| "name": subject_name, | |
| "tasks": [{ | |
| "ChapterName": chapter_name, | |
| "type": type_name, | |
| "subtopic": topic_name, | |
| "time": task["time"], | |
| "task_completed": False, | |
| "completion_timestamp": None | |
| }] | |
| } | |
| day['subjects'].append(new_subject) | |
| break | |
| return roadmap | |
| # Process each task in the shifted roadmap | |
| for day in shifted_roadmap["roadmap"]: | |
| date = day['date'] | |
| tasks = day['tasks'] | |
| for task in tasks: | |
| data = add_task(data, task, date) | |
| st.session_state.updated_roadmap = data | |
| st.success("Tasks merged into roadmap successfully!") | |
| except Exception as e: | |
| st.error(f"Error merging tasks: {e}") | |
| # ---- HOME PAGE ---- | |
| if page == "Home": | |
| st.title("📚 JEE Roadmap Planner") | |
| st.markdown(""" | |
| ### Welcome to your JEE Study Roadmap Planner! | |
| This tool helps you manage your JEE preparation schedule by: | |
| 1. 📊 **Analyzing your study performance** | |
| 2. 🔄 **Redistributing incomplete tasks** | |
| 3. 📝 **Providing personalized feedback** | |
| Get started by loading your roadmap data and following the step-by-step process. | |
| """) | |
| # Settings section | |
| with st.expander("⚙️ Advanced Settings"): | |
| st.session_state.max_optimizer_iterations = st.slider( | |
| "Maximum Optimizer Iterations", | |
| min_value=1, | |
| max_value=5, | |
| value=st.session_state.max_optimizer_iterations, | |
| help="Limit how many times the optimizer will try to improve task distribution" | |
| ) | |
| st.info("Navigate using the sidebar to access different features of the app.") | |
| # Initial data loading | |
| if st.button("📂 Load Roadmap Data"): | |
| success = load_initial_data() | |
| if success: | |
| st.session_state.first_load = True | |
| # ---- ROADMAP MANAGER PAGE ---- | |
| elif page == "Roadmap Manager": | |
| st.title("🗓️ Roadmap Manager") | |
| if st.session_state.data is None: | |
| st.warning("Please load roadmap data first from the Home page.") | |
| else: | |
| st.markdown("### Roadmap Management Steps") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.subheader("Step 1: Process Tasks") | |
| if st.button("1️⃣ Mark Tasks as Incomplete"): | |
| process_task_completion_data() | |
| st.subheader("Step 2: Extract Tasks") | |
| if st.button("2️⃣ Extract Incomplete Tasks"): | |
| extract_incomplete_tasks() | |
| if st.session_state.incomplete_task_list: | |
| st.write(f"Found {len(st.session_state.incomplete_task_list)} incomplete tasks") | |
| with st.expander("View Incomplete Tasks"): | |
| st.json(st.session_state.incomplete_task_list) | |
| with col2: | |
| st.subheader("Step 3: Prepare for Rescheduling") | |
| if st.button("3️⃣ Extract Available Dates"): | |
| extract_available_dates() | |
| if st.session_state.available_dates: | |
| with st.expander("View Available Dates"): | |
| st.write(st.session_state.available_dates) | |
| st.subheader("Step 4: Reschedule Tasks") | |
| if st.button("4️⃣ Optimize Task Distribution"): | |
| if not st.session_state.incomplete_task_list or not st.session_state.available_dates: | |
| st.error("Please complete steps 2 and 3 first!") | |
| else: | |
| shift_incomplete_tasks() | |
| if st.session_state.shifted_roadmap: | |
| with st.expander("View Task Distribution Plan"): | |
| try: | |
| st.json(json.loads(st.session_state.shifted_roadmap)) | |
| except: | |
| st.text(st.session_state.shifted_roadmap) | |
| st.subheader("Step 5: Update Roadmap") | |
| if st.button("5️⃣ Merge Tasks into Main Roadmap"): | |
| if not st.session_state.shifted_roadmap: | |
| st.error("Please complete step 4 first!") | |
| else: | |
| merge_shifted_tasks() | |
| # Display original and updated roadmaps side by side | |
| if st.session_state.data_old and st.session_state.updated_roadmap: | |
| st.subheader("Roadmap Comparison") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("#### Original Roadmap") | |
| with st.expander("View Original Roadmap"): | |
| st.json(st.session_state.data_old) | |
| with col2: | |
| st.markdown("#### Updated Roadmap") | |
| with st.expander("View Updated Roadmap"): | |
| st.json(st.session_state.updated_roadmap) | |
| # ---- TASK ANALYSIS PAGE ---- | |
| elif page == "Task Analysis": | |
| st.title("📊 Task Analysis") | |
| choice = st.selectbox("Choose the roadmap to use for building report", ["Four Day Roadmap", "Full Roadmap"]) | |
| if choice == "Four Day Roadmap": | |
| if st.session_state.data is None: | |
| st.warning("Please load roadmap data first from the Home page.") | |
| st.session_state.report_data = st.session_state.data | |
| elif choice == "Full Roadmap": | |
| with open("synthesized_full_roadmap.json", "r") as f: | |
| st.session_state.report_data = json.load(f) | |
| st.markdown("### Performance Report") | |
| if st.button("🔍 Generate Performance Report"): | |
| generate_report(st.session_state.report_data) | |
| if st.session_state.final_report: | |
| st.markdown(st.session_state.final_report) | |
| else: | |
| st.info("Click the button above to generate your performance report.") | |
| # Add visualization options | |
| if st.session_state.data: | |
| st.subheader("Task Breakdown") | |
| # Simple task statistics | |
| if st.checkbox("Show Task Statistics"): | |
| task_count = 0 | |
| subject_counts = {} | |
| type_counts = {} | |
| for day in st.session_state.report_data["schedule"]: | |
| for subject in day["subjects"]: | |
| subject_name = subject["name"] | |
| if subject_name not in subject_counts: | |
| subject_counts[subject_name] = 0 | |
| for task in subject["tasks"]: | |
| subject_counts[subject_name] += 1 | |
| task_count += 1 | |
| # Count by task type | |
| task_type = task.get("type", "Unknown") | |
| if task_type not in type_counts: | |
| type_counts[task_type] = 0 | |
| type_counts[task_type] += 1 | |
| st.write(f"Total tasks: {task_count}") | |
| # Create charts for data visualization | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.subheader("Subject Distribution") | |
| st.bar_chart(subject_counts) | |
| with col2: | |
| st.subheader("Task Type Distribution") | |
| st.bar_chart(type_counts) | |
| # ---- ROADMAP CHATBOT PAGE ---- | |
| elif page == "Roadmap Chatbot": | |
| st.title("🤖 Roadmap Chatbot Assistant") | |
| user_query = st.text_input("Ask a question about your roadmap:", placeholder="e.g., What are my tasks on 14 Feb 2025?") | |
| if st.button("Ask") and user_query: | |
| with st.spinner("Thinking..."): | |
| try: | |
| response = answer_user_query(user_query) | |
| st.markdown(response) | |
| except Exception as e: | |
| st.error(f"Error: {e}") | |