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
10 changes: 9 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand Down
113 changes: 90 additions & 23 deletions backend/src/api/vdb_list.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,112 @@
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"])
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.")
1 change: 1 addition & 0 deletions backend/src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 4 additions & 21 deletions backend/src/main.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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
13 changes: 11 additions & 2 deletions backend/src/schemas/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import List, Dict, Any
from pydantic import BaseModel
from pydantic import BaseModel, Field


class HealthCheck(BaseModel):
Expand All @@ -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]
6 changes: 2 additions & 4 deletions backend/src/utils/ai_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)


Expand Down
21 changes: 15 additions & 6 deletions backend/src/utils/denodo_client.py
Original file line number Diff line number Diff line change
@@ -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]:
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 4 additions & 6 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"]
# The wrapper script is no longer needed.
# The original Nginx command is used directly.
CMD ["nginx", "-g", "daemon off;"]
20 changes: 7 additions & 13 deletions frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,20 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />

<meta
name="description"
content="VQLForge: Translate from various SQL dialects to VQL"
content="VQLForge - A tool for translating and validating SQL dialects to VQL"
/>
<meta name="keywords" content="VQL, Visual Query Language, SQL, Database, Query Builder, No Code" />
<meta name="author" content="Norman Banick" />

<!--
Apple touch icon.
-->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>VQLForge</title>
</head>
<body>
<noscript>You need to enable JavaScript to run VQLForge.</noscript>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!-- The React build process will automatically inject script tags here -->
</body>
</html>
</html>
Loading