# Project export: KleanSQL

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: An error-resilient Natural language to SQL assistant
- Devpost: https://devpost.com/software/kleansql
- GitHub: https://github.com/shatayu1210/CalHack_CleanSQL.git
- Video: https://www.youtube.com/embed/kK30QcI6veI?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — shatayu1210 (3 commits)

## Devpost submission (written by the team)

### Inspiration

Every data analyst knows the pain: you have a CSV file with thousands of rows, and you need insights fast. Traditional SQL requires learning complex syntax, and even then, you're often stuck debugging queries for hours. We wanted to bridge the gap between natural language and data analysis, making powerful analytics accessible to everyone—not just SQL experts. The inspiration came from watching teammates struggle with basic data questions during hackathons. "What's the average grade for female students?" shouldn't require writing SELECT AVG(G1) FROM students WHERE sex = 'F'. It should be as simple as asking the question in plain English.

### What it does

KleanSQL transforms messy CSV files into intelligent, queryable datasets through a beautiful web interface. Upload any CSV or Excel file, and instantly start asking questions in natural language: "What's the average salary by department?" "Show me students with grades above 15" "Which cities have the highest population?" Behind the scenes, KleanSQL: Profiles your data using DuckDB for lightning-fast analysis Generates clean SQL automatically via Claude Sonnet 4 Handles missing data with intelligent imputation strategies Provides robust queries that work even with messy datasets Offers follow-up suggestions to deepen your analysis The platform delivers both raw SQL (for transparency) and robust SQL (with data cleaning) so you can trust your results and learn from the generated queries.

### How we built it

Architecture Overview: Our system follows a sophisticated pipeline that mirrors the sequence diagram: Data Ingestion & Profiling (profiler.py) DuckDB-powered CSV analysis with automatic type detection Comprehensive statistical profiling (null ratios, outliers, distributions) Semantic type classification (numeric, categorical, datetime, boolean) Optional Weaviate integration for vector-based column search Data Ingestion & Profiling (profiler.py) DuckDB-powered CSV analysis with automatic type detection Comprehensive statistical profiling (null ratios, outliers, distributions) Semantic type classification (numeric, categorical, datetime, boolean) Optional Weaviate integration for vector-based column search LLM Integration (llm_integration.py) Anthropic Claude Sonnet 4 for natural language understanding Dual SQL generation: raw queries + robust queries with data cleaning Intelligent prompt engineering for consistent, safe SQL output JSON-structured responses with metadata and follow-up questions LLM Integration (llm_integration.py) Anthropic Claude Sonnet 4 for natural language understanding Dual SQL generation: raw queries + robust queries with data cleaning Intelligent prompt engineering for consistent, safe SQL output JSON-structured responses with metadata and follow-up questions Query Execution (DataAssistant class) DuckDB in-memory database for sub-second query performance SQL sanitization and safety checks (read-only mode) Automatic result formatting and truncation Comprehensive error handling with user-friendly messages Query Execution (DataAssistant class) DuckDB in-memory database for sub-second query performance SQL sanitization and safety checks (read-only mode) Automatic result formatting and truncation Comprehensive error handling with user-friendly messages Web Interface (app.py) Streamlit-based modern UI with custom CSS styling Real-time query processing with progress indicators Interactive SQL preview modals Responsive design with dark theme Web Interface (app.py) Streamlit-based modern UI with custom CSS styling Real-time query processing with progress indicators Interactive SQL preview modals Responsive design with dark theme CLI Support (data_assistant/cli.py) Command-line interface for power users Automated data cleaning and imputation Interactive Q&A sessions Batch processing capabilities CLI Support (data_assistant/cli.py) Command-line interface for power users Automated data cleaning and imputation Interactive Q&A sessions Batch processing capabilities Key Technologies: DuckDB: High-performance analytical database Anthropic Claude: Advanced language model for SQL generation Streamlit: Rapid web app development Weaviate: Vector database for semantic search (optional) Pandas: Data manipulation and analysis Docker: Containerized deployment with Weaviate services

### Challenges we ran into

1. SQL Safety & Validation Challenge: Preventing malicious SQL injection while allowing complex analytical queries Solution: Implemented comprehensive SQL sanitization, keyword filtering, and read-only mode enforcement. Added column existence validation and query complexity checks. 2. Data Quality & Missing Values Challenge: Real-world datasets are messy with missing values, inconsistent types, and outliers Solution: Built intelligent imputation pipeline with policies based on data types and null ratios. Created both "raw" and "robust" query modes—raw for transparency, robust for production use. 3. LLM Response Consistency Challenge: Getting reliable, parseable SQL from language models Solution: Developed sophisticated prompt engineering with JSON-structured responses, fallback mechanisms, and extensive response normalization. Added regex-based SQL extraction for edge cases. 4. Performance at Scale Challenge: Fast query execution on large datasets Solution: Leveraged DuckDB's columnar storage and vectorized execution. Implemented result truncation and efficient memory management. 5. User Experience Challenge: Making complex data analysis feel intuitive Solution: Created progressive disclosure with SQL previews, follow-up question suggestions, and comprehensive error messages. Built both web and CLI interfaces for different user preferences.

