This commit is contained in:
2026-01-27 17:40:37 +01:00
parent 82947a7bd6
commit adc2cd572a
55 changed files with 4145 additions and 101 deletions

View File

40
backend/app/core/ai.py Normal file
View File

@@ -0,0 +1,40 @@
from openai import AsyncOpenAI
from app.core.config import settings
class AIClient:
def __init__(self):
self.client = AsyncOpenAI(
api_key=settings.OPENAI_API_KEY,
base_url=settings.OPENAI_API_BASE
)
self.model = settings.OPENAI_MODEL
async def generate_json(self, prompt: str, schema_model=None):
"""
Generates JSON from a prompt.
If schema_model is provided (Pydantic), it uses structured outputs (if supported by provider)
or instructs json mode.
"""
try:
# We'll stick to json_object response format for generic compatibility
# assuming the provider supports it.
messages = [{"role": "user", "content": prompt}]
kwargs = {
"model": self.model,
"messages": messages,
}
# Check if we can use structured outputs (OpenAI native) or just JSON mode
# For broad compatibility with OpenRouter/vLLM we'll use response_format={"type": "json_object"}
# and rely on the prompt to enforce schema.
kwargs["response_format"] = {"type": "json_object"}
response = await self.client.chat.completions.create(**kwargs)
return response.choices[0].message.content
except Exception as e:
print(f"AI Generation Error: {e}")
raise e
ai_client = AIClient()

View File

@@ -0,0 +1,41 @@
from typing import List, Optional, Union
from pydantic import AnyHttpUrl, PostgresDsn, computed_field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
PROJECT_NAME: str = "Auteur AI"
API_V1_STR: str = "/api/v1"
# CORS
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
# Database
POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str = "postgres"
POSTGRES_SERVER: str = "db"
POSTGRES_PORT: int = 5432
POSTGRES_DB: str = "auteur"
@computed_field
@property
def DATABASE_URL(self) -> str:
return f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
# MinIO
MINIO_ENDPOINT: str = "minio:9000"
MINIO_ACCESS_KEY: str = "minioadmin"
MINIO_SECRET_KEY: str = "minioadmin"
MINIO_BUCKET: str = "auteur-assets"
# Redis
REDIS_URL: str = "redis://redis:6379/0"
# OpenAI
OPENAI_API_BASE: str
OPENAI_API_KEY: str
OPENAI_MODEL: str = "gemini-2.0-flash-exp"
model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", extra="ignore")
settings = Settings()

View File

@@ -0,0 +1,70 @@
import boto3
from botocore.exceptions import ClientError
from app.core.config import settings
class StorageClient:
def __init__(self):
self.s3_client = boto3.client(
"s3",
endpoint_url=f"http://{settings.MINIO_ENDPOINT}",
aws_access_key_id=settings.MINIO_ACCESS_KEY,
aws_secret_access_key=settings.MINIO_SECRET_KEY,
config=boto3.session.Config(signature_version='s3v4')
)
self.bucket_name = settings.MINIO_BUCKET
self._ensure_bucket_exists()
def _ensure_bucket_exists(self):
try:
self.s3_client.head_bucket(Bucket=self.bucket_name)
except ClientError:
try:
self.s3_client.create_bucket(Bucket=self.bucket_name)
# Set bucket policy to public read if needed, or rely on presigned URLs
# For now, we will rely on presigned URLs for security
except ClientError as e:
print(f"Could not create bucket {self.bucket_name}: {e}")
def upload_file(self, file_obj, object_name: str, content_type: str = None) -> bool:
try:
extra_args = {}
if content_type:
extra_args["ContentType"] = content_type
self.s3_client.upload_fileobj(file_obj, self.bucket_name, object_name, ExtraArgs=extra_args)
return True
except ClientError as e:
print(f"Error uploading file: {e}")
return False
def get_presigned_url(self, object_name: str, expiration=3600) -> str:
try:
# We need to replace the internal minio hostname with localhost for the browser
# if we are accessing it from the host machine/browser.
# But the backend sees "minio".
# This is tricky in docker-compose.
# The client needs a URL that resolves.
# Usually we use a proxy or just configure the endpoint on the frontend.
# For now generate the URL and we might need to swap the host in the frontend or
# ensure the backend generates a URL accessible to the user.
# Actually, standard practice: Backend generates URL using its known endpoint.
# If that endpoint is "minio:9000", the browser can't resolve it.
# So we might need to override the endpoint for presigning.
url = self.s3_client.generate_presigned_url(
'get_object',
Params={'Bucket': self.bucket_name, 'Key': object_name},
ExpiresIn=expiration
)
# Hack for localhost dev: replace minio:9000 with localhost:9000
# dependent on where the request comes from.
# Ideally getting this from config would be better.
return url.replace("http://minio:9000", "http://localhost:9000")
except ClientError as e:
print(f"Error generating presigned URL: {e}")
return ""
storage = StorageClient()