Are you sure you want to reset the current session? This will clear all chat history and start a new conversation.
+
+
+
+
+
+
+ {# Set authentication state for JS before loading chat.js #}
+
+
+
+
+
\ No newline at end of file
diff --git a/api/utils.py b/api/utils.py
index 60d8c7b1..65932588 100644
--- a/api/utils.py
+++ b/api/utils.py
@@ -1,11 +1,20 @@
+"""Utility functions for the text2sql API."""
+
import json
from typing import List, Tuple
+
from litellm import completion
+
from api.config import Config
from api.constants import BENCHMARK
-def generate_db_description(db_name: str, table_names: List[str], temperature: float = 0.5,
- max_tokens: int = 150) -> str:
+
+def generate_db_description(
+ db_name: str,
+ table_names: List[str],
+ temperature: float = 0.5,
+ max_tokens: int = 150,
+) -> str:
"""
Generates a short and concise description of a database.
@@ -20,14 +29,14 @@ def generate_db_description(db_name: str, table_names: List[str], temperature: f
"""
if not isinstance(db_name, str):
raise TypeError("database_name must be a string.")
-
+
if not isinstance(table_names, list):
raise TypeError("table_names must be a list of strings.")
-
+
# Ensure all table names are strings
if not all(isinstance(table, str) for table in table_names):
raise ValueError("All items in table_names must be strings.")
-
+
if not table_names:
return f"{db_name} is a database with no tables."
@@ -40,25 +49,38 @@ def generate_db_description(db_name: str, table_names: List[str], temperature: f
tables_formatted = ", ".join(table_names[:-1]) + f", and {table_names[-1]}"
prompt = (
- f"You are a helpful assistant. Generate a concise description of the database named '{db_name}' "
- f"which contains the following tables: {tables_formatted}.\n\n"
- f"Description:"
+ f"You are a helpful assistant. Generate a concise description of "
+ f"the database named '{db_name}' which contains the following tables: "
+ f"{tables_formatted}.\n\nDescription:"
)
-
- response = completion(model=Config.COMPLETION_MODEL,
- messages=[
- {"role": "system", "content": "You are a helpful assistant."},
- {"role": "user", "content": prompt}
- ],
- temperature=temperature,
- max_tokens=max_tokens,
- n=1,
- stop=None,
- )
- description = response.choices[0].message['content']
+
+ response = completion(
+ model=Config.COMPLETION_MODEL,
+ messages=[
+ {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "user", "content": prompt},
+ ],
+ temperature=temperature,
+ max_tokens=max_tokens,
+ n=1,
+ stop=None,
+ )
+ description = response.choices[0].message["content"]
return description
+
def llm_answer_validator(question: str, answer: str, expected_answer: str = None) -> str:
+ """
+ Validate an answer using LLM.
+
+ Args:
+ question: The original question
+ answer: The generated answer
+ expected_answer: The expected answer for comparison
+
+ Returns:
+ JSON string with validation results
+ """
prompt = """
You are evaluating an answer generated by a text-to-sql RAG-based system. Assess how well the Generated Answer (generated sql) addresses the Question
based on the Expected Answer.
@@ -76,18 +98,37 @@ def llm_answer_validator(question: str, answer: str, expected_answer: str = None
Output Json format:
{{"relevance_score": float, "explanation": "Your assessment here."}}
"""
- response = completion(model=Config.VALIDTOR_MODEL,
- messages=[
- {"role": "system", "content": "You are a Validator assistant."},
- {"role": "user", "content": prompt.format(question=question, expected_answer=expected_answer, generated_answer=answer)}
- ],
- response_format={"type": "json_object"},
-
- )
- validation_set = response.choices[0].message['content'].strip()
+ response = completion(
+ model=Config.VALIDATOR_MODEL,
+ messages=[
+ {"role": "system", "content": "You are a Validator assistant."},
+ {
+ "role": "user",
+ "content": prompt.format(
+ question=question,
+ expected_answer=expected_answer,
+ generated_answer=answer,
+ ),
+ },
+ ],
+ response_format={"type": "json_object"},
+ )
+ validation_set = response.choices[0].message["content"].strip()
return validation_set
+
def llm_table_validator(question: str, answer: str, tables: List[str]) -> Tuple[float, str]:
+ """
+ Validate table relevance using LLM.
+
+ Args:
+ question: The original question
+ answer: The generated answer
+ tables: List of available tables
+
+ Returns:
+ Tuple of relevance score and explanation
+ """
prompt = """
You are evaluating an answer generated by a text-to-sql RAG-based system. Assess how well the retrived Tables relevant to the question and supports the Generated Answer (generated sql).
- The tables are with the following structure:
@@ -106,19 +147,23 @@ def llm_table_validator(question: str, answer: str, tables: List[str]) -> Tuple[
Output Json format:
{{"relevance_score": float, "explanation": "Your assessment here."}}
"""
- response = completion(model=Config.VALIDTOR_MODEL,
- messages=[
- {"role": "system", "content": "You are a Validator assistant."},
- {"role": "user", "content": prompt.format(question=question, tables=tables, generated_answer=answer)}
- ],
- response_format={"type": "json_object"},
- )
- validation_set = response.choices[0].message['content'].strip()
+ response = completion(
+ model=Config.VALIDATOR_MODEL,
+ messages=[
+ {"role": "system", "content": "You are a Validator assistant."},
+ {
+ "role": "user",
+ "content": prompt.format(question=question, tables=tables, generated_answer=answer),
+ },
+ ],
+ response_format={"type": "json_object"},
+ )
+ validation_set = response.choices[0].message["content"].strip()
try:
val_res = json.loads(validation_set)
- score = val_res['relevance_score']
- explanation = val_res['explanation']
- except Exception as e:
+ score = val_res["relevance_score"]
+ explanation = val_res["explanation"]
+ except (json.JSONDecodeError, KeyError) as e:
print(f"Error: {e}")
score = 0.0
explanation = "Error: Unable to parse the response."
@@ -138,8 +183,7 @@ def run_benchmark():
for data in benchmark_data:
success, result = generate_db_description(
- db_name=data['database'],
- table_names=list(data['tables'].keys())
+ db_name=data["database"], table_names=list(data["tables"].keys())
)
if success:
@@ -147,4 +191,4 @@ def run_benchmark():
else:
results.append(f"Error: {result}")
- return results
\ No newline at end of file
+ return results
diff --git a/docs/postgres_loader.md b/docs/postgres_loader.md
new file mode 100644
index 00000000..d4024fd1
--- /dev/null
+++ b/docs/postgres_loader.md
@@ -0,0 +1,240 @@
+# PostgreSQL Schema Loader
+
+This loader connects to a PostgreSQL database and extracts the complete schema information, including tables, columns, relationships, and constraints. The extracted schema is then loaded into a graph database for further analysis and query generation.
+
+## Features
+
+- **Complete Schema Extraction**: Retrieves all tables, columns, data types, constraints, and relationships
+- **Foreign Key Relationships**: Automatically discovers and maps foreign key relationships between tables
+- **Column Metadata**: Extracts column comments, default values, nullability, and key types
+- **Batch Processing**: Efficiently processes large schemas with progress tracking
+- **Error Handling**: Robust error handling for connection issues and malformed schemas
+
+## Installation
+
+{% capture shell_0 %}
+poetry add psycopg2-binary
+{% endcapture %}
+
+{% capture shell_1 %}
+pip install psycopg2-binary
+{% endcapture %}
+
+{% include code_tabs.html id="install_tabs" shell=shell_0 shell2=shell_1 %}
+
+## Usage
+
+### Basic Usage
+
+{% capture python_0 %}
+from api.loaders.postgres_loader import PostgreSQLLoader
+
+# Connection URL format: postgresql://username:password@host:port/database
+connection_url = "postgresql://postgres:password@localhost:5432/mydatabase"
+graph_id = "my_schema_graph"
+
+success, message = PostgreSQLLoader.load(graph_id, connection_url)
+
+if success:
+ print(f"Schema loaded successfully: {message}")
+else:
+ print(f"Failed to load schema: {message}")
+{% endcapture %}
+
+{% capture javascript_0 %}
+import { PostgreSQLLoader } from 'your-pkg';
+
+const connectionUrl = "postgresql://postgres:password@localhost:5432/mydatabase";
+const graphId = "my_schema_graph";
+
+const [success, message] = await PostgreSQLLoader.load(graphId, connectionUrl);
+if (success) {
+ console.log(`Schema loaded successfully: ${message}`);
+} else {
+ console.log(`Failed to load schema: ${message}`);
+}
+{% endcapture %}
+
+{% capture java_0 %}
+String connectionUrl = "postgresql://postgres:password@localhost:5432/mydatabase";
+String graphId = "my_schema_graph";
+Pair result = PostgreSQLLoader.load(graphId, connectionUrl);
+if (result.getLeft()) {
+ System.out.println("Schema loaded successfully: " + result.getRight());
+} else {
+ System.out.println("Failed to load schema: " + result.getRight());
+}
+{% endcapture %}
+
+{% capture rust_0 %}
+let connection_url = "postgresql://postgres:password@localhost:5432/mydatabase";
+let graph_id = "my_schema_graph";
+let (success, message) = postgresql_loader::load(graph_id, connection_url)?;
+if success {
+ println!("Schema loaded successfully: {}", message);
+} else {
+ println!("Failed to load schema: {}", message);
+}
+{% endcapture %}
+
+{% include code_tabs.html id="basic_usage_tabs" python=python_0 javascript=javascript_0 java=java_0 rust=rust_0 %}
+
+### Connection URL Format
+
+```
+postgresql://[username[:password]@][host[:port]][/database][?options]
+```
+
+**Examples:**
+- `postgresql://postgres:password@localhost:5432/mydatabase`
+- `postgresql://user:pass@example.com:5432/production_db`
+- `postgresql://postgres@127.0.0.1/testdb`
+
+### Integration with Graph Database
+
+{% capture python_1 %}
+from api.loaders.postgres_loader import PostgreSQLLoader
+from api.extensions import db
+
+# Load PostgreSQL schema into graph
+graph_id = "customer_db_schema"
+connection_url = "postgresql://postgres:password@localhost:5432/customers"
+
+success, message = PostgreSQLLoader.load(graph_id, connection_url)
+
+if success:
+ # The schema is now available in the graph database
+ graph = db.select_graph(graph_id)
+
+ # Query for all tables
+ result = graph.query("MATCH (t:Table) RETURN t.name")
+ print("Tables:", [record[0] for record in result.result_set])
+{% endcapture %}
+
+{% capture javascript_1 %}
+import { PostgreSQLLoader, db } from 'your-pkg';
+
+const graphId = "customer_db_schema";
+const connectionUrl = "postgresql://postgres:password@localhost:5432/customers";
+
+const [success, message] = await PostgreSQLLoader.load(graphId, connectionUrl);
+if (success) {
+ const graph = db.selectGraph(graphId);
+ const result = await graph.query("MATCH (t:Table) RETURN t.name");
+ console.log("Tables:", result.map(r => r[0]));
+}
+{% endcapture %}
+
+{% capture java_1 %}
+String graphId = "customer_db_schema";
+String connectionUrl = "postgresql://postgres:password@localhost:5432/customers";
+Pair result = PostgreSQLLoader.load(graphId, connectionUrl);
+if (result.getLeft()) {
+ Graph graph = db.selectGraph(graphId);
+ ResultSet rs = graph.query("MATCH (t:Table) RETURN t.name");
+ // Print table names
+ for (Record record : rs) {
+ System.out.println(record.get(0));
+ }
+}
+{% endcapture %}
+
+{% capture rust_1 %}
+let graph_id = "customer_db_schema";
+let connection_url = "postgresql://postgres:password@localhost:5432/customers";
+let (success, message) = postgresql_loader::load(graph_id, connection_url)?;
+if success {
+ let graph = db.select_graph(graph_id);
+ let result = graph.query("MATCH (t:Table) RETURN t.name")?;
+ println!("Tables: {:?}", result.iter().map(|r| &r[0]).collect::>());
+}
+{% endcapture %}
+
+{% include code_tabs.html id="integration_tabs" python=python_1 javascript=javascript_1 java=java_1 rust=rust_1 %}
+
+## Schema Structure
+
+The loader extracts the following information:
+
+### Tables
+- Table name
+- Table description/comment
+- Column information
+- Foreign key relationships
+
+### Columns
+- Column name
+- Data type
+- Nullability
+- Default values
+- Key type (PRIMARY KEY, FOREIGN KEY, or NONE)
+- Column descriptions/comments
+
+### Relationships
+- Foreign key constraints
+- Referenced tables and columns
+- Constraint names and metadata
+
+## Graph Database Schema
+
+The extracted schema is stored in the graph database with the following node types:
+
+- **Database**: Represents the source database
+- **Table**: Represents database tables
+- **Column**: Represents table columns
+
+And the following relationship types:
+
+- **BELONGS_TO**: Connects columns to their tables
+- **REFERENCES**: Connects foreign key columns to their referenced columns
+
+## Error Handling
+
+The loader handles various error conditions:
+
+- **Connection Errors**: Invalid connection URLs or database unavailability
+- **Permission Errors**: Insufficient database permissions
+- **Schema Errors**: Invalid or corrupt schema information
+- **Graph Errors**: Issues with graph database operations
+
+## Example Output
+
+{% capture shell_2 %}
+Extracting table information: 100%|██████████| 15/15 [00:02<00:00, 7.50it/s]
+Creating Graph Table Nodes: 100%|██████████| 15/15 [00:05<00:00, 2.80it/s]
+Creating embeddings for customers columns: 100%|██████████| 2/2 [00:01<00:00, 1.20it/s]
+Creating Graph Columns for customers: 100%|██████████| 8/8 [00:03<00:00, 2.40it/s]
+...
+Creating Graph Table Relationships: 100%|██████████| 12/12 [00:02<00:00, 5.20it/s]
+
+PostgreSQL schema loaded successfully. Found 15 tables.
+{% endcapture %}
+
+{% include code_tabs.html id="output_tabs" shell=shell_2 %}
+
+## Requirements
+
+- Python 3.12+
+- psycopg2-binary
+- Access to a PostgreSQL database
+- Existing graph database infrastructure (FalkorDB)
+
+## Limitations
+
+- Currently only supports PostgreSQL databases
+- Extracts schema from the 'public' schema only
+- Requires read permissions on information_schema and pg_* system tables
+- Large schemas may take time to process due to embedding generation
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Connection Failed**: Verify the connection URL format and database credentials
+2. **Permission Denied**: Ensure the database user has read access to system tables
+3. **Schema Not Found**: Check that tables exist in the 'public' schema
+4. **Graph Database Error**: Verify that the graph database is running and accessible
+
+### Debug Mode
+
+For debugging, you can enable verbose output by modifying the loader to print additional information about the extraction process.
diff --git a/examples/crm.sql b/examples/crm.sql
new file mode 100644
index 00000000..1c05fd6c
--- /dev/null
+++ b/examples/crm.sql
@@ -0,0 +1,611 @@
+-- SQL Script 1 (Extended): Table Creation (DDL) with Comments
+-- This script creates the tables for your CRM database and adds descriptions for each table and column.
+
+-- Drop existing tables to start fresh
+DROP TABLE IF EXISTS SalesOrderItems, SalesOrders, Invoices, Payments, Products, ProductCategories, Leads, Opportunities, Contacts, Customers, Campaigns, CampaignMembers, Tasks, Notes, Attachments, SupportTickets, TicketComments, Users, Roles, UserRoles CASCADE;
+
+-- Roles for access control
+CREATE TABLE Roles (
+ RoleID SERIAL PRIMARY KEY,
+ RoleName VARCHAR(50) UNIQUE NOT NULL
+);
+COMMENT ON TABLE Roles IS 'Defines user roles for access control within the CRM (e.g., Admin, Sales Manager).';
+COMMENT ON COLUMN Roles.RoleID IS 'Unique identifier for the role.';
+COMMENT ON COLUMN Roles.RoleName IS 'Name of the role (e.g., "Admin", "Sales Representative").';
+
+-- Users of the CRM system
+CREATE TABLE Users (
+ UserID SERIAL PRIMARY KEY,
+ Username VARCHAR(50) UNIQUE NOT NULL,
+ PasswordHash VARCHAR(255) NOT NULL,
+ Email VARCHAR(100) UNIQUE NOT NULL,
+ FirstName VARCHAR(50),
+ LastName VARCHAR(50),
+ CreatedAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+COMMENT ON TABLE Users IS 'Stores information about users who can log in to the CRM system.';
+COMMENT ON COLUMN Users.UserID IS 'Unique identifier for the user.';
+COMMENT ON COLUMN Users.Username IS 'The username for logging in.';
+COMMENT ON COLUMN Users.PasswordHash IS 'Hashed password for security.';
+COMMENT ON COLUMN Users.Email IS 'The user''s email address.';
+COMMENT ON COLUMN Users.FirstName IS 'The user''s first name.';
+COMMENT ON COLUMN Users.LastName IS 'The user''s last name.';
+COMMENT ON COLUMN Users.CreatedAt IS 'Timestamp when the user account was created.';
+
+-- Junction table for Users and Roles
+CREATE TABLE UserRoles (
+ UserID INT REFERENCES Users(UserID),
+ RoleID INT REFERENCES Roles(RoleID),
+ PRIMARY KEY (UserID, RoleID)
+);
+COMMENT ON TABLE UserRoles IS 'Maps users to their assigned roles, supporting many-to-many relationships.';
+COMMENT ON COLUMN UserRoles.UserID IS 'Foreign key referencing the Users table.';
+COMMENT ON COLUMN UserRoles.RoleID IS 'Foreign key referencing the Roles table.';
+
+-- Customer accounts
+CREATE TABLE Customers (
+ CustomerID SERIAL PRIMARY KEY,
+ CustomerName VARCHAR(100) NOT NULL,
+ Industry VARCHAR(50),
+ Website VARCHAR(100),
+ Phone VARCHAR(20),
+ Address VARCHAR(255),
+ City VARCHAR(50),
+ State VARCHAR(50),
+ ZipCode VARCHAR(20),
+ Country VARCHAR(50),
+ AssignedTo INT REFERENCES Users(UserID),
+ CreatedAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+COMMENT ON TABLE Customers IS 'Represents customer accounts or companies.';
+COMMENT ON COLUMN Customers.CustomerID IS 'Unique identifier for the customer.';
+COMMENT ON COLUMN Customers.CustomerName IS 'The name of the customer company.';
+COMMENT ON COLUMN Customers.Industry IS 'The industry the customer belongs to.';
+COMMENT ON COLUMN Customers.Website IS 'The customer''s official website.';
+COMMENT ON COLUMN Customers.Phone IS 'The customer''s primary phone number.';
+COMMENT ON COLUMN Customers.Address IS 'The customer''s physical address.';
+COMMENT ON COLUMN Customers.City IS 'The city part of the address.';
+COMMENT ON COLUMN Customers.State IS 'The state or province part of the address.';
+COMMENT ON COLUMN Customers.ZipCode IS 'The postal or zip code.';
+COMMENT ON COLUMN Customers.Country IS 'The country part of the address.';
+COMMENT ON COLUMN Customers.AssignedTo IS 'The user (sales representative) assigned to this customer account.';
+COMMENT ON COLUMN Customers.CreatedAt IS 'Timestamp when the customer was added.';
+
+-- Individual contacts associated with customers
+CREATE TABLE Contacts (
+ ContactID SERIAL PRIMARY KEY,
+ CustomerID INT REFERENCES Customers(CustomerID),
+ FirstName VARCHAR(50) NOT NULL,
+ LastName VARCHAR(50) NOT NULL,
+ Email VARCHAR(100) UNIQUE,
+ Phone VARCHAR(20),
+ JobTitle VARCHAR(50),
+ CreatedAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+COMMENT ON TABLE Contacts IS 'Stores information about individual contacts associated with customer accounts.';
+COMMENT ON COLUMN Contacts.ContactID IS 'Unique identifier for the contact.';
+COMMENT ON COLUMN Contacts.CustomerID IS 'Foreign key linking the contact to a customer account.';
+COMMENT ON COLUMN Contacts.FirstName IS 'The contact''s first name.';
+COMMENT ON COLUMN Contacts.LastName IS 'The contact''s last name.';
+COMMENT ON COLUMN Contacts.Email IS 'The contact''s email address.';
+COMMENT ON COLUMN Contacts.Phone IS 'The contact''s phone number.';
+COMMENT ON COLUMN Contacts.JobTitle IS 'The contact''s job title or position.';
+COMMENT ON COLUMN Contacts.CreatedAt IS 'Timestamp when the contact was created.';
+
+-- Potential sales leads
+CREATE TABLE Leads (
+ LeadID SERIAL PRIMARY KEY,
+ FirstName VARCHAR(50),
+ LastName VARCHAR(50),
+ Email VARCHAR(100),
+ Phone VARCHAR(20),
+ Company VARCHAR(100),
+ Status VARCHAR(50) DEFAULT 'New',
+ Source VARCHAR(50),
+ AssignedTo INT REFERENCES Users(UserID),
+ CreatedAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+COMMENT ON TABLE Leads IS 'Represents potential customers or sales prospects (not yet qualified).';
+COMMENT ON COLUMN Leads.LeadID IS 'Unique identifier for the lead.';
+COMMENT ON COLUMN Leads.Status IS 'Current status of the lead (e.g., New, Contacted, Qualified, Lost).';
+COMMENT ON COLUMN Leads.Source IS 'The source from which the lead was generated (e.g., Website, Referral).';
+COMMENT ON COLUMN Leads.AssignedTo IS 'The user assigned to follow up with this lead.';
+COMMENT ON COLUMN Leads.CreatedAt IS 'Timestamp when the lead was created.';
+
+-- Sales opportunities
+CREATE TABLE Opportunities (
+ OpportunityID SERIAL PRIMARY KEY,
+ CustomerID INT REFERENCES Customers(CustomerID),
+ OpportunityName VARCHAR(100) NOT NULL,
+ Stage VARCHAR(50) DEFAULT 'Prospecting',
+ Amount DECIMAL(12, 2),
+ CloseDate DATE,
+ AssignedTo INT REFERENCES Users(UserID),
+ CreatedAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+COMMENT ON TABLE Opportunities IS 'Tracks qualified sales deals with potential revenue.';
+COMMENT ON COLUMN Opportunities.OpportunityID IS 'Unique identifier for the opportunity.';
+COMMENT ON COLUMN Opportunities.CustomerID IS 'Foreign key linking the opportunity to a customer account.';
+COMMENT ON COLUMN Opportunities.OpportunityName IS 'A descriptive name for the sales opportunity.';
+COMMENT ON COLUMN Opportunities.Stage IS 'The current stage in the sales pipeline (e.g., Prospecting, Proposal, Closed Won).';
+COMMENT ON COLUMN Opportunities.Amount IS 'The estimated value of the opportunity.';
+COMMENT ON COLUMN Opportunities.CloseDate IS 'The expected date the deal will close.';
+COMMENT ON COLUMN Opportunities.AssignedTo IS 'The user responsible for this opportunity.';
+COMMENT ON COLUMN Opportunities.CreatedAt IS 'Timestamp when the opportunity was created.';
+
+-- Product categories
+CREATE TABLE ProductCategories (
+ CategoryID SERIAL PRIMARY KEY,
+ CategoryName VARCHAR(50) NOT NULL,
+ Description TEXT
+);
+COMMENT ON TABLE ProductCategories IS 'Used to group products into categories (e.g., Software, Hardware).';
+COMMENT ON COLUMN ProductCategories.CategoryID IS 'Unique identifier for the category.';
+COMMENT ON COLUMN ProductCategories.CategoryName IS 'Name of the product category.';
+COMMENT ON COLUMN ProductCategories.Description IS 'A brief description of the category.';
+
+-- Products or services offered
+CREATE TABLE Products (
+ ProductID SERIAL PRIMARY KEY,
+ ProductName VARCHAR(100) NOT NULL,
+ CategoryID INT REFERENCES ProductCategories(CategoryID),
+ Description TEXT,
+ Price DECIMAL(10, 2) NOT NULL,
+ StockQuantity INT DEFAULT 0
+);
+COMMENT ON TABLE Products IS 'Stores details of the products or services the company sells.';
+COMMENT ON COLUMN Products.ProductID IS 'Unique identifier for the product.';
+COMMENT ON COLUMN Products.ProductName IS 'Name of the product.';
+COMMENT ON COLUMN Products.CategoryID IS 'Foreign key linking the product to a category.';
+COMMENT ON COLUMN Products.Description IS 'Detailed description of the product.';
+COMMENT ON COLUMN Products.Price IS 'The unit price of the product.';
+COMMENT ON COLUMN Products.StockQuantity IS 'The quantity of the product available in stock.';
+
+-- Sales orders
+CREATE TABLE SalesOrders (
+ OrderID SERIAL PRIMARY KEY,
+ CustomerID INT REFERENCES Customers(CustomerID),
+ OpportunityID INT REFERENCES Opportunities(OpportunityID),
+ OrderDate DATE NOT NULL,
+ Status VARCHAR(50) DEFAULT 'Pending',
+ TotalAmount DECIMAL(12, 2),
+ AssignedTo INT REFERENCES Users(UserID)
+);
+COMMENT ON TABLE SalesOrders IS 'Records of confirmed sales to customers.';
+COMMENT ON COLUMN SalesOrders.OrderID IS 'Unique identifier for the sales order.';
+COMMENT ON COLUMN SalesOrders.CustomerID IS 'Foreign key linking the order to a customer.';
+COMMENT ON COLUMN SalesOrders.OpportunityID IS 'Foreign key linking the order to the sales opportunity it came from.';
+COMMENT ON COLUMN SalesOrders.OrderDate IS 'The date the order was placed.';
+COMMENT ON COLUMN SalesOrders.Status IS 'The current status of the order (e.g., Pending, Shipped, Canceled).';
+COMMENT ON COLUMN SalesOrders.TotalAmount IS 'The total calculated amount for the order.';
+COMMENT ON COLUMN SalesOrders.AssignedTo IS 'The user who processed the order.';
+
+-- Items within a sales order
+CREATE TABLE SalesOrderItems (
+ OrderItemID SERIAL PRIMARY KEY,
+ OrderID INT REFERENCES SalesOrders(OrderID) ON DELETE CASCADE,
+ ProductID INT REFERENCES Products(ProductID),
+ Quantity INT NOT NULL,
+ UnitPrice DECIMAL(10, 2) NOT NULL
+);
+COMMENT ON TABLE SalesOrderItems IS 'Line items for each product within a sales order.';
+COMMENT ON COLUMN SalesOrderItems.OrderItemID IS 'Unique identifier for the order item.';
+COMMENT ON COLUMN SalesOrderItems.OrderID IS 'Foreign key linking this item to a sales order.';
+COMMENT ON COLUMN SalesOrderItems.ProductID IS 'Foreign key linking to the product being ordered.';
+COMMENT ON COLUMN SalesOrderItems.Quantity IS 'The quantity of the product ordered.';
+COMMENT ON COLUMN SalesOrderItems.UnitPrice IS 'The price per unit at the time of sale.';
+
+-- Invoices for sales
+CREATE TABLE Invoices (
+ InvoiceID SERIAL PRIMARY KEY,
+ OrderID INT REFERENCES SalesOrders(OrderID),
+ InvoiceDate DATE NOT NULL,
+ DueDate DATE,
+ TotalAmount DECIMAL(12, 2),
+ Status VARCHAR(50) DEFAULT 'Unpaid'
+);
+COMMENT ON TABLE Invoices IS 'Represents billing invoices sent to customers.';
+COMMENT ON COLUMN Invoices.InvoiceID IS 'Unique identifier for the invoice.';
+COMMENT ON COLUMN Invoices.OrderID IS 'Foreign key linking the invoice to a sales order.';
+COMMENT ON COLUMN Invoices.InvoiceDate IS 'The date the invoice was issued.';
+COMMENT ON COLUMN Invoices.DueDate IS 'The date the payment is due.';
+COMMENT ON COLUMN Invoices.TotalAmount IS 'The total amount due on the invoice.';
+COMMENT ON COLUMN Invoices.Status IS 'The payment status of the invoice (e.g., Unpaid, Paid, Overdue).';
+
+-- Payment records
+CREATE TABLE Payments (
+ PaymentID SERIAL PRIMARY KEY,
+ InvoiceID INT REFERENCES Invoices(InvoiceID),
+ PaymentDate DATE NOT NULL,
+ Amount DECIMAL(12, 2),
+ PaymentMethod VARCHAR(50)
+);
+COMMENT ON TABLE Payments IS 'Tracks payments received from customers against invoices.';
+COMMENT ON COLUMN Payments.PaymentID IS 'Unique identifier for the payment.';
+COMMENT ON COLUMN Payments.InvoiceID IS 'Foreign key linking the payment to an invoice.';
+COMMENT ON COLUMN Payments.PaymentDate IS 'The date the payment was received.';
+COMMENT ON COLUMN Payments.Amount IS 'The amount that was paid.';
+COMMENT ON COLUMN Payments.PaymentMethod IS 'The method of payment (e.g., Credit Card, Bank Transfer).';
+
+-- Marketing campaigns
+CREATE TABLE Campaigns (
+ CampaignID SERIAL PRIMARY KEY,
+ CampaignName VARCHAR(100) NOT NULL,
+ StartDate DATE,
+ EndDate DATE,
+ Budget DECIMAL(12, 2),
+ Status VARCHAR(50),
+ Owner INT REFERENCES Users(UserID)
+);
+COMMENT ON TABLE Campaigns IS 'Stores information about marketing campaigns.';
+COMMENT ON COLUMN Campaigns.CampaignID IS 'Unique identifier for the campaign.';
+COMMENT ON COLUMN Campaigns.CampaignName IS 'The name of the marketing campaign.';
+COMMENT ON COLUMN Campaigns.StartDate IS 'The start date of the campaign.';
+COMMENT ON COLUMN Campaigns.EndDate IS 'The end date of the campaign.';
+COMMENT ON COLUMN Campaigns.Budget IS 'The allocated budget for the campaign.';
+COMMENT ON COLUMN Campaigns.Status IS 'The current status of the campaign (e.g., Planned, Active, Completed).';
+COMMENT ON COLUMN Campaigns.Owner IS 'The user responsible for the campaign.';
+
+-- Members of a marketing campaign (leads or contacts)
+CREATE TABLE CampaignMembers (
+ CampaignMemberID SERIAL PRIMARY KEY,
+ CampaignID INT REFERENCES Campaigns(CampaignID),
+ LeadID INT REFERENCES Leads(LeadID),
+ ContactID INT REFERENCES Contacts(ContactID),
+ Status VARCHAR(50)
+);
+COMMENT ON TABLE CampaignMembers IS 'Links leads and contacts to the marketing campaigns they are a part of.';
+COMMENT ON COLUMN CampaignMembers.CampaignMemberID IS 'Unique identifier for the campaign member record.';
+COMMENT ON COLUMN CampaignMembers.CampaignID IS 'Foreign key linking to the campaign.';
+COMMENT ON COLUMN CampaignMembers.LeadID IS 'Foreign key linking to a lead (if the member is a lead).';
+COMMENT ON COLUMN CampaignMembers.ContactID IS 'Foreign key linking to a contact (if the member is a contact).';
+COMMENT ON COLUMN CampaignMembers.Status IS 'The status of the member in the campaign (e.g., Sent, Responded).';
+
+-- Tasks for users
+CREATE TABLE Tasks (
+ TaskID SERIAL PRIMARY KEY,
+ Title VARCHAR(100) NOT NULL,
+ Description TEXT,
+ DueDate DATE,
+ Status VARCHAR(50) DEFAULT 'Not Started',
+ Priority VARCHAR(20) DEFAULT 'Normal',
+ AssignedTo INT REFERENCES Users(UserID),
+ RelatedToEntity VARCHAR(50),
+ RelatedToID INT
+);
+COMMENT ON TABLE Tasks IS 'Tracks tasks or to-do items for CRM users.';
+COMMENT ON COLUMN Tasks.TaskID IS 'Unique identifier for the task.';
+COMMENT ON COLUMN Tasks.Title IS 'A short title for the task.';
+COMMENT ON COLUMN Tasks.Description IS 'A detailed description of the task.';
+COMMENT ON COLUMN Tasks.DueDate IS 'The date the task is due to be completed.';
+COMMENT ON COLUMN Tasks.Status IS 'The current status of the task (e.g., Not Started, In Progress, Completed).';
+COMMENT ON COLUMN Tasks.Priority IS 'The priority level of the task (e.g., Low, Normal, High).';
+COMMENT ON COLUMN Tasks.AssignedTo IS 'The user the task is assigned to.';
+COMMENT ON COLUMN Tasks.RelatedToEntity IS 'The type of record this task is related to (e.g., ''Lead'', ''Opportunity'').';
+COMMENT ON COLUMN Tasks.RelatedToID IS 'The ID of the related record.';
+
+-- Notes related to various records
+CREATE TABLE Notes (
+ NoteID SERIAL PRIMARY KEY,
+ Content TEXT NOT NULL,
+ CreatedBy INT REFERENCES Users(UserID),
+ RelatedToEntity VARCHAR(50),
+ RelatedToID INT,
+ CreatedAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+COMMENT ON TABLE Notes IS 'Allows users to add notes to various records (e.g., contacts, opportunities).';
+COMMENT ON COLUMN Notes.NoteID IS 'Unique identifier for the note.';
+COMMENT on COLUMN Notes.Content IS 'The text content of the note.';
+COMMENT ON COLUMN Notes.CreatedBy IS 'The user who created the note.';
+COMMENT ON COLUMN Notes.RelatedToEntity IS 'The type of record this note is related to (e.g., ''Contact'', ''Customer'').';
+COMMENT ON COLUMN Notes.RelatedToID IS 'The ID of the related record.';
+COMMENT ON COLUMN Notes.CreatedAt IS 'Timestamp when the note was created.';
+
+-- File attachments
+CREATE TABLE Attachments (
+ AttachmentID SERIAL PRIMARY KEY,
+ FileName VARCHAR(255) NOT NULL,
+ FilePath VARCHAR(255) NOT NULL,
+ FileSize INT,
+ FileType VARCHAR(100),
+ UploadedBy INT REFERENCES Users(UserID),
+ RelatedToEntity VARCHAR(50),
+ RelatedToID INT,
+ CreatedAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+COMMENT ON TABLE Attachments IS 'Stores metadata about files attached to records in the CRM.';
+COMMENT ON COLUMN Attachments.AttachmentID IS 'Unique identifier for the attachment.';
+COMMENT ON COLUMN Attachments.FileName IS 'The original name of the uploaded file.';
+COMMENT ON COLUMN Attachments.FilePath IS 'The path where the file is stored on the server.';
+COMMENT ON COLUMN Attachments.FileSize IS 'The size of the file in bytes.';
+COMMENT ON COLUMN Attachments.FileType IS 'The MIME type of the file (e.g., ''application/pdf'').';
+COMMENT ON COLUMN Attachments.UploadedBy IS 'The user who uploaded the file.';
+COMMENT ON COLUMN Attachments.RelatedToEntity IS 'The type of record this attachment is related to.';
+COMMENT ON COLUMN Attachments.RelatedToID IS 'The ID of the related record.';
+COMMENT ON COLUMN Attachments.CreatedAt IS 'Timestamp when the file was uploaded.';
+
+-- Customer support tickets
+CREATE TABLE SupportTickets (
+ TicketID SERIAL PRIMARY KEY,
+ CustomerID INT REFERENCES Customers(CustomerID),
+ ContactID INT REFERENCES Contacts(ContactID),
+ Subject VARCHAR(255) NOT NULL,
+ Description TEXT,
+ Status VARCHAR(50) DEFAULT 'Open',
+ Priority VARCHAR(20) DEFAULT 'Normal',
+ AssignedTo INT REFERENCES Users(UserID),
+ CreatedAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+COMMENT ON TABLE SupportTickets IS 'Tracks customer service and support requests.';
+COMMENT ON COLUMN SupportTickets.TicketID IS 'Unique identifier for the support ticket.';
+COMMENT ON COLUMN SupportTickets.CustomerID IS 'Foreign key linking the ticket to a customer.';
+COMMENT ON COLUMN SupportTickets.ContactID IS 'Foreign key linking the ticket to a specific contact.';
+COMMENT ON COLUMN SupportTickets.Subject IS 'A brief summary of the support issue.';
+COMMENT ON COLUMN SupportTickets.Description IS 'A detailed description of the issue.';
+COMMENT ON COLUMN SupportTickets.Status IS 'The current status of the ticket (e.g., Open, In Progress, Resolved).';
+COMMENT ON COLUMN SupportTickets.Priority IS 'The priority of the ticket (e.g., Low, Normal, High).';
+COMMENT ON COLUMN SupportTickets.AssignedTo IS 'The support agent the ticket is assigned to.';
+COMMENT ON COLUMN SupportTickets.CreatedAt IS 'Timestamp when the ticket was created.';
+
+-- Comments on support tickets
+CREATE TABLE TicketComments (
+ CommentID SERIAL PRIMARY KEY,
+ TicketID INT REFERENCES SupportTickets(TicketID) ON DELETE CASCADE,
+ Comment TEXT NOT NULL,
+ CreatedBy INT REFERENCES Users(UserID),
+ CreatedAt TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
+COMMENT ON TABLE TicketComments IS 'Stores comments and updates related to a support ticket.';
+COMMENT ON COLUMN TicketComments.CommentID IS 'Unique identifier for the comment.';
+COMMENT ON COLUMN TicketComments.TicketID IS 'Foreign key linking the comment to a support ticket.';
+COMMENT ON COLUMN TicketComments.Comment IS 'The text content of the comment.';
+COMMENT ON COLUMN TicketComments.CreatedBy IS 'The user who added the comment.';
+COMMENT ON COLUMN TicketComments.CreatedAt IS 'Timestamp when the comment was added.';
+
+
+-- SQL Script 2: Data Insertion (DML)
+-- This script populates the tables with sample data.
+
+-- Insert Roles
+INSERT INTO Roles (RoleName) VALUES ('Admin'), ('Sales Manager'), ('Sales Representative'), ('Support Agent');
+
+-- Insert Users
+INSERT INTO Users (Username, PasswordHash, Email, FirstName, LastName) VALUES
+('admin', 'hashed_password', 'admin@example.com', 'Admin', 'User'),
+('sales_manager', 'hashed_password', 'manager@example.com', 'John', 'Doe'),
+('sales_rep1', 'hashed_password', 'rep1@example.com', 'Jane', 'Smith'),
+('sales_rep2', 'hashed_password', 'rep2@example.com', 'Peter', 'Jones'),
+('support_agent1', 'hashed_password', 'support1@example.com', 'Mary', 'Williams');
+
+-- Assign Roles to Users
+INSERT INTO UserRoles (UserID, RoleID) VALUES
+(1, 1), (2, 2), (3, 3), (4, 3), (5, 4);
+
+-- Insert Customers
+INSERT INTO Customers (CustomerName, Industry, Website, Phone, Address, City, State, ZipCode, Country, AssignedTo) VALUES
+('ABC Corporation', 'Technology', 'http://www.abccorp.com', '123-456-7890', '123 Tech Park', 'Techville', 'CA', '90210', 'USA', 3),
+('Innovate Inc.', 'Software', 'http://www.innovate.com', '234-567-8901', '456 Innovation Dr', 'Devtown', 'TX', '75001', 'USA', 4),
+('Global Solutions', 'Consulting', 'http://www.globalsolutions.com', '345-678-9012', '789 Global Ave', 'Businesston', 'NY', '10001', 'USA', 3),
+('Data Dynamics', 'Analytics', 'http://www.datadynamics.com', '456-123-7890', '789 Data Dr', 'Metropolis', 'IL', '60601', 'USA', 4),
+('Synergy Solutions', 'HR', 'http://www.synergysolutions.com', '789-456-1230', '101 Synergy Blvd', 'Union City', 'NJ', '07087', 'USA', 3);
+
+-- Insert Contacts
+INSERT INTO Contacts (CustomerID, FirstName, LastName, Email, Phone, JobTitle) VALUES
+(1, 'Alice', 'Wonder', 'alice.wonder@abccorp.com', '123-456-7891', 'CTO'),
+(1, 'Bob', 'Builder', 'bob.builder@abccorp.com', '123-456-7892', 'Project Manager'),
+(2, 'Charlie', 'Chocolate', 'charlie.chocolate@innovate.com', '234-567-8902', 'CEO'),
+(3, 'Diana', 'Prince', 'diana.prince@globalsolutions.com', '345-678-9013', 'Consultant'),
+(4, 'Leo', 'Lytics', 'leo.lytics@datadynamics.com', '456-123-7891', 'Data Scientist'),
+(5, 'Hannah', 'Resources', 'hannah.r@synergysolutions.com', '789-456-1231', 'HR Manager');
+
+-- Insert Leads
+INSERT INTO Leads (FirstName, LastName, Email, Phone, Company, Status, Source, AssignedTo) VALUES
+('Eve', 'Apple', 'eve.apple@email.com', '456-789-0123', 'Future Gadgets', 'Qualified', 'Website', 3),
+('Frank', 'Stein', 'frank.stein@email.com', '567-890-1234', 'Monster Corp', 'New', 'Referral', 4),
+('Grace', 'Hopper', 'grace.hopper@email.com', '678-901-2345', 'Cobol Inc.', 'Contacted', 'Cold Call', 3),
+('Ivy', 'Green', 'ivy.g@webmail.com', '890-123-4567', 'Eco Systems', 'New', 'Trade Show', 4),
+('Jack', 'Nimble', 'jack.n@fastmail.com', '901-234-5678', 'Quick Corp', 'Qualified', 'Website', 3);
+
+-- Insert Opportunities
+INSERT INTO Opportunities (CustomerID, OpportunityName, Stage, Amount, CloseDate, AssignedTo) VALUES
+(1, 'ABC Corp Website Redesign', 'Proposal', 50000.00, '2025-08-30', 3),
+(2, 'Innovate Inc. Mobile App', 'Qualification', 75000.00, '2025-09-15', 4),
+(3, 'Global Solutions IT Consulting', 'Negotiation', 120000.00, '2025-08-20', 3),
+(4, 'Analytics Platform Subscription', 'Proposal', 90000.00, '2025-09-30', 4),
+(5, 'HR Software Implementation', 'Prospecting', 65000.00, '2025-10-25', 3);
+
+-- Insert Product Categories
+INSERT INTO ProductCategories (CategoryName, Description) VALUES
+('Software', 'Business and productivity software'),
+('Hardware', 'Computer hardware and peripherals'),
+('Services', 'Consulting and support services');
+
+-- Insert Products
+INSERT INTO Products (ProductName, CategoryID, Description, Price, StockQuantity) VALUES
+('CRM Pro', 1, 'Advanced CRM Software Suite', 1500.00, 100),
+('Office Laptop Model X', 2, 'High-performance laptop for business', 1200.00, 50),
+('IT Support Package', 3, '24/7 IT support services', 300.00, 200),
+('Analytics Dashboard Pro', 1, 'Advanced analytics dashboard', 2500.00, 75),
+('Ergonomic Office Chair', 2, 'Comfortable chair for long hours', 350.00, 150);
+
+-- Insert Sales Orders
+INSERT INTO SalesOrders (CustomerID, OpportunityID, OrderDate, Status, TotalAmount, AssignedTo) VALUES
+(1, 1, '2025-07-20', 'Shipped', 1500.00, 3),
+(2, 2, '2025-07-22', 'Pending', 2400.00, 4),
+(3, 3, '2025-07-24', 'Delivered', 300.00, 3),
+(4, 4, '2025-07-25', 'Pending', 2500.00, 4);
+
+-- Insert Sales Order Items
+INSERT INTO SalesOrderItems (OrderID, ProductID, Quantity, UnitPrice) VALUES
+(1, 1, 1, 1500.00),
+(2, 2, 2, 1200.00),
+(3, 3, 1, 300.00),
+(4, 4, 1, 2500.00);
+
+-- Insert Invoices
+INSERT INTO Invoices (OrderID, InvoiceDate, DueDate, TotalAmount, Status) VALUES
+(1, '2025-07-21', '2025-08-20', 1500.00, 'Paid'),
+(2, '2025-07-23', '2025-08-22', 2400.00, 'Unpaid'),
+(3, '2025-07-24', '2025-08-23', 300.00, 'Paid'),
+(4, '2025-07-25', '2025-08-24', 2500.00, 'Unpaid');
+
+-- Insert Payments
+INSERT INTO Payments (InvoiceID, PaymentDate, Amount, PaymentMethod) VALUES
+(1, '2025-07-25', 1500.00, 'Credit Card'),
+(3, '2025-07-25', 300.00, 'Bank Transfer');
+
+-- Insert Campaigns
+INSERT INTO Campaigns (CampaignName, StartDate, EndDate, Budget, Status, Owner) VALUES
+('Summer Sale 2025', '2025-06-01', '2025-08-31', 10000.00, 'Active', 2),
+('Q4 Product Launch', '2025-10-01', '2025-12-31', 25000.00, 'Planned', 2);
+
+-- Insert Campaign Members
+INSERT INTO CampaignMembers (CampaignID, LeadID, Status) VALUES
+(1, 1, 'Responded'),
+(1, 2, 'Sent'),
+(1, 4, 'Sent');
+INSERT INTO CampaignMembers (CampaignID, ContactID, Status) VALUES
+(1, 4, 'Sent'),
+(1, 5, 'Responded');
+
+-- Insert Tasks
+INSERT INTO Tasks (Title, Description, DueDate, Status, Priority, AssignedTo, RelatedToEntity, RelatedToID) VALUES
+('Follow up with ABC Corp', 'Discuss proposal details', '2025-08-01', 'In Progress', 'High', 3, 'Opportunity', 1),
+('Prepare demo for Innovate Inc.', 'Customize demo for their needs', '2025-08-05', 'Not Started', 'Normal', 4, 'Opportunity', 2),
+('Send updated proposal to Global Solutions', 'Include new service terms', '2025-07-28', 'Completed', 'High', 3, 'Opportunity', 3),
+('Schedule initial call with Synergy Solutions', 'Discuss HR software needs', '2025-08-02', 'Not Started', 'Normal', 3, 'Customer', 5);
+
+-- Insert Notes
+INSERT INTO Notes (Content, CreatedBy, RelatedToEntity, RelatedToID) VALUES
+('Alice is very interested in the mobile integration features.', 3, 'Contact', 1),
+('Lead from the tech conference last week.', 4, 'Lead', 2),
+('Customer is looking for a cloud-based solution.', 4, 'Opportunity', 4),
+('Met Ivy at the GreenTech expo. Promising lead.', 4, 'Lead', 4);
+
+-- Insert Attachments
+INSERT INTO Attachments (FileName, FilePath, FileSize, FileType, UploadedBy, RelatedToEntity, RelatedToID) VALUES
+('proposal_v1.pdf', '/attachments/proposal_v1.pdf', 102400, 'application/pdf', 3, 'Opportunity', 1),
+('analytics_brochure.pdf', '/attachments/analytics_brochure.pdf', 256000, 'application/pdf', 4, 'Opportunity', 4);
+
+-- Insert Support Tickets
+INSERT INTO SupportTickets (CustomerID, ContactID, Subject, Description, Status, Priority, AssignedTo) VALUES
+(1, 1, 'Cannot login to portal', 'User Alice Wonder is unable to access the customer portal.', 'Resolved', 'High', 5),
+(2, 3, 'Billing question', 'Question about the last invoice.', 'In Progress', 'Normal', 5),
+(3, 4, 'Feature Request: Dark Mode', 'Requesting dark mode for the user dashboard.', 'Open', 'Low', 5),
+(1, 2, 'Integration issue with calendar', 'Tasks are not syncing with Google Calendar.', 'In Progress', 'High', 5);
+
+-- Insert Ticket Comments
+INSERT INTO TicketComments (TicketID, Comment, CreatedBy) VALUES
+(1, 'Have reset the password. Please ask the user to try again.', 5),
+(1, 'User confirmed they can now log in. Closing the ticket.', 5),
+(2, 'Checking API logs for sync errors.', 5),
+(3, 'Feature has been added to the development backlog.', 5);
+
+-- SQL Script 3: Insert More Demo Data (DML)
+-- This script adds more sample data to the CRM database.
+-- Run this script AFTER running 1_create_tables.sql and 2_insert_data.sql.
+
+-- Insert more Customers (starting from CustomerID 6)
+INSERT INTO Customers (CustomerName, Industry, Website, Phone, Address, City, State, ZipCode, Country, AssignedTo) VALUES
+('Quantum Innovations', 'R&D', 'http://www.quantuminnovate.com', '555-0101', '100 Research Pkwy', 'Quantumville', 'MA', '02139', 'USA', 3),
+('HealthFirst Medical', 'Healthcare', 'http://www.healthfirst.com', '555-0102', '200 Health Blvd', 'Wellnesston', 'FL', '33101', 'USA', 4),
+('GreenScape Solutions', 'Environmental', 'http://www.greenscape.com', '555-0103', '300 Nature Way', 'Ecoville', 'OR', '97201', 'USA', 3),
+('Pinnacle Finance', 'Finance', 'http://www.pinnaclefinance.com', '555-0104', '400 Wall St', 'Financeton', 'NY', '10005', 'USA', 4),
+('Creative Minds Agency', 'Marketing', 'http://www.creativeminds.com', '555-0105', '500 Ad Ave', 'Creator City', 'CA', '90028', 'USA', 3);
+
+-- Insert more Contacts (starting from ContactID 7)
+-- Assuming CustomerIDs 6-10 were just created
+INSERT INTO Contacts (CustomerID, FirstName, LastName, Email, Phone, JobTitle) VALUES
+(6, 'Quentin', 'Physics', 'q.physics@quantuminnovate.com', '555-0101-1', 'Lead Scientist'),
+(7, 'Helen', 'Healer', 'h.healer@healthfirst.com', '555-0102-1', 'Hospital Administrator'),
+(7, 'Marcus', 'Welby', 'm.welby@healthfirst.com', '555-0102-2', 'Chief of Medicine'),
+(8, 'Gary', 'Gardener', 'g.gardener@greenscape.com', '555-0103-1', 'CEO'),
+(9, 'Fiona', 'Funds', 'f.funds@pinnaclefinance.com', '555-0104-1', 'Investment Banker'),
+(10, 'Chris', 'Creative', 'c.creative@creativeminds.com', '555-0105-1', 'Art Director'),
+(1, 'Carol', 'Client', 'c.client@abccorp.com', '123-456-7893', 'IT Director'); -- Contact for existing customer
+
+-- Insert more Leads (starting from LeadID 6)
+INSERT INTO Leads (FirstName, LastName, Email, Phone, Company, Status, Source, AssignedTo) VALUES
+('Ken', 'Knowledge', 'ken.k@university.edu', '555-0201', 'State University', 'Contacted', 'Referral', 4),
+('Laura', 'Legal', 'laura.l@lawfirm.com', '555-0202', 'Law & Order LLC', 'New', 'Website', 3),
+('Mike', 'Mechanic', 'mike.m@autoshop.com', '555-0203', 'Auto Fixers', 'Lost', 'Cold Call', 4),
+('Nancy', 'Nurse', 'nancy.n@clinic.com', '555-0204', 'Community Clinic', 'Qualified', 'Trade Show', 3),
+('Oscar', 'Organizer', 'oscar.o@events.com', '555-0205', 'Events R Us', 'New', 'Website', 4);
+
+-- Insert more Opportunities (starting from OpportunityID 6)
+-- Assuming CustomerIDs 6-10 were just created
+INSERT INTO Opportunities (CustomerID, OpportunityName, Stage, Amount, CloseDate, AssignedTo) VALUES
+(6, 'Quantum Computing Simulation Software', 'Qualification', 250000.00, '2025-11-15', 3),
+(7, 'Patient Management System Upgrade', 'Proposal', 180000.00, '2025-12-01', 4),
+(8, 'Environmental Impact Reporting Tool', 'Negotiation', 75000.00, '2025-10-30', 3),
+(9, 'Wealth Management Platform', 'Closed Won', 300000.00, '2025-07-25', 4),
+(10, 'Digital Marketing Campaign Analytics', 'Prospecting', 45000.00, '2025-11-20', 3);
+
+-- Insert a new Product Category first
+INSERT INTO ProductCategories (CategoryName, Description) VALUES
+('Cloud Solutions', 'Cloud-based infrastructure and platforms'); -- This will be CategoryID 4
+
+-- Insert more Products (starting from ProductID 6)
+INSERT INTO Products (ProductName, CategoryID, Description, Price, StockQuantity) VALUES
+('Wealth Management Suite', 1, 'Comprehensive software for financial advisors', 5000.00, 50),
+('Patient Record System', 1, 'EHR system for clinics and hospitals', 4500.00, 80),
+('Cloud Storage - 10TB Plan', 4, '10TB of enterprise cloud storage', 1000.00, 500);
+
+-- Insert more Sales Orders (starting from OrderID 5)
+-- For the 'Closed Won' opportunity (ID 9)
+INSERT INTO SalesOrders (CustomerID, OpportunityID, OrderDate, Status, TotalAmount, AssignedTo) VALUES
+(9, 9, '2025-07-26', 'Delivered', 5000.00, 4);
+
+-- Insert more Sales Order Items (for OrderID 5)
+INSERT INTO SalesOrderItems (OrderID, ProductID, Quantity, UnitPrice) VALUES
+(5, 6, 1, 5000.00); -- Wealth Management Suite (ProductID 6)
+
+-- Insert more Invoices (starting from InvoiceID 5)
+INSERT INTO Invoices (OrderID, InvoiceDate, DueDate, TotalAmount, Status) VALUES
+(5, '2025-07-26', '2025-08-25', 5000.00, 'Paid');
+
+-- Insert more Payments (starting from PaymentID 3)
+INSERT INTO Payments (InvoiceID, PaymentDate, Amount, PaymentMethod) VALUES
+(2, '2025-07-25', 2400.00, 'Bank Transfer'), -- Payment for an existing unpaid invoice
+(5, '2025-07-26', 5000.00, 'Credit Card');
+
+-- Insert a new Campaign (starting from CampaignID 3)
+INSERT INTO Campaigns (CampaignName, StartDate, EndDate, Budget, Status, Owner) VALUES
+('Healthcare Solutions Webinar', '2025-09-01', '2025-09-30', 7500.00, 'Planned', 2);
+
+-- Insert more Campaign Members
+INSERT INTO CampaignMembers (CampaignID, LeadID, Status) VALUES
+(3, 9, 'Sent'); -- Nancy Nurse (LeadID 9) for Healthcare campaign
+INSERT INTO CampaignMembers (CampaignID, ContactID, Status) VALUES
+(3, 8, 'Sent'), -- Helen Healer (ContactID 8)
+(3, 9, 'Responded'); -- Marcus Welby (ContactID 9)
+
+-- Insert more Tasks (starting from TaskID 5)
+INSERT INTO Tasks (Title, Description, DueDate, Status, Priority, AssignedTo, RelatedToEntity, RelatedToID) VALUES
+('Draft contract for Pinnacle Finance', 'Based on the final negotiation terms.', '2025-07-28', 'Completed', 'High', 4, 'Opportunity', 9),
+('Schedule webinar with HealthFirst', 'Discuss Patient Management System demo.', '2025-08-10', 'Not Started', 'High', 4, 'Opportunity', 7),
+('Research Quantum Innovations needs', 'Prepare for qualification call.', '2025-08-15', 'In Progress', 'Normal', 3, 'Opportunity', 6),
+('Call Nancy Nurse to follow up', 'Follow up from trade show conversation.', '2025-08-05', 'Not Started', 'Normal', 3, 'Lead', 9);
+
+-- Insert more Notes (starting from NoteID 5)
+INSERT INTO Notes (Content, CreatedBy, RelatedToEntity, RelatedToID) VALUES
+('Pinnacle deal closed! Great work team.', 2, 'Opportunity', 9),
+('GreenScape is looking for a solution before year-end for compliance reasons.', 3, 'Opportunity', 8),
+('Nancy was very engaged at the booth, good prospect.', 3, 'Lead', 9);
+
+-- Insert more Support Tickets (starting from TicketID 5)
+INSERT INTO SupportTickets (CustomerID, ContactID, Subject, Description, Status, Priority, AssignedTo) VALUES
+(4, 5, 'Dashboard data not refreshing', 'The main dashboard widgets are not updating in real-time.', 'Open', 'High', 5),
+(5, 6, 'Report generation is slow', 'Generating the quarterly HR report takes over 10 minutes.', 'In Progress', 'Normal', 5),
+(9, 11, 'Login issue for new user', 'Fiona Funds cannot log into the new Wealth Management platform.', 'Open', 'High', 5);
+
+-- Insert more Ticket Comments (starting from CommentID 5)
+INSERT INTO TicketComments (TicketID, Comment, CreatedBy) VALUES
+(2, 'Invoice has been resent to the customer.', 5), -- Comment on existing ticket
+(4, 'The calendar sync issue seems to be related to a recent Google API update. Investigating.', 5), -- Comment on existing ticket
+(5, 'Escalated to engineering to check the database query performance.', 5),
+(6, 'Confirmed the issue is with the real-time data service. Restarting the service.', 5);
+
+-- Update existing records to show data changes
+UPDATE Leads SET Status = 'Contacted' WHERE LeadID = 2; -- Frank Stein
+UPDATE Invoices SET Status = 'Paid' WHERE InvoiceID = 2; -- Innovate Inc. invoice
diff --git a/onthology.py b/onthology.py
index f5f0007c..9ae9a783 100644
--- a/onthology.py
+++ b/onthology.py
@@ -1,9 +1,11 @@
+"""Ontology generation module for CRM system knowledge graph."""
+
from falkordb import FalkorDB
from graphrag_sdk import Ontology
from graphrag_sdk.models.litellm import LiteModel
model = LiteModel(model_name="gemini/gemini-2.0-flash")
-db = FalkorDB(host='localhost', port=6379)
-kg_name = "crm_system"
-ontology = Ontology.from_kg_graph(db.select_graph(kg_name), 1000000000)
-ontology.save_to_graph(db.select_graph(f"{{{kg_name}}}_schema"))
+db = FalkorDB(host="localhost", port=6379)
+KG_NAME = "crm_system"
+ontology = Ontology.from_kg_graph(db.select_graph(KG_NAME), 1000000000)
+ontology.save_to_graph(db.select_graph(f"{{{KG_NAME}}}_schema"))
diff --git a/poetry.lock b/poetry.lock
index 486ad416..2ca47e8e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -478,6 +478,32 @@ werkzeug = ">=3.1.0"
async = ["asgiref (>=3.2)"]
dotenv = ["python-dotenv"]
+[[package]]
+name = "flask-dance"
+version = "7.1.0"
+description = "Doing the OAuth dance with style using Flask, requests, and oauthlib"
+optional = false
+python-versions = ">=3.6"
+groups = ["main"]
+files = [
+ {file = "flask_dance-7.1.0-py3-none-any.whl", hash = "sha256:81599328a2b3604fd4332b3d41a901cf36980c2067e5e38c44ce3b85c4e1ae9c"},
+ {file = "flask_dance-7.1.0.tar.gz", hash = "sha256:6d0510e284f3d6ff05af918849791b17ef93a008628ec33f3a80578a44b51674"},
+]
+
+[package.dependencies]
+Flask = ">=2.0.3"
+oauthlib = ">=3.2"
+requests = ">=2.0"
+requests-oauthlib = ">=1.0.0"
+urlobject = "*"
+Werkzeug = "*"
+
+[package.extras]
+docs = ["Flask-Sphinx-Themes", "betamax", "pillow (<=9.5)", "pytest", "sphinx (>=1.3)", "sphinxcontrib-seqdiag", "sphinxcontrib-spelling", "sqlalchemy (>=1.3.11)"]
+signals = ["blinker"]
+sqla = ["sqlalchemy (>=1.3.11)"]
+test = ["betamax", "coverage", "flask-caching", "flask-login", "flask-sqlalchemy", "freezegun", "oauthlib[signedtoken]", "pytest", "pytest-mock", "responses", "sqlalchemy (>=1.3.11)"]
+
[[package]]
name = "frozenlist"
version = "1.7.0"
@@ -1219,6 +1245,23 @@ files = [
{file = "multidict-6.6.2.tar.gz", hash = "sha256:c1e8b8b0523c0361a78ce9b99d9850c51cf25e1fa3c5686030ce75df6fdf2918"},
]
+[[package]]
+name = "oauthlib"
+version = "3.3.1"
+description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"},
+ {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"},
+]
+
+[package.extras]
+rsa = ["cryptography (>=3.0.0)"]
+signals = ["blinker (>=1.4.0)"]
+signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
+
[[package]]
name = "openai"
version = "1.93.0"
@@ -1947,6 +1990,25 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+[[package]]
+name = "requests-oauthlib"
+version = "2.0.0"
+description = "OAuthlib authentication support for Requests."
+optional = false
+python-versions = ">=3.4"
+groups = ["main"]
+files = [
+ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"},
+ {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"},
+]
+
+[package.dependencies]
+oauthlib = ">=3.0.0"
+requests = ">=2.0.0"
+
+[package.extras]
+rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
+
[[package]]
name = "rpds-py"
version = "0.25.1"
@@ -2276,6 +2338,17 @@ h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
+[[package]]
+name = "urlobject"
+version = "2.4.3"
+description = "A utility class for manipulating URLs."
+optional = false
+python-versions = "*"
+groups = ["main"]
+files = [
+ {file = "URLObject-2.4.3.tar.gz", hash = "sha256:47b2e20e6ab9c8366b2f4a3566b6ff4053025dad311c4bb71279bbcfa2430caa"},
+]
+
[[package]]
name = "werkzeug"
version = "3.1.3"
@@ -2436,4 +2509,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.12,<3.13"
-content-hash = "ee04db8da4b24fe0934a010563416a179dc15ee351d46a380f778fb1086a0898"
+content-hash = "f883bca3ecc7074ea013650a53f67a0a78dca576a65bd5732a4be3cfc6e4a66a"
diff --git a/pyproject.toml b/pyproject.toml
index 5c4298ab..7c896dc4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,6 +21,7 @@ jsonschema = "^4.23.0"
tqdm = "^4.67.1"
boto3 = "^1.37.29"
psycopg2-binary = "^2.9.9"
+flask-dance = "^7.1.0"
[tool.poetry.group.test.dependencies]
pytest = "^8.2.0"
diff --git a/requirements.txt b/requirements.txt
index f5cdb6c5..5bffe872 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,61 +1,62 @@
-aiohappyeyeballs==2.6.1 ; python_version == "3.12"
-aiohttp==3.12.13 ; python_version == "3.12"
-aiosignal==1.3.2 ; python_version == "3.12"
-annotated-types==0.7.0 ; python_version == "3.12"
-anyio==4.9.0 ; python_version == "3.12"
-attrs==25.3.0 ; python_version == "3.12"
-blinker==1.9.0 ; python_version == "3.12"
-boto3==1.38.46 ; python_version == "3.12"
-botocore==1.38.46 ; python_version == "3.12"
-certifi==2025.6.15 ; python_version == "3.12"
-charset-normalizer==3.4.2 ; python_version == "3.12"
-click==8.2.1 ; python_version == "3.12"
-colorama==0.4.6 ; python_version == "3.12" and platform_system == "Windows"
-distro==1.9.0 ; python_version == "3.12"
-falkordb==1.1.2 ; python_version == "3.12"
-filelock==3.18.0 ; python_version == "3.12"
-flask==3.1.1 ; python_version == "3.12"
-frozenlist==1.7.0 ; python_version == "3.12"
-fsspec==2025.5.1 ; python_version == "3.12"
-h11==0.16.0 ; python_version == "3.12"
-hf-xet==1.1.5 ; python_version == "3.12" and (platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "arm64" or platform_machine == "aarch64")
-httpcore==1.0.9 ; python_version == "3.12"
-httpx==0.28.1 ; python_version == "3.12"
-huggingface-hub==0.33.1 ; python_version == "3.12"
-idna==3.10 ; python_version == "3.12"
-importlib-metadata==8.7.0 ; python_version == "3.12"
-itsdangerous==2.2.0 ; python_version == "3.12"
-jinja2==3.1.6 ; python_version == "3.12"
-jiter==0.10.0 ; python_version == "3.12"
-jmespath==1.0.1 ; python_version == "3.12"
-jsonschema-specifications==2025.4.1 ; python_version == "3.12"
-jsonschema==4.24.0 ; python_version == "3.12"
-litellm==1.73.6 ; python_version == "3.12"
-markupsafe==3.0.2 ; python_version == "3.12"
-multidict==6.6.2 ; python_version == "3.12"
-openai==1.93.0 ; python_version == "3.12"
-packaging==25.0 ; python_version == "3.12"
-propcache==0.3.2 ; python_version == "3.12"
-pydantic-core==2.33.2 ; python_version == "3.12"
-pydantic==2.11.7 ; python_version == "3.12"
-pyjwt==2.9.0 ; python_version == "3.12"
-python-dateutil==2.9.0.post0 ; python_version == "3.12"
-python-dotenv==1.1.1 ; python_version == "3.12"
-pyyaml==6.0.2 ; python_version == "3.12"
-redis==5.3.0 ; python_version == "3.12"
-referencing==0.36.2 ; python_version == "3.12"
-regex==2024.11.6 ; python_version == "3.12"
-requests==2.32.4 ; python_version == "3.12"
-rpds-py==0.25.1 ; python_version == "3.12"
-s3transfer==0.13.0 ; python_version == "3.12"
-six==1.17.0 ; python_version == "3.12"
-sniffio==1.3.1 ; python_version == "3.12"
-tiktoken==0.9.0 ; python_version == "3.12"
-tokenizers==0.21.2 ; python_version == "3.12"
-tqdm==4.67.1 ; python_version == "3.12"
-typing-extensions==4.14.0 ; python_version == "3.12"
-typing-inspection==0.4.1 ; python_version == "3.12"
-urllib3==2.5.0 ; python_version == "3.12"
-werkzeug==3.1.3 ; python_version == "3.12"
-yarl==1.20.1 ; python_version == "3.12"
-zipp==3.23.0 ; python_version == "3.12"
+aiohappyeyeballs==2.6.1
+aiohttp==3.12.13
+aiosignal==1.3.2
+annotated-types==0.7.0
+anyio==4.9.0
+attrs==25.3.0
+blinker==1.9.0
+boto3==1.38.46
+botocore==1.38.46
+certifi==2025.6.15
+charset-normalizer==3.4.2
+click==8.2.1
+distro==1.9.0
+falkordb==1.1.2
+filelock==3.18.0
+flask==3.1.1
+flask-dance==7.1.0
+frozenlist==1.7.0
+fsspec==2025.5.1
+h11==0.16.0
+hf-xet==1.1.5
+httpcore==1.0.9
+httpx==0.28.1
+huggingface-hub==0.33.1
+idna==3.10
+importlib-metadata==8.7.0
+itsdangerous==2.2.0
+jinja2==3.1.6
+jiter==0.10.0
+jmespath==1.0.1
+jsonschema-specifications==2025.4.1
+jsonschema==4.24.0
+litellm==1.73.6
+markupsafe==3.0.2
+multidict==6.6.2
+openai==1.93.0
+packaging==25.0
+propcache==0.3.2
+pydantic-core==2.33.2
+pydantic==2.11.7
+pyjwt==2.9.0
+python-dateutil==2.9.0.post0
+python-dotenv==1.1.1
+pyyaml==6.0.2
+redis==5.3.0
+referencing==0.36.2
+regex==2024.11.6
+requests==2.32.4
+rpds-py==0.25.1
+s3transfer==0.13.0
+six==1.17.0
+sniffio==1.3.1
+tiktoken==0.9.0
+tokenizers==0.21.2
+tqdm==4.67.1
+typing-extensions==4.14.0
+typing-inspection==0.4.1
+urllib3==2.5.0
+werkzeug==3.1.3
+yarl==1.20.1
+zipp==3.23.0
+psycopg2-binary==2.9.9
diff --git a/start.sh b/start.sh
new file mode 100644
index 00000000..c0db7ef6
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+set -e
+
+
+# Set default values if not set
+FALKORDB_HOST="${FALKORDB_HOST:-localhost}"
+FALKORDB_PORT="${FALKORDB_PORT:-6379}"
+
+# Start FalkorDB Redis server in background
+redis-server --loadmodule /var/lib/falkordb/bin/falkordb.so &
+
+# Wait until FalkorDB is ready
+echo "Waiting for FalkorDB to start on $FALKORDB_HOST:$FALKORDB_PORT..."
+
+while ! nc -z "$FALKORDB_HOST" "$FALKORDB_PORT"; do
+ sleep 0.5
+done
+
+
+echo "FalkorDB is up - launching Flask..."
+exec python3 -m flask --app api.index run --host=0.0.0.0 --port=5000
\ No newline at end of file