### Accomplishments we're proud of

Technical Innovations: Dual Query Generation: Raw SQL for learning + robust SQL for production Intelligent Data Profiling: Comprehensive statistical analysis with semantic type detection Vector-Based Column Search: Optional Weaviate integration for semantic column discovery Automated Data Cleaning: Smart imputation strategies based on data characteristics User Experience: Natural Language Interface: Ask questions in plain English, get SQL + results Beautiful Web UI: Modern dark theme with responsive design Progressive Disclosure: Show SQL queries for transparency and learning Follow-up Suggestions: AI-powered question recommendations Performance: Sub-second Query Execution: DuckDB's vectorized engine Memory Efficient: Streaming data processing with result truncation Scalable Architecture: Handles datasets from hundreds to millions of rows Developer Experience: Multiple Interfaces: Web app, CLI, and programmatic API Docker Deployment: One-command setup with Weaviate services Comprehensive Testing: Unit tests and integration validation Extensible Design: Modular architecture for easy feature additions

### What we learned

AI Integration Insights: Prompt Engineering is Critical: Small changes in prompts dramatically affect output quality Fallback Strategies Matter: Always have backup plans when LLMs fail Structured Outputs Work: JSON responses are more reliable than free-form text Data Engineering Lessons: Profile First, Query Later: Understanding your data structure is crucial for good SQL generation Handle Edge Cases: Real-world data is messier than you think Performance Matters: Users expect instant results, even on large datasets User Experience Discoveries: Transparency Builds Trust: Showing generated SQL helps users understand and learn Progressive Disclosure: Don't overwhelm users with all features at once Error Messages Matter: Clear, actionable error messages improve user experience significantly Technical Architecture: DuckDB is Incredible: Fast, SQL-compliant, and perfect for analytical workloads Modular Design Pays Off: Separate concerns (profiling, LLM, execution) makes testing and debugging easier Containerization Simplifies Deployment: Docker Compose makes complex setups trivial

### What's next

Short-term Enhancements: Multi-table Joins: Support for relational queries across multiple CSV files Advanced Visualizations: Charts and graphs for query results Query History: Save and replay previous analyses Export Capabilities: Download results as CSV, PDF reports Medium-term Features: Real-time Data Sources: Connect to databases, APIs, and streaming data Collaborative Analysis: Share datasets and queries with team members Custom Imputation Rules: User-defined data cleaning strategies Query Optimization: Automatic query performance tuning Long-term Vision: Enterprise Features: Role-based access, audit logs, and compliance tools AI-Powered Insights: Automatic anomaly detection and trend analysis Natural Language Reports: Generate executive summaries from data Integration Ecosystem: Connect with popular BI tools and data platforms Open Source Goals: Community Contributions: Open-source the core engine for community development Plugin Architecture: Allow third-party extensions and custom functions Educational Resources: Tutorials and examples for learning SQL through natural language KleanSQL represents the future of data analysis—where powerful insights are just a question away. We're excited to continue building tools that make data accessible to everyone, not just SQL experts. Built with ❤️ at CalHacks 12.0

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 91 KB.
- Anthropic (technology) — detected in the code
- Python (language) — detected in the code

## Codebase structure (from repository index)

### Files (28 of 28)

```
.gitignore
app.py
commands.txt
data_assistant.py
data_assistant/__init__.py
data_assistant/cli.py
docker-compose.yml
llm_integration.py
profiler.py
requirements.txt
student_messy.csv
test_data.csv
test_llm_integration.py
test_output_enhanced/columns/age.json
test_output_enhanced/columns/city.json
test_output_enhanced/columns/name.json
test_output_enhanced/columns/salary.json
test_output_enhanced/dataset_profile.json
test_output_final/columns/age.json
test_output_final/columns/city.json
test_output_final/columns/name.json
test_output_final/columns/salary.json
test_output_final/dataset_profile.json
test_output/columns/age.json
test_output/columns/city.json
test_output/columns/name.json
test_output/columns/salary.json
test_output/dataset_profile.json
```

### Dependencies

- requirements.txt: anthropic, dotenv, duckdb@>=1.0.0, orjson@>=3.9.10, sqlparse, weaviate-client@>=4.6.0

### Recent commits (newest first)

