Calendar + NLP modules implemented
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
PEPPER = "LsD7%"
|
||||
JWT_SECRET_KEY="1c8cf3ca6972b365f8108dad247e61abdcb6faff5a6c8ba00cb6fa17396702bf"
|
||||
JWT_SECRET_KEY="1c8cf3ca6972b365f8108dad247e61abdcb6faff5a6c8ba00cb6fa17396702bf"
|
||||
GOOGLE_API_KEY="AIzaSyBrte_mETZJce8qE6cRTSz_fHOjdjlShBk"
|
||||
Binary file not shown.
Binary file not shown.
@@ -18,4 +18,6 @@ class Settings(BaseSettings):
|
||||
PEPPER: str = getenv("PEPPER", "")
|
||||
JWT_SECRET_KEY: str = getenv("JWT_SECRET_KEY", "")
|
||||
|
||||
GOOGLE_API_KEY: str = getenv("GOOGLE_API_KEY", "")
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
# main.py
|
||||
from contextlib import _AsyncGeneratorContextManager, asynccontextmanager
|
||||
from typing import Any, Callable
|
||||
from fastapi import FastAPI, Depends
|
||||
from core.database import get_engine, Base
|
||||
from modules.auth.api import router as auth_router
|
||||
from modules.user.api import router as user_router
|
||||
from modules.admin.api import router as admin_router
|
||||
from modules.auth.dependencies import admin_only
|
||||
from modules import router
|
||||
import logging
|
||||
|
||||
from modules.auth.security import get_current_user
|
||||
|
||||
logging.getLogger('passlib').setLevel(logging.ERROR) # fix bc package logging is broken
|
||||
|
||||
# Create DB tables (remove in production; use migrations instead)
|
||||
def lifespan(app):
|
||||
# Base.metadata.drop_all(bind=get_engine())
|
||||
Base.metadata.create_all(bind=get_engine())
|
||||
yield
|
||||
def lifespan_factory() -> Callable[[FastAPI], _AsyncGeneratorContextManager[Any]]:
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
Base.metadata.drop_all(bind=get_engine())
|
||||
Base.metadata.create_all(bind=get_engine())
|
||||
yield
|
||||
|
||||
return lifespan
|
||||
|
||||
lifespan = lifespan_factory()
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
# Include all module routers
|
||||
app.include_router(auth_router, prefix="/api/auth")
|
||||
app.include_router(user_router, prefix="/api/user")
|
||||
app.include_router(admin_router, prefix="/api/admin", dependencies=[Depends(admin_only)])
|
||||
# Include module router
|
||||
app.include_router(router)
|
||||
@@ -0,0 +1,13 @@
|
||||
from fastapi import APIRouter
|
||||
from .admin.api import router as admin_router
|
||||
from .auth.api import router as auth_router
|
||||
from .user.api import router as user_router
|
||||
from .calendar.api import router as calendar_router
|
||||
from .nlp.api import router as nlp_router
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
router.include_router(admin_router)
|
||||
router.include_router(auth_router)
|
||||
router.include_router(user_router)
|
||||
router.include_router(calendar_router)
|
||||
router.include_router(nlp_router)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,9 +4,10 @@ from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from core.database import Base, get_db
|
||||
from modules.auth.models import User, UserRole
|
||||
from modules.auth.dependencies import admin_only
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter(prefix="/admin", tags=["admin"], dependencies=[Depends(admin_only)])
|
||||
|
||||
@router.get("/")
|
||||
def read_admin():
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,7 +13,7 @@ from datetime import timedelta
|
||||
from core.config import settings # Assuming settings is defined in core.config
|
||||
from core.exceptions import unauthorized_exception
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
def register(user: UserCreate, db: Annotated[Session, Depends(get_db)]):
|
||||
|
||||
@@ -11,7 +11,7 @@ class RoleChecker:
|
||||
|
||||
def __call__(self, user: User = Depends(get_current_user)):
|
||||
if user.role not in self.allowed_roles:
|
||||
forbidden_exception("You do not have permission to perform this action.")
|
||||
raise forbidden_exception("You do not have permission to perform this action.")
|
||||
return user
|
||||
|
||||
admin_only = RoleChecker([UserRole.ADMIN])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# modules/auth/models.py
|
||||
from core.database import Base
|
||||
from sqlalchemy import CheckConstraint, Column, Integer, String, Enum, DateTime
|
||||
from sqlalchemy import Column, Integer, String, Enum, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from enum import Enum as PyEnum
|
||||
|
||||
class UserRole(str, PyEnum):
|
||||
@@ -12,10 +13,11 @@ class User(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String, unique=True)
|
||||
username = Column(String, unique=True)
|
||||
hashed_password = Column(String)
|
||||
role = Column(Enum(UserRole), nullable=False, default=UserRole.USER)
|
||||
|
||||
name = Column(String)
|
||||
role = Column(Enum(UserRole), nullable=False, default=UserRole.USER)
|
||||
hashed_password = Column(String)
|
||||
calendar_events = relationship("CalendarEvent", back_populates="user")
|
||||
|
||||
|
||||
class TokenBlacklist(Base):
|
||||
__tablename__ = "token_blacklist"
|
||||
|
||||
BIN
backend/modules/calendar/__pycache__/api.cpython-312.pyc
Normal file
BIN
backend/modules/calendar/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/modules/calendar/__pycache__/models.cpython-312.pyc
Normal file
BIN
backend/modules/calendar/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/modules/calendar/__pycache__/schemas.cpython-312.pyc
Normal file
BIN
backend/modules/calendar/__pycache__/schemas.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/modules/calendar/__pycache__/service.cpython-312.pyc
Normal file
BIN
backend/modules/calendar/__pycache__/service.cpython-312.pyc
Normal file
Binary file not shown.
46
backend/modules/calendar/api.py
Normal file
46
backend/modules/calendar/api.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# modules/calendar/api.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from modules.auth.dependencies import get_current_user
|
||||
from core.database import get_db
|
||||
from modules.auth.models import User
|
||||
from modules.calendar.schemas import CalendarEventCreate, CalendarEventResponse
|
||||
from modules.calendar.service import create_calendar_event, get_calendar_events, update_calendar_event, delete_calendar_event
|
||||
|
||||
router = APIRouter(prefix="/calendar", tags=["calendar"])
|
||||
|
||||
@router.post("/events", response_model=CalendarEventResponse)
|
||||
def create_event(
|
||||
event: CalendarEventCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return create_calendar_event(db, user.id, event)
|
||||
|
||||
@router.get("/events", response_model=list[CalendarEventResponse])
|
||||
def get_events(
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
start: datetime | None = None,
|
||||
end: datetime | None = None
|
||||
):
|
||||
return get_calendar_events(db, user.id, start, end)
|
||||
|
||||
@router.put("/events/{event_id}", response_model=CalendarEventResponse)
|
||||
def update_event(
|
||||
event_id: int,
|
||||
event: CalendarEventCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
return update_calendar_event(db, user.id, event_id, event)
|
||||
|
||||
@router.delete("/events/{event_id}")
|
||||
def delete_event(
|
||||
event_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
delete_calendar_event(db, user.id, event_id)
|
||||
return {"message": "Event deleted"}
|
||||
18
backend/modules/calendar/models.py
Normal file
18
backend/modules/calendar/models.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# modules/calendar/models.py
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from core.database import Base
|
||||
|
||||
class CalendarEvent(Base):
|
||||
__tablename__ = "calendar_events"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(String)
|
||||
start = Column(DateTime, nullable=False)
|
||||
end = Column(DateTime)
|
||||
location = Column(String)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False) # <-- Relationship
|
||||
|
||||
# Bi-directional relationship (for eager loading)
|
||||
user = relationship("User", back_populates="calendar_events")
|
||||
17
backend/modules/calendar/schemas.py
Normal file
17
backend/modules/calendar/schemas.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# modules/calendar/schemas.py
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
class CalendarEventCreate(BaseModel):
|
||||
title: str
|
||||
description: str | None = None
|
||||
start: datetime
|
||||
end: datetime | None = None
|
||||
location: str | None = None
|
||||
|
||||
class CalendarEventResponse(CalendarEventCreate):
|
||||
id: int
|
||||
user_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
45
backend/modules/calendar/service.py
Normal file
45
backend/modules/calendar/service.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# modules/calendar/service.py
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from modules.calendar.models import CalendarEvent
|
||||
from core.exceptions import not_found_exception
|
||||
|
||||
def create_calendar_event(db: Session, user_id: int, event_data):
|
||||
event = CalendarEvent(**event_data.dict(), user_id=user_id)
|
||||
db.add(event)
|
||||
db.commit()
|
||||
db.refresh(event)
|
||||
return event
|
||||
|
||||
def get_calendar_events(db: Session, user_id: int, start: datetime, end: datetime):
|
||||
query = db.query(CalendarEvent).filter(
|
||||
CalendarEvent.user_id == user_id
|
||||
)
|
||||
if start:
|
||||
query = query.filter(CalendarEvent.start_time >= start)
|
||||
if end:
|
||||
query = query.filter(CalendarEvent.end_time <= end)
|
||||
return query.all()
|
||||
|
||||
def update_calendar_event(db: Session, user_id: int, event_id: int, event_data):
|
||||
event = db.query(CalendarEvent).filter(
|
||||
CalendarEvent.id == event_id,
|
||||
CalendarEvent.user_id == user_id
|
||||
).first()
|
||||
if not event:
|
||||
raise not_found_exception()
|
||||
for key, value in event_data.dict().items():
|
||||
setattr(event, key, value)
|
||||
db.commit()
|
||||
db.refresh(event)
|
||||
return event
|
||||
|
||||
def delete_calendar_event(db: Session, user_id: int, event_id: int):
|
||||
event = db.query(CalendarEvent).filter(
|
||||
CalendarEvent.id == event_id,
|
||||
CalendarEvent.user_id == user_id
|
||||
).first()
|
||||
if not event:
|
||||
raise not_found_exception()
|
||||
db.delete(event)
|
||||
db.commit()
|
||||
BIN
backend/modules/nlp/__pycache__/api.cpython-312.pyc
Normal file
BIN
backend/modules/nlp/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/modules/nlp/__pycache__/service.cpython-312.pyc
Normal file
BIN
backend/modules/nlp/__pycache__/service.cpython-312.pyc
Normal file
Binary file not shown.
53
backend/modules/nlp/api.py
Normal file
53
backend/modules/nlp/api.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# modules/nlp/api.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_db
|
||||
from core.exceptions import bad_request_exception
|
||||
|
||||
from modules.auth.dependencies import get_current_user
|
||||
from modules.auth.models import User
|
||||
from modules.nlp.service import process_request, ask_ai
|
||||
from modules.calendar.service import create_calendar_event, get_calendar_events, update_calendar_event, delete_calendar_event
|
||||
from modules.calendar.schemas import CalendarEventCreate
|
||||
|
||||
|
||||
router = APIRouter(prefix="/nlp", tags=["nlp"])
|
||||
|
||||
@router.post("/process-command")
|
||||
def process_command(user_input: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""
|
||||
Process the user command and return the appropriate action.
|
||||
"""
|
||||
command = process_request(user_input)
|
||||
|
||||
if "error" in command:
|
||||
raise bad_request_exception(command["error"])
|
||||
|
||||
match command["intent"]:
|
||||
case "ask_ai":
|
||||
result = ask_ai(**command["params"])
|
||||
return {"action": "ai_response", "details": result}
|
||||
|
||||
case "get_calendar_events":
|
||||
result = get_calendar_events(db, current_user.id, **command["params"])
|
||||
return {"action": "calendar_events_retrieved", "details": result}
|
||||
|
||||
case "add_calendar_event":
|
||||
event = CalendarEventCreate(**command["params"])
|
||||
result = create_calendar_event(db, current_user.id, event)
|
||||
return {"action": "calendar_event_created", "details": result}
|
||||
|
||||
case "update_calendar_event":
|
||||
event = CalendarEventCreate(**command["params"])
|
||||
result = update_calendar_event(db, current_user.id, 0, event_data=event) ## PLACEHOLDER
|
||||
return {"action": "calendar_event_updated", "details": result}
|
||||
|
||||
case "delete_calendar_event":
|
||||
result = update_calendar_event(db, current_user.id, 0) ## PLACEHOLDER
|
||||
return {"action": "calendar_event_deleted", "details": result}
|
||||
|
||||
case "unknown":
|
||||
return {"action": "unknown_command", "details": command["params"]}
|
||||
case _:
|
||||
raise bad_request_exception(400, detail="Unrecognized command")
|
||||
100
backend/modules/nlp/service.py
Normal file
100
backend/modules/nlp/service.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# modules/nlp/service.py
|
||||
|
||||
from google import genai
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
# from core.config import settings
|
||||
|
||||
# client = genai.Client(api_key=settings.GOOGLE_API_KEY)
|
||||
client = genai.Client(api_key="AIzaSyBrte_mETZJce8qE6cRTSz_fHOjdjlShBk")
|
||||
|
||||
### Base prompt for MAIA, used for inital user requests
|
||||
SYSTEM_PROMPT = """
|
||||
You are MAIA - My AI Assistant. Your job is to parse user requests into structured JSON commands.
|
||||
|
||||
Available functions:
|
||||
1. ask_ai(request: str). If the intent of the request is a simple question (e.x. What is the weather like today?), you should call this function, and forward the user's request as the parameter.
|
||||
2. get_calendar_events(start: Optional[datetime], end: Optional[datetime])
|
||||
3. add_calendar_event(title: str, description: str, start: datetime, end: Optional[datetime], location: str)
|
||||
4. update_calendar_event(event_id: int, title: Optional[str], description: Optional[str], start: Optional[datetime], end: Optional[datetime], location: Optional[str])
|
||||
5. delete_calendar_event(event_id: int)
|
||||
|
||||
Respond **ONLY** with JSON like this:
|
||||
{
|
||||
"intent": "add_calendar_event",
|
||||
"params": {
|
||||
"title": "Team Meeting",
|
||||
"description": "Discuss project updates",
|
||||
"start": "2025-04-16 15:00:00.000000+00:00",
|
||||
"end": "2025-04-16 16:00:00.000000+00:00",
|
||||
"location": "Office"
|
||||
}
|
||||
}
|
||||
|
||||
The datetime right now is """+str(datetime.now(timezone.utc))+""".
|
||||
"""
|
||||
|
||||
### Prompt for MAIA to forward user request to AI
|
||||
SYSTEM_FORWARD_PROMPT = f"""
|
||||
You are MAIA - My AI Assistant. Your job is to answer user simple user requests.
|
||||
Here is some context for you:
|
||||
- The datetime right now is {str(datetime.now(timezone.utc))}.
|
||||
Here is the user request:
|
||||
|
||||
"""
|
||||
def process_request(request: str):
|
||||
"""
|
||||
Process the user request using the Google GenAI API.
|
||||
"""
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=SYSTEM_PROMPT + f"\n\nUser: {request}\nMAIA:",
|
||||
config={
|
||||
"temperature": 0.3, # Less creativity, more factual
|
||||
"response_mime_type": "application/json",
|
||||
# "response_schema": { ### NOT WORKING
|
||||
# "type": "object",
|
||||
# "properties": {
|
||||
# "intent": {
|
||||
# "type": "string",
|
||||
# "enum": [
|
||||
# "get_calendar_events",
|
||||
# "add_calendar_event",
|
||||
# "update_calendar_event",
|
||||
# "delete_calendar_event"
|
||||
# ]
|
||||
# },
|
||||
# "params": {
|
||||
# "type": "object",
|
||||
# "properties": {
|
||||
# "title": {"type": "string"},
|
||||
# "description": {"type": "string"},
|
||||
# "start": {"type": "string", "format": "date-time"},
|
||||
# "end": {"type": "string", "format": "date-time"},
|
||||
# "location": {"type": "string"},
|
||||
# "event_id": {"type": "integer"},
|
||||
|
||||
# },
|
||||
# }
|
||||
# },
|
||||
# "required": ["intent", "params"]
|
||||
# }
|
||||
}
|
||||
)
|
||||
|
||||
# Parse the JSON response
|
||||
try:
|
||||
return json.loads(response.text)
|
||||
except ValueError:
|
||||
raise ValueError("Invalid JSON response from AI")
|
||||
|
||||
def ask_ai(request: str):
|
||||
"""
|
||||
Ask the AI a question.
|
||||
This is only called by MAIA when the intent is a simple question.
|
||||
"""
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.0-flash",
|
||||
contents=SYSTEM_FORWARD_PROMPT+request,
|
||||
)
|
||||
return response.text
|
||||
Binary file not shown.
@@ -9,7 +9,7 @@ from modules.auth.schemas import UserPatch, UserResponse
|
||||
from modules.auth.dependencies import get_current_user
|
||||
from modules.auth.models import User
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter(prefix="/user", tags=["user"])
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def me(db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)]) -> UserResponse:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
44
backend/tests/test_calendar.py
Normal file
44
backend/tests/test_calendar.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
from tests.helpers import generators
|
||||
|
||||
|
||||
def test_create_event(client: TestClient, db: Session) -> None:
|
||||
user, unhashed_password = generators.create_user(db)
|
||||
rsp = generators.login(db, user.username, unhashed_password)
|
||||
access_token = rsp["access_token"]
|
||||
refresh_token = rsp["refresh_token"]
|
||||
|
||||
response = client.post("/api/calendar/events",
|
||||
json={
|
||||
"title": "Test Event",
|
||||
"start_time": "2024-03-20T15:00:00Z"
|
||||
},
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["title"] == "Test Event"
|
||||
|
||||
def test_get_events(client: TestClient, db: Session) -> None:
|
||||
user, unhashed_password = generators.create_user(db)
|
||||
rsp = generators.login(db, user.username, unhashed_password)
|
||||
access_token = rsp["access_token"]
|
||||
refresh_token = rsp["refresh_token"]
|
||||
|
||||
# Create an event to retrieve
|
||||
client.post("/api/calendar/events",
|
||||
json={
|
||||
"title": "Test Event",
|
||||
"start_time": "2024-03-20T15:00:00Z"
|
||||
},
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
)
|
||||
|
||||
response = client.get("/api/calendar/events",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) > 0
|
||||
Reference in New Issue
Block a user