Skip to content
Open
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
56 changes: 56 additions & 0 deletions .github/workflows/deploy-codeengine.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Deploy to IBM Cloud Code Engine

on:
workflow_dispatch:
inputs:
IBM_CLOUD_REGION:
description: 'IBM Cloud region'
required: true
default: 'us-south'
IBM_CLOUD_RESOURCE_GROUP:
description: 'IBM Cloud resource group'
required: true
default: 'Default'
CE_PROJECT_NAME:
description: 'Code Engine project name'
required: true
CE_APP_NAME:
description: 'Code Engine app name'
required: true
DOCKER_IMAGE:
description: 'Docker image name'
required: true

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Install IBM Cloud CLI
run: |
curl -fsSL https://clis.cloud.ibm.com/install/linux | sh
ibmcloud --version
ibmcloud plugin install -f code-engine

- name: Build and push Docker image
env:
IBM_CLOUD_API_KEY: ${{ secrets.IBM_CLOUD_API_KEY }}
IBM_CLOUD_REGION: ${{ github.event.inputs.IBM_CLOUD_REGION }}
DOCKER_IMAGE: ${{ github.event.inputs.DOCKER_IMAGE }}
run: |
ibmcloud login --apikey "$IBM_CLOUD_API_KEY" -r "$IBM_CLOUD_REGION"
ibmcloud cr login
docker build -t "$DOCKER_IMAGE" -f backend/Dockerfile.codeengine backend
docker push "$DOCKER_IMAGE"

- name: Deploy to Code Engine
env:
IBM_CLOUD_API_KEY: ${{ secrets.IBM_CLOUD_API_KEY }}
IBM_CLOUD_REGION: ${{ github.event.inputs.IBM_CLOUD_REGION }}
IBM_CLOUD_RESOURCE_GROUP: ${{ github.event.inputs.IBM_CLOUD_RESOURCE_GROUP }}
CE_PROJECT_NAME: ${{ github.event.inputs.CE_PROJECT_NAME }}
CE_APP_NAME: ${{ github.event.inputs.CE_APP_NAME }}
DOCKER_IMAGE: ${{ github.event.inputs.DOCKER_IMAGE }}
run: ./scripts/deploy_codeengine.sh
68 changes: 68 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# RAG Modulo Agentic Development - Gemini

This document outlines the development process for Gemini, an AI agent, working on the RAG Modulo project.

## 🎯 Current Mission: Agentic RAG Platform Development

**Priority:** Enhance the RAG platform with new features, fix bugs, and improve performance.

## 🧠 Development Philosophy

- **Understand First**: Before making any changes, thoroughly understand the codebase, architecture, and existing conventions.
- **Plan Thoughtfully**: Create a clear and concise plan before implementing any changes.
- **Implement Systematically**: Execute the plan in a structured manner, with regular verification and testing.
- **Test Rigorously**: Ensure all changes are covered by tests and that all tests pass.
- **Document Clearly**: Update documentation to reflect any changes made to the codebase.

## 📋 Project Context Essentials

- **Architecture**: Python FastAPI backend + React frontend.
- **Focus**: Transform basic RAG into an agentic AI platform with agent orchestration.
- **Tech Stack**: Python, FastAPI, React, Docker, a variety of vector databases.
- **Quality Standards**: >90% test coverage, clean code, and comprehensive documentation.

## 🚀 Development Workflow

### **Phase 1: Research**
- Understand the codebase structure and dependencies.
- Validate assumptions before proceeding.
- Use context compaction to focus on key insights.

### **Phase 2: Planning**
- Create precise, detailed implementation plans.
- Outline exact files to edit and verification steps.
- Compress findings into actionable implementation steps.

### **Phase 3: Implementation**
- Execute plans systematically with verification.
- Compact and update context after each stage.
- Maintain high human engagement for quality.

## 🤖 Agent Development Instructions

### **Quality Gates (Must Follow)**
- **Pre-Commit**: Always run `make pre-commit-run` and tests before committing.
- **Test Coverage**: Add comprehensive tests for new features (>90% coverage).
- **Code Patterns**: Follow existing patterns in `backend/` and `frontend/`.
- **Branch Strategy**: Create feature branches for each issue (`feature/issue-XXX`).
- **Commit Messages**: Descriptive commits following conventional format.

### **Technology Stack Commands**
- **Python**: `poetry run <command>` for all Python operations.
- **Frontend**: `npm run dev` for React development.
- **Testing**: `make test-unit-fast`, `make test-integration`.
- **Linting**: `make lint`, `make fix-all`.