- Deliverable
- Commands file and python requirements for profiler
- Profiler with RAG support
- Initial Commit

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
duckdb>=1.0.0
orjson>=3.9.10
weaviate-client>=4.6.0
anthropic
dotenv
sqlparse
```

### docker-compose.yml

```yaml
services:
  weaviate:
    image: cr.weaviate.io/semitechnologies/weaviate:1.25.7
    ports:
      - "8080:8080"
    environment:
      QUERY_DEFAULTS_LIMIT: "25"
      AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true"
      PERSISTENCE_DATA_PATH: "/var/lib/weaviate"
      CLUSTER_HOSTNAME: "node1"
      ENABLE_MODULES: "bm25,text2vec-transformers"
      TRANSFORMERS_INFERENCE_API: "http://t2v:8080"
    volumes:
      - weaviate_data:/var/lib/weaviate
    depends_on:
      - t2v
    restart: unless-stopped

  t2v:
    image: cr.weaviate.io/semitechnologies/transformers-inference:sentence-transformers-all-MiniLM-L6-v2
    environment:
      ENABLE_CUDA: "0"
    deploy:
      resources:
        limits:
          memory: 4G
    restart: unless-stopped

volumes:
  weaviate_data:
    driver: local

```

### app.py

```python
import html
import os
import tempfile
from typing import Any, Dict, Optional, Tuple

import pandas as pd
import streamlit as st

from llm_integration import DataAssistant
from profiler import profile_csv

st.set_page_config(
    page_title="CleanSQL Assistant",
    page_icon="🧼",
    layout="wide",
)

CUSTOM_PAGE_STYLE = """
<style>
:root {
    --bg-950: #020617;
    --bg-900: #0b1220;
    --bg-800: #111b2e;
    --bg-700: #1a2840;
    --text-100: #f8fafc;
    --text-200: #e2e8f0;
    --text-400: #cbd5f5;
    --text-muted: rgba(226, 232, 240, 0.7);
    --accent-sky: #38bdf8;
    --accent-indigo: #6366f1;
    --accent-indigo-dark: #4338ca;
    --pill-bg: rgba(99, 102, 241, 0.22);
    --pill-border: rgba(129, 140, 248, 0.3);
}

body {
    color: var(--text-200);
    background: var(--bg-900);
}

[data-testid="stAppViewContainer"] {
    background:
        radial-gradient(circle at 15% 20%, rgba(79, 70, 229, 0.18), transparent 55%),
        radial-gradient(circle at 85% 25%, rgba(14, 165, 233, 0.14), transparent 60%),
        var(--bg-900);
    color: var(--text-200);
}

[data-testid="stSidebar"] {
    background: linear-gradient(180deg, rgba(15, 23, 42, 0.92), rgba(17, 24, 39, 0.95));
    color: var(--text-200);
}

[data-testid="stAppViewContainer"] * {
    color: inherit;
}

[data-testid="stHeader"] {
    background: transparent;
}

.hero {
    padding: 2.75rem 3rem;
    border-radius: 1.4rem;
    background: linear-gradient(135deg, rgba(14, 23, 42, 0.95), rgba(79, 70, 229, 0.8));
    color: white;
    margin-bottom: 2rem;
    box-shadow: 0 28px 60px rgba(2, 6, 23, 0.45);
}

.hero-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    border-radius: 999px;
    padding: 0.35rem 1.1rem;
    background: #048BCF;
    font-size: 0.95rem;
    font-weight: 800;
    letter-spacing: 0.07em;
    margin-bottom: 0.75rem;
    color: rgba(255, 255, 255, 0.95);
}

.hero h1 {
    font-size: 2.6rem;
    font-weight: 700;
    margin-bottom: 0.65rem;
    color: var(--text-100);
}

.hero p {
    font-size: 1.06rem;
    max-width: 42rem;
    line-height: 1.65;
    color: rgba(241, 245, 249, 0.88);
}

.section-title {
    font-size: 1.25rem;
    font-weight: 700;
    margin-bottom: 0.85rem;
    color: var(--text-100);
    letter-spacing: 0.01em;
}

.helper-text {
    font-size: 0.9rem;
    color: var(--text-muted);
    margin-top: 0.3rem;
}

.assistant-answer {
    font-size: 1rem;
    line-height: 1.7;
    color: var(--text-200);
    margin-bottom: 0.65rem;
}

.assistant-notes {
    font-size: 0.87rem;
    color: var(--text-muted);
}

.dataset-meta {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 0.75rem;
}

.dataset-meta .pill {
    background: var(--pill-bg);
    border: 1px solid var(--pill-border);
    border-radius: 999px;
    padding: 0.45rem 1rem;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-100);
}

div[data-testid="stAlert"] {
    border-radius: 1rem;
    box-shadow: 0 16px 35px rgba(2, 6, 23, 0.4);
    background: rgba(15, 23, 42, 0.85);
    color: var(--text-200);
}

.follow-up-list {
    padding-left: 1.2rem;
    margin-bottom: 0;
}

.follow-up-list li {
    line-height: 1.65;
    color: var(--text-200);
}

[data-testid="stTextArea"] textarea {
    border-radius: 1rem !important;
    border: 1px solid rgba(99, 102, 241, 0.45) !important;
    box-shadow: none !important;
    font-size: 1rem;
    color: var(--text-100) !important;
    background: rgba(17, 24, 39, 0.85) !important;
}

[data-testid="stTextArea"] textarea:focus {
    border-color: rgba(99, 102, 241, 0.85) !important;
    box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.35) !important;
}

.stButton>button {
    border-radius: 999px;
    padding: 0.7rem 1.9rem;
    background: linear-gradient(135deg, var(--accent-indigo), var(--accent-sky));
    border: none;
    color: white;
    font-weight: 600;
    letter-spacing: 0.01em;
    box-shadow: 0 18px 32px rgba(14, 23, 42, 0.55);
}

.stButton>button:hover {
    filter: brightness(1.1);
}

.stDataFrame {
    border-radius: 1rem;
    overflow: hidden;
    border: 1px solid rgba(148, 163, 184, 0.2);
    background: rgba(15, 23, 42, 0.7);
}
</style>
"""

