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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ THEME_SWITCHING.md
VERCEL_CHANGES_SUMMARY.md
VERCEL_DEPLOYMENT_ASSESSMENT.md
VERCEL_MIGRATION_GUIDE.md
node_modules/
876 changes: 488 additions & 388 deletions report_analyst/core/cache_manager.py

Large diffs are not rendered by default.

104 changes: 104 additions & 0 deletions report_analyst/core/database_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""
Database Manager using SQLAlchemy

Provides unified database interface for both SQLite and PostgreSQL.
"""

import logging
import os
from contextlib import contextmanager
from pathlib import Path
from typing import Optional

from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine
from sqlalchemy.exc import SQLAlchemyError

logger = logging.getLogger(__name__)


class DatabaseManager:
"""Manages database connections using SQLAlchemy."""

def __init__(self, database_url: Optional[str] = None):
"""
Initialize database manager.

Args:
database_url: Database connection string. If None, uses SQLite default.
- SQLite: sqlite:///path/to/db
- PostgreSQL: postgresql://user:pass@host:port/db
"""
if database_url is None:
# Check DATABASE_URL environment variable first
database_url = os.getenv("DATABASE_URL")
if database_url is None:
# Default to SQLite
storage_path = os.getenv("STORAGE_PATH", "./storage")
db_path = str(Path(storage_path) / "cache" / "analysis.db")
# Ensure parent directory exists
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
database_url = f"sqlite:///{db_path}"

self.database_url = database_url
self._engine: Optional[Engine] = None
self._is_postgres = database_url.startswith(("postgresql://", "postgres://"))

logger.info(f"Initializing DatabaseManager with URL: {self._mask_url(database_url)}")
logger.info(f"Database type: {'PostgreSQL' if self._is_postgres else 'SQLite'}")

def _mask_url(self, url: str) -> str:
"""Mask password in database URL for logging."""
if "@" in url:
parts = url.split("@")
if len(parts) == 2:
user_pass = parts[0].split("://")[-1]
if ":" in user_pass:
user = user_pass.split(":")[0]
return url.replace(user_pass, f"{user}:***")
return url

def get_engine(self) -> Engine:
"""Get or create SQLAlchemy engine."""
if self._engine is None:
# For SQLite, use check_same_thread=False for compatibility
connect_args = {}
if not self._is_postgres:
connect_args["check_same_thread"] = False

self._engine = create_engine(
self.database_url,
connect_args=connect_args,
echo=False, # Set to True for SQL debugging
)
logger.info("SQLAlchemy engine created")
return self._engine

@contextmanager
def get_connection(self):
"""Get database connection (context manager)."""
engine = self.get_engine()
conn = engine.connect()
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()

def execute(self, query: str, params: Optional[dict] = None):
"""Execute a query and return result."""
with self.get_connection() as conn:
result = conn.execute(text(query), params or {})
return result

def is_postgres(self) -> bool:
"""Check if using PostgreSQL."""
return self._is_postgres

def is_sqlite(self) -> bool:
"""Check if using SQLite."""
return not self._is_postgres

118 changes: 118 additions & 0 deletions report_analyst/core/database_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""
Database Schema Definitions using SQLAlchemy

Defines all tables for the analysis cache system.
"""

from datetime import datetime

from sqlalchemy import (
Boolean,
Column,
DateTime,
Float,
ForeignKey,
Integer,
LargeBinary,
MetaData,
Table,
Text,
UniqueConstraint,
)

# Create metadata object
metadata = MetaData()

# Document chunks table
document_chunks = Table(
"document_chunks",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("file_path", Text, nullable=False),
Column("chunk_text", Text, nullable=False),
Column("chunk_size", Integer, nullable=False),
Column("chunk_overlap", Integer, nullable=False),
Column("embedding", LargeBinary, nullable=True), # BLOB/BYTEA
Column("metadata", Text, nullable=True), # JSON stored as text
Column("created_at", DateTime, default=datetime.now),
UniqueConstraint("file_path", "chunk_text", "chunk_size", "chunk_overlap"),
)

# Questions table
questions = Table(
"questions",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("question_id", Text, nullable=False),
Column("question_set", Text, nullable=False),
Column("question_text", Text, nullable=True),
Column("guidelines", Text, nullable=True),
UniqueConstraint("question_id", "question_set"),
)

# Analysis cache table
analysis_cache = Table(
"analysis_cache",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("file_path", Text, nullable=False),
Column("question_id", Text, nullable=False),
Column("chunk_size", Integer, nullable=False),
Column("chunk_overlap", Integer, nullable=False),
Column("top_k", Integer, nullable=False),
Column("model", Text, nullable=False),
Column("question_set", Text, nullable=False),
Column("result", Text, nullable=False), # JSON stored as text
Column("created_at", DateTime, default=datetime.now),
UniqueConstraint(
"file_path",
"question_id",
"chunk_size",
"chunk_overlap",
"top_k",
"model",
"question_set",
),
)

# Question analysis table
question_analysis = Table(
"question_analysis",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("file_path", Text, nullable=False),
Column("question_id", Integer, ForeignKey("questions.id"), nullable=False),
Column("model", Text, nullable=False),
Column("top_k", Integer, nullable=False),
Column("analysis_result", Text, nullable=False), # JSON stored as text
Column("version", Integer, default=1),
Column("created_at", DateTime, default=datetime.now),
UniqueConstraint("file_path", "question_id", "model", "top_k", "version"),
)

# Chunk relevance table
chunk_relevance = Table(
"chunk_relevance",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("question_analysis_id", Integer, ForeignKey("question_analysis.id"), nullable=False),
Column("document_chunk_id", Integer, ForeignKey("document_chunks.id"), nullable=False),
Column("chunk_order", Integer, nullable=False),
Column("similarity_score", Float, nullable=True),
Column("llm_score", Float, nullable=True),
Column("is_evidence", Boolean, nullable=False, default=False),
Column("evidence_order", Integer, nullable=True),
Column("metadata", Text, nullable=True), # JSON stored as text
UniqueConstraint("question_analysis_id", "document_chunk_id"),
)

# Indexes (defined separately for clarity)
# Note: SQLAlchemy doesn't support "IF NOT EXISTS" in CREATE INDEX directly,
# so we'll handle these in init_db() using raw SQL
indexes = [
# Index on file_path for document_chunks
"CREATE INDEX IF NOT EXISTS idx_file_path ON document_chunks(file_path)",
# Index on chunk parameters
"CREATE INDEX IF NOT EXISTS idx_chunk_params ON document_chunks(chunk_size, chunk_overlap)",
]

17 changes: 17 additions & 0 deletions report_analyst_enterprise/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Climate+Tech Open License for Good

This module (report_analyst_enterprise/) is licensed under the Climate+Tech Open License for Good.

This license allows use for research, educational, and non-commercial purposes.
Commercial use and dual licensing options are available upon request.

For the full text of the Climate+Tech Open License for Good, licensing inquiries,
or commercial/dual licensing options, please contact Climate+Tech:

- https://climateandtech.com/en/climate-ai-solutions/opensustainability-analysis-framework
- https://climateandtech.com/en/research-projects/sustainability-ai-benchmark-and-dataset

Copyright (c) 2025 Climate+Tech

This software is part of the Open Sustainability Analysis project.

9 changes: 9 additions & 0 deletions report_analyst_enterprise/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
Report Analyst Enterprise Module

Enterprise features including PostgreSQL support with pgvector.
Licensed under Climate+Tech Open License for Good.
"""

__version__ = "0.1.0"

Loading
Loading