commit
This commit is contained in:
0
backend/app/core/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
40
backend/app/core/ai.py
Normal file
40
backend/app/core/ai.py
Normal 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()
|
||||
41
backend/app/core/config.py
Normal file
41
backend/app/core/config.py
Normal 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()
|
||||
70
backend/app/core/storage.py
Normal file
70
backend/app/core/storage.py
Normal 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()
|
||||
Reference in New Issue
Block a user