st.markdown(CUSTOM_PAGE_STYLE, unsafe_allow_html=True)

HAS_STREAMLIT_MODAL = hasattr(st, "modal")


def format_bytes(num_bytes: int) -> str:
    """Return a human readable string for a byte count."""
    units = ["B", "KB", "MB", "GB", "TB"]
    value = float(num_bytes)
    for unit in units:
        if value < 1024 or unit == units[-1]:
            return f"{value:,.1f} {unit}"
        value /= 1024
    return f"{value:,.1f} TB"


def render_dataset_summary(dataframe: pd.DataFrame, filename: Optional[str]) -> None:
    """Show dataset headline stats."""
    rows, cols = dataframe.shape
    memory = format_bytes(dataframe.memory_usage(deep=True).sum())
    with st.container():
        st.markdown(
            '<div class="section-title">Dataset overview</div>', unsafe_allow_html=True
        )
        st.markdown(
            f"<p><strong>{filename or 'Uploaded data'}</strong></p>",
            unsafe_allow_html=True,
        )
        st.markdown(
            f"""
            <div class="dataset-meta">
                <span class="pill">{rows:,} rows</span>
                <span class="pill">{cols:,} columns</span>
                <span class="pill">{memory}</span>
            </div>
            """,
            unsafe_allow_html=True,
        )


def parse_question_answer(raw_answer: Any) -> Tuple[Optional[str], str]:
    """Extract question and answer segments from assistant output, if present."""
    if raw_answer is None:
        return None, ""

    text = str(raw_answer).strip()
    lower_text = text.lower()
    question_marker = "question:"
    answer_marker = "answer:"

    question: Optional[str] = None
    answer = text

    if answer_marker in lower_text:
        answer_idx = lower_text.find(answer_marker)
        answer_start = answer_idx + len(answer_marker)
   
[truncated — 22437 more characters]
```

### data_assistant/cli.py

```python
#!/usr/bin/env python3
"""Command-line interface for the AI Data Assistant."""

import argparse
import json
import os
import tempfile
from contextlib import suppress
from typing import Dict, List, Optional, Tuple

import pandas as pd

from llm_integration import DataAssistant
from profiler import profile_csv


TRUE_VALUES = {"true", "t", "yes", "y", "1"}
FALSE_VALUES = {"false", "f", "no", "n", "0"}
BOOLEAN_COLUMNS = {
    "schoolsup",
    "famsup",
    "paid",
    "activities",
    "nursery",
    "higher",
    "internet",
    "romantic",
}
NUMERIC_COLUMNS = [
    "age",
    "Medu",
    "Fedu",
    "traveltime",
    "studytime",
    "failures",
    "famrel",
    "freetime",
    "goout",
    "Dalc",
    "Walc",
    "health",
    "absences",
    "G1",
    "G2",
    "G3",
]
INTEGER_COLUMNS = set(NUMERIC_COLUMNS)
DEFAULT_CATEGORY_FILL = "Unknown"
SUMMARY_PREVIEW_LIMIT = 4


def _format_numeric_value(value: object) -> str:
    if isinstance(value, int):
        return str(value)
    if isinstance(value, float):
        if value.is_integer():
            return str(int(value))
        return f"{value:.2f}"
    return str(value)


