fixed comments

This commit is contained in:
c-d-p
2025-04-26 12:43:19 +02:00
parent 10e5a3c489
commit 22a4fc50a5
49 changed files with 165 additions and 148 deletions

View File

@@ -1,7 +1,6 @@
# core/celery_app.py
from celery import Celery
from core.config import settings # Import your settings
from core.config import settings
celery_app = Celery(
"worker",
broker=settings.REDIS_URL,
@@ -9,9 +8,5 @@ celery_app = Celery(
include=[
"modules.auth.tasks",
"modules.admin.tasks",
], # Add paths to modules containing tasks
# Add other modules with tasks here, e.g., "modules.some_other_module.tasks"
)
# Optional: Update Celery configuration directly if needed
# celery_app.conf.update(task_track_started=True)
],
)

View File

@@ -20,7 +20,7 @@ class Settings(BaseSettings):
JWT_SECRET_KEY: str
# Other settings
GOOGLE_API_KEY: str = "" # Example with a default
GOOGLE_API_KEY: str
class Config:
# Tell pydantic-settings to load variables from a .env file

View File

@@ -1,4 +1,3 @@
# core/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session, declarative_base
from typing import Generator
@@ -13,7 +12,7 @@ _SessionLocal = None
def get_engine():
global _engine
if _engine is None:
if (_engine is None):
if not settings.DB_URL:
raise ValueError("DB_URL is not set in Settings.")
print(f"Connecting to database at {settings.DB_URL}")

View File

@@ -1,4 +1,3 @@
# main.py
from contextlib import _AsyncGeneratorContextManager, asynccontextmanager
from typing import Any, Callable
from fastapi import FastAPI
@@ -7,14 +6,9 @@ from core.database import get_engine, Base
from modules import router
import logging
# import all models to ensure they are registered before create_all
logging.getLogger("passlib").setLevel(logging.ERROR) # fix bc package logging is broken
# Create DB tables (remove in production; use migrations instead)
def lifespan_factory() -> Callable[[FastAPI], _AsyncGeneratorContextManager[Any]]:
@asynccontextmanager
@@ -29,16 +23,11 @@ def lifespan_factory() -> Callable[[FastAPI], _AsyncGeneratorContextManager[Any]
lifespan = lifespan_factory()
app = FastAPI(lifespan=lifespan)
# Include module router
app.include_router(router)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:8081", # Keep for web testing if needed
"http://192.168.1.9:8081", # Add your mobile device/emulator origin (adjust port if needed)
"http://192.168.255.221:8081",
"https://maia.depaoli.id.au",
],
allow_credentials=True,
@@ -47,7 +36,6 @@ app.add_middleware(
)
# Health endpoint
@app.get("/api/health")
def health():
return {"status": "ok"}

View File

@@ -1,7 +1,7 @@
# modules/admin/api.py
from typing import Annotated
from fastapi import APIRouter, Depends # Import Body
from pydantic import BaseModel # Import BaseModel
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session
from core.database import get_db
from modules.auth.dependencies import admin_only
@@ -10,7 +10,6 @@ from .tasks import cleardb
router = APIRouter(prefix="/admin", tags=["admin"], dependencies=[Depends(admin_only)])
# Define a Pydantic model for the request body
class ClearDbRequest(BaseModel):
hard: bool
@@ -20,7 +19,6 @@ def read_admin():
return {"message": "Admin route"}
# Change to POST and use the request body model
@router.post("/cleardb")
def clear_db(payload: ClearDbRequest, db: Annotated[Session, Depends(get_db)]):
"""
@@ -28,6 +26,6 @@ def clear_db(payload: ClearDbRequest, db: Annotated[Session, Depends(get_db)]):
'hard'=True: Drop and recreate all tables.
'hard'=False: Delete data from tables except users.
"""
hard = payload.hard # Get 'hard' from the payload
hard = payload.hard
cleardb.delay(hard)
return {"message": "Clearing database in the background", "hard": hard}

View File

@@ -1,4 +1 @@
# modules/admin/services.py
## temp

View File

@@ -18,16 +18,13 @@ def cleardb(hard: bool):
db = SessionLocal()
if hard:
# Drop and recreate all tables
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
db.commit()
return {"message": "Database reset (HARD)"}
else:
# Delete data from tables except users
tables = Base.metadata.tables.keys()
for table_name in tables:
# delete all tables that isn't the users table
if table_name != "users":
table = Base.metadata.tables[table_name]
print(f"Deleting table: {table_name}")

View File

@@ -1,4 +1,3 @@
# modules/auth/api.py
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from jose import JWTError
@@ -25,7 +24,7 @@ from sqlalchemy.orm import Session
from typing import Annotated
from core.database import get_db
from datetime import timedelta
from core.config import settings # Assuming settings is defined in core.config
from core.config import settings
from core.exceptions import unauthorized_exception
router = APIRouter(prefix="/auth", tags=["auth"])

View File

@@ -1,4 +1,3 @@
# modules/auth/dependencies.py
from fastapi import Depends
from modules.auth.security import get_current_user
from modules.auth.schemas import UserRole

View File

@@ -1,4 +1,3 @@
# modules/auth/schemas.py
from enum import Enum as PyEnum
from pydantic import BaseModel

View File

