Initial commits
This commit is contained in:
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
BIN
backend/app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
BIN
backend/app/api/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/app/api/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/endpoints.cpython-313.pyc
Normal file
BIN
backend/app/api/__pycache__/endpoints.cpython-313.pyc
Normal file
Binary file not shown.
65
backend/app/api/endpoints.py
Normal file
65
backend/app/api/endpoints.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# D:\GNPL-Private\TimeCraft\backend\app\api\endpoints.py
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from typing import List
|
||||
|
||||
from app.models.task import TaskCreateRequest, ScheduleResponse
|
||||
from app.services.llm_service import get_scheduled_tasks
|
||||
|
||||
# Initialize the API router
|
||||
router = APIRouter()
|
||||
|
||||
@router.post(
|
||||
"/schedule/tasks",
|
||||
response_model=ScheduleResponse, # Define the response model for automatic serialization/validation
|
||||
status_code=status.HTTP_200_OK, # Set the default successful status code
|
||||
summary="Create and Schedule Tasks using LLM",
|
||||
description="Receives a list of user-defined tasks, sends them to an LLM for intelligent scheduling, and returns a structured schedule."
|
||||
)
|
||||
async def create_and_schedule_tasks(tasks: List[TaskCreateRequest]):
|
||||
"""
|
||||
Handles the POST request to create and schedule tasks.
|
||||
|
||||
Args:
|
||||
tasks (List[TaskCreateRequest]): A list of task objects received from the frontend.
|
||||
|
||||
Returns:
|
||||
ScheduleResponse: A structured response containing the scheduled tasks.
|
||||
|
||||
Raises:
|
||||
HTTPException: If no tasks are provided, or if there's an error during LLM interaction
|
||||
or internal processing.
|
||||
"""
|
||||
# Validate if any tasks were provided in the request body
|
||||
if not tasks:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No tasks provided for scheduling. Please submit at least one task."
|
||||
)
|
||||
|
||||
try:
|
||||
# Call the LLM service to get the scheduled tasks
|
||||
scheduled_response = await get_scheduled_tasks(tasks)
|
||||
return scheduled_response
|
||||
except RuntimeError as e:
|
||||
# Catch errors specifically from LLM interaction issues (e.g., network, API issues)
|
||||
print(f"LLM interaction failed: {e}") # Log the error for debugging
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to schedule tasks due to an issue with the LLM service. Please try again later. Details: {e}"
|
||||
)
|
||||
except ValueError as e:
|
||||
# Catch errors related to invalid data or parsing issues from the LLM's response
|
||||
print(f"Data validation or parsing error: {e}") # Log the error
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, # 422 for semantically erroneous request syntax
|
||||
detail=f"The LLM returned an invalid schedule structure or there was an internal data processing error. Details: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
# Catch any other unexpected errors
|
||||
print(f"An unexpected error occurred in endpoint: {e}") # Log the error
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"An unexpected error occurred while processing your request. Please contact support. Details: {e}"
|
||||
)
|
||||
|
||||
0
backend/app/models/__init__.py
Normal file
0
backend/app/models/__init__.py
Normal file
BIN
backend/app/models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/app/models/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/task.cpython-313.pyc
Normal file
BIN
backend/app/models/__pycache__/task.cpython-313.pyc
Normal file
Binary file not shown.
69
backend/app/models/task.py
Normal file
69
backend/app/models/task.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# D:\GNPL-Private\TimeCraft\backend\app\models\task.py
|
||||
|
||||
from datetime import time
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Union
|
||||
from pydantic import BaseModel, Field, conint, conlist
|
||||
|
||||
# Type aliases for Pydantic constrained types for better readability
|
||||
PositiveInt = conint(gt=0)
|
||||
WeekdayList = conlist(str, min_length=0, max_length=7)
|
||||
|
||||
# Enum for primary recurrence periods
|
||||
class RecurrenceType(str, Enum):
|
||||
daily = "daily"
|
||||
weekly = "weekly"
|
||||
monthly = "monthly"
|
||||
|
||||
# Enum for task categories
|
||||
class TaskCategory(str, Enum):
|
||||
personal = "personal"
|
||||
work = "work"
|
||||
errands = "errands"
|
||||
extra = "extra"
|
||||
|
||||
# Enum for time units used in reminders or durations
|
||||
class TimeUnit(str, Enum):
|
||||
minutes = "Minutes"
|
||||
hours = "Hours"
|
||||
days = "Days"
|
||||
|
||||
# Pydantic model for time settings of a task
|
||||
class TimeSettings(BaseModel):
|
||||
start_time: Optional[time] = Field(None, description="Specific start time (HH:MM)")
|
||||
end_time: Optional[time] = Field(None, description="Specific end time (HH:MM)")
|
||||
duration: Optional[str] = Field(None, description="Duration of the task (e.g., '1 Hour', '30 Minutes'). Used if start_time and end_time are empty or to validate them.")
|
||||
|
||||
# Pydantic model for reminder settings
|
||||
class ReminderOn(BaseModel):
|
||||
is_active: bool = Field(False, description="Whether a reminder is set (true/false)")
|
||||
numeric_input: Optional[PositiveInt] = Field(None, description="Numeric value for reminder (e.g., 10 minutes before)")
|
||||
time_unit: Optional[TimeUnit] = Field(None, description="Unit for reminder time (Minutes, Hours, Days)")
|
||||
|
||||
# Main Pydantic model for the task creation request from the frontend
|
||||
class TaskCreateRequest(BaseModel):
|
||||
type: RecurrenceType = Field(..., description="Defines the primary recurrence period (daily, weekly, monthly)")
|
||||
task_name: str = Field(..., min_length=1, description="A descriptive name for the task")
|
||||
task_description: Optional[str] = Field(None, description="Detailed notes or description of the task")
|
||||
time_settings: TimeSettings = Field(..., description="Time settings for the task, including start, end, and duration")
|
||||
category: TaskCategory = Field(..., description="Categorizes the task (personal, work, errands, extra)")
|
||||
repeat_on_1: Optional[str] = Field(None, description="CRITICAL for complex recurrence patterns. Free-form text field for custom recurrence rules (e.g., 'Every second Monday')")
|
||||
repeat_on_2: Optional[WeekdayList] = Field(
|
||||
None,
|
||||
description="Checkbox selector for days of the week (S, M, T, W, T, F, S). Users can select multiple days."
|
||||
)
|
||||
reminder_on: ReminderOn = Field(..., description="Reminder settings for the task")
|
||||
|
||||
# Pydantic model for a single scheduled task returned by the LLM
|
||||
class ScheduledTask(BaseModel):
|
||||
task_name: str = Field(..., description="Name of the scheduled task")
|
||||
date: str = Field(..., description="Scheduled date in ISO-MM-DD format")
|
||||
start_time: Optional[str] = Field(None, description="Scheduled start time in HH:MM format")
|
||||
end_time: Optional[str] = Field(None, description="Scheduled end time in HH:MM format")
|
||||
category: TaskCategory = Field(..., description="Category of the scheduled task")
|
||||
original_task_id: Optional[str] = Field(None, description="Optional ID to link back to the original task request, useful for updates/tracking")
|
||||
# reminder_on: Optional[ReminderOn] = Field(None, description="Reminder settings for the scheduled task") # Uncomment if LLM output includes this
|
||||
|
||||
# Pydantic model for the overall schedule response from the LLM
|
||||
class ScheduleResponse(BaseModel):
|
||||
scheduled_tasks: List[ScheduledTask] = Field(..., description="List of intelligently assigned scheduled tasks")
|
||||
0
backend/app/services/__init__.py
Normal file
0
backend/app/services/__init__.py
Normal file
BIN
backend/app/services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/app/services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/services/__pycache__/llm_service.cpython-313.pyc
Normal file
BIN
backend/app/services/__pycache__/llm_service.cpython-313.pyc
Normal file
Binary file not shown.
153
backend/app/services/llm_service.py
Normal file
153
backend/app/services/llm_service.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# D:\GNPL-Private\TimeCraft\backend\app\services\llm_service.py
|
||||
|
||||
import os
|
||||
import json
|
||||
import httpx
|
||||
from typing import List
|
||||
from datetime import date
|
||||
from dotenv import load_dotenv
|
||||
from app.models.task import TaskCreateRequest, ScheduleResponse
|
||||
|
||||
load_dotenv()
|
||||
|
||||
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "OLLAMA")
|
||||
LLM_API_KEY = os.getenv("LLM_API_KEY")
|
||||
LLM_API_URL = os.getenv("LLM_API_URL")
|
||||
HF_MODEL_NAME = os.getenv("HF_MODEL_NAME", "google/gemma-2b-it")
|
||||
OLLAMA_MODEL_NAME = os.getenv("OLLAMA_MODEL_NAME", "deepseek-r1:1.5b")
|
||||
OLLAMA_API_URL = os.getenv("OLLAMA_API_URL", "http://localhost:11434/api/generate")
|
||||
|
||||
tokenizer = None
|
||||
model = None
|
||||
|
||||
if LLM_PROVIDER == "HUGGINGFACE":
|
||||
try:
|
||||
from transformers import AutoTokenizer, AutoModelForCausalLM
|
||||
import torch
|
||||
tokenizer = AutoTokenizer.from_pretrained(HF_MODEL_NAME)
|
||||
model = AutoModelForCausalLM.from_pretrained(HF_MODEL_NAME, torch_dtype=torch.float16)
|
||||
if torch.cuda.is_available():
|
||||
model.to('cuda')
|
||||
print(f"[LLM] Hugging Face model '{HF_MODEL_NAME}' loaded.")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to load Hugging Face model: {e}")
|
||||
raise RuntimeError("Hugging Face model loading failed.")
|
||||
|
||||
def infer_priority(task: TaskCreateRequest) -> str:
|
||||
desc = task.task_description.lower()
|
||||
if any(k in desc for k in ["non-negotiable", "critical", "mandatory", "must", "cannot be moved", "very high priority"]):
|
||||
return "high"
|
||||
if any(k in desc for k in ["optional", "nice to have", "if possible", "fun"]):
|
||||
return "low"
|
||||
return "medium"
|
||||
|
||||
async def get_scheduled_tasks(tasks: List[TaskCreateRequest]) -> ScheduleResponse:
|
||||
today_str = date.today().isoformat()
|
||||
prompt_tasks_data = [
|
||||
{
|
||||
**json.loads(task.model_dump_json()),
|
||||
"original_task_id": f"task{index + 1}",
|
||||
"priority": infer_priority(task)
|
||||
}
|
||||
for index, task in enumerate(tasks)
|
||||
]
|
||||
|
||||
system_message = (
|
||||
f"You are an AI-powered schedule and task planning assistant. Your job is to create a conflict-free, "
|
||||
f"intelligent 30-day schedule from today ({today_str}).\n\n"
|
||||
"Parse task details precisely: type, time_settings (start_time, end_time, duration), category, and priority.\n"
|
||||
"Avoid time conflicts. If two fixed-time tasks clash, prioritize the 'high' priority one. Try to shift "
|
||||
"'medium' priority tasks to nearby times. Skip low priority if no space.\n\n"
|
||||
"Respect fixed times first. For flexible tasks, use hints.\n"
|
||||
"Return strictly valid JSON:\n"
|
||||
"{\n"
|
||||
" \"scheduled_tasks\": [\n"
|
||||
" {\n"
|
||||
" \"task_name\": \"string\",\n"
|
||||
" \"date\": \"YYYY-MM-DD\",\n"
|
||||
" \"start_time\": \"HH:MM\",\n"
|
||||
" \"end_time\": \"HH:MM\",\n"
|
||||
" \"category\": \"personal | work | errands | extra\",\n"
|
||||
" \"original_task_id\": \"string\"\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
"}"
|
||||
)
|
||||
|
||||
user_message = f"Here are the tasks to schedule:\n{json.dumps(prompt_tasks_data, indent=2)}"
|
||||
full_prompt = f"{system_message}\n\n{user_message}"
|
||||
llm_output_content = ""
|
||||
|
||||
try:
|
||||
if LLM_PROVIDER == "GENERIC_API":
|
||||
if not LLM_API_URL or not LLM_API_KEY:
|
||||
raise ValueError("Missing LLM_API_URL or LLM_API_KEY in .env")
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {LLM_API_KEY}"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": "your-preferred-llm-model-name",
|
||||
"messages": [
|
||||
{"role": "system", "content": system_message},
|
||||
{"role": "user", "content": user_message}
|
||||
],
|
||||
"response_format": {"type": "json_object"}
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(600.0, connect=60.0)) as client:
|
||||
response = await client.post(LLM_API_URL, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
llm_output_content = response.json().get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
|
||||
elif LLM_PROVIDER == "HUGGINGFACE":
|
||||
import torch
|
||||
input_ids = tokenizer(full_prompt, return_tensors="pt").input_ids
|
||||
if torch.cuda.is_available():
|
||||
input_ids = input_ids.to("cuda")
|
||||
outputs = model.generate(input_ids, max_new_tokens=1024, pad_token_id=tokenizer.eos_token_id)
|
||||
llm_output_content = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
||||
|
||||
elif LLM_PROVIDER == "OLLAMA":
|
||||
payload = {
|
||||
"model": OLLAMA_MODEL_NAME,
|
||||
"prompt": full_prompt,
|
||||
"stream": False,
|
||||
"format": "json"
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(600.0, connect=60.0)) as client:
|
||||
print("[LLM] Sending prompt to Ollama...")
|
||||
response = await client.post(OLLAMA_API_URL, json=payload)
|
||||
response.raise_for_status()
|
||||
llm_output_content = response.json().get("response", "")
|
||||
|
||||
print("[LLM] Received response (first 500 chars):")
|
||||
print(llm_output_content[:500])
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported LLM_PROVIDER: {LLM_PROVIDER}")
|
||||
|
||||
json_start = llm_output_content.find('{')
|
||||
json_end = llm_output_content.rfind('}')
|
||||
if json_start == -1 or json_end == -1:
|
||||
raise ValueError("No valid JSON object found in LLM output.")
|
||||
json_string = llm_output_content[json_start:json_end + 1]
|
||||
parsed_data = json.loads(json_string)
|
||||
return ScheduleResponse(**parsed_data)
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
print(f"[ERROR] Ollama returned HTTP {e.response.status_code}: {e.response.text}")
|
||||
raise RuntimeError(f"LLM API returned error {e.response.status_code}: {e.response.text}")
|
||||
except httpx.RequestError as e:
|
||||
print(f"[ERROR] Request to LLM service failed: {e}")
|
||||
raise RuntimeError(f"Failed to connect to LLM service. Details: {e}")
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[ERROR] JSON decoding failed: {e}")
|
||||
print(f"[DEBUG] Raw output: {llm_output_content}")
|
||||
raise ValueError("LLM did not return valid JSON.")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Unexpected exception: {e}")
|
||||
raise RuntimeError(f"Failed to schedule tasks due to an issue with the LLM service. Details: {e}")
|
||||
Reference in New Issue
Block a user