def _coerce_boolean_series(series: pd.Series) -> Tuple[pd.Series, Optional[bool], int]:
    """Normalise boolean-like columns and return (series, fill_value, missing_count)."""
    normalized = []
    for value in series:
        if pd.isna(value):
            normalized.append(pd.NA)
            continue
        if isinstance(value, bool):
            normalized.append(value)
            continue
        if isinstance(value, (int, float)) and not pd.isna(value):
            if value == 1:
                normalized.append(True)
                continue
            if value == 0:
                normalized.append(False)
                continue
        if isinstance(value, str):
            stripped = value.strip()
            if not stripped:
                normalized.append(pd.NA)
                continue
            lowered = stripped.lower()
            if lowered in TRUE_VALUES:
                normalized.append(True)
                continue
            if lowered in FALSE_VALUES:
                normalized.append(False)
                continue
        normalized.append(pd.NA)

    normal_series = pd.Series(normalized, index=series.index, dtype="object")
    missing_count = int(normal_series.isna().sum())

    dropna = normal_series.dropna()
    if dropna.empty:
        fill_value = False
    else:
        fill_value = bool(dropna.mode().iloc[0])

    normal_series.loc[normal_series.isna()] = fill_value
    return normal_series.astype(bool), fill_value, missing_count


def _coerce_numeric_series(
    series: pd.Series, as_integer: bool
) -> Tuple[pd.Series, float, int]:
    """Coerce numeric column, fill with median (rounded for ints)."""
    cleaned = series.apply(lambda x: x.strip() if isinstance(x, str) else x)
    numeric = pd.to_numeric(cleaned, errors="coerce")
    missing_count = int(numeric.isna().sum())

    median_value = numeric.median(skipna=True)
    if pd.isna(median_value):
        median_value = 0.0

    if as_integer:
        fill_value = int(round(float(median_value)))
        reported_value = fill_value
    else:
        fill_value = float(median_value)
        reported_value = fill_value

    numeric = numeric.fillna(fill_value)

    if as_integer:
        numeric = numeric.round().astype(int)

    return numeric, reported_value, missing_count


def _coerce_categorical_series(series: pd.Series) -> Tuple[pd.Series, str, int]:
    """Standardise categorical column and fill with mode."""
    cleaned = series.astype("string").str.strip()
    cleaned = cleaned.replace("", pd.NA)
    missing_count = int(cleaned.isna().sum())

    mode_values = cleaned.dropna().mode()
    if mode_values.empty:
        fill_value = DEFAULT_CATEGORY_FILL
    else:
        fill_value = str(mode_values.iloc[0])

    cleaned = cleaned.fillna(fill_value)
    return cleaned.astype(str), fill_value, missing_count


def _summarise_section(
    title: str,
    items: Dict[str, Dict[str, object]],
    value_formatter,
) -> Optional[str]:
    if not items:
        return None
    entries = sorted(items.items(), key=lambda kv: kv[0])
    formatted: List[str] = []
    for idx, (column, meta) in enumerate(entries):
        if idx >= SUMMARY_PREVIEW_LIMIT:
            remaining = len(entries) - SUMMARY_PREVIEW_LIMIT
            formatted.append(f"... (+{remaining} more)")
            break
        formatted.append(f"{column}={value_formatter(meta)}")
    return f"{title}: " + ", ".join(formatted)


def _format_report_lines(report: Dict[str, Dict[str, Dict[str, object]]]) -> List[str]:
    """Return human-readable summary lines for console output."""
    lines: List[str] = []

    numeric_line = _summarise_section(
        "Numeric medians",
        report.get("numeric", {}),
        lambda meta: f"{_format_numeric_value(meta['fill'])} (filled {meta['missing']} rows)",
    )
    if numeric_line:
        lines.append(numeric_line)

    boolean_line = _summarise_section(
        "Boolean mode",
        report.get("boolean", {}),
        lambda meta: f"{'True' if meta['fill'] else 'False'} (filled {meta['missing']} rows)",
    )
    if boolean_line:
        lines.append(boolean_line)

    categorical_line = _summarise_section(
        "Categorical mode",
        report.get("categorical", {}),
        lambda meta: f"{meta['fill']} (filled {meta['missing']} rows)",
    )
    if categorical_line:
        lines.append(categorical_line)

    return lines


def _prepare_dataset(csv_path: str) -> Tuple[str, Dict[str, Dict[str, Dict[str, object]]]]:
    """Load CSV, apply imputation rules, and persist to a temporary cleaned file."""
    df = pd.read_csv(csv_path)

    imputation_report: Dict[str, Dict[str, Dict[str, object]]] = {
        "numeric": {},
        "boolean": {},
        "categorical": {},
    }

    for column in BOOLEAN_COLUMNS:
        if column in df.columns:
            seri
[truncated — 7122 more characters]
```

### data_assistant.py

```python
#!/usr/bin/env python3
"""Backward-compatible entry point for the AI Data Assistant CLI."""

from data_assistant import main

if __name__ == "__main__":
    main()

