initial commit
This commit is contained in:
0
backend/app/routers/__init__.py
Normal file
0
backend/app/routers/__init__.py
Normal file
32
backend/app/routers/auth.py
Normal file
32
backend/app/routers/auth.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""FluentGerman.ai — Auth router."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import create_access_token, get_current_user, verify_password
|
||||
from app.database import get_db
|
||||
from app.models import User
|
||||
from app.schemas import LoginRequest, Token, UserOut
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(User).where(User.username == body.username))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user or not verify_password(body.password, user.hashed_password):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Account disabled")
|
||||
|
||||
token = create_access_token({"sub": str(user.id)})
|
||||
return Token(access_token=token)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserOut)
|
||||
async def me(user: User = Depends(get_current_user)):
|
||||
return user
|
||||
48
backend/app/routers/chat.py
Normal file
48
backend/app/routers/chat.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""FluentGerman.ai — Chat router with SSE streaming."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import get_current_user
|
||||
from app.database import get_db
|
||||
from app.models import User
|
||||
from app.schemas import ChatRequest
|
||||
from app.services.instruction_service import get_system_prompt
|
||||
from app.services.llm_service import chat_stream
|
||||
|
||||
logger = logging.getLogger("fluentgerman.chat")
|
||||
|
||||
router = APIRouter(prefix="/api/chat", tags=["chat"])
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def chat(
|
||||
body: ChatRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Send a message and receive a streamed SSE response."""
|
||||
logger.info("Chat request from user=%s message_len=%d history=%d",
|
||||
user.username, len(body.message), len(body.history))
|
||||
|
||||
system_prompt = await get_system_prompt(db, user.id)
|
||||
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
for msg in body.history:
|
||||
messages.append({"role": msg.role, "content": msg.content})
|
||||
messages.append({"role": "user", "content": body.message})
|
||||
|
||||
async def event_generator():
|
||||
try:
|
||||
async for token in chat_stream(messages):
|
||||
yield f"data: {json.dumps({'token': token})}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
except Exception as e:
|
||||
logger.error("LLM streaming error: %s", e, exc_info=True)
|
||||
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
||||
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
68
backend/app/routers/instructions.py
Normal file
68
backend/app/routers/instructions.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""FluentGerman.ai — Instruction management router (admin only)."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import require_admin
|
||||
from app.database import get_db
|
||||
from app.models import Instruction, InstructionType
|
||||
from app.schemas import InstructionCreate, InstructionOut, InstructionUpdate
|
||||
|
||||
router = APIRouter(prefix="/api/instructions", tags=["instructions"], dependencies=[Depends(require_admin)])
|
||||
|
||||
|
||||
@router.get("/", response_model=list[InstructionOut])
|
||||
async def list_instructions(user_id: int | None = None, db: AsyncSession = Depends(get_db)):
|
||||
query = select(Instruction).order_by(Instruction.created_at.desc())
|
||||
if user_id is not None:
|
||||
# Fetch per-user + global instructions
|
||||
query = query.where((Instruction.user_id == user_id) | Instruction.user_id.is_(None))
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/", response_model=InstructionOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_instruction(body: InstructionCreate, db: AsyncSession = Depends(get_db)):
|
||||
instruction = Instruction(
|
||||
user_id=body.user_id,
|
||||
title=body.title,
|
||||
content=body.content,
|
||||
type=InstructionType(body.type),
|
||||
)
|
||||
db.add(instruction)
|
||||
await db.commit()
|
||||
await db.refresh(instruction)
|
||||
return instruction
|
||||
|
||||
|
||||
@router.put("/{instruction_id}", response_model=InstructionOut)
|
||||
async def update_instruction(
|
||||
instruction_id: int, body: InstructionUpdate, db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
result = await db.execute(select(Instruction).where(Instruction.id == instruction_id))
|
||||
inst = result.scalar_one_or_none()
|
||||
if not inst:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Instruction not found")
|
||||
|
||||
if body.title is not None:
|
||||
inst.title = body.title
|
||||
if body.content is not None:
|
||||
inst.content = body.content
|
||||
if body.type is not None:
|
||||
inst.type = InstructionType(body.type)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(inst)
|
||||
return inst
|
||||
|
||||
|
||||
@router.delete("/{instruction_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_instruction(instruction_id: int, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(Instruction).where(Instruction.id == instruction_id))
|
||||
inst = result.scalar_one_or_none()
|
||||
if not inst:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Instruction not found")
|
||||
|
||||
await db.delete(inst)
|
||||
await db.commit()
|
||||
70
backend/app/routers/users.py
Normal file
70
backend/app/routers/users.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""FluentGerman.ai — User management router (admin only)."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import hash_password, require_admin
|
||||
from app.database import get_db
|
||||
from app.models import User
|
||||
from app.schemas import UserCreate, UserOut, UserUpdate
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["users"], dependencies=[Depends(require_admin)])
|
||||
|
||||
|
||||
@router.get("/", response_model=list[UserOut])
|
||||
async def list_users(db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(User).where(User.is_admin == False).order_by(User.created_at.desc())) # noqa: E712
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/", response_model=UserOut, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(body: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
# Check uniqueness
|
||||
existing = await db.execute(
|
||||
select(User).where((User.username == body.username) | (User.email == body.email))
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Username or email already exists")
|
||||
|
||||
user = User(
|
||||
username=body.username,
|
||||
email=body.email,
|
||||
hashed_password=hash_password(body.password),
|
||||
)
|
||||
db.add(user)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/{user_id}", response_model=UserOut)
|
||||
async def update_user(user_id: int, body: UserUpdate, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
if body.username is not None:
|
||||
user.username = body.username
|
||||
if body.email is not None:
|
||||
user.email = body.email
|
||||
if body.password is not None:
|
||||
user.hashed_password = hash_password(body.password)
|
||||
if body.is_active is not None:
|
||||
user.is_active = body.is_active
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(user_id: int, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
await db.delete(user)
|
||||
await db.commit()
|
||||
49
backend/app/routers/voice.py
Normal file
49
backend/app/routers/voice.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""FluentGerman.ai — Admin voice-to-instruction & voice API router."""
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, File
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import require_admin, get_current_user
|
||||
from app.config import get_settings
|
||||
from app.database import get_db
|
||||
from app.models import User
|
||||
from app.schemas import VoiceConfigOut, VoiceInstructionRequest
|
||||
from app.services.llm_service import summarize_instruction
|
||||
from app.services.voice_service import synthesize, transcribe
|
||||
from fastapi.responses import Response
|
||||
|
||||
router = APIRouter(prefix="/api/voice", tags=["voice"])
|
||||
|
||||
|
||||
@router.get("/config", response_model=VoiceConfigOut)
|
||||
async def voice_config(user: User = Depends(get_current_user)):
|
||||
"""Return current voice mode so frontend knows whether to use browser or API."""
|
||||
return VoiceConfigOut(voice_mode=get_settings().voice_mode)
|
||||
|
||||
|
||||
@router.post("/transcribe")
|
||||
async def transcribe_audio(
|
||||
audio: UploadFile = File(...),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Transcribe uploaded audio to text (API mode only)."""
|
||||
audio_bytes = await audio.read()
|
||||
text = await transcribe(audio_bytes, filename=audio.filename or "audio.webm")
|
||||
return {"text": text}
|
||||
|
||||
|
||||
@router.post("/synthesize")
|
||||
async def synthesize_text(
|
||||
text: str,
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Convert text to speech audio (API mode only)."""
|
||||
audio_bytes = await synthesize(text)
|
||||
return Response(content=audio_bytes, media_type="audio/mpeg")
|
||||
|
||||
|
||||
@router.post("/generate-instruction", dependencies=[Depends(require_admin)])
|
||||
async def generate_instruction(body: VoiceInstructionRequest):
|
||||
"""Admin: takes raw transcript and returns a structured instruction via LLM."""
|
||||
structured = await summarize_instruction(body.raw_text)
|
||||
return {"instruction": structured}
|
||||
Reference in New Issue
Block a user