### **Docker Compose Commands (V2 Required)**
- **Local Development**: `docker compose -f docker-compose.dev.yml up -d`
- **Build Development**: `docker compose -f docker-compose.dev.yml build backend`
- **Production Testing**: `make run-ghcr` (uses pre-built GHCR images)
- **Stop Services**: `docker compose -f docker-compose.dev.yml down`

## ✅ Success Criteria

- All tests pass.
- Code follows project style.
- Security guidelines followed.
- Documentation updated.
- Issues properly implemented.
39 changes: 39 additions & 0 deletions backend/Dockerfile.codeengine
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Use a slim Python image as a base
FROM python:3.11-slim as builder

# Set the working directory
WORKDIR /app

# Install poetry
RUN pip install poetry

# Copy only the dependency files to leverage Docker cache
COPY pyproject.toml poetry.lock ./

# Install dependencies into a virtual environment
RUN poetry config virtualenvs.in-project true && \
poetry install --only main --no-root

# Copy the rest of the application code
COPY . .

# Final stage
FROM python:3.11-slim

# Set the working directory
WORKDIR /app

# Copy the virtual environment from the builder stage
COPY --from=builder /app/.venv ./.venv

# Copy the application code from the builder stage
COPY --from=builder /app/ .

# Activate the virtual environment
ENV PATH="/app/.venv/bin:$PATH"

# Expose the port the app runs on
EXPOSE 8000

# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
24 changes: 23 additions & 1 deletion backend/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from functools import lru_cache
from typing import Annotated

from pydantic import field_validator
from pydantic import computed_field, field_validator
from pydantic.fields import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy import URL

from core.logging_utils import get_logger

Expand Down Expand Up @@ -266,10 +267,31 @@ class Settings(BaseSettings):
oidc_token_url: Annotated[str | None, Field(default=None, alias="OIDC_TOKEN_URL")]
oidc_userinfo_endpoint: Annotated[str | None, Field(default=None, alias="OIDC_USERINFO_ENDPOINT")]
oidc_introspection_endpoint: Annotated[str | None, Field(default=None, alias="OIDC_INTROSPECTION_ENDPOINT")]
ibm_cloud_api_key: Annotated[str | None, Field(default=None, alias="IBM_CLOUD_API_KEY")]

# JWT settings
jwt_algorithm: Annotated[str, Field(default="HS256", alias="JWT_ALGORITHM")]

@computed_field # type: ignore[misc]
@property
def database_url(self) -> URL:
"""Construct database URL from components."""
host = self.collectiondb_host
# In a test environment, if the host is localhost, it's likely a local dev setup
# where the test container is running on a Docker network.
# The service name in docker-compose is 'postgres', so we switch to that.
if self.testing and host == "localhost":
host = os.environ.get("DB_HOST", "postgres")

return URL.create(
drivername="postgresql",
username=self.collectiondb_user,
password=self.collectiondb_pass,
host=host,
port=self.collectiondb_port,
database=self.collectiondb_name,
)