```

### test_llm_integration.py

```python
#!/usr/bin/env python3
"""
Test script for LLM integration
This script tests the LLM integration without requiring API keys
"""
import json
import os

from llm_integration import AnthropicSQLGenerator


def test_sql_generation():
    """Test SQL generation with sample profile"""
    
    # Sample profile data
    sample_profile = {
        "dataset": {
            "filename": "test_data.csv",
            "row_count": 5,
            "column_count": 4
        },
        "columns": [
            {
                "name": "name",
                "duckdb_type": "VARCHAR",
                "semantic_type": "category",
                "examples": ["John", "Jane", "Bob"]
            },
            {
                "name": "age", 
                "duckdb_type": "BIGINT",
                "semantic_type": "numeric",
                "examples": [25, 30, 35]
            },
            {
                "name": "city",
                "duckdb_type": "VARCHAR", 
                "semantic_type": "category",
                "examples": ["New York", "San Francisco", "Chicago"]
            },
            {
                "name": "salary",
                "duckdb_type": "BIGINT",
                "semantic_type": "numeric", 
                "examples": [50000, 75000, 60000]
            }
        ]
    }
    
    print("🧪 Testing LLM Integration...")
    
    # Test 1: Check if API key is configured
    api_key = os.getenv("ANTHROPIC_API_KEY")
    if not api_key or api_key == "your_api_key_here":
        print("❌ Anthropic API key not configured!")
        print("Please set ANTHROPIC_API_KEY in your .env file")
        return False
    
    print("✅ API key configured")
    
    # Test 2: Test SQL generator initialization
    try:
        generator = AnthropicSQLGenerator()
        print("✅ SQL generator initialized")
    except Exception as e:
        print(f"❌ Failed to initialize SQL generator: {e}")
        return False
    
    # Test 3: Test fallback schema generation
    try:
        fallback_schema = generator._fallback_schema(sample_profile)
        print("✅ Fallback schema generation works")
        print("Sample fallback schema:")
        print(fallback_schema)
    except Exception as e:
        print(f"❌ Fallback schema generation failed: {e}")
        return False
    
    print("\n🎉 All tests passed! LLM integration is ready.")
    print("\nTo use the full features:")
    print("1. Set your Anthropic API key in .env file")
    print("2. Run: python3 data_assistant.py --csv your_file.csv")
    print("3. Or run: python3 profiler.py --csv your_file.csv --out output_dir")
    
    return True

if __name__ == "__main__":
    test_sql_generation()

```

### profiler.py

```python
#!/usr/bin/env python3
import argparse
import hashlib
import json
import os
import sys
import time
from typing import Any, Dict, List, Optional

import duckdb

try:
    import orjson as fastjson
    def dumps(obj):
        return fastjson.dumps(obj, option=fastjson.OPT_INDENT_2).decode()
except Exception:
    def dumps(obj):
        return json.dumps(obj, indent=2, ensure_ascii=False)

W_CLIENT = None
EMBED = None


# Set up connectivity to a local or remote Weaviate instance and, if requested,
# load a lightweight embedding model. This makes the profiler capable of pushing
# profiles into a RAG store with vectors, without forcing you to install heavy
# ML dependencies unless you actually want embeddings.
def init_weaviate(url: Optional[str], api_key: Optional[str], embed_model: Optional[str]):
    global W_CLIENT, EMBED
    if not url:
        return
    try:
        import weaviate
        if api_key:
            W_CLIENT = weaviate.Client(url=url, auth_client_secret=weaviate.AuthApiKey(api_key))
        else:
            W_CLIENT = weaviate.Client(url=url)
    except Exception as e:
        print(f"[warn] Weaviate init failed: {e}", file=sys.stderr)
        W_CLIENT = None
    if embed_model:
        try:
            from sentence_transformers import SentenceTransformer
            EMBED = SentenceTransformer(embed_model)
        except Exception as e:
            print(f"[warn] embedding model load failed: {e}", file=sys.stderr)
            EMBED = None


# Create the collection (aka class) in Weaviate on the fly if it doesn't exist.
# We keep the schema minimal and pragmatic so you can start searching right away
# and iterate later without ceremony.
def ensure_class(class_name: str):
    if not W_CLIENT or not class_name:
        return
    try:
        schema = W_CLIENT.schema.get()
        names = {c.get("class") for c in schema.get("classes", [])}
        if class_name in names:
            return
    except Exception:
        pass
    try:
        class_obj = {
            "class": class_name,
            "description": "CleanSQL column profiles for hybrid search",
            "vectorizer": "text2vec-transformers",
            "properties": [
                {"name": "dataset", "dataType": ["text"]},
                {"name": "column", "dataType": ["text"]},
                {"name": "duckdb_type", "dataType": ["text"]},
                {"name": "semantic_type", "dataType": ["text"]},
                {"name": "null_ratio", "dataType": ["number"]},
                {"name": "distinct_count", "dataType": ["number"]},
                {"name": "search_text", "dataType": ["text"]},
                {"name": "profile_json", "dataType": ["text"]}
            ]
        }
        W_CLIENT.schema.create_class(class_obj)
    except Exception as e:
        print(f"[warn] Weaviate class create failed: {e}", file=sys.stderr)


# Turn a short piece of text into a vector using the local model if available.
# If no model is loaded, we simply skip vectors so the flow remains optional
# and never blocks your basic profiling.
def embed_text(text: str) -> Optional[List[float]]:
    if EMBED is None:
        return None
    try:
        v = EMBED.encode([text], normalize_embeddings=True)
        return v[0].tolist()
    except Exception as e:
        print(f"[warn] embedding failed: {e}", file=sys.stderr)
        return None


# Quick helper to fetch the file size. Not all filesystems behave the same,
# so we guard with a try/except and return -1 if anything looks off.
def size_bytes(path: str) -> int:
    try:
        return os.path.getsize(path)
    except Exception:
        return -1


# Read a CSV with DuckDB and compute a compact but useful profile. The goal is
# speed and clarity: types, nulls, distincts, numeric stats with outlier bounds,
# categorical top values, and basic datetime ranges. No data is modified—this
# is purely descriptive so you can build robust plans later.
def profile_csv(csv_path: str,
                sample_rows: Optional[int],
                topk: int,
                approx_distinct: bool,
                date_try_cast_for_varchar: bool) -> Dict[str, Any]:
    con = duckdb.connect()
    con.execute("PRAGMA threads=" + str(os.cpu_count() or 8))
    file_lower = csv_path.lower()
    auto_opts = []
    if sample_rows:
        auto_opts.append(f"SAMPLE_SIZE={sample_rows}")
    if file_lower.endswith('.parquet'):
        src_sql = f"SELECT * FROM parquet_scan('{csv_path}')"
    else:
        src_sql = f"SELECT * FROM read_csv_auto('{csv_path}', {', '.join(auto_opts)})" if auto_opts else f"SELECT * FROM read_csv_auto('{csv_path}')"
    con.execute(f"CREATE VIEW v AS {src_sql}")
    schema_rows = con.execute("PRAGMA table_info('v')").fetchall()
    columns = [{"name": r[1], "duckdb_type": r[2]} for r in schema_rows]
    row_count = con.execute("SELECT count(*) FROM v").fetchone()[0]
    col_count = len(columns)
    dup_rows = 0
    try:
        hash_expr = "hash(" + ", ".join([f'"{c["name"]}"' for c in columns]) + ")"
        dup_rows = con.execute(f"""
            WITH h AS (
              SELECT {hash_expr} AS h
              FROM v
            )
            SELECT COALESCE(SUM(cnt - 1), 0)
            FROM (SELECT h, COUNT(*) AS cnt FROM h GROUP BY h) t
            WHERE cnt > 1
        """).fetchone()[0]
    except Exception:
        dup_rows = None
    profiles: List[Dict[str, Any]] = []
    for col in columns:
        name = col["name"]
        dtype = col["duckdb_type"].upper()
        nulls, non_nulls = con.execute(f"""
            SELECT SUM(CASE WHEN "{name}" IS NULL THEN 1 ELSE 0 END),
                   SUM(CASE WHEN "{name}" IS NOT NULL THEN 1 ELSE 0 END)
            FROM v
        """).fetchone()
        null_ratio = (nulls or 0) / row_count if row_count else 0.0
        if approx_distinct:
            distinct_count = con.execute(f'SELECT approx_count_distinct("{name}") FROM v').fetchone()[0]
        else:
            try:
                distinct_count = con.execut
[truncated — 11755 more characters]
```

### data_assistant/__init__.py

```python
"""Exports for the AI Data Assistant package."""

from .cli import main, run

__all__ = ["main", "run"]

```

### llm_integration.py

```python
#!/usr/bin/env python3
import json
import os
import re
from typing import Any, Dict, List, Optional

import pandas as pd

import anthropic
from dotenv import load_dotenv

load_dotenv()

