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 # core/celery_app.py
from celery import Celery from celery import Celery
from core.config import settings # Import your settings from core.config import settings
celery_app = Celery( celery_app = Celery(
"worker", "worker",
broker=settings.REDIS_URL, broker=settings.REDIS_URL,
@@ -9,9 +8,5 @@ celery_app = Celery(
include=[ include=[
"modules.auth.tasks", "modules.auth.tasks",
"modules.admin.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 JWT_SECRET_KEY: str
# Other settings # Other settings
GOOGLE_API_KEY: str = "" # Example with a default GOOGLE_API_KEY: str
class Config: class Config:
# Tell pydantic-settings to load variables from a .env file # 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 import create_engine
from sqlalchemy.orm import sessionmaker, Session, declarative_base from sqlalchemy.orm import sessionmaker, Session, declarative_base
from typing import Generator from typing import Generator
@@ -13,7 +12,7 @@ _SessionLocal = None
def get_engine(): def get_engine():
global _engine global _engine
if _engine is None: if (_engine is None):
if not settings.DB_URL: if not settings.DB_URL:
raise ValueError("DB_URL is not set in Settings.") raise ValueError("DB_URL is not set in Settings.")
print(f"Connecting to database at {settings.DB_URL}") print(f"Connecting to database at {settings.DB_URL}")

View File

@@ -1,4 +1,3 @@
# main.py
from contextlib import _AsyncGeneratorContextManager, asynccontextmanager from contextlib import _AsyncGeneratorContextManager, asynccontextmanager
from typing import Any, Callable from typing import Any, Callable
from fastapi import FastAPI from fastapi import FastAPI
@@ -7,14 +6,9 @@ from core.database import get_engine, Base
from modules import router from modules import router
import logging 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 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]]: def lifespan_factory() -> Callable[[FastAPI], _AsyncGeneratorContextManager[Any]]:
@asynccontextmanager @asynccontextmanager
@@ -29,16 +23,11 @@ def lifespan_factory() -> Callable[[FastAPI], _AsyncGeneratorContextManager[Any]
lifespan = lifespan_factory() lifespan = lifespan_factory()
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
# Include module router
app.include_router(router) app.include_router(router)
# CORS
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=[ 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", "https://maia.depaoli.id.au",
], ],
allow_credentials=True, allow_credentials=True,
@@ -47,7 +36,6 @@ app.add_middleware(
) )
# Health endpoint
@app.get("/api/health") @app.get("/api/health")
def health(): def health():
return {"status": "ok"} return {"status": "ok"}

View File

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

View File

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

View File

@@ -18,16 +18,13 @@ def cleardb(hard: bool):
db = SessionLocal() db = SessionLocal()
if hard: if hard:
# Drop and recreate all tables
Base.metadata.drop_all(bind=engine) Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
db.commit() db.commit()
return {"message": "Database reset (HARD)"} return {"message": "Database reset (HARD)"}
else: else:
# Delete data from tables except users
tables = Base.metadata.tables.keys() tables = Base.metadata.tables.keys()
for table_name in tables: for table_name in tables:
# delete all tables that isn't the users table
if table_name != "users": if table_name != "users":
table = Base.metadata.tables[table_name] table = Base.metadata.tables[table_name]
print(f"Deleting table: {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 import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from jose import JWTError from jose import JWTError
@@ -25,7 +24,7 @@ from sqlalchemy.orm import Session
from typing import Annotated from typing import Annotated
from core.database import get_db from core.database import get_db
from datetime import timedelta 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 from core.exceptions import unauthorized_exception
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ password_hasher = PasswordHasher()
def hash_password(password: str) -> str: def hash_password(password: str) -> str:
"""Hash a password with Argon2 (and optional pepper).""" """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) 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. Authenticate a user by checking username/password against the database.
Returns User object if valid, None otherwise. Returns User object if valid, None otherwise.
""" """
# Get user from database
user = db.query(User).filter(User.username == username).first() 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): if not user or not verify_password(password, user.hashed_password):
return None return None
@@ -65,7 +63,6 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
expire = datetime.now(timezone.utc) + timedelta( expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
) )
# expire = datetime.now(timezone.utc) + timedelta(seconds=5)
to_encode.update({"exp": expire, "token_type": TokenType.ACCESS}) to_encode.update({"exp": expire, "token_type": TokenType.ACCESS})
return jwt.encode( return jwt.encode(
to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM 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( def verify_token(
token: str, expected_token_type: TokenType, db: Session token: str, expected_token_type: TokenType, db: Session
) -> TokenData | None: ) -> 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 = ( is_blacklisted = (
db.query(TokenBlacklist).filter(TokenBlacklist.token == token).first() db.query(TokenBlacklist).filter(TokenBlacklist.token == token).first()
is not None is not None
@@ -137,7 +118,6 @@ def get_current_user(
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
# Check if the token is blacklisted
is_blacklisted = ( is_blacklisted = (
db.query(TokenBlacklist).filter(TokenBlacklist.token == token).first() db.query(TokenBlacklist).filter(TokenBlacklist.token == token).first()
is not None 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")) expires_at = datetime.fromtimestamp(payload.get("exp"))
# Add the token to the blacklist
blacklisted_token = TokenBlacklist(token=token, expires_at=expires_at) blacklisted_token = TokenBlacklist(token=token, expires_at=expires_at)
db.add(blacklisted_token) db.add(blacklisted_token)
@@ -191,7 +170,6 @@ def blacklist_token(token: str, db: Session) -> None:
) )
expires_at = datetime.fromtimestamp(payload.get("exp")) expires_at = datetime.fromtimestamp(payload.get("exp"))
# Add the token to the blacklist
blacklisted_token = TokenBlacklist(token=token, expires_at=expires_at) blacklisted_token = TokenBlacklist(token=token, expires_at=expires_at)
db.add(blacklisted_token) db.add(blacklisted_token)
db.commit() db.commit()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,4 +16,4 @@ class Todo(Base):
owner = relationship( owner = relationship(
"User" "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 typing import Annotated
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session

View File

@@ -20,7 +20,8 @@
"foregroundImage": "./assets/adaptive-icon.png", "foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"softwareKeyboardLayoutMode": "resize" "softwareKeyboardLayoutMode": "resize",
"package": "com.seedeep.maia"
}, },
"web": { "web": {
"favicon": "./assets/favicon.png" "favicon": "./assets/favicon.png"
@@ -28,6 +29,12 @@
"plugins": [ "plugins": [
"expo-secure-store", "expo-secure-store",
"expo-font" "expo-font"
] ],
"extra": {
"eas": {
"projectId": "4d7d70ce-a4d8-4307-8827-8ef713b95b78"
}
},
"owner": "cdp202"
} }
} }

View File

@@ -0,0 +1,21 @@
{
"cli": {
"version": ">= 16.3.2",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}

View File

@@ -18,6 +18,7 @@
"axios": "^1.8.4", "axios": "^1.8.4",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"expo": "^52.0.46", "expo": "^52.0.46",
"expo-dev-client": "~5.0.20",
"expo-font": "~13.0.4", "expo-font": "~13.0.4",
"expo-secure-store": "~14.0.1", "expo-secure-store": "~14.0.1",
"expo-splash-screen": "~0.29.24", "expo-splash-screen": "~0.29.24",
@@ -3746,6 +3747,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/anser": { "node_modules/anser": {
"version": "1.4.10", "version": "1.4.10",
"resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
@@ -5307,6 +5323,54 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo-dev-client": {
"version": "5.0.20",
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-5.0.20.tgz",
"integrity": "sha512-bLNkHdU7V3I4UefgJbJnIDUBUL0LxIal/xYEx9BbgDd3B7wgQKY//+BpPIxBOKCQ22lkyiHY8y9tLhO903sAgg==",
"dependencies": {
"expo-dev-launcher": "5.0.35",
"expo-dev-menu": "6.0.25",
"expo-dev-menu-interface": "1.9.3",
"expo-manifests": "~0.15.8",
"expo-updates-interface": "~1.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-launcher": {
"version": "5.0.35",
"resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-5.0.35.tgz",
"integrity": "sha512-hEQr0ZREnUMxZ6wtQgfK1lzYnbb0zar3HqYZhmANzXmE6UEPbQ4GByLzhpfz/d+xxdBVQZsrHdtiV28KPG2sog==",
"dependencies": {
"ajv": "8.11.0",
"expo-dev-menu": "6.0.25",
"expo-manifests": "~0.15.8",
"resolve-from": "^5.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-menu": {
"version": "6.0.25",
"resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-6.0.25.tgz",
"integrity": "sha512-K2m4z/I+CPWbMtHlDzU68lHaQs52De0v5gbsjAmA5ig8FrYh4MKZvPxSVANaiKENzgmtglu8qaFh7ua9Gt2TfA==",
"dependencies": {
"expo-dev-menu-interface": "1.9.3"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-menu-interface": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.9.3.tgz",
"integrity": "sha512-KY/dWTBE1l47i9V366JN5rC6YIdOc9hz8yAmZzkl5DrPia5l3M2WIjtnpHC9zUkNjiSiG2urYoOAq4H/uLdmyg==",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-file-system": { "node_modules/expo-file-system": {
"version": "18.0.12", "version": "18.0.12",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.12.tgz", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.12.tgz",
@@ -5331,6 +5395,11 @@
"react": "*" "react": "*"
} }
}, },
"node_modules/expo-json-utils": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.14.0.tgz",
"integrity": "sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw=="
},
"node_modules/expo-keep-awake": { "node_modules/expo-keep-awake": {
"version": "14.0.3", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz",
@@ -5340,6 +5409,18 @@
"react": "*" "react": "*"
} }
}, },
"node_modules/expo-manifests": {
"version": "0.15.8",
"resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.15.8.tgz",
"integrity": "sha512-VuIyaMfRfLZeETNsRohqhy1l7iZ7I+HKMPfZXVL2Yn17TT0WkOhZoq1DzYwPbOHPgp1Uk6phNa86EyaHrD2DLw==",
"dependencies": {
"@expo/config": "~10.0.11",
"expo-json-utils": "~0.14.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-modules-autolinking": { "node_modules/expo-modules-autolinking": {
"version": "2.0.8", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.0.8.tgz", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.0.8.tgz",
@@ -5427,6 +5508,14 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo-updates-interface": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.0.0.tgz",
"integrity": "sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ==",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/exponential-backoff": { "node_modules/exponential-backoff": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz",
@@ -6596,6 +6685,11 @@
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
}, },
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -10308,6 +10402,14 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/use-latest-callback": { "node_modules/use-latest-callback": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.3.tgz", "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.3.tgz",

View File

@@ -33,7 +33,8 @@
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0", "react-native-screens": "~4.4.0",
"react-native-vector-icons": "^10.2.0", "react-native-vector-icons": "^10.2.0",
"react-native-web": "~0.19.13" "react-native-web": "~0.19.13",
"expo-dev-client": "~5.0.20"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",

View File

@@ -1,15 +1,13 @@
// src/api/client.ts // src/api/client.ts
import axios, { AxiosError } from 'axios'; // Import AxiosError import axios, { AxiosError } from 'axios';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import * as SecureStore from 'expo-secure-store'; import * as SecureStore from 'expo-secure-store';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
// const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.255.221:8000/api'; // Use your machine's IP const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'https://maia.depaoli.id.au/api';
// const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://192.168.1.9:8000/api'; // Use your machine's IP const ACCESS_TOKEN_KEY = 'maia_access_token';
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'https://maia.depaoli.id.au/api'; // Use your machine's IP const REFRESH_TOKEN_KEY = 'maia_refresh_token';
const ACCESS_TOKEN_KEY = 'maia_access_token'; // Renamed for clarity
const REFRESH_TOKEN_KEY = 'maia_refresh_token'; // Key for refresh token
console.log("Using API Base URL:", API_BASE_URL); console.log("Using API Base URL:", API_BASE_URL);
@@ -34,7 +32,7 @@ const deleteToken = async (key: string): Promise<void> => {
if (Platform.OS === 'web') { if (Platform.OS === 'web') {
await AsyncStorage.removeItem(key); await AsyncStorage.removeItem(key);
} else { } else {
await SecureStore.deleteItemAsync(key).catch(() => {}); // Ignore delete error await SecureStore.deleteItemAsync(key).catch(() => {});
} }
}; };

View File

@@ -13,7 +13,7 @@ const AuthNavigator = () => {
return ( return (
<Stack.Navigator screenOptions={{ headerShown: false }}> <Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Login" component={LoginScreen} /> <Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} /> {/* Add Register screen */} <Stack.Screen name="Register" component={RegisterScreen} />
</Stack.Navigator> </Stack.Navigator>
); );
}; };

View File

@@ -35,11 +35,7 @@ const RegisterScreen: React.FC<RegisterScreenProps> = ({ navigation }) => {
await register(username, password, name); await register(username, password, name);
console.log("[RegisterScreen] handleRegister: Registration successful (from context perspective)."); console.log("[RegisterScreen] handleRegister: Registration successful (from context perspective).");
// Show success message and navigate back to Login // Show success message and navigate back to Login
Alert.alert( navigation.navigate('Login');
'Registration Successful',
'Your account has been created. Please log in.',
[{ text: 'OK', onPress: () => navigation.navigate('Login') }]
);
} catch (err: any) { } catch (err: any) {
console.log("[RegisterScreen] handleRegister: Caught error from context register."); console.log("[RegisterScreen] handleRegister: Caught error from context register.");
const errorMessage = err.response?.data?.detail || const errorMessage = err.response?.data?.detail ||