@@ -29,7 +29,7 @@ password_hasher = PasswordHasher()
def hash_password(password: str) -> str:
"""Hash a password with Argon2 (and optional pepper)."""
peppered_password = password + settings.PEPPER # Prepend/append pepper
peppered_password = password + settings.PEPPER
return password_hasher.hash(peppered_password)
@@ -47,10 +47,8 @@ def authenticate_user(username: str, password: str, db: Session) -> User | None:
Authenticate a user by checking username/password against the database.
Returns User object if valid, None otherwise.
"""
# Get user from database
user = db.query(User).filter(User.username == username).first()
# If user not found or password doesn't match
if not user or not verify_password(password, user.hashed_password):
return None
@@ -65,7 +63,6 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
# expire = datetime.now(timezone.utc) + timedelta(seconds=5)
to_encode.update({"exp": expire, "token_type": TokenType.ACCESS})
return jwt.encode(
to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM
@@ -89,22 +86,6 @@ def create_refresh_token(data: dict, expires_delta: timedelta | None = None):
def verify_token(
token: str, expected_token_type: TokenType, db: Session
) -> TokenData | None:
"""Verify a JWT token and return TokenData if valid.
Parameters
----------
token: str
The JWT token to be verified.
expected_token_type: TokenType
The expected type of token (access or refresh)
db: Session
Database session to fetch user data.
Returns
-------
TokenData | None
TokenData instance if the token is valid, None otherwise.
"""
is_blacklisted = (
db.query(TokenBlacklist).filter(TokenBlacklist.token == token).first()
is not None
@@ -137,7 +118,6 @@ def get_current_user(
headers={"WWW-Authenticate": "Bearer"},
)
# Check if the token is blacklisted
is_blacklisted = (
db.query(TokenBlacklist).filter(TokenBlacklist.token == token).first()
is not None
@@ -178,7 +158,6 @@ def blacklist_tokens(access_token: str, refresh_token: str, db: Session) -> None
)
expires_at = datetime.fromtimestamp(payload.get("exp"))
# Add the token to the blacklist
blacklisted_token = TokenBlacklist(token=token, expires_at=expires_at)
db.add(blacklisted_token)
@@ -191,7 +170,6 @@ def blacklist_token(token: str, db: Session) -> None:
)
expires_at = datetime.fromtimestamp(payload.get("exp"))
# Add the token to the blacklist
blacklisted_token = TokenBlacklist(token=token, expires_at=expires_at)
db.add(blacklisted_token)
db.commit()

View File

@@ -1,4 +1,3 @@
# modules/auth/services.py
from sqlalchemy.orm import Session
from modules.auth.models import User
from modules.auth.schemas import UserResponse

View File

@@ -1,4 +1,3 @@
# modules/calendar/api.py
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from datetime import datetime

View File

@@ -1,7 +1,6 @@
# modules/calendar/schemas.py
from datetime import datetime
from pydantic import BaseModel, field_validator # Add field_validator
from typing import List, Optional # Add List and Optional
from pydantic import BaseModel, field_validator
from typing import List, Optional
# Base schema for common fields, including tags

View File

@@ -1,4 +1,3 @@
# modules/nlp/api.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
@@ -8,7 +7,6 @@ from core.database import get_db
from modules.auth.dependencies import get_current_user
from modules.auth.models import User
# Import the new service functions and Enum
from modules.nlp.service import (
process_request,
ask_ai,
@@ -17,7 +15,6 @@ from modules.nlp.service import (
MessageSender,
)
# Import the response schema and the new ChatMessage model for response type hinting
from modules.nlp.schemas import ProcessCommandRequest, ProcessCommandResponse
from modules.calendar.service import (
create_calendar_event,
@@ -28,7 +25,6 @@ from modules.calendar.service import (
from modules.calendar.models import CalendarEvent
from modules.calendar.schemas import CalendarEventCreate, CalendarEventUpdate
# Import TODO services, schemas, and model
from modules.todo import service as todo_service
from modules.todo.models import Todo
from modules.todo.schemas import TodoCreate, TodoUpdate
@@ -38,24 +34,22 @@ from datetime import datetime
class ChatMessageResponse(BaseModel):
id: int
sender: MessageSender # Use the enum directly
sender: MessageSender
text: str
timestamp: datetime
class Config:
from_attributes = True # Allow Pydantic to work with ORM models
from_attributes = True
router = APIRouter(prefix="/nlp", tags=["nlp"])
# Helper to format calendar events (expects list of CalendarEvent models)
def format_calendar_events(events: List[CalendarEvent]) -> List[str]:
if not events:
return ["You have no events matching that criteria."]
formatted = ["Here are the events:"]
for event in events:
# Access attributes directly from the model instance
start_str = (
event.start.strftime("%Y-%m-%d %H:%M") if event.start else "No start time"
)
@@ -65,7 +59,6 @@ def format_calendar_events(events: List[CalendarEvent]) -> List[str]:
return formatted
# Helper to format TODO items (expects list of Todo models)
def format_todos(todos: List[Todo]) -> List[str]:
if not todos:
return ["Your TODO list is empty."]
@@ -80,7 +73,6 @@ def format_todos(todos: List[Todo]) -> List[str]:
return formatted
# Update the response model for the endpoint
@router.post("/process-command", response_model=ProcessCommandResponse)
def process_command(
request_data: ProcessCommandRequest,
@@ -92,34 +84,25 @@ def process_command(
"""
user_input = request_data.user_input
# --- Save User Message ---
save_chat_message(
db, user_id=current_user.id, sender=MessageSender.USER, text=user_input
)
# ------------------------
command_data = process_request(user_input)
intent = command_data["intent"]
params = command_data["params"]
response_text = command_data["response_text"]
responses = [response_text] # Start with the initial response
responses = [response_text]
# --- Save Initial AI Response ---
# Save the first response generated by process_request
save_chat_message(
db, user_id=current_user.id, sender=MessageSender.AI, text=response_text
)
# -----------------------------
if intent == "error":
# Don't raise HTTPException here if we want to save the error message
# Instead, return the error response directly
# save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=response_text) # Already saved above
return ProcessCommandResponse(responses=responses)
if intent == "clarification_needed" or intent == "unknown":
# save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=response_text) # Already saved above
return ProcessCommandResponse(responses=responses)
try:
@@ -127,11 +110,9 @@ def process_command(
case "ask_ai":
ai_answer = ask_ai(**params)
responses.append(ai_answer)
# --- Save Additional AI Response ---
save_chat_message(
db, user_id=current_user.id, sender=MessageSender.AI, text=ai_answer
)
# ---------------------------------
return ProcessCommandResponse(responses=responses)
case "get_calendar_events":
@@ -140,12 +121,10 @@ def process_command(
)
formatted_responses = format_calendar_events(events)
responses.extend(formatted_responses)
# --- Save Additional AI Responses ---
for resp in formatted_responses:
save_chat_message(
db, user_id=current_user.id, sender=MessageSender.AI, text=resp
)
# ----------------------------------
return ProcessCommandResponse(responses=responses)
case "add_calendar_event":
@@ -159,20 +138,17 @@ def process_command(
title = created_event.title or "Untitled Event"
add_response = f"Added: {title} starting at {start_str}."
responses.append(add_response)
# --- Save Additional AI Response ---
save_chat_message(
db,
user_id=current_user.id,
sender=MessageSender.AI,
text=add_response,
)
# ---------------------------------
return ProcessCommandResponse(responses=responses)
case "update_calendar_event":
event_id = params.pop("event_id", None)
if event_id is None:
# Save the error message before raising
error_msg = "Event ID is required for update."
save_chat_message(
db,
@@ -188,20 +164,17 @@ def process_command(
title = updated_event.title or "Untitled Event"
update_response = f"Updated event ID {updated_event.id}: {title}."
responses.append(update_response)
# --- Save Additional AI Response ---
save_chat_message(
db,
user_id=current_user.id,
sender=MessageSender.AI,
text=update_response,
)
# ---------------------------------
return ProcessCommandResponse(responses=responses)
case "delete_calendar_event":
event_id = params.get("event_id")
if event_id is None:
# Save the error message before raising
error_msg = "Event ID is required for delete."
save_chat_message(
db,
@@ -213,29 +186,24 @@ def process_command(
delete_calendar_event(db, current_user.id, event_id)
delete_response = f"Deleted event ID {event_id}."
responses.append(delete_response)
# --- Save Additional AI Response ---
save_chat_message(
db,
user_id=current_user.id,
sender=MessageSender.AI,
text=delete_response,
)
# ---------------------------------
return ProcessCommandResponse(responses=responses)
# --- Add TODO Cases ---
case "get_todos":
todos: List[Todo] = todo_service.get_todos(
db, user=current_user, **params
)
formatted_responses = format_todos(todos)
responses.extend(formatted_responses)
# --- Save Additional AI Responses ---
for resp in formatted_responses:
save_chat_message(
db, user_id=current_user.id, sender=MessageSender.AI, text=resp
)
# ----------------------------------
return ProcessCommandResponse(responses=responses)
case "add_todo":
@@ -247,14 +215,12 @@ def process_command(
f"Added TODO: '{created_todo.task}' (ID: {created_todo.id})."
)
responses.append(add_response)
# --- Save Additional AI Response ---
save_chat_message(
db,
user_id=current_user.id,
sender=MessageSender.AI,
text=add_response,
)
# ---------------------------------
return ProcessCommandResponse(responses=responses)
case "update_todo":
@@ -279,14 +245,12 @@ def process_command(
status = "complete" if params["complete"] else "incomplete"
update_response += f" Marked as {status}."
responses.append(update_response)
# --- Save Additional AI Response ---
save_chat_message(
db,
user_id=current_user.id,
sender=MessageSender.AI,
text=update_response,
)
# ---------------------------------
return ProcessCommandResponse(responses=responses)
case "delete_todo":
@@ -307,26 +271,21 @@ def process_command(
f"Deleted TODO ID {deleted_todo.id}: '{deleted_todo.task}'."
)
responses.append(delete_response)
# --- Save Additional AI Response ---
save_chat_message(
db,
user_id=current_user.id,
sender=MessageSender.AI,
text=delete_response,
)
# ---------------------------------
return ProcessCommandResponse(responses=responses)
# --- End TODO Cases ---
case _:
print(
f"Warning: Unhandled intent '{intent}' reached api.py match statement."
)
# The initial response_text was already saved
return ProcessCommandResponse(responses=responses)
except HTTPException as http_exc:
# Don't save again if already saved before raising
if http_exc.status_code != 400 or ("event_id" not in http_exc.detail.lower()):
save_chat_message(
db,
@@ -340,11 +299,9 @@ def process_command(
error_response = (
"Sorry, I encountered an error while trying to perform that action."
)
# --- Save Final Error AI Response ---
save_chat_message(
db, user_id=current_user.id, sender=MessageSender.AI, text=error_response
)
# ----------------------------------
return ProcessCommandResponse(responses=[error_response])
@@ -355,6 +312,3 @@ def read_chat_history(
"""Retrieves the last 50 chat messages for the current user."""
history = get_chat_history(db, user_id=current_user.id, limit=50)
return history
# -------------------------------------

View File

@@ -1,4 +1,3 @@
# /home/cdp/code/MAIA/backend/modules/nlp/models.py
from sqlalchemy import Column, Integer, Text, DateTime, ForeignKey, Enum as SQLEnum
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func

View File

@@ -1,4 +1,3 @@
# modules/nlp/schemas.py
from pydantic import BaseModel
from typing import List
@@ -9,5 +8,4 @@ class ProcessCommandRequest(BaseModel):
class ProcessCommandResponse(BaseModel):
responses: List[str]
# Optional: Keep details if needed for specific frontend logic beyond display
# details: dict | None = None

View File

@@ -1,5 +1,3 @@
# modules/nlp/service.py
from sqlalchemy.orm import Session
from sqlalchemy import desc # Import desc for ordering
from google import genai

View File

@@ -1,2 +1 @@
# backend/modules/todo/__init__.py
# This file makes the 'todo' directory a Python package.

View File

@@ -1,17 +1,16 @@
# backend/modules/todo/api.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from . import service, schemas
from core.database import get_db
from modules.auth.dependencies import get_current_user # Corrected import
from modules.auth.models import User # Assuming User model is in auth.models
from modules.auth.dependencies import get_current_user
from modules.auth.models import User
router = APIRouter(
prefix="/todos",
tags=["todos"],
dependencies=[Depends(get_current_user)], # Corrected dependency
dependencies=[Depends(get_current_user)],
responses={404: {"description": "Not found"}},
)
@@ -20,7 +19,7 @@ router = APIRouter(
def create_todo_endpoint(
todo: schemas.TodoCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), # Corrected dependency
current_user: User = Depends(get_current_user),
):
return service.create_todo(db=db, todo=todo, user=current_user)
@@ -30,7 +29,7 @@ def read_todos_endpoint(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), # Corrected dependency
current_user: User = Depends(get_current_user),
):
todos = service.get_todos(db=db, user=current_user, skip=skip, limit=limit)
return todos
@@ -40,7 +39,7 @@ def read_todos_endpoint(
def read_todo_endpoint(
todo_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), # Corrected dependency
current_user: User = Depends(get_current_user),
):
db_todo = service.get_todo(db=db, todo_id=todo_id, user=current_user)
if db_todo is None:
@@ -53,7 +52,7 @@ def update_todo_endpoint(
todo_id: int,
todo_update: schemas.TodoUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), # Corrected dependency
current_user: User = Depends(get_current_user),
):
return service.update_todo(
db=db, todo_id=todo_id, todo_update=todo_update, user=current_user
@@ -64,6 +63,6 @@ def update_todo_endpoint(
def delete_todo_endpoint(
todo_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), # Corrected dependency
current_user: User = Depends(get_current_user),
):
return service.delete_todo(db=db, todo_id=todo_id, user=current_user)

View File

@@ -16,4 +16,4 @@ class Todo(Base):
owner = relationship(
"User"
) # Add relationship if needed, assuming User model exists in auth.models
)

View File

@@ -1,4 +1,3 @@
# modules/user/api.py
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session