class AnthropicSQLGenerator:
    def __init__(self):
        self.client = anthropic.Anthropic(
            api_key=os.getenv("ANTHROPIC_API_KEY")
        )

    @staticmethod
    def _normalize_sql_output(raw_text: str) -> str:
        """Extract a usable SQL statement from LLM output."""
        if not raw_text:
            return raw_text

        text = raw_text.strip()

        # Prefer content inside fenced code blocks if present
        code_blocks = re.findall(r"```(?:sql)?\s*(.*?)```", text, flags=re.IGNORECASE | re.DOTALL)
        if code_blocks:
            text = code_blocks[0].strip()

        # Strip leading explanatory sentences before the actual SQL
        sql_match = re.search(
            r"(?is)\b(WITH|SELECT|INSERT|UPDATE|DELETE)\b.*", text
        )
        if sql_match:
            text = sql_match.group(0).strip()

        # Remove trailing explanations after the SQL statement
        end_match = re.search(r";[^;]*$", text, flags=re.DOTALL)
        if end_match and end_match.start() != -1:
            tail = text[end_match.start():]
            if not re.search(r";\s*\Z", tail):
                text = text[: end_match.end()].strip()

        return text.strip()
    
    def generate_schema_sql(self, profile: Dict[str, Any]) -> str:
        """Generate SQL schema creation from data profile"""
        
        prompt = f"""
        Based on this CSV data profile, generate a simple SQL schema for DuckDB.
        
        Dataset Info:
        - File: {profile['dataset']['filename']}
        - Rows: {profile['dataset']['row_count']}
        - Columns: {profile['dataset']['column_count']}
        
        Column Details:
        {json.dumps(profile['columns'], indent=2)}
        
        Generate ONLY a CREATE TABLE statement with appropriate data types.
        Do NOT include indexes, constraints, or ALTER TABLE statements.
        Use simple DuckDB-compatible syntax.
        
        Return only the CREATE TABLE statement, no explanations.
        """
        
        try:
            response = self.client.messages.create(
                model="claude-sonnet-4-5-20250929",
                max_tokens=1000,
                messages=[{"role": "user", "content": prompt}]
            )
            # Clean the response to remove markdown formatting
            sql_text = response.content[0].text
            # Remove ```sql and ``` markers
            sql_text = sql_text.replace('```sql', '').replace('```', '').strip()
            return sql_text
        except Exception as e:
            print(f"Error generating schema: {e}")
            return self._fallback_schema(profile)
    
    @staticmethod
    def _extract_json_payload(text: str) -> Optional[Dict[str, Any]]:
        """Find and parse the first JSON object in a text blob."""
        if not text:
            return None

        text = text.strip()
        json_matches = re.findall(r"\{.*\}", text, flags=re.DOTALL)
        for candidate in json_matches:
            try:
                return json.loads(candidate)
            except json.JSONDecodeError:
                continue
        try:
            return json.loads(text)
        except Exception:
            return None

    # --- Begin User-Specified RAG Imputation Pipeline ---
    def choose_policy(dtype, nullp, profile, df):
        """Minimal policy chooser (works today)"""
        if dtype == "numeric":
            if nullp <= 0.01: return {"method":"drop_rows"}
            if nullp <= 0.15:
                key = pick_stable_key(df)
                return {"method":"median", "by":key}
            return {"method":"iterative", "max_iter":15, "by":None, "fallback":"median"}
        if dtype in ("categorical","boolean"):
            key = pick_stable_key(df)
            if nullp <= 0.10: return {"method":"mode", "by":key}
            return {"method":"mode", "by":key, "unknown":"Unknown"}
        if dtype == "date":
            return {"method":"ffill_bfill"}  # or "median_date"
        return {"method":"drop_rows"}

    def pick_stable_key(df):
        """Heuristic: choose 1–2 categorical columns with low cardinality (≤10), good association, enough support (≥30 rows)."""
        cats = [c for c in df.columns if df[c].dtype == 'object' and df[c].nunique() <= 10]
        for c in cats:
            if df[c].value_counts().min() >= 30:
                return c
        return None

    def apply_policies(df, policies, cols):
        """Apply imputations only to columns actually used; return (df_imputed, report)."""
        import numpy as np
        dfq = df.copy()
        report = {}
        for c in cols:
            pol = policies[c]
            null_mask = dfq[c].isnull()
            n_missing = null_mask.sum()
            method = pol.get("method")
            if method == "drop_rows":
                dfq = dfq[~null_mask]
                report[c] = {"imputed":0, "method":"drop_rows"}
            elif method == "median":
                by = pol.get("by")
                if by:
                    medians = dfq.groupby(by)[c].transform('median')
                    dfq.loc[null_mask, c] = medians[null_mask]
                else:
                    med = dfq[c].median()
                    dfq.loc[null_mask, c] = med
                report[c] = {"imputed":int(n_missing), "method":"median", "by":by}
            elif method == "iterative":
                # Placeholder for IterativeImputer or KNN
                dfq.loc[null_mask, c] = dfq[c].median()
                report[c] = {"imputed":int(n_missing), "method":"iterative->median"}
            elif method == "mode":
                by = pol.get("by")
                if by:
                    modes = dfq.groupby(by)[c].transform(lambda x: x.mode().iloc[0] if not x.mode().empty else "Unknown")
                    dfq.loc[n
[truncated — 23945 more characters]
```