# RBAC settings
rbac_mapping: Annotated[
dict[str, dict[str, list[str]]],
Expand Down
5 changes: 4 additions & 1 deletion backend/rag_solution/doc_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
import uuid

from core.config import get_settings
from vectordbs.data_types import Document, DocumentChunk, DocumentChunkMetadata, DocumentMetadata, Source


Expand All @@ -30,14 +31,16 @@ def _get_embeddings_for_doc_utils(text: str | list[str]) -> list[list[float]]:
Exception: If other unexpected errors occur
"""
# Import here to avoid circular imports
from core.config import get_settings
from core.custom_exceptions import LLMProviderError # pylint: disable=import-outside-toplevel
from sqlalchemy.exc import SQLAlchemyError # pylint: disable=import-outside-toplevel

from rag_solution.file_management.database import create_session_factory # pylint: disable=import-outside-toplevel
from rag_solution.generation.providers.factory import LLMProviderFactory # pylint: disable=import-outside-toplevel

# Create session and get embeddings in one clean flow
session_factory = create_session_factory()
settings = get_settings()
session_factory = create_session_factory(settings)
db = session_factory()

try:
Expand Down
52 changes: 11 additions & 41 deletions backend/rag_solution/file_management/database.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# backend/rag_solution/file_management/database.py
"""Database management for the RAG Modulo application."""
import logging
import os
from collections.abc import Generator

from core.config import Settings, get_settings
from sqlalchemy import URL, create_engine
from sqlalchemy import create_engine
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, declarative_base, sessionmaker

Expand All @@ -16,53 +16,23 @@
if not os.environ.get("PYTEST_CURRENT_TEST"):
logger.info("Database module is being imported")

# Get settings once at module level
settings = get_settings()

# Initialize database components with dependency injection
def create_database_url(settings: Settings | None = None) -> URL:
"""Create database URL from settings."""
if settings is None:
settings = get_settings()

host = os.environ.get("DB_HOST", settings.collectiondb_host)
# When running in Docker test container, use "postgres" as host
# When running locally, use "localhost" as host
if os.environ.get("PYTEST_CURRENT_TEST") and host == "localhost":
host = "postgres"

database_url = URL.create(
drivername="postgresql",
username=settings.collectiondb_user,
password=settings.collectiondb_pass,
host=host, # Use the adjusted host
port=settings.collectiondb_port,
database=settings.collectiondb_name,
)

if not os.environ.get("PYTEST_CURRENT_TEST"):
logger.debug(f"Database URL: {database_url}")

return database_url


# Create database components using default settings
# Create database components using settings
# This maintains backward compatibility while enabling dependency injection
_default_database_url = create_database_url()
engine = create_engine(_default_database_url, echo=not bool(os.environ.get("PYTEST_CURRENT_TEST")))
engine = create_engine(settings.database_url, echo=not bool(os.environ.get("PYTEST_CURRENT_TEST")))
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()
if not os.environ.get("PYTEST_CURRENT_TEST"):
logger.info("Base has been created")


def create_session_factory(settings: Settings | None = None) -> sessionmaker[Session]:
def create_session_factory(db_settings: Settings) -> sessionmaker:
"""Create a sessionmaker with injected settings for dependency injection."""
if settings is None:
settings = get_settings()

database_url = create_database_url(settings)
engine = create_engine(database_url, echo=not bool(os.environ.get("PYTEST_CURRENT_TEST")))
return sessionmaker(autocommit=False, autoflush=False, bind=engine)
db_engine = create_engine(db_settings.database_url, echo=not bool(os.environ.get("PYTEST_CURRENT_TEST")))
return sessionmaker(autocommit=False, autoflush=False, bind=db_engine)


def get_db() -> Generator[Session, None, None]:
Expand All @@ -83,11 +53,11 @@ def get_db() -> Generator[Session, None, None]:
logger.info("Creating a new database session.")
yield db
except SQLAlchemyError as e:
logger.error(f"A database error occurred: {e}", exc_info=True)
logger.error("A database error occurred: %s", e, exc_info=True)
db.rollback()
raise
except Exception as e:
logger.error(f"An unexpected error occurred: {e}", exc_info=True)
logger.error("An unexpected error occurred: %s", e, exc_info=True)
raise
finally:
db.close()
Expand Down
32 changes: 21 additions & 11 deletions backend/tests/unit/test_core_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from unittest.mock import patch

import pytest
from core.config import Settings
from backend.core.config import Settings


@pytest.mark.unit
Expand Down Expand Up @@ -80,15 +80,25 @@ def test_cot_token_budget_multiplier_type(self) -> None:
assert isinstance(settings.cot_token_budget_multiplier, float)
assert settings.cot_token_budget_multiplier > 0.0

def test_cot_integration_with_existing_settings(self, integration_settings: Any) -> None:
"""Test CoT settings integrate properly with existing configuration."""
settings = integration_settings

# Verify existing settings still work
assert hasattr(settings, "jwt_secret_key")
assert hasattr(settings, "rag_llm")
@pytest.mark.unit
class TestDatabaseUrlConfiguration:
"""Test database_url computed property in Settings."""

# Verify CoT settings are available
assert hasattr(settings, "cot_max_reasoning_depth")
assert hasattr(settings, "cot_reasoning_strategy")
assert hasattr(settings, "cot_token_budget_multiplier")
def test_database_url_construction(self) -> None:
"""Test that the database_url is constructed correctly from default settings."""
settings = Settings() # type: ignore[call-arg]
expected_url = (
f"postgresql://{settings.collectiondb_user}:{settings.collectiondb_pass}@"
f"{settings.collectiondb_host}:{settings.collectiondb_port}/{settings.collectiondb_name}"
)
assert str(settings.database_url) == expected_url

def test_database_url_testing_environment(self) -> None:
"""Test that the database_url switches host in a testing environment."""
settings = Settings(testing=True) # type: ignore[call-arg]
expected_url = (
f"postgresql://{settings.collectiondb_user}:{settings.collectiondb_pass}@"
f"postgres:{settings.collectiondb_port}/{settings.collectiondb_name}"
)
assert str(settings.database_url) == expected_url
Loading
Loading