Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 26 additions & 21 deletions backend/fastapi/src/adapter/factory/service_factory.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@


from typing import Optional
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from src.application.ports.input.health_input_port import HealthInputPort
from src.application.usecases.health_usecase import HealthUsecase
from src.adapter.output.mysql.db.base import get_async_session
from src.adapter.output.mysql.db.base import get_async_session_dependency
from src.adapter.output.mysql.repositories.conversation_repository import ConversationRepository
from src.adapter.output.mysql.repositories.message_repository import MessageRepository
from src.application.ports.input.conversation_input_port import ConversationInputPort
Expand All @@ -17,14 +18,8 @@
from src.application.usecases.gemini_usecase import GeminiUseCase


_cached_ports = {}


def _make_repos_and_ports() -> tuple[ConversationOutputPort, MessageOutputPort, ConversationInputPort, HealthInputPort]:
"""Create fresh DB-backed repositories and usecases. This is created lazily
per-call to avoid binding AsyncSession/engine to a different event loop at import time.
"""
db: AsyncSession = get_async_session()
def _make_repos_and_ports(db: AsyncSession) -> tuple[ConversationOutputPort, MessageOutputPort, ConversationInputPort, HealthInputPort]:
"""Create DB-backed repositories and usecases using provided session."""
conv_repo: ConversationOutputPort = ConversationRepository(db)
msg_repo: MessageOutputPort = MessageRepository(db)
conv_input: ConversationInputPort = ConversationUseCase(conv_repo, msg_repo)
Expand All @@ -33,35 +28,45 @@ def _make_repos_and_ports() -> tuple[ConversationOutputPort, MessageOutputPort,


def _make_gemini_input_port(msg_repo: MessageOutputPort, conv_repo: ConversationOutputPort) -> GeminiInputPort:
# instantiate GeminiClient/Service lazily to avoid any async client being
# created at import time (which can bind to an unrelated event loop).
"""Create Gemini input port with provided repositories."""
client = GeminiClient()
svc = GeminiService(client)
return GeminiUseCase(svc, msg_repo, conv_repo)

class ServiceFactory:
"""Factory for creating services with proper dependency injection."""

@staticmethod
def get_conversation_input_port() -> ConversationInputPort:
_, _, conv_input, _ = _make_repos_and_ports()
def get_conversation_input_port(
db: AsyncSession = Depends(get_async_session_dependency)
) -> ConversationInputPort:
_, _, conv_input, _ = _make_repos_and_ports(db)
return conv_input

@staticmethod
def get_conversation_output_port() -> ConversationOutputPort:
conv_repo, _, _, _ = _make_repos_and_ports()
def get_conversation_output_port(
db: AsyncSession = Depends(get_async_session_dependency)
) -> ConversationOutputPort:
conv_repo, _, _, _ = _make_repos_and_ports(db)
return conv_repo

@staticmethod
def get_message_output_port() -> MessageOutputPort:
_, msg_repo, _, _ = _make_repos_and_ports()
def get_message_output_port(
db: AsyncSession = Depends(get_async_session_dependency)
) -> MessageOutputPort:
_, msg_repo, _, _ = _make_repos_and_ports(db)
return msg_repo

@staticmethod
def get_health_input_port() -> HealthInputPort:
_, _, _, health_input = _make_repos_and_ports()
def get_health_input_port(
db: AsyncSession = Depends(get_async_session_dependency)
) -> HealthInputPort:
_, _, _, health_input = _make_repos_and_ports(db)
return health_input

@staticmethod
def get_gemini_input_port() -> GeminiInputPort:
conv_repo, msg_repo, _, _ = _make_repos_and_ports()
def get_gemini_input_port(
db: AsyncSession = Depends(get_async_session_dependency)
) -> GeminiInputPort:
conv_repo, msg_repo, _, _ = _make_repos_and_ports(db)
return _make_gemini_input_port(msg_repo, conv_repo)
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async def update_conversation(
return success_response(data=data, message="updated", status_code=200)


@router.delete("/{conversation_id}", response_model=ConversationResponse)
@router.delete("/{conversation_id}")
async def delete_conversation(
conversation_id: str,
conversation_service: ConversationInputPort = Depends(ServiceFactory.get_conversation_input_port)
Expand Down
23 changes: 22 additions & 1 deletion backend/fastapi/src/adapter/output/mysql/db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,16 @@ def _create_engine_and_session() -> None:
)
else:
# attempt to create MySQL async engine; may raise ModuleNotFoundError if driver missing
_async_engine = create_async_engine(_mysql_async_url(), echo=False, future=True)
_async_engine = create_async_engine(
_mysql_async_url(),
echo=False,
future=True,
pool_size=20, # Increase pool size from default 5
max_overflow=40, # Increase overflow from default 10
pool_timeout=30, # Connection timeout in seconds
pool_recycle=3600, # Recycle connections after 1 hour
pool_pre_ping=True, # Verify connections before using
)
except ModuleNotFoundError as exc:
# Fail fast in non-testing environments: do not silently fall back to in-memory sqlite.
raise RuntimeError(
Expand Down Expand Up @@ -89,6 +98,18 @@ def init_db():


def get_async_session() -> AsyncSession:
"""Create a new AsyncSession. IMPORTANT: Caller must close the session when done."""
_create_engine_and_session()
assert _AsyncSessionLocal is not None, "Async sessionmaker was not initialized"
return _AsyncSessionLocal()


async def get_async_session_dependency():
"""FastAPI dependency that yields a session and ensures it's closed after use."""
_create_engine_and_session()
assert _AsyncSessionLocal is not None, "Async sessionmaker was not initialized"
session: AsyncSession = _AsyncSessionLocal()
try:
yield session
finally:
await session.close()
4 changes: 4 additions & 0 deletions backend/fastapi/src/application/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class Settings(BaseSettings): # type: ignore
GEMINI_URL: str | None = None
GEMINI_API_KEY: str | None = None
GEMINI_TIMEOUT_SECONDS: int = 300
# CORS
# Comma-separated list of allowed origins, or '*' to allow all origins.
# Example: "http://localhost:5173,http://127.0.0.1:5173"
FRONTEND_ALLOWED_ORIGINS: str = "*"

# Testing
TESTING: bool = False
Expand Down
17 changes: 17 additions & 0 deletions backend/fastapi/src/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.adapter.input.controllers import conversation_controller, health_controller, gemini_controller, messages_controller, webapp_controller
from fastapi import Request
from fastapi.responses import JSONResponse
Expand All @@ -16,6 +17,22 @@
redoc_url=f"{settings.API_PREFIX}/redoc"
)

# Add CORS middleware for frontend
# Configure CORS origins from settings (supports comma-separated env value or "*")
_raw_origins = getattr(settings, "FRONTEND_ALLOWED_ORIGINS", "*") or "*"
if isinstance(_raw_origins, str) and _raw_origins.strip() == "*":
_origins = ["*"]
else:
_origins = [o.strip() for o in _raw_origins.split(",") if o.strip()]

app.add_middleware(
CORSMiddleware,
allow_origins=_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# include routers under a API prefix
app.include_router(gemini_controller.router, prefix=settings.API_PREFIX)
app.include_router(conversation_controller.router, prefix=settings.API_PREFIX)
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading