diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9d855d3..b953ebd 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "vqlforge" -version = "0.1.0" +version = "0.1.5" description = "VQLForge Backend" readme = "README.md" requires-python = ">=3.12" @@ -10,10 +10,18 @@ dependencies = [ "fastapi[standard]>=0.115.12", "pydantic-ai>=0.1.3", "sqlalchemy>=2.0.40", + "structlog>=24.1.0", "uvicorn[standard]>=0.34.0", "sqlglot @ file:///app/sqlglot-vql" ] +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-fastapi-client>=0.6.0", + "pytest-mock>=3.12.0", +] + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/backend/src/api/vdb_list.py b/backend/src/api/vdb_list.py index 38245fa..bf803e1 100644 --- a/backend/src/api/vdb_list.py +++ b/backend/src/api/vdb_list.py @@ -1,23 +1,73 @@ import logging from fastapi import APIRouter, HTTPException -from src.schemas.common import VDBResponse +from src.schemas.common import VDBResponse, VDBConfigFile, VDBResponseItem from src.config import settings import os import yaml from typing import List, Dict, Any router = APIRouter() +logger = logging.getLogger(__name__) -def load_config_values() -> dict[list[str]]: - logging.info(f"Loading configuration from {settings.APP_VDB_CONF}") - with open(settings.APP_VDB_CONF, 'r') as f: - raw_config = yaml.safe_load(f) # Use safe_load for security - return raw_config + +def load_vdb_config_from_file() -> VDBConfigFile: + """ + Loads and parses the VDB configuration from the YAML file. + + Returns: + VDBConfigFile: A Pydantic model representing the loaded configuration. + + Raises: + HTTPException: If the file is not found, cannot be parsed, or is invalid. + """ + config_path = settings.APP_VDB_CONF + logger.info(f"Attempting to load VDB configuration from: {config_path}") + + if not os.path.exists(config_path): + logger.error(f"VDB configuration file not found at: {config_path}") + raise HTTPException( + status_code=500, + detail=f"Server configuration error: VDB config file not found at {config_path}" + ) + if not os.path.isfile(config_path): + logger.error(f"VDB configuration path is not a file: {config_path}") + raise HTTPException( + status_code=500, + detail=f"Server configuration error: VDB config path is not a file at {config_path}" + ) + + try: + with open(config_path, 'r') as f: + raw_config = yaml.safe_load(f) + # Validate the loaded YAML against the Pydantic model + vdb_config = VDBConfigFile.model_validate(raw_config) + logger.info("Successfully loaded and validated VDB configuration.") + return vdb_config + except yaml.YAMLError as e: + logger.error(f"Error parsing vdb_conf.yaml: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Server configuration error: Invalid YAML format in {config_path}. Details: {e}" + ) + except Exception as e: + logger.error(f"Unexpected error loading VDB config: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Server configuration error: Failed to load VDB config from {config_path}. Details: {e}" + ) -def transform_values(string_list: List[str]) -> List[Dict[str, str]]: - """Transforms a list of strings into a list of {'value': string, 'label': string}.""" - return [{"value": item, "label": item} for item in string_list] +def transform_vdb_strings_to_response_items(string_list: List[str]) -> List[VDBResponseItem]: + """ + Transforms a list of VDB names (strings) into a list of VDBResponseItem models. + + Args: + string_list: A list of strings, where each string is a VDB name. + + Returns: + A list of VDBResponseItem models, each with 'value' and 'label' set to the VDB name. + """ + return [VDBResponseItem(value=item, label=item) for item in string_list] @router.get("/vdbs", response_model=VDBResponse, tags=["VQL Forge"]) @@ -25,21 +75,38 @@ async def get_vdb_list() -> VDBResponse: """ Retrieves a list of VDBs from the configuration file. """ - if not settings.APP_VDB_CONF: - logging.error("No VDB CONFIG FILE") - raise HTTPException( - status_code=500, detail="VDB service error: config missing." - ) - logging.info( - f"Request received for /vdbs. Using config file: {os.path.abspath(settings.APP_VDB_CONF)}") - config = load_config_values() # Your config loading function + try: + # Load and validate the config using the Pydantic model + vdb_config = load_vdb_config_from_file() - if config['vdbs'] is None: - logging.warning("'vdbs' list empty in configuration. Returning empty list.") - return VDBResponse(results=[]) + # Transform the list of strings from the config into the desired response format + transformed_vdbs = transform_vdb_strings_to_response_items(vdb_config.vdbs) + + return VDBResponse(results=transformed_vdbs) + except HTTPException as http_exc: + # Re-raise HTTPExceptions as they are already formatted for FastAPI + raise http_exc + except Exception as e: + logger.error(f"Unhandled error in get_vdb_list: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="An unexpected error occurred while fetching VDB list.") + +@router.get("/vdbs", response_model=VDBResponse, tags=["VQL Forge"]) +async def get_vdb_list() -> VDBResponse: + """ + Retrieves a list of VDBs from the configuration file. + """ try: - return VDBResponse(results=transform_values(config['vdbs'])) + # Load and validate the config using the Pydantic model + vdb_config = load_vdb_config_from_file() + + # Transform the list of strings from the config into the desired response format + transformed_vdbs = transform_vdb_strings_to_response_items(vdb_config.vdbs) + + return VDBResponse(results=transformed_vdbs) + except HTTPException as http_exc: + # Re-raise HTTPExceptions as they are already formatted for FastAPI + raise http_exc except Exception as e: - logging.error(f"Error creating VDBResponse: {e}. Data was: {config}", exc_info=True) - raise HTTPException(status_code=500, detail="Error processing VDB list.") + logger.error(f"Unhandled error in get_vdb_list: {e}", exc_info=True) + raise HTTPException(status_code=500, detail="An unexpected error occurred while fetching VDB list.") diff --git a/backend/src/config.py b/backend/src/config.py index 81d79f5..8fa3527 100644 --- a/backend/src/config.py +++ b/backend/src/config.py @@ -12,6 +12,7 @@ class Settings(BaseSettings): DENODO_USER: str DENODO_PW: str GEMINI_API_KEY: str + AI_MODEL_NAME: str DATABASE_URL: str | None = None # Will be constructed APP_VDB_CONF: str diff --git a/backend/src/main.py b/backend/src/main.py index 59788ba..079e126 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,14 +1,14 @@ # src/main.py import logging from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware from src.api.router import api_router # Import the main router from src.config import settings # To access settings if needed for app config from src.db.session import engine as db_engine, init_db_engine # To ensure DB is up +from src.utils.logging_config import setup_logging # Import the new logging setup -# Configure logging -logging.basicConfig(level=logging.INFO) # You can make level configurable via settings +# Configure logging using the new setup function +setup_logging() logger = logging.getLogger(__name__) # Initialize services @@ -25,21 +25,7 @@ version="1.0.0", ) -# --- CORS Configuration --- -origins = [ - "http://localhost:4999", - "http://127.0.0.1:4999", - # Add production frontend origins here -] - -app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) -# --- End CORS Configuration --- +# CORS is now handled by the NGINX reverse proxy. # Include the main API router app.include_router(api_router) @@ -50,6 +36,3 @@ @app.get("/", tags=["Default"]) def read_root(): return {"message": "Welcome to VQLForge Backend!"} - -# For UVicorn ASGI server: -# Run with: uv uvicorn src.main:app --reload diff --git a/backend/src/schemas/common.py b/backend/src/schemas/common.py index 95d6a89..f287bee 100644 --- a/backend/src/schemas/common.py +++ b/backend/src/schemas/common.py @@ -1,5 +1,5 @@ from typing import List, Dict, Any -from pydantic import BaseModel +from pydantic import BaseModel, Field class HealthCheck(BaseModel): @@ -16,5 +16,14 @@ class QueryResponse(BaseModel): message: str | None = None +class VDBConfigFile(BaseModel): + vdbs: List[str] = Field(default_factory=list) # Changed from List[VDBConfigItem] + + +class VDBResponseItem(BaseModel): + value: str + label: str + + class VDBResponse(BaseModel): - results: List[Dict[str, str]] + results: List[VDBResponseItem] diff --git a/backend/src/utils/ai_analyzer.py b/backend/src/utils/ai_analyzer.py index 145e603..b8d8167 100644 --- a/backend/src/utils/ai_analyzer.py +++ b/backend/src/utils/ai_analyzer.py @@ -15,19 +15,17 @@ def _initialize_ai_agent(system_prompt: str, output_type: Type, tools: list[Tool] = []) -> Agent: if not settings.GEMINI_API_KEY: - logger.error("GEMINI_API_KEY environment variable not set.") + logger.error("AI_API_KEY environment variable not set.") raise HTTPException( status_code=500, detail="AI service configuration error: API key missing." ) return Agent( - # Consider making model name a config variable - "gemini-2.5-flash-preview-04-17", # "gemini-1.5-flash-latest" might be more current + settings.AI_MODEL_NAME, system_prompt=system_prompt, output_type=output_type, deps_type=set[str], tools=tools - # llm_kwargs={"api_key": settings.GEMINI_API_KEY} # pydantic-ai typically handles GOOGLE_API_KEY env var directly ) diff --git a/backend/src/utils/denodo_client.py b/backend/src/utils/denodo_client.py index 6a7a2a8..70d3122 100644 --- a/backend/src/utils/denodo_client.py +++ b/backend/src/utils/denodo_client.py @@ -1,17 +1,26 @@ import logging from fastapi import HTTPException -from sqlalchemy import text +from sqlalchemy import Engine, text from src.db.session import get_engine # Use the centralized engine logger = logging.getLogger(__name__) def get_available_views_from_denodo(vdb_name: str | None = None) -> list[str]: - # This needs actual implementation to query Denodo's metadata. - # Example: "LIST VIEWS ALL" or query information_schema views - # For now, returning a placeholder - logger.warning("get_available_views_from_denodo is using placeholder data.") - return ["placeholder_view1", f"placeholder_view_in_{vdb_name}" if vdb_name else "placeholder_view2"] + engine: Engine = get_engine() + vql = "SELECT database_name, name FROM get_views()" + try: + with engine.connect() as connection: + result = connection.execute(text(vql)) + views: list[dict[str, str]] = [dict(row._mapping) for row in result] + logger.info(f"Successfully retrieved Denodo functions: {len(views)} functions found.") + return views + except Exception as e: + logger.error(f"Error executing VQL query '{vql}' to get views: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail=f"Failed to retrieve views from Denodo: {str(e)}", + ) def get_denodo_functions_list() -> list[str]: diff --git a/docker-compose.yml b/docker-compose.yml index 3ae5a95..816876e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,7 @@ services: - DENODO_PW=${DENODO_PW} # AI KEY - GEMINI_API_KEY=${AI_API_KEY} + - AI_MODEL_NAME=${AI_MODEL_NAME} - CONTAINER_BACKEND_PORT=${CONTAINER_BACKEND_PORT:-5000} # Files - APP_VDB_CONF=/opt/vdb_conf.yaml diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 869c1a1..26f29f8 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,3 +1,4 @@ +# frontend/Dockerfile # ---- Stage 1: Build ---- # Use an official Node runtime as a parent image for the build stage FROM node:24-alpine AS builder @@ -30,9 +31,6 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf # Expose port 4999 for Nginx EXPOSE 4999 -# Add a wrapper script -COPY entrypoint-wrapper.sh /entrypoint-wrapper.sh -RUN chmod +x /entrypoint-wrapper.sh - -# Use the wrapper script as the command -CMD ["/entrypoint-wrapper.sh"] \ No newline at end of file +# The wrapper script is no longer needed. +# The original Nginx command is used directly. +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/public/index.html b/frontend/public/index.html index 107db8e..723cc3d 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -5,26 +5,20 @@ - - - - - - - + + + +